diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3da67a0c..3584715fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,32 @@ jobs: - name: Run fuzz session run: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s fuzz + # Opt-in, non-blocking, runtime-gated container integration tests (RUN-314). + # Kept out of the hermetic `verify` graph; the `docker` marker tests self-skip + # when no runtime is present, and this whole job never fails the build. + integration-docker: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 + - name: Probe for a container runtime + id: runtime + run: | + if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "No container runtime available; skipping docker integration session." + fi + - name: Run docker integration session + if: steps.runtime.outputs.available == 'true' + run: uv tool run --from 'nox[uv]==2026.4.10' nox -f noxfile.py -s integration_docker + sonar: runs-on: ubuntu-latest needs: [verify] diff --git a/.ground-control.yaml b/.ground-control.yaml index ec92cb271..261023e92 100644 --- a/.ground-control.yaml +++ b/.ground-control.yaml @@ -16,6 +16,11 @@ requirements: - GOV-918 - RUN-311 - ADR-012 +routing: + enabled: true + default_provider: claude + default_fallback: parent + stages: {} sonarcloud: project_key: Brad-Edwards_aces organization: brad-edwards diff --git a/README.md b/README.md index 2e6ed8da9..a6a9aecee 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ The repository is not a managed cyber range and does not include a production backend. Backend contracts, stubs, conformance checks, and examples are present; real deployment backends remain separate implementations. +A worked example of ACES SDL driving a concrete range is +[APTL (Advanced Purple Team Lab)](https://github.com/Brad-Edwards/aptl), a +separate project that specifies its scenarios as ACES SDL documents and +realizes the selected topology on a Docker Compose backend. + ## Contents - [What ACES SDL Describes](#what-aces-sdl-describes) diff --git a/changelog.d/+readme-aptl-example.added.md b/changelog.d/+readme-aptl-example.added.md new file mode 100644 index 000000000..7763b77a6 --- /dev/null +++ b/changelog.d/+readme-aptl-example.added.md @@ -0,0 +1 @@ +Noted in the README that APTL (Advanced Purple Team Lab) is a worked example of a separate project specifying its scenarios as ACES SDL documents and realizing the selected topology on a concrete Docker Compose backend. diff --git a/changelog.d/13.security.md b/changelog.d/13.security.md new file mode 100644 index 000000000..24da67d6c --- /dev/null +++ b/changelog.d/13.security.md @@ -0,0 +1 @@ +Harden OCI module bundle extraction to fail closed on every supported Python runtime. The resolver now validates the entire tar archive before writing — rejecting path traversal, symlinks, hard links, and special files, and stripping setuid/setgid/sticky bits — instead of relying on `tarfile`'s `filter="data"`, which is unavailable on Python 3.11.0–3.11.3 (the PEP 706 backport landed in 3.11.4) and previously allowed an unsafe extraction path on those supported runtimes. diff --git a/changelog.d/143.security.md b/changelog.d/143.security.md new file mode 100644 index 000000000..77504df71 --- /dev/null +++ b/changelog.d/143.security.md @@ -0,0 +1 @@ +Port runtime control-plane and SDL module-registry security hardening onto the current package layout. diff --git a/changelog.d/194.added.md b/changelog.d/194.added.md new file mode 100644 index 000000000..e809b2f49 --- /dev/null +++ b/changelog.d/194.added.md @@ -0,0 +1,3 @@ +### Added +- Added first-class RUN-307 shared operational state records/history to runtime snapshots with revision-aware validation. +- Added semantic diagnostics for malformed shared-state records, access markers, and append-only history violations. diff --git a/changelog.d/195.added.md b/changelog.d/195.added.md new file mode 100644 index 000000000..0a724e5d4 --- /dev/null +++ b/changelog.d/195.added.md @@ -0,0 +1 @@ +Added RUN-308 participant-runtime contract surfaces for joint action records, time-management contexts, runtime snapshot concurrency validation, and coverage for the concurrency guardrails. diff --git a/changelog.d/196.added.md b/changelog.d/196.added.md new file mode 100644 index 000000000..2dbda03be --- /dev/null +++ b/changelog.d/196.added.md @@ -0,0 +1 @@ +Add a repository-owned reference processor (`aces_processor.reference.run_reference_processor` / `ReferenceProcessor`) that realizes the normative processing model: it carries SDL authoring input through instantiation, compilation, and planning to a portable execution plan and exposes the published processor manifest. Per ADR-008 the processor stops at the execution plan; backend realization stays in the runtime. The backend-conformance live probe now consumes the reference processor instead of inlining the compile/plan chain, and new tests drive its plan through the reference runtime to prove every contract version the processor manifest declares is exercised end to end. (RUN-313) diff --git a/changelog.d/197.added.md b/changelog.d/197.added.md new file mode 100644 index 000000000..e2097d609 --- /dev/null +++ b/changelog.d/197.added.md @@ -0,0 +1 @@ +Add a repository-owned reference emulation backend (`aces_reference_backend`) that implements the four backend protocol roles (Provisioner, Orchestrator, Evaluator, ParticipantRuntime) over a pluggable deployment driver. The default in-process driver is hermetic; an opt-in OCI driver realizes plans against a real container runtime (docker/podman) through fixed-argv subprocess calls with bounded timeouts and no secret/native-id leakage into any portable artifact. The backend publishes identity/capability through the standard `BackendManifest`, registers on the existing `BackendRegistry` descriptor seam as `reference-emulation`, and passes `run_target_conformance` at the `FULL_REMOTE_CONTROL_PLANE` profile. Provenance flows through the SEM-218 apply gate; only portable ACES facts reach snapshots, diagnostics, and conformance reports. A `docker`-marked, runtime-gated integration test and a non-blocking `integration_docker` nox session / CI job exercise real-container realization without touching the hermetic `verify` graph. (RUN-314, ADR-063) diff --git a/changelog.d/200.added.md b/changelog.d/200.added.md new file mode 100644 index 000000000..6b90390ba --- /dev/null +++ b/changelog.d/200.added.md @@ -0,0 +1,5 @@ +### Added + +- Made the API-406 participant lifecycle-event, observation-envelope, and + shared-state record contracts required by the full remote control-plane + backend profile and registered their conformance model validators. diff --git a/changelog.d/201.added.md b/changelog.d/201.added.md new file mode 100644 index 000000000..031c3ee02 --- /dev/null +++ b/changelog.d/201.added.md @@ -0,0 +1 @@ +Expose API-407 participant feature-support declarations through backend manifest capability helpers and preserve them in rendered backend-manifest v2 payloads. diff --git a/changelog.d/202.added.md b/changelog.d/202.added.md new file mode 100644 index 000000000..a991a10d6 --- /dev/null +++ b/changelog.d/202.added.md @@ -0,0 +1 @@ +Expose API-408 participant status, history, and reference/provenance context retrieval views through the runtime control plane and HTTP API. diff --git a/changelog.d/203.changed.md b/changelog.d/203.changed.md new file mode 100644 index 000000000..b1079b1e5 --- /dev/null +++ b/changelog.d/203.changed.md @@ -0,0 +1 @@ +Require API-411 participant outcome reports to carry at least one explicit state relationship in the published contract model and generated schema. diff --git a/changelog.d/204.changed.md b/changelog.d/204.changed.md new file mode 100644 index 000000000..a8aec51da --- /dev/null +++ b/changelog.d/204.changed.md @@ -0,0 +1 @@ +Added a runtime-snapshot conformance gate requiring participant behavior history to be tied to a compiled participant behavior binding before history is accepted. diff --git a/changelog.d/233.added.md b/changelog.d/233.added.md new file mode 100644 index 000000000..f4bb06255 --- /dev/null +++ b/changelog.d/233.added.md @@ -0,0 +1 @@ +Add negative conformance coverage and an invalid fixture for the EXP-707 experiment-capture-spec-v1 contract: a dedicated rejection test exercising the capture-requirement key-equality, window-reference resolution, capture-window time-ordering, and under-specified-window invariants, plus a schema-and-model invalid fixture for a window that declares no start, end, or trigger. diff --git a/changelog.d/234.added.md b/changelog.d/234.added.md new file mode 100644 index 000000000..25b7588c4 --- /dev/null +++ b/changelog.d/234.added.md @@ -0,0 +1 @@ +Add negative conformance coverage and invalid fixtures for the EXP-708 experiment-evidence-record-v1 contract: a dedicated rejection test exercising the content-uri-requires-checksum, non-empty source-refs, RFC 3339 captured-at, and redaction-requires-loss-disclosure invariants, plus schema-and-model invalid fixtures for a content URI without a checksum, an empty source-refs list, and a malformed captured-at timestamp. The model and published schema shipped under #88; this change adds the conformance tests of record without changing them. diff --git a/changelog.d/235.added.md b/changelog.d/235.added.md new file mode 100644 index 000000000..85bbc7ea6 --- /dev/null +++ b/changelog.d/235.added.md @@ -0,0 +1 @@ +Add negative conformance coverage and invalid fixtures for the EXP-709 experiment-derived-measure-v1 contract: a dedicated rejection test exercising the reported-requires-value, non-reported-must-not-carry-value, and RFC 3339 generated-at invariants, plus schema-and-model invalid fixtures for a reported measure without a value, a withheld measure carrying a value, and a malformed generated-at timestamp. The model and published schema shipped under #88; this change adds the conformance tests of record without changing them. diff --git a/changelog.d/238.added.md b/changelog.d/238.added.md new file mode 100644 index 000000000..e997b29a9 --- /dev/null +++ b/changelog.d/238.added.md @@ -0,0 +1 @@ +Add negative conformance coverage and invalid fixtures for the EXP-720 experiment-run-v1 canonical run provenance contract: a dedicated rejection test exercising the run-traceability claim-grounding and duplicate-reference invariants, the realized-form-disclosure substantive and processor/backend authority invariants, and the required traceability capture-spec surface, plus schema-and-model invalid fixtures for a realized-form disclosure missing a realized target, a backend-realized disclosure carrying a processor realization authority, and a run whose traceability omits capture-spec references. The model and published schema shipped under #89; this change adds the conformance tests of record without changing them. diff --git a/changelog.d/239.added.md b/changelog.d/239.added.md new file mode 100644 index 000000000..633f7ffc1 --- /dev/null +++ b/changelog.d/239.added.md @@ -0,0 +1 @@ +Add negative conformance coverage and an invalid fixture for the EXP-722 experiment-run-v1 realized-form disclosure contract: a dedicated rejection test exercising the realized-form substantive invariants (a disclosure must name a realized reference or value summary and use the matching processor/backend realization authority) and the run-level invariant that disclosure evidence refs must be listed in the run traceability evidence refs and must be duplicate-free, plus a schema-and-model invalid fixture for a processor-realized disclosure carrying a backend realization authority. The model and published schema shipped under #89; this change adds the conformance tests of record without changing them. diff --git a/changelog.d/247.added.md b/changelog.d/247.added.md new file mode 100644 index 000000000..e7e28068b --- /dev/null +++ b/changelog.d/247.added.md @@ -0,0 +1,3 @@ +### Added + +- Published SEM-214 meaning and comparability semantics for API-408 participant context views. diff --git a/changelog.d/248.added.md b/changelog.d/248.added.md new file mode 100644 index 000000000..556965668 --- /dev/null +++ b/changelog.d/248.added.md @@ -0,0 +1,3 @@ +### Added + +- Published SEM-216 boundary semantics distinguishing runtime-observable state, captured evidence, derived evaluations, analysis outputs, and audience-specific views over the existing contract families. Participant-visible context views drawing on archival `evidence_record` or `derived_measure` source layers must now declare a governed view rule and redaction policy and mediate the source through the transformation, and redacted or withheld evidence records must disclose redaction/loss at the schema boundary. diff --git a/changelog.d/249.added.md b/changelog.d/249.added.md new file mode 100644 index 000000000..6a7b11436 --- /dev/null +++ b/changelog.d/249.added.md @@ -0,0 +1 @@ +Added SEM-217 external knowledge binding effect semantics, including a typed classifier for annotation, alignment, refinement, and constraint effects over existing concept-authority and semantic-profile artifacts. diff --git a/changelog.d/334.added.md b/changelog.d/334.added.md new file mode 100644 index 000000000..b43ab5b73 --- /dev/null +++ b/changelog.d/334.added.md @@ -0,0 +1,3 @@ +### Added + +- Published SEM-224 observability plane separation semantics: a carrier-oriented plane classifier (`aces_sdl.observability_plane_semantics`) that assigns each claim-bearing observability/evidence artifact exactly one of the five named planes — scenario-native observability, authored evidence requirement, processor/backend operational observability, captured evidence, and derived analysis — by carrier role rather than by free-form strings such as `log`, `trace`, or `evidence`. The three claim-bearing experiment-core contracts (`experiment-capture-spec-v1`, `experiment-evidence-record-v1`, `experiment-derived-measure-v1`) now publish their plane as a portable `x-aces-plane` schema annotation sourced from that classifier. diff --git a/changelog.d/335.added.md b/changelog.d/335.added.md new file mode 100644 index 000000000..cf0382d73 --- /dev/null +++ b/changelog.d/335.added.md @@ -0,0 +1,2 @@ +Added SEM-225 run-level augmentation disclosures to `experiment-run-v1`, with validation for processor/backend authority, environment-visible carriers, participant-visible markings, comparability observer effects, and run-traced evidence provenance. +Refactored the SEM-225 disclosure validator into focused helper checks so the published contract validation stays maintainable. diff --git a/changelog.d/336.changed.md b/changelog.d/336.changed.md new file mode 100644 index 000000000..44f125dca --- /dev/null +++ b/changelog.d/336.changed.md @@ -0,0 +1 @@ +Documented and test-backed DSL-123 scenario-native observability reference coverage. diff --git a/changelog.d/353.changed.md b/changelog.d/353.changed.md new file mode 100644 index 000000000..ebd8cd0cd --- /dev/null +++ b/changelog.d/353.changed.md @@ -0,0 +1,2 @@ +Document the ACES asset-inventory issue-template fragment and reconcile the +methodology closeout notes for ACES #353. diff --git a/changelog.d/77.added.md b/changelog.d/77.added.md new file mode 100644 index 000000000..6c1749f56 --- /dev/null +++ b/changelog.d/77.added.md @@ -0,0 +1,2 @@ +Added the participant behavior model ADR and formal spec covering ACT-602, +ACT-603, ACT-606, ACT-607, and ACT-608. diff --git a/changelog.d/88.added.md b/changelog.d/88.added.md new file mode 100644 index 000000000..35e8355de --- /dev/null +++ b/changelog.d/88.added.md @@ -0,0 +1,2 @@ +Add the experiment evidence and measure contract boundary for EXP-707, EXP-708, EXP-709, and EXP-715. The experiment-core schema family now publishes `experiment-capture-spec-v1`, `experiment-evidence-record-v1`, and `experiment-derived-measure-v1`, with valid/invalid fixtures, semantic invariant annotations, and conformance validators that keep declarative capture intent, raw evidence, and derived measures separate. Backend manifests now support an optional `capabilities.observation` block with governed capture-kind, channel-kind, and sealing-mode vocabularies, and conformance rejects observation claims that lack the published evidence contracts. ADR-064 and the formal experiment-core spec record the boundary; runtime capture, storage, APIs, schedulers, and statistical engines remain out of scope for this contract-only change. +Refactor the reported-value invariant helper and observation capability gap reporting so SonarCloud quality gates remain clean for the published contract surface. diff --git a/changelog.d/89.added.md b/changelog.d/89.added.md new file mode 100644 index 000000000..acb43f301 --- /dev/null +++ b/changelog.d/89.added.md @@ -0,0 +1 @@ +Extend `experiment-run-v1` as the canonical run provenance record for EXP-710, EXP-720, and EXP-722. Run records now include required traceability links from capture specs to raw evidence, derived measures, and claims, plus realized-form disclosures for processor/backend/operator choices that were not fully authored in the scenario or task. ADR-065 and the formal experiment-core spec document the boundary; generated schemas, fixtures, and contract tests enforce the new provenance surface. Reference de-duplication now also tolerates constrained experiment reference models that omit optional digest, path, or subject fields. diff --git a/contracts/README.md b/contracts/README.md index 17d905274..eef1dac7d 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -54,11 +54,23 @@ It includes: - live runtime/control-plane contracts - experiment, evidence, and provenance artifact boundaries -The first published experiment-core contract family includes task, run, -apparatus-context, and study/collection schemas under -`contracts/schemas/experiment-core/`. These contracts are archival design -artifacts for scientific experiment records; they do not add runtime execution, -storage, or API behavior by themselves. +The control-plane `participant-context-view-v1` contract includes the SEM-214 +meaning and comparability envelope for derived operational context views: +participant-local scope, audience scope, observation point, governed source +layers, transformation rule, evidence/provenance basis, semantic limitations, +and explicit comparability disclosure. + +The published experiment-core contract family includes task, run, +apparatus-context, study/collection, capture specification, raw evidence record, +and derived measure schemas under `contracts/schemas/experiment-core/`. These +contracts are archival design artifacts for scientific experiment records; they +do not add runtime execution, capture, storage, scheduling, statistical engines, +or API behavior by themselves. + +`experiment-run-v1` is the canonical run provenance record. It carries the +task/run/apparatus context, result and evidence pointers, traceability links to +capture specs, evidence records, derived measures, and claims, plus +realized-form disclosures for underspecified concerns resolved during a run. Within experiment-core contracts, identifier-bearing collections that require uniqueness are object maps keyed by that identifier. This keeps uniqueness diff --git a/contracts/concept-authority/controlled-vocabularies-v1.json b/contracts/concept-authority/controlled-vocabularies-v1.json index 496670402..0ef7ce05b 100644 --- a/contracts/concept-authority/controlled-vocabularies-v1.json +++ b/contracts/concept-authority/controlled-vocabularies-v1.json @@ -609,6 +609,106 @@ } } }, + "observation-capture-kinds": { + "title": "Observation Capture Kinds", + "description": "Backend-supported evidence capture categories for experiment observation capability declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_capture_kinds" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "artifact": { + "title": "Artifact", + "description": "Backend can collect named artifact references as experiment evidence." + }, + "log": { + "title": "Log", + "description": "Backend can collect log records as experiment evidence." + }, + "observation": { + "title": "Observation", + "description": "Backend can collect direct observed facts or annotations as experiment evidence." + }, + "packet-capture": { + "title": "Packet Capture", + "description": "Backend can collect packet-capture outputs as experiment evidence." + }, + "telemetry": { + "title": "Telemetry", + "description": "Backend can collect telemetry streams or snapshots as experiment evidence." + }, + "trace": { + "title": "Trace", + "description": "Backend can collect execution traces as experiment evidence." + } + } + }, + "observation-channel-kinds": { + "title": "Observation Channel Kinds", + "description": "Backend-supported source channel categories for experiment observation capability declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_channel_kinds" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "backend-log": { + "title": "Backend Log", + "description": "Backend can capture evidence from backend-owned logs." + }, + "evaluation-history": { + "title": "Evaluation History", + "description": "Backend can capture evidence from evaluation history surfaces." + }, + "file-artifact": { + "title": "File Artifact", + "description": "Backend can capture evidence from file-backed artifacts." + }, + "packet-capture": { + "title": "Packet Capture", + "description": "Backend can capture evidence from packet-capture channels." + }, + "participant-observation": { + "title": "Participant Observation", + "description": "Backend can capture evidence from participant observation surfaces." + }, + "runtime-snapshot": { + "title": "Runtime Snapshot", + "description": "Backend can capture evidence from runtime snapshot surfaces." + }, + "workflow-history": { + "title": "Workflow History", + "description": "Backend can capture evidence from workflow history surfaces." + } + } + }, + "observation-sealing-modes": { + "title": "Observation Sealing Modes", + "description": "Backend-supported integrity sealing modes for experiment observation evidence.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_sealing_modes" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "digest": { + "title": "Digest", + "description": "Backend can bind evidence to a checksum or digest." + }, + "immutable-store": { + "title": "Immutable Store", + "description": "Backend can store evidence in an append-only or immutable store." + }, + "signed-attestation": { + "title": "Signed Attestation", + "description": "Backend can attach a signed attestation to evidence collection." + } + } + }, "participant-runtime-feature-support-levels": { "title": "Participant Runtime Feature Support Levels", "description": "Closed ADR-054 guarantee-strength scale for per-feature participant runtime support declarations.", 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 c7e6eb19b..71c184525 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 @@ -22,6 +22,8 @@ "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" ], "compatibility": { 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 3686c00cb..7f47efc16 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 @@ -22,6 +22,8 @@ "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" ], "compatibility": { 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 059afe0e5..cb723bd8b 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 @@ -22,6 +22,8 @@ "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" ], "compatibility": { 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 3ded4bbfc..d44e93375 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 @@ -22,6 +22,8 @@ "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" ], "compatibility": { 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 3da8eb510..8918c8417 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 @@ -22,6 +22,8 @@ "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" ], "compatibility": { 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 86912d74a..b7b7412ea 100644 --- a/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json +++ b/contracts/fixtures/backend-manifest/backend-manifest-v2/valid/stub.json @@ -22,10 +22,17 @@ "participant-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", - "participant-outcome-report-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" ], "compatibility": { - "processors": ["aces-reference-processor"] + "processors": [ + "aces-reference-processor" + ] }, "realization_support": [ { @@ -39,7 +46,9 @@ "workflow-feature", "workflow-state-predicate" ], - "supported_exact_requirement_kinds": ["declared-capability-match"], + "supported_exact_requirement_kinds": [ + "declared-capability-match" + ], "disclosure_kinds": [ "backend-manifest-v2", "operation-status-v1", @@ -49,24 +58,84 @@ } ], "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.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" + }, + { + "scope": "capabilities.observation.supported_sealing_modes", + "family": "provenance-and-evidence" + } ], "constraints": {}, "capabilities": { "provisioner": { "name": "stub-provisioner", - "supported_node_types": ["switch", "vm"], - "supported_os_families": ["freebsd", "linux", "macos", "other", "windows"], - "supported_content_types": ["dataset", "directory", "file"], - "supported_account_features": ["auth_method", "disabled", "groups", "home", "mail", "shell", "spn"], + "supported_node_types": [ + "switch", + "vm" + ], + "supported_os_families": [ + "freebsd", + "linux", + "macos", + "other", + "windows" + ], + "supported_content_types": [ + "dataset", + "directory", + "file" + ], + "supported_account_features": [ + "auth_method", + "disabled", + "groups", + "home", + "mail", + "shell", + "spn" + ], "max_total_nodes": null, "supports_acls": true, "supports_accounts": true, @@ -74,7 +143,13 @@ }, "orchestrator": { "name": "stub-orchestrator", - "supported_sections": ["events", "injects", "scripts", "stories", "workflows"], + "supported_sections": [ + "events", + "injects", + "scripts", + "stories", + "workflows" + ], "supports_workflows": true, "supports_condition_refs": true, "supports_inject_bindings": true, @@ -89,19 +164,34 @@ "switch", "timeouts" ], - "supported_workflow_state_predicates": ["attempt-counts", "outcome-matching"], + "supported_workflow_state_predicates": [ + "attempt-counts", + "outcome-matching" + ], "constraints": {} }, "evaluator": { "name": "stub-evaluator", - "supported_sections": ["conditions", "evaluations", "goals", "metrics", "objectives", "tlos"], + "supported_sections": [ + "conditions", + "evaluations", + "goals", + "metrics", + "objectives", + "tlos" + ], "supports_scoring": true, "supports_objectives": true, "constraints": {} }, "participant_runtime": { "name": "stub-participant-runtime", - "supported_participant_roles": ["blue", "green", "red", "white"], + "supported_participant_roles": [ + "blue", + "green", + "red", + "white" + ], "supported_behavior_features": [ "action_contracts", "attribution_support", @@ -114,9 +204,49 @@ "state_transitions", "temporal_contracts" ], - "supported_interaction_features": ["contention", "coordination", "interference", "shared_state_change"], + "supported_interaction_features": [ + "contention", + "coordination", + "interference", + "shared_state_change" + ], "feature_support": [], "constraints": {} + }, + "observation": { + "name": "stub-observation", + "supported_capture_kinds": [ + "artifact", + "log", + "observation", + "telemetry", + "trace" + ], + "supported_channel_kinds": [ + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history" + ], + "supported_evidence_contracts": [ + "experiment-capture-spec-v1", + "experiment-derived-measure-v1", + "experiment-evidence-record-v1" + ], + "supported_media_types": [ + "application/json", + "text/plain" + ], + "supported_sealing_modes": [ + "digest", + "immutable-store" + ], + "supports_redaction": true, + "supports_loss_disclosure": true, + "supports_chain_of_custody": false, + "constraints": {} } } } 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 496670402..0ef7ce05b 100644 --- a/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json +++ b/contracts/fixtures/concept-authority/controlled-vocabularies-v1/valid/reference.json @@ -609,6 +609,106 @@ } } }, + "observation-capture-kinds": { + "title": "Observation Capture Kinds", + "description": "Backend-supported evidence capture categories for experiment observation capability declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_capture_kinds" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "artifact": { + "title": "Artifact", + "description": "Backend can collect named artifact references as experiment evidence." + }, + "log": { + "title": "Log", + "description": "Backend can collect log records as experiment evidence." + }, + "observation": { + "title": "Observation", + "description": "Backend can collect direct observed facts or annotations as experiment evidence." + }, + "packet-capture": { + "title": "Packet Capture", + "description": "Backend can collect packet-capture outputs as experiment evidence." + }, + "telemetry": { + "title": "Telemetry", + "description": "Backend can collect telemetry streams or snapshots as experiment evidence." + }, + "trace": { + "title": "Trace", + "description": "Backend can collect execution traces as experiment evidence." + } + } + }, + "observation-channel-kinds": { + "title": "Observation Channel Kinds", + "description": "Backend-supported source channel categories for experiment observation capability declarations.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_channel_kinds" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "backend-log": { + "title": "Backend Log", + "description": "Backend can capture evidence from backend-owned logs." + }, + "evaluation-history": { + "title": "Evaluation History", + "description": "Backend can capture evidence from evaluation history surfaces." + }, + "file-artifact": { + "title": "File Artifact", + "description": "Backend can capture evidence from file-backed artifacts." + }, + "packet-capture": { + "title": "Packet Capture", + "description": "Backend can capture evidence from packet-capture channels." + }, + "participant-observation": { + "title": "Participant Observation", + "description": "Backend can capture evidence from participant observation surfaces." + }, + "runtime-snapshot": { + "title": "Runtime Snapshot", + "description": "Backend can capture evidence from runtime snapshot surfaces." + }, + "workflow-history": { + "title": "Workflow History", + "description": "Backend can capture evidence from workflow history surfaces." + } + } + }, + "observation-sealing-modes": { + "title": "Observation Sealing Modes", + "description": "Backend-supported integrity sealing modes for experiment observation evidence.", + "kind": "vocabulary", + "governed_scopes": [ + "capabilities.observation.supported_sealing_modes" + ], + "extension_policy": "governed-extension", + "extension_pattern": "^x-[a-z0-9]+(?:-[a-z0-9]+)*:[a-z0-9]+(?:-[a-z0-9]+)*$", + "terms": { + "digest": { + "title": "Digest", + "description": "Backend can bind evidence to a checksum or digest." + }, + "immutable-store": { + "title": "Immutable Store", + "description": "Backend can store evidence in an append-only or immutable store." + }, + "signed-attestation": { + "title": "Signed Attestation", + "description": "Backend can attach a signed attestation to evidence collection." + } + } + }, "participant-runtime-feature-support-levels": { "title": "Participant Runtime Feature Support Levels", "description": "Closed ADR-054 guarantee-strength scale for per-feature participant runtime support declarations.", diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/audience-neutral-scope.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/audience-neutral-scope.json new file mode 100644 index 000000000..c8745ac6e --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/audience-neutral-scope.json @@ -0,0 +1,62 @@ +{ + "view_id": "views.context.participants.blue.rl.network-posture.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "audience_neutral", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "evidence.context.blue.network-posture.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/empty-derived-from-refs.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/empty-derived-from-refs.json index 59c7ed320..1a3ebe643 100644 --- a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/empty-derived-from-refs.json +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/empty-derived-from-refs.json @@ -3,8 +3,53 @@ "participant_address": "participants.blue.rl", "episode_id": "ep-blue-002", "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", "derived_from_refs": [], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], "derivation_basis_ref": "rules.context.network-posture.v1", "payload_ref": "evidence.context.blue.network-posture.tick42", "visibility_projection_ref": "projections.blue.context.v1", diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/future-state-source-layer.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/future-state-source-layer.json new file mode 100644 index 000000000..083f67314 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/future-state-source-layer.json @@ -0,0 +1,62 @@ +{ + "view_id": "views.context.participants.blue.rl.network-posture.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "future_state", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "evidence.context.blue.network-posture.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/global-runtime-state-source.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/global-runtime-state-source.json new file mode 100644 index 000000000..3ffd6e130 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/global-runtime-state-source.json @@ -0,0 +1,62 @@ +{ + "view_id": "views.context.participants.blue.rl.network-posture.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "global_runtime_state", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "evidence.context.blue.network-posture.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-bounded-staleness-basis.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-bounded-staleness-basis.json new file mode 100644 index 000000000..5e798e3be --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-bounded-staleness-basis.json @@ -0,0 +1,62 @@ +{ + "view_id": "views.context.participants.blue.rl.network-posture.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "bounded_staleness", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "evidence.context.blue.network-posture.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-weakened-comparability-disclosure.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-weakened-comparability-disclosure.json new file mode 100644 index 000000000..d99e41725 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/missing-weakened-comparability-disclosure.json @@ -0,0 +1,62 @@ +{ + "view_id": "views.context.participants.blue.rl.network-posture.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:21:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot", + "input_source_ids": [ + "snapshot-tick42" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_with_disclosed_weakening", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "evidence.context.blue.network-posture.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-archival-evidence-participant-visible.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-archival-evidence-participant-visible.json new file mode 100644 index 000000000..afb8f1f39 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-archival-evidence-participant-visible.json @@ -0,0 +1,76 @@ +{ + "view_id": "views.context.participants.blue.rl.evidence-leak.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:24:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.evidence-summary.v1", + "meaning_ref": "semantics.context.evidence-summary.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42", + "evidence.archive.blue.0001" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + }, + { + "source_id": "evidence-archive-0001", + "source_layer": "evidence_record", + "ref": "evidence.archive.blue.0001", + "temporal_relation": "historical_replay", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "runs.run-778.evidence-manifest" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.evidence-summary.v1", + "description": "Surface an archived evidence record to the participant without a governed view rule", + "input_source_ids": [ + "snapshot-tick42", + "evidence-archive-0001" + ], + "output_semantics_ref": "semantics.context.evidence-summary.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.evidence-summary.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule, visibility projection, and redaction policy" + ] + }, + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "runs.run-778.evidence-manifest" + ], + "semantic_limitations": [ + "The payload reference exposes the archived evidence record directly to the participant" + ], + "payload_ref": "evidence.archive.blue.0001", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-evidence-summary.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-backend-observability-as-observation.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-backend-observability-as-observation.json new file mode 100644 index 000000000..1a3bc001d --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-backend-observability-as-observation.json @@ -0,0 +1,77 @@ +{ + "view_id": "views.context.participants.blue.rl.backend-observability.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:24:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42", + "backend.telemetry.collector.0001" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + }, + { + "source_id": "backend-telemetry-0001", + "source_layer": "backend_observability_stream", + "ref": "backend.telemetry.collector.0001", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "backend.telemetry.collector.0001" + ], + "provenance_refs": [ + "backend.telemetry.collector.0001" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Treat raw backend observability telemetry as a portable participant observation", + "input_source_ids": [ + "snapshot-tick42", + "backend-telemetry-0001" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule and visibility projection" + ] + }, + "evidence_refs": [ + "backend.telemetry.collector.0001" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived from backend-native observability telemetry" + ], + "derivation_basis_ref": "rules.context.network-posture.v1", + "payload_ref": "backend.telemetry.collector.0001", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-observation.v1" +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-hidden-adjudication-in-evaluation-output.json b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-hidden-adjudication-in-evaluation-output.json new file mode 100644 index 000000000..10e7b0229 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/invalid/sem216-hidden-adjudication-in-evaluation-output.json @@ -0,0 +1,76 @@ +{ + "view_id": "views.context.participants.blue.rl.adjudication-leak.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:24:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.evaluation-output.v1", + "meaning_ref": "semantics.context.evaluation-output.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42", + "measures.adjudication.hidden.0001" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + }, + { + "source_id": "adjudication-measure-0001", + "source_layer": "derived_measure", + "ref": "measures.adjudication.hidden.0001", + "temporal_relation": "historical_replay", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "evidence.adjudication.hidden.0001" + ], + "provenance_refs": [ + "runs.run-778.evaluation-manifest" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.evaluation-output.v1", + "description": "Surface a hidden adjudication derived measure into participant-visible evaluation output without a redaction policy", + "input_source_ids": [ + "snapshot-tick42", + "adjudication-measure-0001" + ], + "output_semantics_ref": "semantics.context.evaluation-output.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.evaluation-output.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule, visibility projection, and redaction policy" + ] + }, + "evidence_refs": [ + "evidence.adjudication.hidden.0001" + ], + "provenance_refs": [ + "runs.run-778.evaluation-manifest" + ], + "semantic_limitations": [ + "The payload reference surfaces a hidden adjudication measure to the participant" + ], + "derivation_basis_ref": "rules.context.evaluation-output.v1", + "payload_ref": "measures.adjudication.hidden.0001", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ] +} diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/valid/network-posture-context.json b/contracts/fixtures/control-plane/participant-context-view-v1/valid/network-posture-context.json index 0f8d526f1..98636df7e 100644 --- a/contracts/fixtures/control-plane/participant-context-view-v1/valid/network-posture-context.json +++ b/contracts/fixtures/control-plane/participant-context-view-v1/valid/network-posture-context.json @@ -5,10 +5,70 @@ "generated_at": "2026-05-26T10:21:00Z", "source_snapshot_ref": "snapshots.run-778.tick42", "view_ref": "views.context.network-posture.v1", + "meaning_ref": "semantics.context.network-posture.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", "derived_from_refs": [ "snapshots.run-778.tick42", "obs-blue-43" ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + }, + { + "source_id": "observation-blue-43", + "source_layer": "participant_observation", + "ref": "obs-blue-43", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.network-posture.v1", + "description": "Derive a participant-visible network-posture view from the declared snapshot and terminal observation", + "input_source_ids": [ + "snapshot-tick42", + "observation-blue-43" + ], + "output_semantics_ref": "semantics.context.network-posture.v1" + }, + "comparability": { + "comparability_class": "portable_with_disclosed_weakening", + "comparison_basis_ref": "comparability.network-posture.same-rule-and-projection.v1", + "backend_disclosure_refs": [ + "backend-disclosures.packet-loss.v1" + ], + "limitations": [ + "Comparable only for backends that use the same view rule, visibility projection, and disclosed packet-loss weakening" + ] + }, + "evidence_refs": [ + "obs-blue-43" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ], + "semantic_limitations": [ + "The payload reference is derived context and does not expose backend-private global state" + ], "derivation_basis_ref": "rules.context.network-posture.v1", "payload_ref": "evidence.context.blue.network-posture.tick42", "visibility_projection_ref": "projections.blue.context.v1", diff --git a/contracts/fixtures/control-plane/participant-context-view-v1/valid/sem216-mediated-evidence-view.json b/contracts/fixtures/control-plane/participant-context-view-v1/valid/sem216-mediated-evidence-view.json new file mode 100644 index 000000000..7b377f916 --- /dev/null +++ b/contracts/fixtures/control-plane/participant-context-view-v1/valid/sem216-mediated-evidence-view.json @@ -0,0 +1,77 @@ +{ + "view_id": "views.context.participants.blue.rl.evidence-summary.0001", + "participant_address": "participants.blue.rl", + "episode_id": "ep-blue-002", + "generated_at": "2026-05-26T10:24:00Z", + "source_snapshot_ref": "snapshots.run-778.tick42", + "view_ref": "views.context.evidence-summary.v1", + "meaning_ref": "semantics.context.evidence-summary.v1", + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": "episode-step:tick42", + "derived_from_refs": [ + "snapshots.run-778.tick42", + "evidence.archive.blue.0001" + ], + "source_layers": [ + { + "source_id": "snapshot-tick42", + "source_layer": "source_snapshot", + "ref": "snapshots.run-778.tick42", + "temporal_relation": "same_observation_point", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "snapshots.run-778.tick42" + ] + }, + { + "source_id": "evidence-archive-0001", + "source_layer": "evidence_record", + "ref": "evidence.archive.blue.0001", + "temporal_relation": "historical_replay", + "observation_point": "episode-step:tick42", + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "runs.run-778.evidence-manifest" + ] + } + ], + "transformation": { + "transformation_rule_ref": "rules.context.evidence-summary.v1", + "description": "Project an archived evidence record into a participant-visible summary through the governed view rule", + "input_source_ids": [ + "snapshot-tick42", + "evidence-archive-0001" + ], + "output_semantics_ref": "semantics.context.evidence-summary.v1" + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": "comparability.evidence-summary.same-rule-and-projection.v1", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only for the same view rule, visibility projection, and redaction policy" + ] + }, + "evidence_refs": [ + "evidence.archive.blue.0001" + ], + "provenance_refs": [ + "runs.run-778.evidence-manifest" + ], + "semantic_limitations": [ + "The payload reference is a redacted derived summary and does not expose the raw archived evidence payload" + ], + "derivation_basis_ref": "rules.context.evidence-summary.v1", + "payload_ref": "views.context.blue.evidence-summary.tick42", + "visibility_projection_ref": "projections.blue.context.v1", + "marking_definition_refs": [ + "markings.participant_visible.v1" + ], + "redaction_policy_ref": "redaction.blue-evidence-summary.v1" +} diff --git a/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/missing-capture-requirements.json b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/missing-capture-requirements.json new file mode 100644 index 000000000..01428f1e7 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/missing-capture-requirements.json @@ -0,0 +1,22 @@ +{ + "schema_version": "experiment-capture-spec/v1", + "capture_spec_id": "capture-techvault-evidence-v1", + "spec_version": "1.0.0", + "title": "TechVault evidence capture specification", + "description": "Invalid capture spec without any capture requirements.", + "scope_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "capture_windows": [ + { + "window_id": "run-window", + "window_kind": "run", + "starts_at": "2026-05-26T00:10:00Z" + } + ], + "capture_requirements": {} +} diff --git a/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/under-specified-capture-window.json b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/under-specified-capture-window.json new file mode 100644 index 000000000..7266c1197 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/invalid/under-specified-capture-window.json @@ -0,0 +1,49 @@ +{ + "schema_version": "experiment-capture-spec/v1", + "capture_spec_id": "capture-techvault-evidence-v1", + "spec_version": "1.0.0", + "title": "TechVault evidence capture specification", + "description": "Invalid capture spec whose capture window declares no start, end, or trigger.", + "scope_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "capture_windows": [ + { + "window_id": "run-window", + "window_kind": "run", + "description": "Window with no start, end, or trigger declared." + } + ], + "capture_requirements": { + "network-trace": { + "requirement_id": "network-trace", + "title": "Network trace evidence", + "capture_kind": "trace", + "capture_scope": "network", + "channel_ref": { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + }, + "window_refs": [ + "run-window" + ], + "expected_media_types": [ + "application/json" + ], + "required_artifact_roles": [ + "observation" + ], + "sensitivity": "internal", + "integrity_requirements": [ + "sha256-digest" + ], + "retention_policy": "Retain raw evidence for the experiment review window.", + "loss_disclosure_required": true + } + } +} diff --git a/contracts/fixtures/experiment-core/experiment-capture-spec-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/valid/reference.json new file mode 100644 index 000000000..5158fb034 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-capture-spec-v1/valid/reference.json @@ -0,0 +1,78 @@ +{ + "schema_version": "experiment-capture-spec/v1", + "capture_spec_id": "capture-techvault-evidence-v1", + "spec_version": "1.0.0", + "title": "TechVault evidence capture specification", + "description": "Declarative capture requirements for the TechVault red-team evaluation task.", + "scope_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + } + ], + "capture_windows": [ + { + "window_id": "run-window", + "window_kind": "run", + "starts_at": "2026-05-26T00:10:00Z", + "ends_at": "2026-05-26T00:40:00Z", + "description": "Capture evidence for the active run interval." + } + ], + "capture_requirements": { + "network-trace": { + "requirement_id": "network-trace", + "title": "Network trace evidence", + "capture_kind": "trace", + "capture_scope": "network", + "channel_ref": { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + }, + "window_refs": [ + "run-window" + ], + "expected_media_types": [ + "application/json" + ], + "required_artifact_roles": [ + "observation" + ], + "sensitivity": "internal", + "integrity_requirements": [ + "sha256-digest" + ], + "retention_policy": "Retain raw evidence for the experiment review window.", + "loss_disclosure_required": true + } + }, + "validity_notes": [ + { + "category": "apparatus", + "note": "The capture specification defines evidence intent only; it does not imply runtime collection." + } + ], + "artifact_refs": [ + { + "artifact_id": "capture-spec-review", + "role": "protocol", + "media_type": "text/markdown", + "uri": "docs/protocols/techvault-capture-spec.md", + "checksum": { + "algorithm": "sha256", + "value": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "size_bytes": 2048, + "created_at": "2026-05-26T00:00:00Z", + "source": "experiment evidence design review", + "sensitivity": "internal" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json new file mode 100644 index 000000000..a378ddb93 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/invalid-generated-at.json @@ -0,0 +1,45 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-techvault-foothold-001", + "measure_version": "1.0.0", + "measure_kind": "metric", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "foothold-achieved", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "foothold-event-detector", + "method_version": "1.0.0", + "name": "Foothold event detector", + "description": "Derives the foothold-achieved boolean from raw evaluation history evidence.", + "parameters": [ + { + "name": "success-event", + "value": "foothold-achieved", + "value_kind": "analysis" + } + ] + }, + "source_evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "generated_at": "not-a-timestamp", + "value_status": "reported", + "value": true, + "uncertainty": "Single-run boolean outcome; no across-run uncertainty is claimed.", + "limitations": [ + "The measure depends on the evaluation history event taxonomy." + ], + "provenance_refs": [ + { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/missing-source-evidence.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/missing-source-evidence.json new file mode 100644 index 000000000..5b8cb7c0d --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/missing-source-evidence.json @@ -0,0 +1,20 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-techvault-foothold-001", + "measure_version": "1.0.0", + "measure_kind": "metric", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "foothold-achieved", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "foothold-event-detector", + "method_version": "1.0.0", + "name": "Foothold event detector" + }, + "source_evidence_refs": [], + "generated_at": "2026-05-26T00:41:00Z", + "value_status": "reported", + "value": true +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json new file mode 100644 index 000000000..348e6db7a --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/non-reported-with-value.json @@ -0,0 +1,45 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-techvault-foothold-001", + "measure_version": "1.0.0", + "measure_kind": "metric", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "foothold-achieved", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "foothold-event-detector", + "method_version": "1.0.0", + "name": "Foothold event detector", + "description": "Derives the foothold-achieved boolean from raw evaluation history evidence.", + "parameters": [ + { + "name": "success-event", + "value": "foothold-achieved", + "value_kind": "analysis" + } + ] + }, + "source_evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "generated_at": "2026-05-26T00:41:00Z", + "value_status": "withheld", + "value": true, + "uncertainty": "Single-run boolean outcome; no across-run uncertainty is claimed.", + "limitations": [ + "The measure depends on the evaluation history event taxonomy." + ], + "provenance_refs": [ + { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json new file mode 100644 index 000000000..102288713 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/invalid/reported-without-value.json @@ -0,0 +1,44 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-techvault-foothold-001", + "measure_version": "1.0.0", + "measure_kind": "metric", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "foothold-achieved", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "foothold-event-detector", + "method_version": "1.0.0", + "name": "Foothold event detector", + "description": "Derives the foothold-achieved boolean from raw evaluation history evidence.", + "parameters": [ + { + "name": "success-event", + "value": "foothold-achieved", + "value_kind": "analysis" + } + ] + }, + "source_evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "generated_at": "2026-05-26T00:41:00Z", + "value_status": "reported", + "uncertainty": "Single-run boolean outcome; no across-run uncertainty is claimed.", + "limitations": [ + "The measure depends on the evaluation history event taxonomy." + ], + "provenance_refs": [ + { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json new file mode 100644 index 000000000..81115f617 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-derived-measure-v1/valid/reference.json @@ -0,0 +1,45 @@ +{ + "schema_version": "experiment-derived-measure/v1", + "derived_measure_id": "measure-techvault-foothold-001", + "measure_version": "1.0.0", + "measure_kind": "metric", + "metric_ref": { + "ref_kind": "metric-definition", + "ref_id": "foothold-achieved", + "ref_version": "1.0.0" + }, + "method": { + "method_id": "foothold-event-detector", + "method_version": "1.0.0", + "name": "Foothold event detector", + "description": "Derives the foothold-achieved boolean from raw evaluation history evidence.", + "parameters": [ + { + "name": "success-event", + "value": "foothold-achieved", + "value_kind": "analysis" + } + ] + }, + "source_evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "generated_at": "2026-05-26T00:41:00Z", + "value_status": "reported", + "value": true, + "uncertainty": "Single-run boolean outcome; no across-run uncertainty is claimed.", + "limitations": [ + "The measure depends on the evaluation history event taxonomy." + ], + "provenance_refs": [ + { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/content-uri-without-checksum.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/content-uri-without-checksum.json new file mode 100644 index 000000000..e4f19c96b --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/content-uri-without-checksum.json @@ -0,0 +1,44 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "payload_summary": "Evaluation history export containing the observed foothold event." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/empty-source-refs.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/empty-source-refs.json new file mode 100644 index 000000000..f013cdff2 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/empty-source-refs.json @@ -0,0 +1,42 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Evaluation history export containing the observed foothold event." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/invalid-captured-at.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/invalid-captured-at.json new file mode 100644 index 000000000..7175cb8b7 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/invalid-captured-at.json @@ -0,0 +1,48 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "not-a-timestamp", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Evaluation history export containing the observed foothold event." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/missing-raw-content.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/missing-raw-content.json new file mode 100644 index 000000000..d75a1c3d9 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/missing-raw-content.json @@ -0,0 +1,29 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": {}, + "sensitivity": "internal", + "redaction_state": "none" +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-analysis-output-as-evidence.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-analysis-output-as-evidence.json new file mode 100644 index 000000000..d004e340d --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-analysis-output-as-evidence.json @@ -0,0 +1,50 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-002", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Evaluation history export containing the observed foothold event." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ], + "measure_kind": "score", + "value": 0.91 +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-withheld-without-loss-disclosure.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-withheld-without-loss-disclosure.json new file mode 100644 index 000000000..55ac15e01 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem216-withheld-without-loss-disclosure.json @@ -0,0 +1,48 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-003", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Redacted evaluation history export." + }, + "sensitivity": "redacted", + "redaction_state": "withheld", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem224-capture-record-without-requirement-ref.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem224-capture-record-without-requirement-ref.json new file mode 100644 index 000000000..eebb9c994 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/invalid/sem224-capture-record-without-requirement-ref.json @@ -0,0 +1,47 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Evaluation history export presented as authored-requirement satisfaction with no capture_requirement_ref." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/reference.json new file mode 100644 index 000000000..15d82b9c5 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-evidence-record-v1/valid/reference.json @@ -0,0 +1,48 @@ +{ + "schema_version": "experiment-evidence-record/v1", + "evidence_record_id": "evidence-techvault-network-trace-001", + "record_version": "1.0.0", + "capture_spec_ref": { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + }, + "capture_requirement_ref": "network-trace", + "run_ref": { + "ref_kind": "run", + "ref_id": "run-techvault-001", + "ref_version": "1.0.0" + }, + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "source_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "evidence_kind": "trace", + "captured_at": "2026-05-26T00:40:00Z", + "capture_window_ref": "run-window", + "raw_content": { + "content_uri": "runs/run-techvault-001/evaluation-history.json", + "content_checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "payload_summary": "Evaluation history export containing the observed foothold event." + }, + "sensitivity": "internal", + "redaction_state": "none", + "provenance_refs": [ + { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json new file mode 100644 index 000000000..c14338bf8 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-authority-mismatch.json @@ -0,0 +1,353 @@ +{ + "schema_version": "experiment-run/v1", + "run_id": "run-techvault-001", + "run_version": "1.0.0", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "scenario_snapshot_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "apparatus_context": { + "schema_version": "experiment-apparatus-context/v1", + "apparatus_context_id": "apparatus-techvault-reference", + "context_version": "1.0.0", + "declared_at": "2026-05-26T00:00:00Z", + "components": { + "processor": { + "component_kind": "processor", + "identity": { + "name": "aces-reference-processor", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "backend": { + "component_kind": "backend", + "identity": { + "name": "stub-backend", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "participant-policy": { + "component_kind": "participant-implementation", + "identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + }, + "observed": true + } + }, + "selected_manifests": [ + { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + } + ], + "compatibility_declarations": [ + { + "ref_kind": "profile", + "ref_id": "reference-stack-v1", + "ref_version": "semantic-profile/v1" + }, + { + "ref_kind": "capability", + "ref_id": "workflow-results" + }, + { + "ref_kind": "capability", + "ref_id": "evaluation-results" + } + ], + "configuration_parameters": [ + { + "name": "worker-count", + "value": 1, + "value_kind": "apparatus" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345, + "description": "Seed supplied to task randomization." + } + ], + "clocks": [ + { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock", + "synchronization": "NTP synchronized before run seal." + } + ], + "measurement_channels": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "observed_setup_evidence": [ + { + "artifact_id": "setup-attestation", + "role": "apparatus-evidence", + "media_type": "application/json", + "uri": "runs/run-techvault-001/setup.json", + "checksum": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "size_bytes": 2048, + "created_at": "2026-05-26T00:09:30Z", + "source": "stub-backend setup attestation", + "sensitivity": "internal" + } + ], + "known_limitations": [ + { + "category": "apparatus", + "note": "The stub backend is a conformance apparatus, not a fidelity claim." + } + ] + }, + "participant_implementation_provenance": { + "schema_version": "participant-implementation-provenance/v1", + "run_id": "run-techvault-001", + "participant_implementations": [ + { + "participant_address": "participants.red", + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": "contracts/fixtures/participant-implementation-manifest/reference.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "selected_decision_surface_mode": "policy-directed", + "participant_contract_versions": [ + "participant-episode-state-envelope-v1", + "participant-behavior-history-event-stream-v1" + ], + "exposure_policy": { + "policy_id": "red-agent-policy", + "policy_version": "1.0.0", + "policy_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "exposure_policy_kinds": [ + "task-statement", + "tool-affordance" + ], + "disclosed_refs": [ + "scenario.tasks.red" + ], + "withheld_refs": [ + "scenario.hidden.answer-key" + ], + "tool_affordance_refs": [ + "tool.shell" + ], + "visibility_scope_refs": [ + "participants.red.visible" + ] + } + } + ], + "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "backend_manifest_ref": "backend-manifest-v2:stub-backend" + }, + "parameter_set": [ + { + "name": "difficulty", + "value": "standard", + "value_kind": "protocol" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345 + } + ], + "started_at": "2026-05-26T00:10:00Z", + "ended_at": "2026-05-26T00:40:00Z", + "clock_context": { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock" + }, + "run_status": "completed", + "outcome_status": "succeeded", + "traceability": { + "capture_spec_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + } + ], + "evidence_record_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "derived_measure_refs": [ + { + "ref_kind": "derived-measure", + "ref_id": "measure-techvault-foothold-001", + "ref_version": "1.0.0" + } + ], + "claim_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ], + "notes": [ + "The run provenance chain binds the declared capture spec, raw evidence record, derived foothold measure, and result claim." + ] + }, + "realized_form_disclosures": [ + { + "concern_id": "composed-scenario-snapshot", + "concern_kind": "scenario-module", + "basis": "backend-realized", + "realized_by_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + }, + "authored_ref": { + "ref_kind": "scenario", + "ref_id": "scenario-techvault" + }, + "realized_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "disclosure": "The processor resolved the authored TechVault scenario into the canonical composed scenario snapshot used by this run.", + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ] + } + ], + "evidence_artifacts": [ + { + "artifact_id": "evaluation-history", + "role": "observation", + "media_type": "application/json", + "uri": "runs/run-techvault-001/evaluation-history.json", + "checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "size_bytes": 4096, + "created_at": "2026-05-26T00:40:00Z", + "source": "stub-backend evaluator history export", + "satisfies_refs": [ + { + "ref_kind": "evidence", + "ref_id": "auth-log-evidence" + } + ], + "sensitivity": "internal" + } + ], + "result_summaries": { + "foothold-achieved-result": { + "metric_id": "foothold-achieved", + "value": true, + "value_status": "reported", + "evidence_refs": [ + { + "ref_kind": "evidence", + "ref_id": "evaluation-history" + } + ] + } + }, + "used_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "generated_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json new file mode 100644 index 000000000..6f48cbeb1 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-missing-target.json @@ -0,0 +1,349 @@ +{ + "schema_version": "experiment-run/v1", + "run_id": "run-techvault-001", + "run_version": "1.0.0", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "scenario_snapshot_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "apparatus_context": { + "schema_version": "experiment-apparatus-context/v1", + "apparatus_context_id": "apparatus-techvault-reference", + "context_version": "1.0.0", + "declared_at": "2026-05-26T00:00:00Z", + "components": { + "processor": { + "component_kind": "processor", + "identity": { + "name": "aces-reference-processor", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "backend": { + "component_kind": "backend", + "identity": { + "name": "stub-backend", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "participant-policy": { + "component_kind": "participant-implementation", + "identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + }, + "observed": true + } + }, + "selected_manifests": [ + { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + } + ], + "compatibility_declarations": [ + { + "ref_kind": "profile", + "ref_id": "reference-stack-v1", + "ref_version": "semantic-profile/v1" + }, + { + "ref_kind": "capability", + "ref_id": "workflow-results" + }, + { + "ref_kind": "capability", + "ref_id": "evaluation-results" + } + ], + "configuration_parameters": [ + { + "name": "worker-count", + "value": 1, + "value_kind": "apparatus" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345, + "description": "Seed supplied to task randomization." + } + ], + "clocks": [ + { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock", + "synchronization": "NTP synchronized before run seal." + } + ], + "measurement_channels": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "observed_setup_evidence": [ + { + "artifact_id": "setup-attestation", + "role": "apparatus-evidence", + "media_type": "application/json", + "uri": "runs/run-techvault-001/setup.json", + "checksum": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "size_bytes": 2048, + "created_at": "2026-05-26T00:09:30Z", + "source": "stub-backend setup attestation", + "sensitivity": "internal" + } + ], + "known_limitations": [ + { + "category": "apparatus", + "note": "The stub backend is a conformance apparatus, not a fidelity claim." + } + ] + }, + "participant_implementation_provenance": { + "schema_version": "participant-implementation-provenance/v1", + "run_id": "run-techvault-001", + "participant_implementations": [ + { + "participant_address": "participants.red", + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": "contracts/fixtures/participant-implementation-manifest/reference.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "selected_decision_surface_mode": "policy-directed", + "participant_contract_versions": [ + "participant-episode-state-envelope-v1", + "participant-behavior-history-event-stream-v1" + ], + "exposure_policy": { + "policy_id": "red-agent-policy", + "policy_version": "1.0.0", + "policy_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "exposure_policy_kinds": [ + "task-statement", + "tool-affordance" + ], + "disclosed_refs": [ + "scenario.tasks.red" + ], + "withheld_refs": [ + "scenario.hidden.answer-key" + ], + "tool_affordance_refs": [ + "tool.shell" + ], + "visibility_scope_refs": [ + "participants.red.visible" + ] + } + } + ], + "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "backend_manifest_ref": "backend-manifest-v2:stub-backend" + }, + "parameter_set": [ + { + "name": "difficulty", + "value": "standard", + "value_kind": "protocol" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345 + } + ], + "started_at": "2026-05-26T00:10:00Z", + "ended_at": "2026-05-26T00:40:00Z", + "clock_context": { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock" + }, + "run_status": "completed", + "outcome_status": "succeeded", + "traceability": { + "capture_spec_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + } + ], + "evidence_record_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "derived_measure_refs": [ + { + "ref_kind": "derived-measure", + "ref_id": "measure-techvault-foothold-001", + "ref_version": "1.0.0" + } + ], + "claim_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ], + "notes": [ + "The run provenance chain binds the declared capture spec, raw evidence record, derived foothold measure, and result claim." + ] + }, + "realized_form_disclosures": [ + { + "concern_id": "composed-scenario-snapshot", + "concern_kind": "scenario-module", + "basis": "processor-realized", + "realized_by_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + }, + "authored_ref": { + "ref_kind": "scenario", + "ref_id": "scenario-techvault" + }, + "realized_ref": null, + "disclosure": "The processor resolved the authored TechVault scenario into the canonical composed scenario snapshot used by this run.", + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "realized_value_summary": null + } + ], + "evidence_artifacts": [ + { + "artifact_id": "evaluation-history", + "role": "observation", + "media_type": "application/json", + "uri": "runs/run-techvault-001/evaluation-history.json", + "checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "size_bytes": 4096, + "created_at": "2026-05-26T00:40:00Z", + "source": "stub-backend evaluator history export", + "satisfies_refs": [ + { + "ref_kind": "evidence", + "ref_id": "auth-log-evidence" + } + ], + "sensitivity": "internal" + } + ], + "result_summaries": { + "foothold-achieved-result": { + "metric_id": "foothold-achieved", + "value": true, + "value_status": "reported", + "evidence_refs": [ + { + "ref_kind": "evidence", + "ref_id": "evaluation-history" + } + ] + } + }, + "used_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "generated_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json new file mode 100644 index 000000000..0fecb50c9 --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/realized-form-processor-authority-mismatch.json @@ -0,0 +1,353 @@ +{ + "schema_version": "experiment-run/v1", + "run_id": "run-techvault-001", + "run_version": "1.0.0", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "scenario_snapshot_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "apparatus_context": { + "schema_version": "experiment-apparatus-context/v1", + "apparatus_context_id": "apparatus-techvault-reference", + "context_version": "1.0.0", + "declared_at": "2026-05-26T00:00:00Z", + "components": { + "processor": { + "component_kind": "processor", + "identity": { + "name": "aces-reference-processor", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "backend": { + "component_kind": "backend", + "identity": { + "name": "stub-backend", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "participant-policy": { + "component_kind": "participant-implementation", + "identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + }, + "observed": true + } + }, + "selected_manifests": [ + { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + } + ], + "compatibility_declarations": [ + { + "ref_kind": "profile", + "ref_id": "reference-stack-v1", + "ref_version": "semantic-profile/v1" + }, + { + "ref_kind": "capability", + "ref_id": "workflow-results" + }, + { + "ref_kind": "capability", + "ref_id": "evaluation-results" + } + ], + "configuration_parameters": [ + { + "name": "worker-count", + "value": 1, + "value_kind": "apparatus" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345, + "description": "Seed supplied to task randomization." + } + ], + "clocks": [ + { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock", + "synchronization": "NTP synchronized before run seal." + } + ], + "measurement_channels": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "observed_setup_evidence": [ + { + "artifact_id": "setup-attestation", + "role": "apparatus-evidence", + "media_type": "application/json", + "uri": "runs/run-techvault-001/setup.json", + "checksum": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "size_bytes": 2048, + "created_at": "2026-05-26T00:09:30Z", + "source": "stub-backend setup attestation", + "sensitivity": "internal" + } + ], + "known_limitations": [ + { + "category": "apparatus", + "note": "The stub backend is a conformance apparatus, not a fidelity claim." + } + ] + }, + "participant_implementation_provenance": { + "schema_version": "participant-implementation-provenance/v1", + "run_id": "run-techvault-001", + "participant_implementations": [ + { + "participant_address": "participants.red", + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": "contracts/fixtures/participant-implementation-manifest/reference.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "selected_decision_surface_mode": "policy-directed", + "participant_contract_versions": [ + "participant-episode-state-envelope-v1", + "participant-behavior-history-event-stream-v1" + ], + "exposure_policy": { + "policy_id": "red-agent-policy", + "policy_version": "1.0.0", + "policy_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "exposure_policy_kinds": [ + "task-statement", + "tool-affordance" + ], + "disclosed_refs": [ + "scenario.tasks.red" + ], + "withheld_refs": [ + "scenario.hidden.answer-key" + ], + "tool_affordance_refs": [ + "tool.shell" + ], + "visibility_scope_refs": [ + "participants.red.visible" + ] + } + } + ], + "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "backend_manifest_ref": "backend-manifest-v2:stub-backend" + }, + "parameter_set": [ + { + "name": "difficulty", + "value": "standard", + "value_kind": "protocol" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345 + } + ], + "started_at": "2026-05-26T00:10:00Z", + "ended_at": "2026-05-26T00:40:00Z", + "clock_context": { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock" + }, + "run_status": "completed", + "outcome_status": "succeeded", + "traceability": { + "capture_spec_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + } + ], + "evidence_record_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "derived_measure_refs": [ + { + "ref_kind": "derived-measure", + "ref_id": "measure-techvault-foothold-001", + "ref_version": "1.0.0" + } + ], + "claim_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ], + "notes": [ + "The run provenance chain binds the declared capture spec, raw evidence record, derived foothold measure, and result claim." + ] + }, + "realized_form_disclosures": [ + { + "concern_id": "composed-scenario-snapshot", + "concern_kind": "scenario-module", + "basis": "processor-realized", + "realized_by_ref": { + "ref_kind": "backend", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + }, + "authored_ref": { + "ref_kind": "scenario", + "ref_id": "scenario-techvault" + }, + "realized_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "disclosure": "The processor resolved the authored TechVault scenario into the canonical composed scenario snapshot used by this run.", + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ] + } + ], + "evidence_artifacts": [ + { + "artifact_id": "evaluation-history", + "role": "observation", + "media_type": "application/json", + "uri": "runs/run-techvault-001/evaluation-history.json", + "checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "size_bytes": 4096, + "created_at": "2026-05-26T00:40:00Z", + "source": "stub-backend evaluator history export", + "satisfies_refs": [ + { + "ref_kind": "evidence", + "ref_id": "auth-log-evidence" + } + ], + "sensitivity": "internal" + } + ], + "result_summaries": { + "foothold-achieved-result": { + "metric_id": "foothold-achieved", + "value": true, + "value_status": "reported", + "evidence_refs": [ + { + "ref_kind": "evidence", + "ref_id": "evaluation-history" + } + ] + } + }, + "used_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "generated_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json new file mode 100644 index 000000000..e53297caa --- /dev/null +++ b/contracts/fixtures/experiment-core/experiment-run-v1/invalid/traceability-empty-capture-specs.json @@ -0,0 +1,347 @@ +{ + "schema_version": "experiment-run/v1", + "run_id": "run-techvault-001", + "run_version": "1.0.0", + "task_ref": { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + }, + "scenario_snapshot_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "apparatus_context": { + "schema_version": "experiment-apparatus-context/v1", + "apparatus_context_id": "apparatus-techvault-reference", + "context_version": "1.0.0", + "declared_at": "2026-05-26T00:00:00Z", + "components": { + "processor": { + "component_kind": "processor", + "identity": { + "name": "aces-reference-processor", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "backend": { + "component_kind": "backend", + "identity": { + "name": "stub-backend", + "version": "0.1.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + "observed": true + }, + "participant-policy": { + "component_kind": "participant-implementation", + "identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + }, + "observed": true + } + }, + "selected_manifests": [ + { + "ref_kind": "manifest", + "ref_id": "aces-reference-processor", + "ref_version": "processor-manifest/v2", + "subject_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "stub-backend", + "ref_version": "backend-manifest/v2", + "subject_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0" + } + }, + { + "ref_kind": "manifest", + "ref_id": "reference-red-agent", + "ref_version": "participant-implementation-manifest/v1", + "subject_ref": { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0" + } + } + ], + "compatibility_declarations": [ + { + "ref_kind": "profile", + "ref_id": "reference-stack-v1", + "ref_version": "semantic-profile/v1" + }, + { + "ref_kind": "capability", + "ref_id": "workflow-results" + }, + { + "ref_kind": "capability", + "ref_id": "evaluation-results" + } + ], + "configuration_parameters": [ + { + "name": "worker-count", + "value": 1, + "value_kind": "apparatus" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345, + "description": "Seed supplied to task randomization." + } + ], + "clocks": [ + { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock", + "synchronization": "NTP synchronized before run seal." + } + ], + "measurement_channels": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0" + } + ], + "observed_setup_evidence": [ + { + "artifact_id": "setup-attestation", + "role": "apparatus-evidence", + "media_type": "application/json", + "uri": "runs/run-techvault-001/setup.json", + "checksum": { + "algorithm": "sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "size_bytes": 2048, + "created_at": "2026-05-26T00:09:30Z", + "source": "stub-backend setup attestation", + "sensitivity": "internal" + } + ], + "known_limitations": [ + { + "category": "apparatus", + "note": "The stub backend is a conformance apparatus, not a fidelity claim." + } + ] + }, + "participant_implementation_provenance": { + "schema_version": "participant-implementation-provenance/v1", + "run_id": "run-techvault-001", + "participant_implementations": [ + { + "participant_address": "participants.red", + "implementation_identity": { + "name": "reference-red-agent", + "version": "1.0.0" + }, + "manifest_ref": "contracts/fixtures/participant-implementation-manifest/reference.json", + "manifest_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "selected_decision_surface_mode": "policy-directed", + "participant_contract_versions": [ + "participant-episode-state-envelope-v1", + "participant-behavior-history-event-stream-v1" + ], + "exposure_policy": { + "policy_id": "red-agent-policy", + "policy_version": "1.0.0", + "policy_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "exposure_policy_kinds": [ + "task-statement", + "tool-affordance" + ], + "disclosed_refs": [ + "scenario.tasks.red" + ], + "withheld_refs": [ + "scenario.hidden.answer-key" + ], + "tool_affordance_refs": [ + "tool.shell" + ], + "visibility_scope_refs": [ + "participants.red.visible" + ] + } + } + ], + "processor_manifest_ref": "processor-manifest-v2:aces-reference-processor", + "backend_manifest_ref": "backend-manifest-v2:stub-backend" + }, + "parameter_set": [ + { + "name": "difficulty", + "value": "standard", + "value_kind": "protocol" + } + ], + "stochastic_controls": [ + { + "control_id": "task-seed", + "role": "seed", + "value": 12345 + } + ], + "started_at": "2026-05-26T00:10:00Z", + "ended_at": "2026-05-26T00:40:00Z", + "clock_context": { + "clock_id": "range-wall-clock", + "authority": "backend wall clock", + "time_domain": "wall-clock" + }, + "run_status": "completed", + "outcome_status": "succeeded", + "traceability": { + "capture_spec_refs": [], + "evidence_record_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "derived_measure_refs": [ + { + "ref_kind": "derived-measure", + "ref_id": "measure-techvault-foothold-001", + "ref_version": "1.0.0" + } + ], + "claim_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ], + "notes": [ + "The run provenance chain binds the declared capture spec, raw evidence record, derived foothold measure, and result claim." + ] + }, + "realized_form_disclosures": [ + { + "concern_id": "composed-scenario-snapshot", + "concern_kind": "scenario-module", + "basis": "processor-realized", + "realized_by_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + }, + "authored_ref": { + "ref_kind": "scenario", + "ref_id": "scenario-techvault" + }, + "realized_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "disclosure": "The processor resolved the authored TechVault scenario into the canonical composed scenario snapshot used by this run.", + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ] + } + ], + "evidence_artifacts": [ + { + "artifact_id": "evaluation-history", + "role": "observation", + "media_type": "application/json", + "uri": "runs/run-techvault-001/evaluation-history.json", + "checksum": { + "algorithm": "sha256", + "value": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "size_bytes": 4096, + "created_at": "2026-05-26T00:40:00Z", + "source": "stub-backend evaluator history export", + "satisfies_refs": [ + { + "ref_kind": "evidence", + "ref_id": "auth-log-evidence" + } + ], + "sensitivity": "internal" + } + ], + "result_summaries": { + "foothold-achieved-result": { + "metric_id": "foothold-achieved", + "value": true, + "value_status": "reported", + "evidence_refs": [ + { + "ref_kind": "evidence", + "ref_id": "evaluation-history" + } + ] + } + }, + "used_refs": [ + { + "ref_kind": "task", + "ref_id": "task-techvault-red-team-v1", + "ref_version": "1.0.0" + } + ], + "generated_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ] +} diff --git a/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json b/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json index 3ebebc660..d2897fe27 100644 --- a/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json +++ b/contracts/fixtures/experiment-core/experiment-run-v1/valid/reference.json @@ -240,6 +240,68 @@ }, "run_status": "completed", "outcome_status": "succeeded", + "traceability": { + "capture_spec_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0" + } + ], + "evidence_record_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ], + "derived_measure_refs": [ + { + "ref_kind": "derived-measure", + "ref_id": "measure-techvault-foothold-001", + "ref_version": "1.0.0" + } + ], + "claim_refs": [ + { + "ref_kind": "result", + "ref_id": "foothold-achieved-result" + } + ], + "notes": [ + "The run provenance chain binds the declared capture spec, raw evidence record, derived foothold measure, and result claim." + ] + }, + "realized_form_disclosures": [ + { + "concern_id": "composed-scenario-snapshot", + "concern_kind": "scenario-module", + "basis": "processor-realized", + "realized_by_ref": { + "ref_kind": "processor", + "ref_id": "aces-reference-processor", + "ref_version": "0.1.0" + }, + "authored_ref": { + "ref_kind": "scenario", + "ref_id": "scenario-techvault" + }, + "realized_ref": { + "ref_kind": "scenario-snapshot", + "ref_id": "scenario-techvault", + "ref_version": "2026-05-26", + "ref_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "disclosure": "The processor resolved the authored TechVault scenario into the canonical composed scenario snapshot used by this run.", + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0" + } + ] + } + ], "evidence_artifacts": [ { "artifact_id": "evaluation-history", diff --git a/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/invalid/implicit-last-writer-wins.json b/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/invalid/implicit-last-writer-wins.json new file mode 100644 index 000000000..27c35e357 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/invalid/implicit-last-writer-wins.json @@ -0,0 +1,42 @@ +{ + "access_sets": [ + { + "member_event_ref": "scan-red-0001", + "shared_state_write_refs": [ + "hosts.web01.service.http" + ] + }, + { + "member_event_ref": "scan-blue-0001", + "shared_state_write_refs": [ + "hosts.web01.service.http" + ] + } + ], + "actor_ref": "runtime.coordinator", + "atomicity_scope": "single_object", + "authorization_scope": "operators", + "clock_authority": "clock.logical.runtime", + "conflict_class": "none", + "conflict_policy": "last_write_wins", + "event_id": "joint-red-blue-0001", + "event_type": "joint_action_recorded", + "exact_concurrency_claim": true, + "extension_policy": "forbid_unknown_fields", + "ingested_at": "2026-06-20T08:00:00Z", + "isolation_guarantee": "none", + "joint_action_set_id": "joint-red-blue-0001", + "logical_order_ref": "order.joint-red-blue-0001", + "member_event_refs": [ + "scan-red-0001", + "scan-blue-0001" + ], + "occurred_at": "2026-06-20T08:00:00Z", + "ordering_basis": "wall_clock_only", + "producer_ref": "backend.stub", + "realized_order": [], + "recorded_at": "2026-06-20T08:00:00Z", + "schema_name": "participant-joint-action-record", + "schema_version": "participant-joint-action-record/v1", + "unsupported_disclosure": false +} diff --git a/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/valid/serialized-red-blue-shared-state.json b/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/valid/serialized-red-blue-shared-state.json new file mode 100644 index 000000000..4954c067d --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-joint-action-record-v1/valid/serialized-red-blue-shared-state.json @@ -0,0 +1,46 @@ +{ + "access_sets": [ + { + "member_event_ref": "scan-red-0001", + "shared_state_write_refs": [ + "hosts.web01.service.http" + ] + }, + { + "member_event_ref": "scan-blue-0001", + "shared_state_read_refs": [ + "hosts.web01.service.http" + ] + } + ], + "actor_ref": "runtime.coordinator", + "atomicity_scope": "single_object", + "authorization_scope": "operators", + "clock_authority": "clock.logical.runtime", + "conflict_class": "read_write", + "conflict_policy": "serialize", + "event_id": "joint-red-blue-0001", + "event_type": "joint_action_recorded", + "exact_concurrency_claim": false, + "extension_policy": "forbid_unknown_fields", + "ingested_at": "2026-06-20T08:00:00Z", + "isolation_guarantee": "serializable", + "joint_action_set_id": "joint-red-blue-0001", + "logical_order_ref": "order.joint-red-blue-0001", + "member_event_refs": [ + "scan-red-0001", + "scan-blue-0001" + ], + "occurred_at": "2026-06-20T08:00:00Z", + "ordering_basis": "serialized_backend_order", + "producer_ref": "backend.stub", + "realized_order": [ + "scan-red-0001", + "scan-blue-0001" + ], + "recorded_at": "2026-06-20T08:00:00Z", + "schema_name": "participant-joint-action-record", + "schema_version": "participant-joint-action-record/v1", + "time_management_context_ref": "tm-red-blue-0001", + "unsupported_disclosure": false +} diff --git a/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json new file mode 100644 index 000000000..d954303f8 --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-outcome-report-v1/invalid/empty-state-relationships.json @@ -0,0 +1,71 @@ +{ + "event_id": "outcome-red-17", + "schema_name": "aces.participant_runtime.outcome_report", + "schema_version": "1.0.0", + "event_type": "outcome_report", + "extension_policy": "reject_unknown_required", + "event_classification": null, + "source_status": { + "status_id": 1, + "status": "success", + "status_code": "outcome_interpreted", + "status_detail": "interpretation rule grounded the outcome in recorded sources", + "source_status_label": "outcome_interpreted", + "source_status_mapping": "aces.outcome.interpreted" + }, + "participant_address": "participants.red.llm", + "episode_id": "ep-red-004", + "sequence_number": 19, + "occurred_at": "2026-05-26T10:15:05Z", + "recorded_at": "2026-05-26T10:15:06Z", + "ingested_at": "2026-05-26T10:15:06Z", + "clock_authority": "backend.logical_clock.red-range", + "temporal_context": "tick-118", + "ordering_basis": "logical_clock", + "logical_order_ref": "order.red.118.19", + "predecessor_event_refs": ["evt-llm-17-exec"], + "actor_ref": "runtime.outcome-interpreter", + "producer_ref": "adapters.llm-tool-runtime.v2", + "source_system_ref": "tool-gateway.red", + "source_record_ref": "gateway-call-992", + "source_raw_ref": null, + "source_pipeline": { + "product_ref": "products.tool-gateway.red", + "product_version": "2.4.1", + "log_provider": "tool-gateway", + "log_source": "tool-gateway.red", + "log_name": "outcome-report", + "original_event_uid": "gateway-call-992", + "original_time": "2026-05-26T10:15:01Z", + "processed_time": "2026-05-26T10:15:06Z", + "logged_time": "2026-05-26T10:15:06Z", + "transmit_time": "2026-05-26T10:15:06Z", + "correlation_uid": "corr-tool-call-992", + "sequence": 993 + }, + "raw_data_integrity": { + "raw_data_hash": null, + "raw_data_hash_algorithm": null, + "raw_data_size": null, + "raw_data_is_truncated": null, + "raw_data_untruncated_size": null + }, + "confidence": 0.9, + "provenance_refs": ["provenance.backend_realized"], + "evidence_refs": ["evidence.tool-call-992-redacted"], + "marking_definition_refs": ["markings.internal.v1"], + "object_marking_refs": ["markings.internal.v1"], + "markings": ["internal"], + "granular_markings": {}, + "redaction_policy_ref": "redaction.no-prompts-or-secrets.v1", + "authorization_scope": "runtime_review", + "outcome_id": "outcomes.red.exfiltration.17", + "interpretation_rule_ref": "rules.outcome.exfiltration.v1", + "outcome_sources": [ + { + "source_kind": "action_result", + "source_ref": "results.red.act-17" + } + ], + "state_relationships": [] +} diff --git a/contracts/fixtures/participant-runtime/participant-time-management-context-v1/invalid/timestamp-only-exact.json b/contracts/fixtures/participant-runtime/participant-time-management-context-v1/invalid/timestamp-only-exact.json new file mode 100644 index 000000000..8fb5232af --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-time-management-context-v1/invalid/timestamp-only-exact.json @@ -0,0 +1,23 @@ +{ + "actor_ref": "runtime.coordinator", + "authorization_scope": "operators", + "backend_serialized": false, + "basis": "wall_clock_only", + "claim_strength": "precise", + "clock_authority": "clock.wall", + "clock_ref": "clock.wall", + "context_id": "tm-red-blue-0001", + "event_id": "tm-red-blue-0001", + "event_type": "time_management_context_recorded", + "extension_policy": "forbid_unknown_fields", + "ingested_at": "2026-06-20T08:00:00Z", + "logical_order_ref": "order.wall-clock", + "mode": "display", + "occurred_at": "2026-06-20T08:00:00Z", + "ordering_basis": "wall_clock_only", + "producer_ref": "backend.stub", + "recorded_at": "2026-06-20T08:00:00Z", + "schema_name": "participant-time-management-context", + "schema_version": "participant-time-management-context/v1", + "unsupported_disclosure": false +} diff --git a/contracts/fixtures/participant-runtime/participant-time-management-context-v1/valid/backend-serialized-logical-clock.json b/contracts/fixtures/participant-runtime/participant-time-management-context-v1/valid/backend-serialized-logical-clock.json new file mode 100644 index 000000000..2fb00037e --- /dev/null +++ b/contracts/fixtures/participant-runtime/participant-time-management-context-v1/valid/backend-serialized-logical-clock.json @@ -0,0 +1,23 @@ +{ + "actor_ref": "runtime.coordinator", + "authorization_scope": "operators", + "backend_serialized": true, + "basis": "serialized_backend_order", + "claim_strength": "bounded", + "clock_authority": "clock.logical.runtime", + "clock_ref": "clock.logical.runtime", + "context_id": "tm-red-blue-0001", + "event_id": "tm-red-blue-0001", + "event_type": "time_management_context_recorded", + "extension_policy": "forbid_unknown_fields", + "ingested_at": "2026-06-20T08:00:00Z", + "logical_order_ref": "order.joint-red-blue-0001", + "mode": "backend_serialized", + "occurred_at": "2026-06-20T08:00:00Z", + "ordering_basis": "serialized_backend_order", + "producer_ref": "backend.stub", + "recorded_at": "2026-06-20T08:00:00Z", + "schema_name": "participant-time-management-context", + "schema_version": "participant-time-management-context/v1", + "unsupported_disclosure": false +} diff --git a/contracts/profiles/backend/full-remote-control-plane.json b/contracts/profiles/backend/full-remote-control-plane.json index 3dab094ad..2a39c7d3d 100644 --- a/contracts/profiles/backend/full-remote-control-plane.json +++ b/contracts/profiles/backend/full-remote-control-plane.json @@ -15,6 +15,9 @@ "evaluation-history-event-stream-v1", "participant-episode-state-envelope-v1", "participant-episode-history-event-stream-v1", - "participant-behavior-history-event-stream-v1" + "participant-behavior-history-event-stream-v1", + "participant-lifecycle-event-v1", + "participant-observation-envelope-v1", + "participant-shared-state-record-v1" ] } diff --git a/contracts/schema-publication-manifest.json b/contracts/schema-publication-manifest.json index d28290f7e..2c6904bfd 100644 --- a/contracts/schema-publication-manifest.json +++ b/contracts/schema-publication-manifest.json @@ -12,20 +12,20 @@ "contract_id": "backend-manifest-v2", "schema_path": "contracts/schemas/backend-manifest/backend-manifest-v2.json", "stability": "draft", - "content_hash": "da156411d9877ad75045569b47e3b98597b6dec23a827d176c1fc64d9532cc90", + "content_hash": "645831fb013c9450a1354a609b0645f6d045c85b3d92edcdd3cdbce19a0e3064", "last_change": { - "summary": "Extended the backend manifest with the participant backend-facing runtime capability surface (API-407 manifest extension; ADR-060, issue #76).", - "content_hash": "da156411d9877ad75045569b47e3b98597b6dec23a827d176c1fc64d9532cc90" + "summary": "Added the EXP-715 observation capability declaration and experiment evidence contract identifiers to backend manifest v2.", + "content_hash": "645831fb013c9450a1354a609b0645f6d045c85b3d92edcdd3cdbce19a0e3064" } }, { "contract_id": "backend-profile-v1", "schema_path": "contracts/schemas/profiles/backend-profile-v1.json", "stability": "draft", - "content_hash": "1c9ffe10f1c2c420610d1353dc1406bb0e0d63001c4f6a32e1aca061b164af1d", + "content_hash": "85ddac10a896b39cc9c5ef3f73141e84566236d12d0127511007610ccd700694", "last_change": { - "summary": "Added the participant backend-facing contract identifiers (lifecycle-event, observation-envelope, shared-state-record, outcome-report) to the backend profile contract vocabulary (ADR-060, issue #76).", - "content_hash": "1c9ffe10f1c2c420610d1353dc1406bb0e0d63001c4f6a32e1aca061b164af1d" + "summary": "Added the EXP-707/EXP-708/EXP-709 experiment evidence contracts to the backend profile contract vocabulary.", + "content_hash": "85ddac10a896b39cc9c5ef3f73141e84566236d12d0127511007610ccd700694" } }, { @@ -62,25 +62,71 @@ "contract_id": "experiment-apparatus-context-v1", "schema_path": "contracts/schemas/experiment-core/experiment-apparatus-context-v1.json", "stability": "draft", - "content_hash": "6e1f9885835bc6b6e8af922f5624ef09084c2fc8992b2a7f8fdb4e078f6ae2c0" + "content_hash": "565558814655c9fc3cd790fb441633622e4846d416ffaba43812143024e33ae3", + "last_change": { + "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", + "content_hash": "565558814655c9fc3cd790fb441633622e4846d416ffaba43812143024e33ae3" + } + }, + { + "contract_id": "experiment-capture-spec-v1", + "schema_path": "contracts/schemas/experiment-core/experiment-capture-spec-v1.json", + "stability": "draft", + "content_hash": "1c479291aed4c60aa7a28d93839d3633f8dee585ca3098b83bd4fbb03bea86ad", + "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" + } + }, + { + "contract_id": "experiment-derived-measure-v1", + "schema_path": "contracts/schemas/experiment-core/experiment-derived-measure-v1.json", + "stability": "draft", + "content_hash": "162aeeb41906008b03ef211b20646f451101c59cdcabc1614427ca3fa7945afe", + "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" + } + }, + { + "contract_id": "experiment-evidence-record-v1", + "schema_path": "contracts/schemas/experiment-core/experiment-evidence-record-v1.json", + "stability": "draft", + "content_hash": "3483ac4ff2cb61278d64253257f70dc73d14eaff3bc5501eee20a86a55d7d707", + "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" + } }, { "contract_id": "experiment-run-v1", "schema_path": "contracts/schemas/experiment-core/experiment-run-v1.json", "stability": "draft", - "content_hash": "ceeead6df3a0d71477221bba8d8ac64b5a5f234a315d7c695357b04101bbb155" + "content_hash": "e1e7ca5e74439140cd27340a7754d8b871266e518ce93c98bf6ff0dbaa8bd134", + "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" + } }, { "contract_id": "experiment-study-v1", "schema_path": "contracts/schemas/experiment-core/experiment-study-v1.json", "stability": "draft", - "content_hash": "8065247bb9185692c7de922e60b88e49420a32f3e57300801c3218c629194fa6" + "content_hash": "b769512ccf43a3cae303d363da84cf1d4bbd97099d7f9085f065bbe4d801b4d3", + "last_change": { + "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", + "content_hash": "b769512ccf43a3cae303d363da84cf1d4bbd97099d7f9085f065bbe4d801b4d3" + } }, { "contract_id": "experiment-task-v1", "schema_path": "contracts/schemas/experiment-core/experiment-task-v1.json", "stability": "draft", - "content_hash": "fcdbd1d6d1d455ef2f43360dd0408c006ac2d1cd6f6454f46f7ba63365e9a661" + "content_hash": "45fb2eb011d89b9be057d236b9d4e58c2ce01f8c1365f7d521afc8192d68a73a", + "last_change": { + "summary": "Extended experiment-core references for the EXP-707/EXP-708/EXP-709 evidence and measure contract boundary.", + "content_hash": "45fb2eb011d89b9be057d236b9d4e58c2ce01f8c1365f7d521afc8192d68a73a" + } }, { "contract_id": "instantiated-scenario-v1", @@ -124,10 +170,10 @@ "contract_id": "participant-context-view-v1", "schema_path": "contracts/schemas/control-plane/participant-context-view-v1.json", "stability": "draft", - "content_hash": "ff2e5866bde73898ead051cd5cfb20d32168ad3f8a6f9775d8481c728c40c24b", + "content_hash": "2946f3d2cb6e74a5848f57b3729545b4939cacd84aecdc2a5b86add7e431c0de", "last_change": { - "summary": "Initial publication of the participant context-view control-plane contract: the participant-visible context projection in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "ff2e5866bde73898ead051cd5cfb20d32168ad3f8a6f9775d8481c728c40c24b" + "summary": "Added the SEM-216 audience-boundary rules to participant-visible context views: archival evidence_record/derived_measure source layers require a derivation_basis_ref view rule and redaction_policy_ref (published allOf), must be mediated by the transformation, and payload_ref must not alias a raw archival ref (both published as x-aces-invariants).", + "content_hash": "2946f3d2cb6e74a5848f57b3729545b4939cacd84aecdc2a5b86add7e431c0de" } }, { @@ -164,6 +210,16 @@ "stability": "draft", "content_hash": "a4c050fb2a53129148f7d2960a7cb06d36c4682483064734e505acda4677d7ec" }, + { + "contract_id": "participant-joint-action-record-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-joint-action-record-v1.json", + "stability": "draft", + "content_hash": "bfd0f43f8b19f3658d71ca81520c481aa447f0486eee669edfc4521d0cdbd1a8", + "last_change": { + "summary": "Initial publication of the RUN-308 participant joint-action runtime contract: membership, access-set, conflict, isolation, and realized-order evidence.", + "content_hash": "bfd0f43f8b19f3658d71ca81520c481aa447f0486eee669edfc4521d0cdbd1a8" + } + }, { "contract_id": "participant-lifecycle-event-v1", "schema_path": "contracts/schemas/participant-runtime/participant-lifecycle-event-v1.json", @@ -188,20 +244,20 @@ "contract_id": "participant-outcome-report-v1", "schema_path": "contracts/schemas/participant-runtime/participant-outcome-report-v1.json", "stability": "draft", - "content_hash": "1b65d36136812ae18c80ba7822df69b0ca65b569fd4cc2e2bd6c1fd07e5e4abf", + "content_hash": "e7c8cb7fa41803e16c7fd99f71365621cce9e1cf21f13701c8d482cc030dcea7", "last_change": { - "summary": "Initial publication of the participant outcome-report runtime contract: participant outcome reporting in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "1b65d36136812ae18c80ba7822df69b0ca65b569fd4cc2e2bd6c1fd07e5e4abf" + "summary": "Require participant outcome reports to carry at least one explicit state relationship for API-411.", + "content_hash": "e7c8cb7fa41803e16c7fd99f71365621cce9e1cf21f13701c8d482cc030dcea7" } }, { "contract_id": "participant-shared-state-record-v1", "schema_path": "contracts/schemas/participant-runtime/participant-shared-state-record-v1.json", "stability": "draft", - "content_hash": "a3ad56415946417bfbd19cd637886d8442a3e2be510212471e1fcb139cf241d1", + "content_hash": "8de5075b682a2ef7f7bf9d18f9dcb51667f2efeb496f283d0bb54b9bb0ffd38b", "last_change": { - "summary": "Initial publication of the participant shared-state-record runtime contract: shared-state records in the backend-facing contract family (ADR-060, issue #76).", - "content_hash": "a3ad56415946417bfbd19cd637886d8442a3e2be510212471e1fcb139cf241d1" + "summary": "Require participant shared-state records and accesses to carry revision or digest markers for RUN-307 version discipline.", + "content_hash": "8de5075b682a2ef7f7bf9d18f9dcb51667f2efeb496f283d0bb54b9bb0ffd38b" } }, { @@ -214,6 +270,16 @@ "content_hash": "aef73b7d52c99de2a51cb2a58de0e30c37fc7fb2af5627cd6241a6aad288d07c" } }, + { + "contract_id": "participant-time-management-context-v1", + "schema_path": "contracts/schemas/participant-runtime/participant-time-management-context-v1.json", + "stability": "draft", + "content_hash": "e4711ff9a4674697fb8aed40af863ea0327ad31824ef0a8f07d182ccfa9f28a1", + "last_change": { + "summary": "Initial publication of the RUN-308 participant time-management context contract: clock, ordering, lookahead, pacing, rollback, and backend-serialization basis.", + "content_hash": "e4711ff9a4674697fb8aed40af863ea0327ad31824ef0a8f07d182ccfa9f28a1" + } + }, { "contract_id": "processor-manifest-v2", "schema_path": "contracts/schemas/processor-manifest/processor-manifest-v2.json", @@ -236,10 +302,10 @@ "contract_id": "runtime-snapshot-v1", "schema_path": "contracts/schemas/snapshots/runtime-snapshot-v1.json", "stability": "draft", - "content_hash": "348a25b49e30081797164f77cb344dbf27e85007fcf5d9e93c2e57e33795074a", + "content_hash": "cb6a599ef5137bcc5954ffd4f451167562186112c7595ec20b50eb94b97e1550", "last_change": { - "summary": "Add SEM-218 realization_provenance ledger (RealizationProvenanceEntryModel) to the runtime snapshot envelope: per-concern author-declared/processor-derived/backend-realized provenance for realized realization concerns, enforcing invariant I5 (issue #491).", - "content_hash": "348a25b49e30081797164f77cb344dbf27e85007fcf5d9e93c2e57e33795074a" + "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" } }, { diff --git a/contracts/schemas/README.md b/contracts/schemas/README.md index ecef66aad..64a8adffb 100644 --- a/contracts/schemas/README.md +++ b/contracts/schemas/README.md @@ -23,7 +23,10 @@ Current published schemas cover: - evaluation result envelopes - evaluation history streams - operation receipts and statuses -- experiment-core task, run, apparatus-context, and study/collection contracts +- control-plane participant status/history/context views, including SEM-214 + context-view meaning and comparability semantics +- experiment-core task, run, apparatus-context, study/collection, capture + specification, raw evidence, and derived measure contracts Current filenames still use `runtime` for some live-execution artifacts. That naming is preserved for compatibility while the repository migrates toward the @@ -154,18 +157,28 @@ The `experiment-core` schema family publishes: - `experiment-apparatus-context-v1` - `experiment-run-v1` - `experiment-study-v1` +- `experiment-capture-spec-v1` +- `experiment-evidence-record-v1` +- `experiment-derived-measure-v1` These schemas keep SDL scenario authoring, experiment task protocol, execution -apparatus context, archival run provenance, and study/collection analysis -separate. The normative invariant set lives in -`specs/formal/experiment-core/`, and ADR-037 records the architectural -boundary. +apparatus context, archival run provenance, study/collection analysis, +declarative capture requirements, raw evidence records, and derived +measure/evaluation outputs separate. The normative invariant set lives in +`specs/formal/experiment-core/`. ADR-055 records the original task/run/study +boundary, and ADR-064 records the evidence/measure and backend observation +capability extension. ADR-065 records `experiment-run-v1` as the canonical run +provenance record with required traceability links and realized-form +disclosures. Schema-expressible invariants are encoded in the published schemas. In particular, task/run reference-kind constraints and invalidated-run requirements are part of `experiment-task-v1` and `experiment-run-v1`, while identifier uniqueness for metrics, apparatus components, result summaries, study members, and study factors is represented with keyed object maps. +Run traceability and realized-form disclosure invariants keep claims grounded +in evidence/derived-measure refs and keep realized choices distinct from +authored scenario meaning and result values. Cross-artifact or graph invariants that standard JSON Schema cannot express are published under the ACES semantic-invariant profile with `x-aces-invariants` entries that name the validator and input contract paths. The generated schemas @@ -173,3 +186,8 @@ declare draft 2020-12 identity, and the annotation profile shape is published as `aces-semantic-invariants-v1` and checked during generation. Generic JSON Schema validation remains structural; consumers of experiment-core records must apply the named semantic validators before accepting records as ACES-conformant. + +The optional backend-manifest `capabilities.observation` block declares EXP-715 +observation/evidence collection support. Backends that declare it must also +declare the published capture-spec, evidence-record, and derived-measure +contracts that make the claim inspectable. diff --git a/contracts/schemas/backend-manifest/backend-manifest-v2.json b/contracts/schemas/backend-manifest/backend-manifest-v2.json index a68de21d9..636f622dc 100644 --- a/contracts/schemas/backend-manifest/backend-manifest-v2.json +++ b/contracts/schemas/backend-manifest/backend-manifest-v2.json @@ -35,6 +35,17 @@ ], "default": null }, + "observation": { + "anyOf": [ + { + "$ref": "#/$defs/ObservationCapabilitiesModel" + }, + { + "type": "null" + } + ], + "default": null + }, "orchestrator": { "anyOf": [ { @@ -180,6 +191,99 @@ "title": "EvaluatorCapabilitiesModel", "type": "object" }, + "ObservationCapabilitiesModel": { + "additionalProperties": false, + "description": "EXP-715 backend observation and evidence-collection capability declaration.", + "properties": { + "constraints": { + "additionalProperties": { + "type": "string" + }, + "title": "Constraints", + "type": "object" + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "supported_capture_kinds": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Capture Kinds", + "type": "array", + "uniqueItems": true + }, + "supported_channel_kinds": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Channel Kinds", + "type": "array", + "uniqueItems": true + }, + "supported_evidence_contracts": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Evidence Contracts", + "type": "array", + "uniqueItems": true + }, + "supported_media_types": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Media Types", + "type": "array", + "uniqueItems": true + }, + "supported_sealing_modes": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Supported Sealing Modes", + "type": "array", + "uniqueItems": true + }, + "supports_chain_of_custody": { + "default": false, + "title": "Supports Chain Of Custody", + "type": "boolean" + }, + "supports_loss_disclosure": { + "default": false, + "title": "Supports Loss Disclosure", + "type": "boolean" + }, + "supports_redaction": { + "default": false, + "title": "Supports Redaction", + "type": "boolean" + } + }, + "required": [ + "name", + "supported_capture_kinds", + "supported_channel_kinds", + "supported_evidence_contracts", + "supported_media_types", + "supported_sealing_modes" + ], + "title": "ObservationCapabilitiesModel", + "type": "object" + }, "OrchestratorCapabilitiesModel": { "additionalProperties": false, "allOf": [ @@ -763,7 +867,12 @@ "participant-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", - "participant-outcome-report-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" ], "minLength": 1, "type": "string" diff --git a/contracts/schemas/control-plane/participant-context-view-v1.json b/contracts/schemas/control-plane/participant-context-view-v1.json index 27d1747ad..f26bc3d07 100644 --- a/contracts/schemas/control-plane/participant-context-view-v1.json +++ b/contracts/schemas/control-plane/participant-context-view-v1.json @@ -1,9 +1,305 @@ { + "$defs": { + "ParticipantContextComparabilityModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "comparability_class": { + "enum": [ + "portable_with_disclosed_weakening", + "backend_specific_non_comparable" + ] + } + }, + "required": [ + "comparability_class" + ] + }, + "then": { + "properties": { + "backend_disclosure_refs": { + "minItems": 1 + } + }, + "required": [ + "backend_disclosure_refs" + ] + } + } + ], + "description": "Explicit comparability claim for a SEM-214 context view.", + "properties": { + "backend_disclosure_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Backend Disclosure Refs", + "type": "array" + }, + "comparability_class": { + "enum": [ + "portable_equivalent", + "portable_with_disclosed_weakening", + "backend_specific_non_comparable" + ], + "title": "Comparability Class", + "type": "string" + }, + "comparison_basis_ref": { + "minLength": 1, + "title": "Comparison Basis Ref", + "type": "string" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + } + }, + "required": [ + "comparability_class", + "comparison_basis_ref", + "limitations" + ], + "title": "ParticipantContextComparabilityModel", + "type": "object" + }, + "ParticipantContextSourceLayerModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "temporal_relation": { + "const": "bounded_staleness" + } + }, + "required": [ + "temporal_relation" + ] + }, + "then": { + "properties": { + "freshness_basis_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "freshness_basis_ref" + ] + } + } + ], + "description": "One governed source layer consumed by a SEM-214 context view.", + "properties": { + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "freshness_basis_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Freshness Basis Ref" + }, + "observation_point": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observation Point" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "source_layer": { + "enum": [ + "source_snapshot", + "participant_observation", + "participant_behavior_history", + "participant_episode_state", + "participant_status_view", + "participant_history_view", + "evidence_record", + "derived_measure", + "control_plane_operation" + ], + "title": "Source Layer", + "type": "string" + }, + "temporal_relation": { + "enum": [ + "same_observation_point", + "bounded_staleness", + "historical_replay" + ], + "title": "Temporal Relation", + "type": "string" + } + }, + "required": [ + "source_id", + "source_layer", + "ref", + "temporal_relation", + "evidence_refs", + "provenance_refs" + ], + "title": "ParticipantContextSourceLayerModel", + "type": "object" + }, + "ParticipantContextTransformationModel": { + "additionalProperties": false, + "description": "Governed transformation relation for a SEM-214 context view.", + "properties": { + "description": { + "minLength": 1, + "title": "Description", + "type": "string" + }, + "input_source_ids": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Input Source Ids", + "type": "array" + }, + "output_semantics_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Semantics Ref" + }, + "transformation_rule_ref": { + "minLength": 1, + "title": "Transformation Rule Ref", + "type": "string" + } + }, + "required": [ + "transformation_rule_ref", + "description", + "input_source_ids" + ], + "title": "ParticipantContextTransformationModel", + "type": "object" + } + }, "$id": "https://aces.dev/schemas/participant-context-view-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "API-408 derived operational context view (reference-and-provenance only).", + "allOf": [ + { + "if": { + "properties": { + "audience_scope": { + "const": "participant_visible" + }, + "source_layers": { + "contains": { + "properties": { + "source_layer": { + "enum": [ + "evidence_record", + "derived_measure" + ] + } + }, + "required": [ + "source_layer" + ] + } + } + }, + "required": [ + "audience_scope", + "source_layers" + ] + }, + "then": { + "properties": { + "derivation_basis_ref": { + "minLength": 1, + "type": "string" + }, + "redaction_policy_ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "derivation_basis_ref", + "redaction_policy_ref" + ] + } + } + ], + "description": "API-408 derived operational context view with SEM-214 semantics.", "properties": { + "audience_scope": { + "enum": [ + "participant_visible", + "operator_visible", + "evaluator_visible", + "auditor_visible" + ], + "title": "Audience Scope", + "type": "string" + }, + "comparability": { + "$ref": "#/$defs/ParticipantContextComparabilityModel" + }, "derivation_basis_ref": { "anyOf": [ { @@ -39,6 +335,15 @@ "default": null, "title": "Episode Id" }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, "generated_at": { "format": "date-time", "minLength": 1, @@ -54,11 +359,26 @@ "title": "Marking Definition Refs", "type": "array" }, + "meaning_ref": { + "minLength": 1, + "title": "Meaning Ref", + "type": "string" + }, + "observation_point": { + "minLength": 1, + "title": "Observation Point", + "type": "string" + }, "participant_address": { "minLength": 1, "title": "Participant Address", "type": "string" }, + "participant_scope": { + "const": "participant_local", + "title": "Participant Scope", + "type": "string" + }, "payload_ref": { "anyOf": [ { @@ -72,6 +392,15 @@ "default": null, "title": "Payload Ref" }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Provenance Refs", + "type": "array" + }, "redaction_policy_ref": { "anyOf": [ { @@ -85,11 +414,31 @@ "default": null, "title": "Redaction Policy Ref" }, + "semantic_limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Semantic Limitations", + "type": "array" + }, + "source_layers": { + "items": { + "$ref": "#/$defs/ParticipantContextSourceLayerModel" + }, + "minItems": 1, + "title": "Source Layers", + "type": "array" + }, "source_snapshot_ref": { "minLength": 1, "title": "Source Snapshot Ref", "type": "string" }, + "transformation": { + "$ref": "#/$defs/ParticipantContextTransformationModel" + }, "view_id": { "minLength": 1, "title": "View Id", @@ -112,9 +461,54 @@ "generated_at", "source_snapshot_ref", "view_ref", + "meaning_ref", + "participant_scope", + "audience_scope", + "observation_point", "derived_from_refs", + "source_layers", + "transformation", + "comparability", + "evidence_refs", + "provenance_refs", + "semantic_limitations", "visibility_projection_ref" ], "title": "ParticipantContextViewModel", - "type": "object" + "type": "object", + "x-aces-invariants": [ + { + "description": "Participant-visible context views drawing on an archival evidence_record or derived_measure source layer must mediate that source through transformation.input_source_ids.", + "id": "context-view-sem216-archival-source-mediated", + "inputs": [ + { + "contract_id": "participant-context-view-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary" + }, + { + "description": "Participant-visible context views must not set payload_ref to a raw archival evidence_record or derived_measure source ref; payload_ref must identify the transformed, redacted view output.", + "id": "context-view-sem216-payload-not-raw-archival", + "inputs": [ + { + "contract_id": "participant-context-view-v1", + "instance_path": "#/payload_ref" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary" + } + ], + "x-aces-semantic-profile": { + "contract_id": "participant-context-view-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } } diff --git a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json index ccf1fbc2b..01dee6072 100644 --- a/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json +++ b/contracts/schemas/experiment-core/experiment-apparatus-context-v1.json @@ -793,12 +793,16 @@ "protocol", "apparatus-context", "run", + "metric-definition", "result", "study", "manifest", "profile", "capability", + "capture-spec", "evidence", + "evidence-record", + "derived-measure", "measurement-channel", "analysis-artifact", "other" diff --git a/contracts/schemas/experiment-core/experiment-capture-spec-v1.json b/contracts/schemas/experiment-core/experiment-capture-spec-v1.json new file mode 100644 index 000000000..75a330107 --- /dev/null +++ b/contracts/schemas/experiment-core/experiment-capture-spec-v1.json @@ -0,0 +1,809 @@ +{ + "$defs": { + "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", + "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" + }, + "ExperimentCaptureRequirementModel": { + "additionalProperties": false, + "description": "One evidence capture requirement inside a capture specification.", + "properties": { + "capture_kind": { + "enum": [ + "artifact", + "observation", + "trace", + "telemetry", + "log", + "packet-capture", + "other" + ], + "title": "Capture Kind", + "type": "string" + }, + "capture_scope": { + "enum": [ + "task", + "run", + "apparatus", + "participant", + "backend", + "processor", + "network", + "service" + ], + "title": "Capture Scope", + "type": "string" + }, + "channel_ref": { + "$ref": "#/$defs/ExperimentMeasurementChannelReferenceModel" + }, + "expected_media_types": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Expected Media Types", + "type": "array", + "uniqueItems": true + }, + "integrity_requirements": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Integrity Requirements", + "type": "array" + }, + "loss_disclosure_required": { + "default": true, + "title": "Loss Disclosure Required", + "type": "boolean" + }, + "notes": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "redaction_policy": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy" + }, + "required_artifact_roles": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Required Artifact Roles", + "type": "array", + "uniqueItems": true + }, + "requirement_id": { + "minLength": 1, + "title": "Requirement Id", + "type": "string" + }, + "retention_policy": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retention Policy" + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "redacted" + ], + "title": "Sensitivity", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "window_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Window Refs", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "requirement_id", + "title", + "capture_kind", + "capture_scope", + "channel_ref", + "window_refs", + "expected_media_types", + "sensitivity", + "integrity_requirements" + ], + "title": "ExperimentCaptureRequirementModel", + "type": "object" + }, + "ExperimentCaptureWindowModel": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "starts_at": { + "not": { + "type": "null" + } + } + }, + "required": [ + "starts_at" + ] + }, + { + "properties": { + "ends_at": { + "not": { + "type": "null" + } + } + }, + "required": [ + "ends_at" + ] + }, + { + "properties": { + "trigger_ref": { + "not": { + "type": "null" + } + } + }, + "required": [ + "trigger_ref" + ] + } + ], + "description": "Declarative scope/window over which evidence must be captured.", + "properties": { + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "ends_at": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ends At" + }, + "starts_at": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Starts At" + }, + "trigger_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "window_id": { + "minLength": 1, + "title": "Window Id", + "type": "string" + }, + "window_kind": { + "enum": [ + "task", + "run", + "apparatus", + "event", + "interval", + "manual" + ], + "title": "Window Kind", + "type": "string" + } + }, + "required": [ + "window_id", + "window_kind" + ], + "title": "ExperimentCaptureWindowModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Capture window ends_at must not precede starts_at when both timestamps are present.", + "id": "capture-window-interval-valid", + "inputs": [ + { + "contract_id": "experiment-capture-spec-v1", + "instance_path": "#/capture_windows" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentCaptureWindowModel._validate_capture_window" + } + ] + }, + "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" + }, + "ExperimentMeasurementChannelReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a declared measurement channel.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "measurement-channel", + "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": "ExperimentMeasurementChannelReferenceModel", + "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", + "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" + }, + "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-capture-spec-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Declarative EXP-707 specification of what experiment evidence to capture.", + "properties": { + "artifact_refs": { + "items": { + "$ref": "#/$defs/ExperimentArtifactRefModel" + }, + "title": "Artifact Refs", + "type": "array" + }, + "capture_requirements": { + "additionalProperties": { + "$ref": "#/$defs/ExperimentCaptureRequirementModel" + }, + "minProperties": 1, + "propertyNames": { + "minLength": 1 + }, + "title": "Capture Requirements", + "type": "object" + }, + "capture_spec_id": { + "minLength": 1, + "title": "Capture Spec Id", + "type": "string" + }, + "capture_windows": { + "items": { + "$ref": "#/$defs/ExperimentCaptureWindowModel" + }, + "minItems": 1, + "title": "Capture Windows", + "type": "array" + }, + "description": { + "minLength": 1, + "title": "Description", + "type": "string" + }, + "schema_version": { + "const": "experiment-capture-spec/v1", + "title": "Schema Version", + "type": "string" + }, + "scope_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "minItems": 1, + "title": "Scope Refs", + "type": "array" + }, + "spec_version": { + "minLength": 1, + "title": "Spec Version", + "type": "string" + }, + "title": { + "minLength": 1, + "title": "Title", + "type": "string" + }, + "validity_notes": { + "items": { + "$ref": "#/$defs/ExperimentValidityNoteModel" + }, + "title": "Validity Notes", + "type": "array" + } + }, + "required": [ + "schema_version", + "capture_spec_id", + "spec_version", + "title", + "description", + "scope_refs", + "capture_windows", + "capture_requirements" + ], + "title": "ExperimentCaptureSpecModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Every capture_requirements object key must match the embedded requirement_id value, and window_refs must resolve to declared capture_windows.", + "id": "capture-requirement-key-matches-requirement-id", + "inputs": [ + { + "contract_id": "experiment-capture-spec-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentCaptureSpecModel._validate_capture_spec" + } + ], + "x-aces-plane": "authored_evidence_requirement", + "x-aces-semantic-profile": { + "contract_id": "experiment-capture-spec-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/experiment-core/experiment-derived-measure-v1.json b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json new file mode 100644 index 000000000..85711af88 --- /dev/null +++ b/contracts/schemas/experiment-core/experiment-derived-measure-v1.json @@ -0,0 +1,466 @@ +{ + "$defs": { + "ExperimentDerivedMeasureMethodModel": { + "additionalProperties": false, + "description": "Method metadata for deriving measures from raw evidence.", + "properties": { + "description": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "method_id": { + "minLength": 1, + "title": "Method Id", + "type": "string" + }, + "method_version": { + "minLength": 1, + "title": "Method Version", + "type": "string" + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "parameters": { + "items": { + "$ref": "#/$defs/ExperimentParameterModel" + }, + "title": "Parameters", + "type": "array" + } + }, + "required": [ + "method_id", + "method_version", + "name" + ], + "title": "ExperimentDerivedMeasureMethodModel", + "type": "object" + }, + "ExperimentEvidenceRecordReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a raw captured evidence record.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "evidence-record", + "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": "ExperimentEvidenceRecordReferenceModel", + "type": "object" + }, + "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" + }, + "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", + "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" + } + }, + "$id": "https://aces.dev/schemas/experiment-derived-measure-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "value_status": { + "const": "reported" + } + }, + "required": [ + "value_status" + ] + }, + "then": { + "properties": { + "value": { + "not": { + "type": "null" + } + } + }, + "required": [ + "value" + ] + } + }, + { + "if": { + "properties": { + "value_status": { + "enum": [ + "missing", + "withheld", + "not-applicable" + ] + } + }, + "required": [ + "value_status" + ] + }, + "then": { + "properties": { + "value": { + "type": "null" + } + } + } + } + ], + "description": "EXP-709 derived measure/evaluation output computed from raw evidence.", + "properties": { + "derived_measure_id": { + "minLength": 1, + "title": "Derived Measure Id", + "type": "string" + }, + "generated_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": "Generated At", + "type": "string" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Limitations", + "type": "array" + }, + "measure_kind": { + "enum": [ + "metric", + "evaluation", + "score", + "summary", + "analysis-output", + "other" + ], + "title": "Measure Kind", + "type": "string" + }, + "measure_version": { + "minLength": 1, + "title": "Measure Version", + "type": "string" + }, + "method": { + "$ref": "#/$defs/ExperimentDerivedMeasureMethodModel" + }, + "metric_ref": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "provenance_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "title": "Provenance Refs", + "type": "array" + }, + "schema_version": { + "const": "experiment-derived-measure/v1", + "title": "Schema Version", + "type": "string" + }, + "source_evidence_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceRecordReferenceModel" + }, + "minItems": 1, + "title": "Source Evidence Refs", + "type": "array" + }, + "uncertainty": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Uncertainty" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Value" + }, + "value_status": { + "enum": [ + "reported", + "missing", + "withheld", + "not-applicable" + ], + "title": "Value Status", + "type": "string" + } + }, + "required": [ + "schema_version", + "derived_measure_id", + "measure_version", + "measure_kind", + "metric_ref", + "method", + "source_evidence_refs", + "generated_at", + "value_status" + ], + "title": "ExperimentDerivedMeasureModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Reported derived measures must include a value; missing/withheld/not-applicable measures must not.", + "id": "derived-measure-reported-value-present", + "inputs": [ + { + "contract_id": "experiment-derived-measure-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure" + }, + { + "description": "generated_at must be a valid RFC 3339 date-time.", + "id": "derived-measure-generated-at-valid", + "inputs": [ + { + "contract_id": "experiment-derived-measure-v1", + "instance_path": "#/generated_at" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure" + } + ], + "x-aces-plane": "derived_analysis", + "x-aces-semantic-profile": { + "contract_id": "experiment-derived-measure-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/experiment-core/experiment-evidence-record-v1.json b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json new file mode 100644 index 000000000..c252e3213 --- /dev/null +++ b/contracts/schemas/experiment-core/experiment-evidence-record-v1.json @@ -0,0 +1,749 @@ +{ + "$defs": { + "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", + "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" + }, + "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" + }, + "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" + }, + "ExperimentRawEvidenceContentModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "content_uri": { + "not": { + "type": "null" + } + } + }, + "required": [ + "content_uri" + ] + }, + "then": { + "properties": { + "content_checksum": { + "not": { + "type": "null" + } + } + }, + "required": [ + "content_checksum" + ] + } + } + ], + "anyOf": [ + { + "properties": { + "artifact_ref": { + "not": { + "type": "null" + } + } + }, + "required": [ + "artifact_ref" + ] + }, + { + "properties": { + "content_uri": { + "not": { + "type": "null" + } + } + }, + "required": [ + "content_uri" + ] + }, + { + "properties": { + "payload_summary": { + "not": { + "type": "null" + } + } + }, + "required": [ + "payload_summary" + ] + } + ], + "description": "Raw captured payload reference or bounded summary for EXP-708 records.", + "properties": { + "artifact_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentArtifactRefModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "content_checksum": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentChecksumModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "content_uri": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Content Uri" + }, + "loss_disclosure": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Loss Disclosure" + }, + "payload_summary": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Payload Summary" + } + }, + "title": "ExperimentRawEvidenceContentModel", + "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", + "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" + }, + "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" + } + }, + "$id": "https://aces.dev/schemas/experiment-evidence-record-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "redaction_state": { + "enum": [ + "redacted", + "withheld" + ] + } + }, + "required": [ + "redaction_state" + ] + }, + "then": { + "properties": { + "raw_content": { + "properties": { + "loss_disclosure": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "loss_disclosure" + ] + } + }, + "required": [ + "raw_content" + ] + } + } + ], + "description": "Raw captured EXP-708 evidence record, distinct from derived measures.", + "properties": { + "apparatus_context_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "capture_requirement_ref": { + "minLength": 1, + "title": "Capture Requirement Ref", + "type": "string" + }, + "capture_spec_ref": { + "$ref": "#/$defs/ExperimentCaptureSpecReferenceModel" + }, + "capture_window_ref": { + "minLength": 1, + "title": "Capture Window Ref", + "type": "string" + }, + "captured_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": "Captured At", + "type": "string" + }, + "evidence_kind": { + "enum": [ + "artifact", + "observation", + "trace", + "telemetry", + "log", + "packet-capture", + "other" + ], + "title": "Evidence Kind", + "type": "string" + }, + "evidence_record_id": { + "minLength": 1, + "title": "Evidence Record Id", + "type": "string" + }, + "provenance_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_content": { + "$ref": "#/$defs/ExperimentRawEvidenceContentModel" + }, + "record_version": { + "minLength": 1, + "title": "Record Version", + "type": "string" + }, + "redaction_state": { + "enum": [ + "none", + "redacted", + "withheld" + ], + "title": "Redaction State", + "type": "string" + }, + "run_ref": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "schema_version": { + "const": "experiment-evidence-record/v1", + "title": "Schema Version", + "type": "string" + }, + "sensitivity": { + "enum": [ + "public", + "internal", + "restricted", + "redacted" + ], + "title": "Sensitivity", + "type": "string" + }, + "source_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "minItems": 1, + "title": "Source Refs", + "type": "array" + }, + "task_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentTaskReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "schema_version", + "evidence_record_id", + "record_version", + "capture_spec_ref", + "capture_requirement_ref", + "run_ref", + "source_refs", + "evidence_kind", + "captured_at", + "capture_window_ref", + "raw_content", + "sensitivity", + "redaction_state" + ], + "title": "ExperimentEvidenceRecordModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Evidence records must carry raw content as an artifact reference, content URI with checksum, or bounded payload summary; redacted/withheld records must disclose loss.", + "id": "evidence-record-raw-content-present", + "inputs": [ + { + "contract_id": "experiment-evidence-record-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record" + }, + { + "description": "captured_at must be a valid RFC 3339 date-time.", + "id": "evidence-record-captured-at-valid", + "inputs": [ + { + "contract_id": "experiment-evidence-record-v1", + "instance_path": "#/captured_at" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record" + } + ], + "x-aces-plane": "captured_evidence", + "x-aces-semantic-profile": { + "contract_id": "experiment-evidence-record-v1", + "entry_schema_contract_id": "aces-semantic-invariants-v1", + "entry_schema_pointer": "#/$defs/AcesSemanticInvariantEntryModel", + "id": "aces-semantic-invariants-v1", + "keyword": "x-aces-invariants", + "required": true, + "uri": "https://aces.dev/schemas/semantic-invariants/v1" + } +} diff --git a/contracts/schemas/experiment-core/experiment-run-v1.json b/contracts/schemas/experiment-core/experiment-run-v1.json index 7a0a00d0a..fcdae6567 100644 --- a/contracts/schemas/experiment-core/experiment-run-v1.json +++ b/contracts/schemas/experiment-core/experiment-run-v1.json @@ -429,6 +429,354 @@ "title": "ExperimentArtifactRefModel", "type": "object" }, + "ExperimentAugmentationDisclosureModel": { + "additionalProperties": false, + "allOf": [ + { + "properties": { + "augmented_by_ref": { + "properties": { + "ref_kind": { + "enum": [ + "processor", + "backend" + ] + } + }, + "required": [ + "ref_kind" + ] + } + } + }, + { + "if": { + "properties": { + "classifications": { + "contains": { + "const": "environment_visible" + } + } + }, + "required": [ + "classifications" + ] + }, + "then": { + "properties": { + "carrier_refs": { + "contains": { + "properties": { + "ref_kind": { + "enum": [ + "apparatus-context", + "capture-spec", + "derived-measure", + "evidence-record", + "manifest", + "measurement-channel", + "profile", + "run", + "scenario-snapshot" + ] + } + }, + "required": [ + "ref_kind" + ] + } + }, + "environment_effect": { + "minLength": 1, + "type": "string" + }, + "evidence_refs": { + "minItems": 1 + } + }, + "required": [ + "carrier_refs", + "environment_effect", + "evidence_refs" + ] + } + }, + { + "if": { + "properties": { + "classifications": { + "contains": { + "const": "participant_visible" + } + } + }, + "required": [ + "classifications" + ] + }, + "then": { + "properties": { + "evidence_refs": { + "minItems": 1 + }, + "markings": { + "minItems": 1 + }, + "participant_visibility": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "participant_visibility", + "markings", + "evidence_refs" + ] + } + }, + { + "if": { + "properties": { + "classifications": { + "contains": { + "const": "comparability_relevant" + } + } + }, + "required": [ + "classifications" + ] + }, + "then": { + "properties": { + "comparability_effect": { + "minLength": 1, + "type": "string" + }, + "evidence_refs": { + "minItems": 1 + }, + "observer_effect": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "comparability_effect", + "observer_effect", + "evidence_refs" + ] + } + } + ], + "description": "Disclosure for processor/backend augmentation used by a run.", + "properties": { + "affected_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "title": "Affected Refs", + "type": "array" + }, + "augmentation_id": { + "minLength": 1, + "title": "Augmentation Id", + "type": "string" + }, + "augmented_by_ref": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "carrier_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "minItems": 1, + "title": "Carrier Refs", + "type": "array" + }, + "classifications": { + "items": { + "enum": [ + "apparatus_only", + "environment_visible", + "participant_visible", + "comparability_relevant" + ], + "type": "string" + }, + "minItems": 1, + "title": "Classifications", + "type": "array", + "uniqueItems": true + }, + "comparability_effect": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Comparability Effect" + }, + "disclosure_policy": { + "minLength": 1, + "title": "Disclosure Policy", + "type": "string" + }, + "environment_effect": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Environment Effect" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceRecordReferenceModel" + }, + "title": "Evidence Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array", + "uniqueItems": true + }, + "notes": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "observer_effect": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Observer Effect" + }, + "participant_visibility": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Visibility" + }, + "purpose": { + "enum": [ + "evidence", + "evaluation", + "operational", + "comparability", + "other" + ], + "title": "Purpose", + "type": "string" + }, + "realization_layer": { + "enum": [ + "processor", + "backend", + "apparatus", + "runtime-environment", + "participant-runtime", + "measurement-channel", + "analysis", + "other" + ], + "title": "Realization Layer", + "type": "string" + } + }, + "required": [ + "augmentation_id", + "purpose", + "realization_layer", + "classifications", + "augmented_by_ref", + "carrier_refs", + "disclosure_policy" + ], + "title": "ExperimentAugmentationDisclosureModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Augmentation disclosures must keep environment-visible, participant-visible, and comparability-relevant semantics explicit and must use processor/backend authority.", + "id": "augmentation-disclosure-semantics-valid", + "inputs": [ + { + "contract_id": "experiment-run-v1", + "instance_path": "#/augmentation_disclosures" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentAugmentationDisclosureModel._validate_augmentation_disclosure" + } + ] + }, + "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": [ @@ -582,6 +930,76 @@ "title": "ExperimentClockContextModel", "type": "object" }, + "ExperimentDerivedMeasureReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a derived measure or analysis output.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "derived-measure", + "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": "ExperimentDerivedMeasureReferenceModel", + "type": "object" + }, + "ExperimentEvidenceRecordReferenceModel": { + "additionalProperties": false, + "description": "Reference constrained to a raw captured evidence record.", + "properties": { + "ref_id": { + "minLength": 1, + "title": "Ref Id", + "type": "string" + }, + "ref_kind": { + "const": "evidence-record", + "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": "ExperimentEvidenceRecordReferenceModel", + "type": "object" + }, "ExperimentEvidenceSatisfactionReferenceModel": { "additionalProperties": false, "description": "Evidence concept reference that an artifact claims to satisfy.", @@ -992,6 +1410,197 @@ "title": "ExperimentParameterModel", "type": "object" }, + "ExperimentRealizedFormDisclosureModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "basis": { + "const": "processor-realized" + } + }, + "required": [ + "basis" + ] + }, + "then": { + "properties": { + "realized_by_ref": { + "properties": { + "ref_kind": { + "const": "processor" + } + }, + "required": [ + "ref_kind" + ] + } + } + } + }, + { + "if": { + "properties": { + "basis": { + "const": "backend-realized" + } + }, + "required": [ + "basis" + ] + }, + "then": { + "properties": { + "realized_by_ref": { + "properties": { + "ref_kind": { + "const": "backend" + } + }, + "required": [ + "ref_kind" + ] + } + } + } + } + ], + "anyOf": [ + { + "properties": { + "realized_ref": { + "not": { + "type": "null" + } + } + }, + "required": [ + "realized_ref" + ] + }, + { + "properties": { + "realized_value_summary": { + "not": { + "type": "null" + } + } + }, + "required": [ + "realized_value_summary" + ] + } + ], + "description": "Disclosure of one realized form chosen for an underspecified run concern.", + "properties": { + "authored_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "basis": { + "enum": [ + "author-declared", + "processor-realized", + "backend-realized", + "operator-supplied", + "observed" + ], + "title": "Basis", + "type": "string" + }, + "concern_id": { + "minLength": 1, + "title": "Concern Id", + "type": "string" + }, + "concern_kind": { + "enum": [ + "scenario-module", + "processor-selection", + "backend-selection", + "participant-implementation", + "apparatus-configuration", + "parameter-default", + "stochastic-control", + "measurement-channel", + "capture-window", + "other" + ], + "title": "Concern Kind", + "type": "string" + }, + "disclosure": { + "minLength": 1, + "title": "Disclosure", + "type": "string" + }, + "evidence_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceRecordReferenceModel" + }, + "title": "Evidence Refs", + "type": "array" + }, + "realized_by_ref": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "realized_ref": { + "anyOf": [ + { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "realized_value_summary": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Realized Value Summary" + } + }, + "required": [ + "concern_id", + "concern_kind", + "basis", + "realized_by_ref", + "disclosure" + ], + "title": "ExperimentRealizedFormDisclosureModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Every realized-form disclosure must name a realized reference or value summary and use the right processor/backend realization authority for processor-realized and backend-realized concerns.", + "id": "realized-form-disclosure-substantive", + "inputs": [ + { + "contract_id": "experiment-run-v1", + "instance_path": "#/realized_form_disclosures" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRealizedFormDisclosureModel._validate_realized_form_disclosure" + } + ] + }, "ExperimentReferenceModel": { "additionalProperties": false, "description": "Typed reference to an experiment-core or adjacent ACES artifact.", @@ -1026,12 +1635,16 @@ "protocol", "apparatus-context", "run", + "metric-definition", "result", "study", "manifest", "profile", "capability", + "capture-spec", "evidence", + "evidence-record", + "derived-measure", "measurement-channel", "analysis-artifact", "other" @@ -1227,6 +1840,70 @@ "title": "ExperimentRunEvidenceArtifactReferenceModel", "type": "object" }, + "ExperimentRunTraceabilityModel": { + "additionalProperties": false, + "description": "Canonical run provenance links across capture, evidence, measures, and claims.", + "properties": { + "capture_spec_refs": { + "items": { + "$ref": "#/$defs/ExperimentCaptureSpecReferenceModel" + }, + "minItems": 1, + "title": "Capture Spec Refs", + "type": "array" + }, + "claim_refs": { + "items": { + "$ref": "#/$defs/ExperimentReferenceModel" + }, + "title": "Claim Refs", + "type": "array" + }, + "derived_measure_refs": { + "items": { + "$ref": "#/$defs/ExperimentDerivedMeasureReferenceModel" + }, + "title": "Derived Measure Refs", + "type": "array" + }, + "evidence_record_refs": { + "items": { + "$ref": "#/$defs/ExperimentEvidenceRecordReferenceModel" + }, + "minItems": 1, + "title": "Evidence Record Refs", + "type": "array" + }, + "notes": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Notes", + "type": "array" + } + }, + "required": [ + "capture_spec_refs", + "evidence_record_refs" + ], + "title": "ExperimentRunTraceabilityModel", + "type": "object", + "x-aces-invariants": [ + { + "description": "Run provenance traceability references must be duplicate-free, and claim refs must be grounded by at least one derived measure ref.", + "id": "run-traceability-refs-unique", + "inputs": [ + { + "contract_id": "experiment-run-v1", + "instance_path": "#/traceability" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRunTraceabilityModel._validate_run_traceability" + } + ] + }, "ExperimentScenarioSnapshotReferenceModel": { "additionalProperties": false, "description": "Reference constrained to a sealed scenario snapshot.", @@ -1689,6 +2366,13 @@ "apparatus_context": { "$ref": "#/$defs/ExperimentApparatusContextModel" }, + "augmentation_disclosures": { + "items": { + "$ref": "#/$defs/ExperimentAugmentationDisclosureModel" + }, + "title": "Augmentation Disclosures", + "type": "array" + }, "clock_context": { "$ref": "#/$defs/ExperimentClockContextModel" }, @@ -1770,6 +2454,13 @@ ], "default": null }, + "realized_form_disclosures": { + "items": { + "$ref": "#/$defs/ExperimentRealizedFormDisclosureModel" + }, + "title": "Realized Form Disclosures", + "type": "array" + }, "result_summaries": { "additionalProperties": { "$ref": "#/$defs/ExperimentResultSummaryModel" @@ -1829,6 +2520,9 @@ "task_ref": { "$ref": "#/$defs/ExperimentTaskReferenceModel" }, + "traceability": { + "$ref": "#/$defs/ExperimentRunTraceabilityModel" + }, "used_refs": { "items": { "$ref": "#/$defs/ExperimentReferenceModel" @@ -1851,6 +2545,7 @@ "clock_context", "run_status", "outcome_status", + "traceability", "evidence_artifacts", "result_summaries" ], @@ -1893,6 +2588,30 @@ "level": "error", "validator": "aces_contracts.contracts.ExperimentRunModel._validate_archival_run" }, + { + "description": "Every realized-form disclosure evidence ref must also appear in the run traceability evidence refs.", + "id": "realized-form-evidence-refs-traced", + "inputs": [ + { + "contract_id": "experiment-run-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRunModel._validate_archival_run" + }, + { + "description": "Every augmentation disclosure evidence ref must also appear in the run traceability evidence refs, and augmentation_id values must be unique within the run.", + "id": "augmentation-disclosure-evidence-refs-traced", + "inputs": [ + { + "contract_id": "experiment-run-v1", + "instance_path": "#" + } + ], + "level": "error", + "validator": "aces_contracts.contracts.ExperimentRunModel._validate_archival_run" + }, { "description": "Run apparatus, result metric ids, and concrete evidence artifacts must satisfy the referenced task protocol.", "id": "task-run-protocol-binding-valid", diff --git a/contracts/schemas/experiment-core/experiment-study-v1.json b/contracts/schemas/experiment-core/experiment-study-v1.json index 039eb7940..7d9a49e54 100644 --- a/contracts/schemas/experiment-core/experiment-study-v1.json +++ b/contracts/schemas/experiment-core/experiment-study-v1.json @@ -657,12 +657,16 @@ "protocol", "apparatus-context", "run", + "metric-definition", "result", "study", "manifest", "profile", "capability", + "capture-spec", "evidence", + "evidence-record", + "derived-measure", "measurement-channel", "analysis-artifact", "other" diff --git a/contracts/schemas/experiment-core/experiment-task-v1.json b/contracts/schemas/experiment-core/experiment-task-v1.json index fc6b8c5f0..e92cbfac6 100644 --- a/contracts/schemas/experiment-core/experiment-task-v1.json +++ b/contracts/schemas/experiment-core/experiment-task-v1.json @@ -958,12 +958,16 @@ "protocol", "apparatus-context", "run", + "metric-definition", "result", "study", "manifest", "profile", "capability", + "capture-spec", "evidence", + "evidence-record", + "derived-measure", "measurement-channel", "analysis-artifact", "other" diff --git a/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json b/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json new file mode 100644 index 000000000..0f1df41c0 --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-joint-action-record-v1.json @@ -0,0 +1,928 @@ +{ + "$defs": { + "EventClassificationModel": { + "additionalProperties": false, + "description": "ACES-native normalized event classification tuple (ADR-054).", + "properties": { + "activity_id": { + "title": "Activity Id", + "type": "integer" + }, + "activity_name": { + "minLength": 1, + "title": "Activity Name", + "type": "string" + }, + "category_name": { + "minLength": 1, + "title": "Category Name", + "type": "string" + }, + "category_uid": { + "title": "Category Uid", + "type": "integer" + }, + "class_name": { + "minLength": 1, + "title": "Class Name", + "type": "string" + }, + "class_uid": { + "title": "Class Uid", + "type": "integer" + }, + "severity": { + "minLength": 1, + "title": "Severity", + "type": "string" + }, + "severity_id": { + "title": "Severity Id", + "type": "integer" + }, + "type_name": { + "minLength": 1, + "title": "Type Name", + "type": "string" + }, + "type_uid": { + "title": "Type Uid", + "type": "integer" + } + }, + "required": [ + "category_uid", + "category_name", + "class_uid", + "class_name", + "activity_id", + "activity_name", + "type_uid", + "type_name", + "severity_id", + "severity" + ], + "title": "EventClassificationModel", + "type": "object" + }, + "ParticipantJointActionAccessSetModel": { + "additionalProperties": false, + "description": "Read/write footprint for one member event in a joint action record.", + "properties": { + "evidence_stream_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Stream Refs", + "type": "array" + }, + "exclusive_resource_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Exclusive Resource Refs", + "type": "array" + }, + "member_event_ref": { + "minLength": 1, + "title": "Member Event Ref", + "type": "string" + }, + "shared_state_read_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Shared State Read Refs", + "type": "array" + }, + "shared_state_write_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Shared State Write Refs", + "type": "array" + }, + "visibility_effect_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Visibility Effect Refs", + "type": "array" + } + }, + "required": [ + "member_event_ref" + ], + "title": "ParticipantJointActionAccessSetModel", + "type": "object" + }, + "RawDataIntegrityModel": { + "additionalProperties": false, + "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", + "properties": { + "raw_data_hash": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash" + }, + "raw_data_hash_algorithm": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash Algorithm" + }, + "raw_data_is_truncated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Is Truncated" + }, + "raw_data_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Size" + }, + "raw_data_untruncated_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Untruncated Size" + } + }, + "title": "RawDataIntegrityModel", + "type": "object" + }, + "SourcePipelineModel": { + "additionalProperties": false, + "description": "Source product, identity, and pipeline-time facts for a mapped record.", + "properties": { + "correlation_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Correlation Uid" + }, + "log_name": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Name" + }, + "log_provider": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Provider" + }, + "log_source": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Source" + }, + "logged_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logged Time" + }, + "original_event_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Event Uid" + }, + "original_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Time" + }, + "processed_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Processed Time" + }, + "product_ref": { + "minLength": 1, + "title": "Product Ref", + "type": "string" + }, + "product_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Product Version" + }, + "sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence" + }, + "transmit_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transmit Time" + } + }, + "required": [ + "product_ref" + ], + "title": "SourcePipelineModel", + "type": "object" + }, + "SourceStatusModel": { + "additionalProperties": false, + "description": "Normalized source status claim for one participant runtime record.", + "properties": { + "source_status_label": { + "minLength": 1, + "title": "Source Status Label", + "type": "string" + }, + "source_status_mapping": { + "minLength": 1, + "title": "Source Status Mapping", + "type": "string" + }, + "status": { + "minLength": 1, + "title": "Status", + "type": "string" + }, + "status_code": { + "minLength": 1, + "title": "Status Code", + "type": "string" + }, + "status_detail": { + "minLength": 1, + "title": "Status Detail", + "type": "string" + }, + "status_id": { + "title": "Status Id", + "type": "integer" + } + }, + "required": [ + "status_id", + "status", + "status_code", + "status_detail", + "source_status_label", + "source_status_mapping" + ], + "title": "SourceStatusModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/participant-joint-action-record-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "RUN-308 joint action / concurrency record over behavior events.", + "properties": { + "access_sets": { + "items": { + "$ref": "#/$defs/ParticipantJointActionAccessSetModel" + }, + "minItems": 1, + "title": "Access Sets", + "type": "array" + }, + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "atomicity_scope": { + "enum": [ + "single_object", + "multi_object", + "coordination_interval", + "unsupported" + ], + "title": "Atomicity Scope", + "type": "string" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "conflict_class": { + "enum": [ + "none", + "read_write", + "write_write", + "unsupported" + ], + "title": "Conflict Class", + "type": "string" + }, + "conflict_policy": { + "enum": [ + "none", + "coordinate", + "serialize", + "reject", + "retry", + "withhold", + "merge", + "rollback", + "disclose_weak_guarantee", + "unsupported" + ], + "title": "Conflict Policy", + "type": "string" + }, + "episode_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Episode Id" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "minLength": 1, + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "exact_concurrency_claim": { + "default": false, + "title": "Exact Concurrency Claim", + "type": "boolean" + }, + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "fairness_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fairness Policy Ref" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "isolation_guarantee": { + "enum": [ + "none", + "serializable", + "snapshot", + "causal", + "unsupported" + ], + "title": "Isolation Guarantee", + "type": "string" + }, + "joint_action_set_id": { + "minLength": 1, + "title": "Joint Action Set Id", + "type": "string" + }, + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "member_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Member Event Refs", + "type": "array" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Object Marking Refs", + "type": "array" + }, + "occurred_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", + "type": "string" + }, + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Address" + }, + "participant_observation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Participant Observation Refs", + "type": "array" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "realized_order": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Realized Order", + "type": "array" + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "retry_limit": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retry Limit" + }, + "rollback_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Rollback Event Refs", + "type": "array" + }, + "schema_name": { + "minLength": 1, + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "minLength": 1, + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "simultaneity_group_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Simultaneity Group Ref" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "time_management_context_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Time Management Context Ref" + }, + "timeout_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Policy Ref" + }, + "unsupported_disclosure": { + "default": false, + "title": "Unsupported Disclosure", + "type": "boolean" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "authorization_scope", + "joint_action_set_id", + "member_event_refs", + "access_sets", + "conflict_class", + "conflict_policy", + "isolation_guarantee", + "atomicity_scope" + ], + "title": "ParticipantJointActionRecordModel", + "type": "object" +} diff --git a/contracts/schemas/participant-runtime/participant-outcome-report-v1.json b/contracts/schemas/participant-runtime/participant-outcome-report-v1.json index f4803d8c2..c8f369fe9 100644 --- a/contracts/schemas/participant-runtime/participant-outcome-report-v1.json +++ b/contracts/schemas/participant-runtime/participant-outcome-report-v1.json @@ -747,6 +747,7 @@ "items": { "$ref": "#/$defs/ParticipantOutcomeReportStateRelationshipModel" }, + "minItems": 1, "title": "State Relationships", "type": "array" }, @@ -780,7 +781,8 @@ "authorization_scope", "outcome_id", "interpretation_rule_ref", - "outcome_sources" + "outcome_sources", + "state_relationships" ], "title": "ParticipantOutcomeReportModel", "type": "object" diff --git a/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json b/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json index 527638654..74cc9aa94 100644 --- a/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json +++ b/contracts/schemas/participant-runtime/participant-shared-state-record-v1.json @@ -67,6 +67,86 @@ }, "ParticipantSharedStateAccessModel": { "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "access_kind": { + "enum": [ + "read", + "read_write" + ] + } + }, + "required": [ + "access_kind" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "read_revision": { + "type": "string" + } + }, + "required": [ + "read_revision" + ] + }, + { + "properties": { + "read_digest": { + "type": "string" + } + }, + "required": [ + "read_digest" + ] + } + ] + } + }, + { + "if": { + "properties": { + "access_kind": { + "enum": [ + "write", + "read_write" + ] + } + }, + "required": [ + "access_kind" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "write_revision": { + "type": "string" + } + }, + "required": [ + "write_revision" + ] + }, + { + "properties": { + "write_digest": { + "type": "string" + } + }, + "required": [ + "write_digest" + ] + } + ] + } + } + ], "description": "RUN-307 read/write access record over one shared-state address.", "properties": { "access_kind": { @@ -474,6 +554,28 @@ "$id": "https://aces.dev/schemas/participant-shared-state-record-v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "anyOf": [ + { + "properties": { + "revision": { + "type": "string" + } + }, + "required": [ + "revision" + ] + }, + { + "properties": { + "digest": { + "type": "string" + } + }, + "required": [ + "digest" + ] + } + ], "description": "RUN-307 versioned shared operational state-change report.", "properties": { "accesses": { diff --git a/contracts/schemas/participant-runtime/participant-time-management-context-v1.json b/contracts/schemas/participant-runtime/participant-time-management-context-v1.json new file mode 100644 index 000000000..ed29fb4be --- /dev/null +++ b/contracts/schemas/participant-runtime/participant-time-management-context-v1.json @@ -0,0 +1,804 @@ +{ + "$defs": { + "EventClassificationModel": { + "additionalProperties": false, + "description": "ACES-native normalized event classification tuple (ADR-054).", + "properties": { + "activity_id": { + "title": "Activity Id", + "type": "integer" + }, + "activity_name": { + "minLength": 1, + "title": "Activity Name", + "type": "string" + }, + "category_name": { + "minLength": 1, + "title": "Category Name", + "type": "string" + }, + "category_uid": { + "title": "Category Uid", + "type": "integer" + }, + "class_name": { + "minLength": 1, + "title": "Class Name", + "type": "string" + }, + "class_uid": { + "title": "Class Uid", + "type": "integer" + }, + "severity": { + "minLength": 1, + "title": "Severity", + "type": "string" + }, + "severity_id": { + "title": "Severity Id", + "type": "integer" + }, + "type_name": { + "minLength": 1, + "title": "Type Name", + "type": "string" + }, + "type_uid": { + "title": "Type Uid", + "type": "integer" + } + }, + "required": [ + "category_uid", + "category_name", + "class_uid", + "class_name", + "activity_id", + "activity_name", + "type_uid", + "type_name", + "severity_id", + "severity" + ], + "title": "EventClassificationModel", + "type": "object" + }, + "RawDataIntegrityModel": { + "additionalProperties": false, + "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", + "properties": { + "raw_data_hash": { + "anyOf": [ + { + "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash" + }, + "raw_data_hash_algorithm": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Hash Algorithm" + }, + "raw_data_is_truncated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Is Truncated" + }, + "raw_data_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Size" + }, + "raw_data_untruncated_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Untruncated Size" + } + }, + "title": "RawDataIntegrityModel", + "type": "object" + }, + "SourcePipelineModel": { + "additionalProperties": false, + "description": "Source product, identity, and pipeline-time facts for a mapped record.", + "properties": { + "correlation_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Correlation Uid" + }, + "log_name": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Name" + }, + "log_provider": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Provider" + }, + "log_source": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Source" + }, + "logged_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logged Time" + }, + "original_event_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Event Uid" + }, + "original_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Time" + }, + "processed_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Processed Time" + }, + "product_ref": { + "minLength": 1, + "title": "Product Ref", + "type": "string" + }, + "product_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Product Version" + }, + "sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence" + }, + "transmit_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transmit Time" + } + }, + "required": [ + "product_ref" + ], + "title": "SourcePipelineModel", + "type": "object" + }, + "SourceStatusModel": { + "additionalProperties": false, + "description": "Normalized source status claim for one participant runtime record.", + "properties": { + "source_status_label": { + "minLength": 1, + "title": "Source Status Label", + "type": "string" + }, + "source_status_mapping": { + "minLength": 1, + "title": "Source Status Mapping", + "type": "string" + }, + "status": { + "minLength": 1, + "title": "Status", + "type": "string" + }, + "status_code": { + "minLength": 1, + "title": "Status Code", + "type": "string" + }, + "status_detail": { + "minLength": 1, + "title": "Status Detail", + "type": "string" + }, + "status_id": { + "title": "Status Id", + "type": "integer" + } + }, + "required": [ + "status_id", + "status", + "status_code", + "status_detail", + "source_status_label", + "source_status_mapping" + ], + "title": "SourceStatusModel", + "type": "object" + } + }, + "$id": "https://aces.dev/schemas/participant-time-management-context-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "RUN-308 time-management basis for concurrent or distributed runtime claims.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "advance_by": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Advance By" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "backend_serialized": { + "default": false, + "title": "Backend Serialized", + "type": "boolean" + }, + "basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Basis", + "type": "string" + }, + "claim_strength": { + "enum": [ + "display", + "bounded", + "exact", + "unsupported" + ], + "title": "Claim Strength", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "clock_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Clock Ref" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "context_id": { + "minLength": 1, + "title": "Context Id", + "type": "string" + }, + "episode_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Episode Id" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "minLength": 1, + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" + }, + "lookahead": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lookahead" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "mode": { + "enum": [ + "display", + "pacing", + "lookahead", + "rollback", + "devs", + "fmi", + "backend_serialized", + "unsupported" + ], + "title": "Mode", + "type": "string" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Object Marking Refs", + "type": "array" + }, + "occurred_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", + "type": "string" + }, + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Address" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "rollback_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Rollback Event Refs", + "type": "array" + }, + "schema_name": { + "minLength": 1, + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "minLength": 1, + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "unsupported_disclosure": { + "default": false, + "title": "Unsupported Disclosure", + "type": "boolean" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "authorization_scope", + "context_id", + "mode", + "claim_strength", + "basis" + ], + "title": "ParticipantTimeManagementContextModel", + "type": "object" +} diff --git a/contracts/schemas/profiles/backend-profile-v1.json b/contracts/schemas/profiles/backend-profile-v1.json index 200a8a8d2..8e3e08bc5 100644 --- a/contracts/schemas/profiles/backend-profile-v1.json +++ b/contracts/schemas/profiles/backend-profile-v1.json @@ -29,7 +29,12 @@ "participant-lifecycle-event-v1", "participant-observation-envelope-v1", "participant-shared-state-record-v1", - "participant-outcome-report-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" ], "type": "string" }, diff --git a/contracts/schemas/snapshots/runtime-snapshot-v1.json b/contracts/schemas/snapshots/runtime-snapshot-v1.json index 89fadca95..abe93d366 100644 --- a/contracts/schemas/snapshots/runtime-snapshot-v1.json +++ b/contracts/schemas/snapshots/runtime-snapshot-v1.json @@ -185,6 +185,71 @@ "title": "EvaluationResultStateModel", "type": "object" }, + "EventClassificationModel": { + "additionalProperties": false, + "description": "ACES-native normalized event classification tuple (ADR-054).", + "properties": { + "activity_id": { + "title": "Activity Id", + "type": "integer" + }, + "activity_name": { + "minLength": 1, + "title": "Activity Name", + "type": "string" + }, + "category_name": { + "minLength": 1, + "title": "Category Name", + "type": "string" + }, + "category_uid": { + "title": "Category Uid", + "type": "integer" + }, + "class_name": { + "minLength": 1, + "title": "Class Name", + "type": "string" + }, + "class_uid": { + "title": "Class Uid", + "type": "integer" + }, + "severity": { + "minLength": 1, + "title": "Severity", + "type": "string" + }, + "severity_id": { + "title": "Severity Id", + "type": "integer" + }, + "type_name": { + "minLength": 1, + "title": "Type Name", + "type": "string" + }, + "type_uid": { + "title": "Type Uid", + "type": "integer" + } + }, + "required": [ + "category_uid", + "category_name", + "class_uid", + "class_name", + "activity_id", + "activity_name", + "type_uid", + "type_name", + "severity_id", + "severity" + ], + "title": "EventClassificationModel", + "type": "object" + }, "ExplicitnessClass": { "description": "SEM-218 author-intent class for a declaration.", "enum": [ @@ -1194,78 +1259,171 @@ "title": "ParticipantInteractionClass", "type": "string" }, - "ParticipantLifecycleOperationState": { - "description": "RUN-306 operation states for execution-attempt records.", - "enum": [ - "submitted", - "acknowledged", - "running", - "blocked", - "completed", - "partial", - "failed", - "timed_out", - "cancelled", - "unknown", - "unsupported" - ], - "title": "ParticipantLifecycleOperationState", - "type": "string" - }, - "ParticipantObservationDetailsModel": { + "ParticipantJointActionAccessSetModel": { "additionalProperties": false, + "description": "Read/write footprint for one member event in a joint action record.", "properties": { - "disclosed_refs": { + "evidence_stream_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Disclosed Refs", + "title": "Evidence Stream Refs", "type": "array" }, - "evidence_refs": { + "exclusive_resource_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Evidence Refs", + "title": "Exclusive Resource Refs", "type": "array" }, - "visible_refs": { + "member_event_ref": { + "minLength": 1, + "title": "Member Event Ref", + "type": "string" + }, + "shared_state_read_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Visible Refs", + "title": "Shared State Read Refs", + "type": "array" + }, + "shared_state_write_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Shared State Write Refs", + "type": "array" + }, + "visibility_effect_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Visibility Effect Refs", "type": "array" } }, - "title": "ParticipantObservationDetailsModel", - "type": "object" - }, - "ParticipantObservationStatus": { - "description": "Terminal interpretation of a participant observation event.", - "enum": [ - "terminal", - "orphaned_action" + "required": [ + "member_event_ref" ], - "title": "ParticipantObservationStatus", - "type": "string" + "title": "ParticipantJointActionAccessSetModel", + "type": "object" }, - "ParticipantOutcomeInterpretationRecordModel": { + "ParticipantJointActionRecordModel": { "additionalProperties": false, + "description": "RUN-308 joint action / concurrency record over behavior events.", "properties": { - "diagnostics": { + "access_sets": { "items": { - "minLength": 1, - "type": "string" + "$ref": "#/$defs/ParticipantJointActionAccessSetModel" }, - "title": "Diagnostics", + "minItems": 1, + "title": "Access Sets", "type": "array" }, + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "atomicity_scope": { + "enum": [ + "single_object", + "multi_object", + "coordination_interval", + "unsupported" + ], + "title": "Atomicity Scope", + "type": "string" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "conflict_class": { + "enum": [ + "none", + "read_write", + "write_write", + "unsupported" + ], + "title": "Conflict Class", + "type": "string" + }, + "conflict_policy": { + "enum": [ + "none", + "coordinate", + "serialize", + "reject", + "retry", + "withhold", + "merge", + "rollback", + "disclose_weak_guarantee", + "unsupported" + ], + "title": "Conflict Policy", + "type": "string" + }, "episode_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Episode Id" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { "minLength": 1, - "title": "Episode Id", + "title": "Event Id", + "type": "string" + }, + "event_type": { + "minLength": 1, + "title": "Event Type", "type": "string" }, "evidence_refs": { @@ -1273,146 +1431,1660 @@ "minLength": 1, "type": "string" }, - "minItems": 1, "title": "Evidence Refs", "type": "array" }, - "interpretation_id": { + "exact_concurrency_claim": { + "default": false, + "title": "Exact Concurrency Claim", + "type": "boolean" + }, + "extension_policy": { "minLength": 1, - "title": "Interpretation Id", + "title": "Extension Policy", "type": "string" }, - "limitations": { - "items": { - "minLength": 1, - "type": "string" + "fairness_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fairness Policy Ref" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" }, - "minItems": 1, - "title": "Limitations", - "type": "array" + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" }, - "observation_point": { + "ingested_at": { + "format": "date-time", "minLength": 1, - "title": "Observation Point", + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", "type": "string" }, - "participant_address": { - "minLength": 1, - "title": "Participant Address", + "isolation_guarantee": { + "enum": [ + "none", + "serializable", + "snapshot", + "causal", + "unsupported" + ], + "title": "Isolation Guarantee", "type": "string" }, - "rule_address": { + "joint_action_set_id": { "minLength": 1, - "title": "Rule Address", + "title": "Joint Action Set Id", "type": "string" }, - "source_bindings": { - "items": { - "$ref": "#/$defs/ParticipantOutcomeSourceRecordModel" - }, - "minItems": 1, - "title": "Source Bindings", - "type": "array" + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" }, - "target_bindings": { + "marking_definition_refs": { "items": { - "$ref": "#/$defs/ParticipantOutcomeTargetRecordModel" + "minLength": 1, + "type": "string" }, - "minItems": 1, - "title": "Target Bindings", + "title": "Marking Definition Refs", "type": "array" - } - }, - "required": [ - "interpretation_id", - "rule_address", - "participant_address", - "episode_id", - "observation_point", - "source_bindings", - "target_bindings", - "evidence_refs", - "limitations" - ], - "title": "ParticipantOutcomeInterpretationRecordModel", - "type": "object" - }, - "ParticipantOutcomeSourceRecordModel": { - "additionalProperties": false, - "properties": { - "diagnostics": { + }, + "markings": { "items": { "minLength": 1, "type": "string" }, - "title": "Diagnostics", + "title": "Markings", "type": "array" }, - "evidence_refs": { + "member_event_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Evidence Refs", + "minItems": 1, + "title": "Member Event Refs", "type": "array" }, - "observed_value": { - "minLength": 1, - "title": "Observed Value", - "type": "string" - }, - "provenance_refs": { + "object_marking_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Provenance Refs", + "title": "Object Marking Refs", "type": "array" }, - "ref": { + "occurred_at": { + "format": "date-time", "minLength": 1, - "title": "Ref", + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", "type": "string" }, - "source_id": { - "minLength": 1, - "title": "Source Id", + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", "type": "string" }, - "source_layer": { - "$ref": "#/$defs/OutcomeInterpretationSourceLayer" - } - }, + "participant_address": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Address" + }, + "participant_observation_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Participant Observation Refs", + "type": "array" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "realized_order": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Realized Order", + "type": "array" + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "retry_limit": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Retry Limit" + }, + "rollback_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Rollback Event Refs", + "type": "array" + }, + "schema_name": { + "minLength": 1, + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "minLength": 1, + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "simultaneity_group_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Simultaneity Group Ref" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "time_management_context_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Time Management Context Ref" + }, + "timeout_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Timeout Policy Ref" + }, + "unsupported_disclosure": { + "default": false, + "title": "Unsupported Disclosure", + "type": "boolean" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "authorization_scope", + "joint_action_set_id", + "member_event_refs", + "access_sets", + "conflict_class", + "conflict_policy", + "isolation_guarantee", + "atomicity_scope" + ], + "title": "ParticipantJointActionRecordModel", + "type": "object" + }, + "ParticipantLifecycleOperationState": { + "description": "RUN-306 operation states for execution-attempt records.", + "enum": [ + "submitted", + "acknowledged", + "running", + "blocked", + "completed", + "partial", + "failed", + "timed_out", + "cancelled", + "unknown", + "unsupported" + ], + "title": "ParticipantLifecycleOperationState", + "type": "string" + }, + "ParticipantObservationDetailsModel": { + "additionalProperties": false, + "properties": { + "disclosed_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Disclosed Refs", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "visible_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Visible Refs", + "type": "array" + } + }, + "title": "ParticipantObservationDetailsModel", + "type": "object" + }, + "ParticipantObservationStatus": { + "description": "Terminal interpretation of a participant observation event.", + "enum": [ + "terminal", + "orphaned_action" + ], + "title": "ParticipantObservationStatus", + "type": "string" + }, + "ParticipantOutcomeInterpretationRecordModel": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "episode_id": { + "minLength": 1, + "title": "Episode Id", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "interpretation_id": { + "minLength": 1, + "title": "Interpretation Id", + "type": "string" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "observation_point": { + "minLength": 1, + "title": "Observation Point", + "type": "string" + }, + "participant_address": { + "minLength": 1, + "title": "Participant Address", + "type": "string" + }, + "rule_address": { + "minLength": 1, + "title": "Rule Address", + "type": "string" + }, + "source_bindings": { + "items": { + "$ref": "#/$defs/ParticipantOutcomeSourceRecordModel" + }, + "minItems": 1, + "title": "Source Bindings", + "type": "array" + }, + "target_bindings": { + "items": { + "$ref": "#/$defs/ParticipantOutcomeTargetRecordModel" + }, + "minItems": 1, + "title": "Target Bindings", + "type": "array" + } + }, + "required": [ + "interpretation_id", + "rule_address", + "participant_address", + "episode_id", + "observation_point", + "source_bindings", + "target_bindings", + "evidence_refs", + "limitations" + ], + "title": "ParticipantOutcomeInterpretationRecordModel", + "type": "object" + }, + "ParticipantOutcomeSourceRecordModel": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "observed_value": { + "minLength": 1, + "title": "Observed Value", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "source_id": { + "minLength": 1, + "title": "Source Id", + "type": "string" + }, + "source_layer": { + "$ref": "#/$defs/OutcomeInterpretationSourceLayer" + } + }, + "required": [ + "source_id", + "source_layer", + "ref", + "observed_value" + ], + "title": "ParticipantOutcomeSourceRecordModel", + "type": "object" + }, + "ParticipantOutcomeTargetRecordModel": { + "additionalProperties": false, + "properties": { + "diagnostics": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Diagnostics", + "type": "array" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Evidence Refs", + "type": "array" + }, + "governance_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Governance Ref" + }, + "interpreted_value": { + "minLength": 1, + "title": "Interpreted Value", + "type": "string" + }, + "limitations": { + "items": { + "minLength": 1, + "type": "string" + }, + "minItems": 1, + "title": "Limitations", + "type": "array" + }, + "ref": { + "minLength": 1, + "title": "Ref", + "type": "string" + }, + "target_id": { + "minLength": 1, + "title": "Target Id", + "type": "string" + }, + "target_layer": { + "$ref": "#/$defs/OutcomeInterpretationTargetLayer" + } + }, + "required": [ + "target_id", + "target_layer", + "ref", + "interpreted_value", + "evidence_refs", + "limitations" + ], + "title": "ParticipantOutcomeTargetRecordModel", + "type": "object" + }, + "ParticipantPhaseRealization": { + "description": "RUN-306 realization modes for an observable lifecycle phase.", + "enum": [ + "observed", + "runtime_mediated", + "externally_supplied", + "opaque", + "unknown", + "not_applicable", + "unsupported" + ], + "title": "ParticipantPhaseRealization", + "type": "string" + }, + "ParticipantPreconditionClass": { + "description": "SEM-211 precondition classes for participant action applicability.", + "enum": [ + "authority", + "capability", + "target", + "knowledge", + "resource", + "temporal", + "interaction", + "realization" + ], + "title": "ParticipantPreconditionClass", + "type": "string" + }, + "ParticipantRuntimeLifecyclePhase": { + "description": "RUN-306 observable participant runtime lifecycle phases.", + "enum": [ + "intent_or_proposal", + "selection_or_admission", + "execution_attempt", + "observation_emission", + "state_update_commit" + ], + "title": "ParticipantRuntimeLifecyclePhase", + "type": "string" + }, + "ParticipantSharedStateAccessModel": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "access_kind": { + "enum": [ + "read", + "read_write" + ] + } + }, + "required": [ + "access_kind" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "read_revision": { + "type": "string" + } + }, + "required": [ + "read_revision" + ] + }, + { + "properties": { + "read_digest": { + "type": "string" + } + }, + "required": [ + "read_digest" + ] + } + ] + } + }, + { + "if": { + "properties": { + "access_kind": { + "enum": [ + "write", + "read_write" + ] + } + }, + "required": [ + "access_kind" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "write_revision": { + "type": "string" + } + }, + "required": [ + "write_revision" + ] + }, + { + "properties": { + "write_digest": { + "type": "string" + } + }, + "required": [ + "write_digest" + ] + } + ] + } + } + ], + "description": "RUN-307 read/write access record over one shared-state address.", + "properties": { + "access_kind": { + "enum": [ + "read", + "write", + "read_write" + ], + "title": "Access Kind", + "type": "string" + }, + "access_purpose": { + "minLength": 1, + "title": "Access Purpose", + "type": "string" + }, + "atomic_group_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Atomic Group Ref" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "read_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": "Read Digest" + }, + "read_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Read Revision" + }, + "snapshot_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Snapshot Ref" + }, + "state_address": { + "minLength": 1, + "title": "State Address", + "type": "string" + }, + "write_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": "Write Digest" + }, + "write_revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Write Revision" + } + }, + "required": [ + "state_address", + "access_kind", + "access_purpose" + ], + "title": "ParticipantSharedStateAccessModel", + "type": "object" + }, + "ParticipantSharedStateRecordModel": { + "additionalProperties": false, + "anyOf": [ + { + "properties": { + "revision": { + "type": "string" + } + }, + "required": [ + "revision" + ] + }, + { + "properties": { + "digest": { + "type": "string" + } + }, + "required": [ + "digest" + ] + } + ], + "description": "RUN-307 versioned shared operational state-change report.", + "properties": { + "accesses": { + "items": { + "$ref": "#/$defs/ParticipantSharedStateAccessModel" + }, + "title": "Accesses", + "type": "array" + }, + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "conflict_policy": { + "enum": [ + "coordinate", + "serialize", + "reject", + "retry", + "withhold", + "merge", + "rollback", + "disclose_weak_guarantee", + "unsupported" + ], + "title": "Conflict Policy", + "type": "string" + }, + "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": "Digest" + }, + "episode_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Episode Id" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "minLength": 1, + "title": "Event Type", + "type": "string" + }, + "evidence_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Evidence Refs", + "type": "array" + }, + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "logical_order_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logical Order Ref" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "object_marking_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Object Marking Refs", + "type": "array" + }, + "occurred_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", + "type": "string" + }, + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Address" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "predecessor_revision_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Revision Refs", + "type": "array" + }, + "producer_ref": { + "minLength": 1, + "title": "Producer Ref", + "type": "string" + }, + "provenance": { + "minLength": 1, + "title": "Provenance", + "type": "string" + }, + "provenance_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Provenance Refs", + "type": "array" + }, + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "recorded_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", + "type": "string" + }, + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "revision": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Revision" + }, + "schema_name": { + "minLength": 1, + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "minLength": 1, + "title": "Schema Version", + "type": "string" + }, + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "state_address": { + "minLength": 1, + "title": "State Address", + "type": "string" + }, + "state_kind": { + "minLength": 1, + "title": "State Kind", + "type": "string" + }, + "state_scope": { + "minLength": 1, + "title": "State Scope", + "type": "string" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "value_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Value Ref" + }, + "visibility_projection_basis": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Visibility Projection Basis" + } + }, "required": [ - "source_id", - "source_layer", - "ref", - "observed_value" + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "authorization_scope", + "state_address", + "state_scope", + "state_kind", + "conflict_policy", + "provenance" ], - "title": "ParticipantOutcomeSourceRecordModel", + "title": "ParticipantSharedStateRecordModel", "type": "object" }, - "ParticipantOutcomeTargetRecordModel": { + "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" + }, + "ParticipantTemporalRuntimeContextModel": { "additionalProperties": false, "properties": { - "diagnostics": { + "backend_disclosure_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Diagnostics", + "title": "Backend Disclosure Refs", "type": "array" }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "event_points": { + "items": { + "$ref": "#/$defs/ParticipantTemporalEventPoint" + }, + "minItems": 1, + "title": "Event Points", + "type": "array" + }, + "observation_point": { + "minLength": 1, + "title": "Observation Point", + "type": "string" + }, + "replay_boundary": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Replay Boundary" + }, + "reset_boundary": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reset Boundary" + }, + "temporal_contract_id": { + "minLength": 1, + "title": "Temporal Contract Id", + "type": "string" + }, + "time_domain": { + "$ref": "#/$defs/ParticipantTimeDomain" + } + }, + "required": [ + "temporal_contract_id", + "time_domain", + "clock_authority", + "event_points", + "observation_point" + ], + "title": "ParticipantTemporalRuntimeContextModel", + "type": "object" + }, + "ParticipantTimeDomain": { + "description": "Distinct SEM-213 time domains.", + "enum": [ + "episode_step", + "scenario_time", + "simulation_time", + "backend_time", + "wall_clock_time" + ], + "title": "ParticipantTimeDomain", + "type": "string" + }, + "ParticipantTimeManagementContextModel": { + "additionalProperties": false, + "description": "RUN-308 time-management basis for concurrent or distributed runtime claims.", + "properties": { + "actor_ref": { + "minLength": 1, + "title": "Actor Ref", + "type": "string" + }, + "advance_by": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Advance By" + }, + "authorization_scope": { + "minLength": 1, + "title": "Authorization Scope", + "type": "string" + }, + "backend_serialized": { + "default": false, + "title": "Backend Serialized", + "type": "boolean" + }, + "basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Basis", + "type": "string" + }, + "claim_strength": { + "enum": [ + "display", + "bounded", + "exact", + "unsupported" + ], + "title": "Claim Strength", + "type": "string" + }, + "clock_authority": { + "minLength": 1, + "title": "Clock Authority", + "type": "string" + }, + "clock_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Clock Ref" + }, + "confidence": { + "anyOf": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Confidence" + }, + "context_id": { + "minLength": 1, + "title": "Context Id", + "type": "string" + }, + "episode_id": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Episode Id" + }, + "event_classification": { + "anyOf": [ + { + "$ref": "#/$defs/EventClassificationModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "event_id": { + "minLength": 1, + "title": "Event Id", + "type": "string" + }, + "event_type": { + "minLength": 1, + "title": "Event Type", + "type": "string" + }, "evidence_refs": { "items": { "minLength": 1, "type": "string" }, - "minItems": 1, "title": "Evidence Refs", "type": "array" }, - "governance_ref": { + "extension_policy": { + "minLength": 1, + "title": "Extension Policy", + "type": "string" + }, + "granular_markings": { + "additionalProperties": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "propertyNames": { + "minLength": 1 + }, + "title": "Granular Markings", + "type": "object" + }, + "ingested_at": { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Ingested At", + "type": "string" + }, + "logical_order_ref": { "anyOf": [ { "minLength": 1, @@ -1423,138 +3095,290 @@ } ], "default": null, - "title": "Governance Ref" + "title": "Logical Order Ref" }, - "interpreted_value": { - "minLength": 1, - "title": "Interpreted Value", + "lookahead": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lookahead" + }, + "marking_definition_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Marking Definition Refs", + "type": "array" + }, + "markings": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Markings", + "type": "array" + }, + "mode": { + "enum": [ + "display", + "pacing", + "lookahead", + "rollback", + "devs", + "fmi", + "backend_serialized", + "unsupported" + ], + "title": "Mode", "type": "string" }, - "limitations": { + "object_marking_refs": { "items": { "minLength": 1, "type": "string" }, - "minItems": 1, - "title": "Limitations", + "title": "Object Marking Refs", "type": "array" }, - "ref": { + "occurred_at": { + "format": "date-time", "minLength": 1, - "title": "Ref", + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Occurred At", "type": "string" }, - "target_id": { + "ordering_basis": { + "enum": [ + "total_order", + "partial_order", + "simultaneous", + "serialized_backend_order", + "simulation_tick", + "control_plane_order", + "logical_clock", + "vector_clock", + "wall_clock_only", + "unknown", + "unsupported" + ], + "title": "Ordering Basis", + "type": "string" + }, + "participant_address": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Participant Address" + }, + "predecessor_event_refs": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Predecessor Event Refs", + "type": "array" + }, + "producer_ref": { "minLength": 1, - "title": "Target Id", + "title": "Producer Ref", "type": "string" }, - "target_layer": { - "$ref": "#/$defs/OutcomeInterpretationTargetLayer" - } - }, - "required": [ - "target_id", - "target_layer", - "ref", - "interpreted_value", - "evidence_refs", - "limitations" - ], - "title": "ParticipantOutcomeTargetRecordModel", - "type": "object" - }, - "ParticipantPhaseRealization": { - "description": "RUN-306 realization modes for an observable lifecycle phase.", - "enum": [ - "observed", - "runtime_mediated", - "externally_supplied", - "opaque", - "unknown", - "not_applicable", - "unsupported" - ], - "title": "ParticipantPhaseRealization", - "type": "string" - }, - "ParticipantPreconditionClass": { - "description": "SEM-211 precondition classes for participant action applicability.", - "enum": [ - "authority", - "capability", - "target", - "knowledge", - "resource", - "temporal", - "interaction", - "realization" - ], - "title": "ParticipantPreconditionClass", - "type": "string" - }, - "ParticipantRuntimeLifecyclePhase": { - "description": "RUN-306 observable participant runtime lifecycle phases.", - "enum": [ - "intent_or_proposal", - "selection_or_admission", - "execution_attempt", - "observation_emission", - "state_update_commit" - ], - "title": "ParticipantRuntimeLifecyclePhase", - "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" - }, - "ParticipantTemporalRuntimeContextModel": { - "additionalProperties": false, - "properties": { - "backend_disclosure_refs": { + "provenance_refs": { "items": { "minLength": 1, "type": "string" }, - "title": "Backend Disclosure Refs", + "title": "Provenance Refs", "type": "array" }, - "clock_authority": { + "raw_data_integrity": { + "anyOf": [ + { + "$ref": "#/$defs/RawDataIntegrityModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "recorded_at": { + "format": "date-time", "minLength": 1, - "title": "Clock Authority", + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "title": "Recorded At", "type": "string" }, - "event_points": { + "redaction_policy_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Redaction Policy Ref" + }, + "rollback_event_refs": { "items": { - "$ref": "#/$defs/ParticipantTemporalEventPoint" + "minLength": 1, + "type": "string" }, - "minItems": 1, - "title": "Event Points", + "title": "Rollback Event Refs", "type": "array" }, - "observation_point": { + "schema_name": { "minLength": 1, - "title": "Observation Point", + "title": "Schema Name", + "type": "string" + }, + "schema_version": { + "minLength": 1, + "title": "Schema Version", "type": "string" }, - "replay_boundary": { + "sequence_number": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence Number" + }, + "source_pipeline": { + "anyOf": [ + { + "$ref": "#/$defs/SourcePipelineModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_raw_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Raw Ref" + }, + "source_record_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Record Ref" + }, + "source_status": { + "anyOf": [ + { + "$ref": "#/$defs/SourceStatusModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_system_ref": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source System Ref" + }, + "temporal_context": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Temporal Context" + }, + "unsupported_disclosure": { + "default": false, + "title": "Unsupported Disclosure", + "type": "boolean" + } + }, + "required": [ + "event_id", + "schema_name", + "schema_version", + "event_type", + "extension_policy", + "occurred_at", + "recorded_at", + "ingested_at", + "clock_authority", + "ordering_basis", + "actor_ref", + "producer_ref", + "authorization_scope", + "context_id", + "mode", + "claim_strength", + "basis" + ], + "title": "ParticipantTimeManagementContextModel", + "type": "object" + }, + "RawDataIntegrityModel": { + "additionalProperties": false, + "description": "Hash, size, and truncation facts for raw data behind a runtime claim.", + "properties": { + "raw_data_hash": { "anyOf": [ { "minLength": 1, + "pattern": "^(?:sha256:[A-Fa-f0-9]{64}|sha384:[A-Fa-f0-9]{96}|sha512:[A-Fa-f0-9]{128}|blake3:[A-Fa-f0-9]{64})$", "type": "string" }, { @@ -1562,9 +3386,9 @@ } ], "default": null, - "title": "Replay Boundary" + "title": "Raw Data Hash" }, - "reset_boundary": { + "raw_data_hash_algorithm": { "anyOf": [ { "minLength": 1, @@ -1575,39 +3399,50 @@ } ], "default": null, - "title": "Reset Boundary" + "title": "Raw Data Hash Algorithm" }, - "temporal_contract_id": { - "minLength": 1, - "title": "Temporal Contract Id", - "type": "string" + "raw_data_is_truncated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Is Truncated" }, - "time_domain": { - "$ref": "#/$defs/ParticipantTimeDomain" + "raw_data_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Size" + }, + "raw_data_untruncated_size": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Raw Data Untruncated Size" } }, - "required": [ - "temporal_contract_id", - "time_domain", - "clock_authority", - "event_points", - "observation_point" - ], - "title": "ParticipantTemporalRuntimeContextModel", + "title": "RawDataIntegrityModel", "type": "object" }, - "ParticipantTimeDomain": { - "description": "Distinct SEM-213 time domains.", - "enum": [ - "episode_step", - "scenario_time", - "simulation_time", - "backend_time", - "wall_clock_time" - ], - "title": "ParticipantTimeDomain", - "type": "string" - }, "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.", @@ -1698,6 +3533,218 @@ "title": "SnapshotEntryModel", "type": "object" }, + "SourcePipelineModel": { + "additionalProperties": false, + "description": "Source product, identity, and pipeline-time facts for a mapped record.", + "properties": { + "correlation_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Correlation Uid" + }, + "log_name": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Name" + }, + "log_provider": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Provider" + }, + "log_source": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Log Source" + }, + "logged_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Logged Time" + }, + "original_event_uid": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Event Uid" + }, + "original_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Original Time" + }, + "processed_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Processed Time" + }, + "product_ref": { + "minLength": 1, + "title": "Product Ref", + "type": "string" + }, + "product_version": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Product Version" + }, + "sequence": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sequence" + }, + "transmit_time": { + "anyOf": [ + { + "format": "date-time", + "minLength": 1, + "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt](?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:[Zz]|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Transmit Time" + } + }, + "required": [ + "product_ref" + ], + "title": "SourcePipelineModel", + "type": "object" + }, + "SourceStatusModel": { + "additionalProperties": false, + "description": "Normalized source status claim for one participant runtime record.", + "properties": { + "source_status_label": { + "minLength": 1, + "title": "Source Status Label", + "type": "string" + }, + "source_status_mapping": { + "minLength": 1, + "title": "Source Status Mapping", + "type": "string" + }, + "status": { + "minLength": 1, + "title": "Status", + "type": "string" + }, + "status_code": { + "minLength": 1, + "title": "Status Code", + "type": "string" + }, + "status_detail": { + "minLength": 1, + "title": "Status Detail", + "type": "string" + }, + "status_id": { + "title": "Status Id", + "type": "integer" + } + }, + "required": [ + "status_id", + "status", + "status_code", + "status_detail", + "source_status_label", + "source_status_mapping" + ], + "title": "SourceStatusModel", + "type": "object" + }, "WorkflowExecutionStateModel": { "additionalProperties": false, "properties": { @@ -1922,6 +3969,13 @@ "title": "Evaluation Results", "type": "object" }, + "joint_action_records": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantJointActionRecordModel" + }, + "title": "Joint Action Records", + "type": "object" + }, "metadata": { "additionalProperties": true, "title": "Metadata", @@ -1983,6 +4037,30 @@ "default": "runtime-snapshot/v1", "title": "Schema Version", "type": "string" + }, + "shared_state_history": { + "additionalProperties": { + "items": { + "$ref": "#/$defs/ParticipantSharedStateRecordModel" + }, + "type": "array" + }, + "title": "Shared State History", + "type": "object" + }, + "shared_state_records": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantSharedStateRecordModel" + }, + "title": "Shared State Records", + "type": "object" + }, + "time_management_contexts": { + "additionalProperties": { + "$ref": "#/$defs/ParticipantTimeManagementContextModel" + }, + "title": "Time Management Contexts", + "type": "object" } }, "title": "RuntimeSnapshotEnvelopeModel", diff --git a/docs/aces/inventory/asset-inventory-issue-template.md b/docs/aces/inventory/asset-inventory-issue-template.md new file mode 100644 index 000000000..e9998d6f4 --- /dev/null +++ b/docs/aces/inventory/asset-inventory-issue-template.md @@ -0,0 +1,124 @@ +--- +name: ACES Asset Inventory +about: Capture participant-discoverable asset evidence and map it to ACES. +title: "[INVENTORY] : " +labels: enhancement +assignees: "" +--- + +# ACES Asset Inventory Issue Template + +Copy this file, or the body below, into a downstream backend repository's +`.github/ISSUE_TEMPLATE/` tree when that repository needs an issue form for +inventorying realized scenario assets against the ACES methodology. + +## Methodology Authority + +Use the ACES participant-discoverable asset inventory methodology as the +authority: + +- Methodology: + `docs/aces/inventory/asset-inventory-methodology.md` +- Assurance report: + `docs/aces/inventory/methodology-assurance-report.md` +- Agent workflow: + `.codex-skills/aces-asset-inventory-capture/SKILL.md` and + `.claude/skills/aces-asset-inventory-capture/SKILL.md` + +## Target + +- Scenario: +- Asset id: +- Asset kind: image / running container / VM / host / composed service / other +- Backend/runtime: +- Expected source class: custom-build / upstream-image / runtime-composed +- Allowed discovery vantages: +- Destructive reset allowed: yes / no + +## Scope + +Capture every fact that a participant or in-range agent could discover from +the realized range. Do not filter by relevance, intent, current backend +support, or elegance. Host-side Docker or backend evidence may support +provenance, but it does not shrink the participant-discoverable boundary. + +Operator/out-of-scenario secrets are outside the inventory boundary. Exclude +them or record a first-class `capture-limits.txt` entry. Scenario-target +secrets are capture facts when an in-range participant or agent could discover +them. + +## Required Bundle + +The asset bundle should include: + +- `README.md` with scope, target identity, commands, and limits. +- `capture-evidence.sh` or equivalent committed capture commands. +- `mapping-ledger.yaml`. +- `evidence/captured-at-utc.txt`. +- `evidence/capture-limits.txt`. +- `evidence/evidence-sha256sums.txt`. +- Raw evidence for discovery vantage, runtime state, provenance, package/SBOM, + vulnerability, filesystem, relationship, and trust-surface facts. + +For Docker/Compose/container-image captures, start from: + +- `.codex-skills/aces-asset-inventory-capture/scripts/capture-container-evidence-template.sh` +- `.codex-skills/aces-asset-inventory-capture/scripts/normalize-syft-cyclonedx.jq` + +## Capture Checklist + +- [ ] Defined all in-range discovery vantages. +- [ ] Captured participant-discoverable network, service, process, package, + filesystem, credential, trust, relationship, data, and configuration + facts. +- [ ] Captured source provenance, immutable image/artifact identifiers, runtime + configuration, and scanner/tool versions. +- [ ] Captured required Trivy CycloneDX SBOM and Trivy vulnerability JSON, or + recorded a first-class limit with ledger reference. +- [ ] Attempted Syft, osquery, and filesystem-manifest capture where + applicable, or recorded first-class limits with ledger references. +- [ ] Kept every evidence-affecting normalization deterministic and committed + as script or jq. +- [ ] Generated `evidence-sha256sums.txt` after evidence capture. + +## Mapping And Gap Triage + +For every captured fact, record one mapping disposition in +`mapping-ledger.yaml`: + +- `encoded` +- `encoded_with_caveat` +- `blocked_by_aces_gap` +- `blocked_by_aptl_gap` +- `needs_gap_triage` only while actively triaging + +Before completion: + +- [ ] No `needs_gap_triage` mapping remains. +- [ ] Every evidence file is referenced by a fact, provenance entry, + correspondence check, or capture-limit fact. +- [ ] Every `blocked_by_aces_gap` row links an ACES issue. +- [ ] Every `blocked_by_aptl_gap` row links a downstream backend issue. +- [ ] Correspondence checks describe how later encoding work will compare ACES + surfaces against fresh realized evidence. + +## Validation + +Run the downstream ledger validator before closing: + +```shell +aptl aces-inventory schema +aptl aces-inventory validate +aptl aces-inventory gaps +``` + +Also run the backend repository's normal test and documentation checks for any +changed code, docs, templates, or committed evidence. + +## Completion Claim + +This issue is complete only when every participant-discoverable fact in scope +is captured, mapped to ACES, or blocked by a linked ACES/downstream issue with +an explicit limitation. Evidence bundles, scanner output, screenshots, +summaries, and backend support limits are proof inputs only; they do not +replace ACES specification or explicit gap records. diff --git a/docs/aces/inventory/asset-inventory-methodology.md b/docs/aces/inventory/asset-inventory-methodology.md index 9c5d0dd6a..53a2f2232 100644 --- a/docs/aces/inventory/asset-inventory-methodology.md +++ b/docs/aces/inventory/asset-inventory-methodology.md @@ -55,6 +55,31 @@ the code, environment, parameters, and artifacts needed to assess a computational claim, without overstating that the result is an independent scientific replication. +## Requirement Alignment + +This methodology is support for the ACES requirements that govern apparatus, +provenance, realized-form disclosure, replay, and realization honesty. It does +not replace those requirements or define final SDL syntax. Its role is to make +the evidence and gap trail explicit enough for those requirement surfaces to +consume: + +- `EXP-704` apparatus context: record discovery vantages, tool agents, backend + identity, runtime identities, and capture limits. +- `EXP-712` reproducibility and replay: preserve rerunnable commands, + immutable artifact identifiers, scanner/tool versions, checksums, and known + non-claims. +- `EXP-720` canonical run provenance: separate entities, activities, agents, + and evidence artifacts so later run records can cite the inventory. +- `EXP-722` realized-form disclosure: capture realized runtime state even when + it differs from authored intent or backend defaults. +- `ASR-519` realization honesty: force explicit mappings, caveats, and gap + issues instead of silent approximation or evidence-only claims. +- `DSL-115` and follow-on DSL gap work: route missing SDL expressivity to ACES + issue records with evidence and checked surfaces. + +The reusable downstream issue skeleton for applying this method lives in +`docs/aces/inventory/asset-inventory-issue-template.md`. + ## Evidence Model Each asset is described across five layers: diff --git a/docs/aces/inventory/index.md b/docs/aces/inventory/index.md index 9ddccd92a..308401991 100644 --- a/docs/aces/inventory/index.md +++ b/docs/aces/inventory/index.md @@ -12,6 +12,9 @@ Use these documents as the ACES authority: - [Methodology assurance report](methodology-assurance-report.md) records the DevOps, supply-chain, reproducible-research, and verification/validation basis for the method. +- [Reference asset-inventory issue template](asset-inventory-issue-template.md) + is the vendorable GitHub issue skeleton downstream backends can copy into + their own `.github/ISSUE_TEMPLATE/` trees. - [SCN-010 expressivity gap analysis](scn010-expressivity-gap-analysis.md) is the peer-review-grade analysis of the ACES SDL runtime-surface expressivity gaps found while holding the 16 remaining APTL TechVault @@ -32,6 +35,7 @@ methodology owner. asset-inventory-methodology methodology-assurance-report +asset-inventory-issue-template scn010-expressivity-gap-analysis issue-516-redaction-boundary-preflight webapp-preflight diff --git a/docs/aces/inventory/methodology-assurance-report.md b/docs/aces/inventory/methodology-assurance-report.md index 5e7bca929..ebf43aa5a 100644 --- a/docs/aces/inventory/methodology-assurance-report.md +++ b/docs/aces/inventory/methodology-assurance-report.md @@ -71,7 +71,8 @@ later issues must not silently carry them into final claims: - The Trivy SBOM and vulnerability outputs are scanner state tied to tool, database, advisory, and capture time. They are not permanent ground truth. - The ledger proves mapping accountability, not semantic completeness of ACES. - ACES #354 remains a blocker for typed runtime configuration surfaces. + ACES #354 added the first typed runtime configuration surfaces, but later + captures may still expose additional SDL gaps that need their own issues. - Correspondence checks are planned. Final encoding issues must implement them by comparing ACES/source-package content against fresh realized evidence. diff --git a/docs/decisions/adrs/README.md b/docs/decisions/adrs/README.md index 2d4cc693f..d7ca45515 100644 --- a/docs/decisions/adrs/README.md +++ b/docs/decisions/adrs/README.md @@ -107,6 +107,11 @@ adr-059-adr-amendment-policy-and-pin-gate adr-060-participant-backend-facing-contract-surface adr-061-published-schema-evolution-policy adr-062-concept-authority-catalog-governance-gate +adr-063-reference-emulation-backend +adr-064-experiment-evidence-and-measure-contract-boundary +adr-065-experiment-run-provenance-contract-boundary +adr-066-observability-evidence-plane-separation +adr-067-participant-behavior-model ``` | ADR | Title | Status | Date | @@ -174,3 +179,8 @@ adr-062-concept-authority-catalog-governance-gate | [060](adr-060-participant-backend-facing-contract-surface.md) | Participant Backend-Facing Contract Surface | proposed | 2026-06-11 | | [061](adr-061-published-schema-evolution-policy.md) | Published Schema Evolution Policy | accepted | 2026-06-14 | | [062](adr-062-concept-authority-catalog-governance-gate.md) | Concept-Authority Catalog Governance Gate | accepted | 2026-06-14 | +| [063](adr-063-reference-emulation-backend.md) | Reference Emulation Backend | accepted | 2026-06-20 | +| [064](adr-064-experiment-evidence-and-measure-contract-boundary.md) | Experiment Evidence and Measure Contract Boundary | accepted | 2026-06-21 | +| [065](adr-065-experiment-run-provenance-contract-boundary.md) | Experiment Run Provenance Contract Boundary | accepted | 2026-06-22 | +| [066](adr-066-observability-evidence-plane-separation.md) | Observability and Evidence Plane Separation | accepted | 2026-06-23 | +| [067](adr-067-participant-behavior-model.md) | Participant Behavior Model | proposed | 2026-06-23 | diff --git a/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md b/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md index 15df8bd26..d542121ce 100644 --- a/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md +++ b/docs/decisions/adrs/adr-060-participant-backend-facing-contract-surface.md @@ -110,10 +110,11 @@ following structure. `participant-context-view-v1` (derived operational context views). All three apply visibility projection and marking/redaction enforcement before publication and carry no retrieval-only state that does not exist in - recorded contracts. The *semantics* of derived context views (meaning and - comparability) belong to `SEM-214` (wave 3); `participant-context-view-v1` - carries the view reference, provenance, and marking discipline only and - makes no comparability claim. + recorded contracts. `participant-context-view-v1` carries the SEM-214 + meaning/comparability envelope: a governed meaning reference, participant + and audience scope, observation point, consumed source layers, + transformation rule, evidence/provenance basis, semantic limitations, and + explicit comparability basis/disclosures. 6. **`API-411` outcomes are interpretation records.** `participant-outcome-report-v1` reports participant-local outcomes as @@ -163,9 +164,10 @@ Negative: Risks: -- The `SEM-214` deferral means `participant-context-view-v1` could prove - inadequate when derived-context-view semantics are designed; mitigated by - keeping that carrier reference-and-provenance only. +- SEM-214 semantics make `participant-context-view-v1` stricter: existing + producers must provide explicit meaning, source, transformation, + evidence/provenance, and comparability metadata rather than treating schema + validity alone as a portability claim. - The API-407 guarantee-strength scale could be misread as SEM-218 realization support; mitigated by the explicit boundary rule in this ADR and by distinct manifest fields. diff --git a/docs/decisions/adrs/adr-063-reference-emulation-backend.md b/docs/decisions/adrs/adr-063-reference-emulation-backend.md new file mode 100644 index 000000000..00c91b13e --- /dev/null +++ b/docs/decisions/adrs/adr-063-reference-emulation-backend.md @@ -0,0 +1,165 @@ +# ADR-063: Reference Emulation Backend + +## Status + +accepted + +## Date + +2026-06-20 + +## Classification + +Classification: FM0 +Required artifacts: ADR, unit tests +Waivers: none + +The reference emulation backend is a concrete implementation of existing +backend contracts whose correctness is established structurally and verified by +unit tests plus the existing backend-conformance runner. It introduces no new +semantic, graph, or stateful reasoning that would warrant a higher level; the +RUN-311 episode-state invariants and SEM-218 realization gate it relies on are +already formalized and gated elsewhere, and this backend consumes them rather +than redefining them. + +## Context + +[ADR-004](adr-004-sdl-runtime-layer.md) defines the +compile/plan/execute runtime and requires every backend to provide explicit +domain protocols plus a `BackendManifest`. +[ADR-036](adr-036-sdl-processor-runtime-module-boundaries.md) assigns package +ownership: `aces_runtime` owns live control, `aces_backend_protocols` owns +backend declarations, `aces_backend_stubs` owns the non-normative in-memory +stub, and `aces_contracts` owns neutral DTOs. The repository has shipped the +manifest, registry, conformance, and SEM-218 realization seams, and an in-memory +stub that exercises them — but no concrete backend that realizes plans against +real infrastructure. + +RUN-314 (issue #197) closes that gap with a repository-owned **reference +emulation backend** that realizes provisioning plans against a container +substrate while remaining a faithful, conformant implementation of the existing +backend contracts. The architecture preflight note +(`docs/decisions/issue-197-run-314-reference-emulation-backend-preflight.md`) +records the binding guardrails: reuse existing seams, never add new +manifest/schema/profile/fixture/vocabulary/exception/store authority, keep +emulator-native facts out of portable artifacts, and route apply/control through +`RuntimeManager` / `RuntimeControlPlane`. + +## Decision + +Add a new implementation package +`implementations/python/packages/aces_reference_backend/` that implements the +four `aces_backend_protocols.protocols` roles (Provisioner, Orchestrator, +Evaluator, ParticipantRuntime) and registers on the existing `BackendRegistry` +descriptor seam under the name `reference-emulation`. + +### 1. Place in the ADR-004 / ADR-036 boundary + +The reference backend is implementation-side code. It consumes +`aces_backend_protocols`, `aces_contracts`, and the public runtime registry +seam; core packages do not import it, and no implementation logic lives in the +compatibility-only `implementations/python/src/aces/` tree. It is not a new +processor, runtime manager, conformance authority, schema authority, or +experiment archive. It publishes identity/capability through the standard +`BackendManifest` with the `reference-emulation` identity, declaring only the +evidence-backed contract ids, concept bindings, realization-support +declaration, and capability terms the conformance runner actually exercises (the +same evidence set the stub declares). The in-memory stub stays non-normative and +is used in tests only as a comparison oracle; this backend does not import or +subclass it. + +### 2. Driver abstraction + +Plan interpretation is split from realization. A pure +`interpret_provisioning_plan(plan) -> Realization` maps node/network/placement +resources into portable, secret-free specs (`NetworkSpec` / `ContainerSpec`) +plus diagnostics for unsupported or malformed resources. A `DeploymentDriver` +protocol is the host-process boundary; two drivers ship: + +- `InProcessDriver` (default): hermetic, records ops and synthesizes portable + handles, no subprocess and no runtime — safe in CI and in the default + conformance/apply path. +- `OciDeploymentDriver`: realizes against a real container runtime + (docker/podman). The next reasonable variation (provider, workspace, network + namespace, image source, resource limits) is selected through the registry + descriptor `**config` seam without rewriting `RuntimeManager`, + `RuntimeControlPlane`, conformance, or manifest rendering. + +### 3. Opt-in Docker conformance + +Real-container realization is verified by a `@pytest.mark.docker` integration +test that provisions a container through the control plane, confirms the +realized inventory, tears it down, and runs `run_target_conformance` against the +OCI driver. The `docker` marker is excluded from the default hermetic suite and +the test self-skips when no runtime is present. A dedicated `integration_docker` +nox session and a non-blocking, runtime-gated CI job run it; the canonical +`verify` graph stays hermetic and never depends on a container runtime. + +### 4. Portable-fact / provenance boundary + +Snapshot, result, and history payloads carry only portable ACES facts — the +same shape the stub produces. Container/VM/network ids, daemon inspect payloads, +host paths, environment, argv, tokens, credentials, SSH keys, and backend-native +reprs never reach manifests, snapshots, diagnostics, conformance reports, or +examples; the portable surface is references, digests, and classification +labels. Real realization is a **driver side effect**: the provisioner preserves +planned payloads honestly into snapshot entries, and SEM-218 provenance flows +through the existing `_call_backend_apply` gate +(`RuntimeManager(target).apply(plan)` fills +`RuntimeSnapshot.realization_provenance`) rather than by mutating the portable +snapshot. Public failures are `Diagnostic` / `OperationReceipt` / +`OperationStatus` only; there is no backend-specific exception hierarchy, log +channel, or raw traceback. The OCI driver uses fixed argv (never `shell=True`), +a closed runtime allowlist, bounded timeouts, and structured handling that keeps +native stdout/stderr out of every returned handle and diagnostic. Because a plan +author controls the container image (via `node.source`) and `run` pulls and +executes it, the driver enforces an operator image-trust policy: only the +configured `default_image`, an explicit `allowed_images` entry, or a +digest-pinned ref is realized, so plan submission cannot become arbitrary-image +code execution. Realization is transactional at the driver boundary — a partial +failure rolls back the resources that did succeed, and a failed teardown stays +tracked for retry — and a container is attached to every network its plan +declared, so realized topology matches the plan rather than silently landing on +the runtime default network. + +### 5. APTL as a downstream consumer + +The split between pure interpretation and a driver protocol mirrors the pattern +APTL already uses to consume ACES provisioning plans. APTL remains a downstream +consumer of ACES contracts; this backend lifts the interpret/driver **pattern** +without depending on APTL, and APTL continues to depend on ACES, never the +reverse. + +## Consequences + +**Positive** + +- The repository now ships a concrete, conformant backend that realizes plans + against real infrastructure, proving the manifest/registry/conformance/SEM-218 + seams end to end with a non-stub implementation. +- The driver abstraction lets the same backend run hermetically (default) or + against a real container runtime (opt-in) without changing the runtime, + control plane, or conformance surfaces. + +**Negative / costs** + +- A second full backend implementation now tracks the portable result/history + envelope shapes; drift from the contracts is caught by the shared conformance + runner the backend must keep passing. + +**Risks** + +- The opt-in Docker path depends on a host container runtime; it is gated and + self-skipping so it can never make the hermetic verification graph flaky. + +## Alternatives Considered + +- **Promote the in-memory stub into the reference backend.** Rejected: ADR-036 + keeps the stub non-normative, and a container-backed backend has materially + different realization behavior. The stub stays a test oracle. +- **Add Docker/Podman-specific SDL syntax or runtime fields.** Rejected: the + existing runtime surfaces already carry the portable facts; emulator selection + is driver configuration through the registry seam, not new authored syntax. +- **Add a backend-specific exception hierarchy / log channel.** Rejected: public + failures must remain `Diagnostic` / `OperationReceipt` / `OperationStatus` so + the control plane and conformance runner see a uniform error envelope. diff --git a/docs/decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary.md b/docs/decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary.md new file mode 100644 index 000000000..9936f7e58 --- /dev/null +++ b/docs/decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary.md @@ -0,0 +1,126 @@ +# ADR-064: Experiment Evidence and Measure Contract Boundary + +## Status + +accepted + +## Date + +2026-06-21 + +## Classification + +Classification: FM1 +Required artifacts: ADR, formal spec, schemas, fixtures, conformance tests +Waivers: none + +Issue #88 publishes a contract boundary for EXP-707, EXP-708, EXP-709, and +EXP-715. The work adds declarative data contracts and backend capability +declarations; it does not implement runtime capture, storage, scheduling, +statistical analysis, or HTTP APIs. FM1 is appropriate because the decision +adds schema-governed external artifacts and semantic invariants, but no new +state-machine or solver behavior. + +## Context + +[ADR-055](adr-055-experiment-core-contract-boundary.md) established the +experiment-core family for tasks, runs, studies, and apparatus context. That +boundary intentionally left evidence capture and downstream measure publication +to follow-on work so the first experiment contracts would not blur protocol +intent, raw observations, and interpreted results. + +Issue #88 fills that gap as a joint design issue for: + +- EXP-707: declare what evidence an experiment intends to capture. +- EXP-708: publish raw captured observations and artifacts as evidence records. +- EXP-709: publish metrics, evaluations, summaries, and analysis outputs + derived from raw evidence. +- EXP-715: let a backend declare its observation and evidence-collection + capability without implying execution or evaluator semantics. + +The spawned implementation issues remain responsible for actual capture, +retention, API, and processor/runtime behavior. This ADR records only the +schema-first contract split those issues must consume. + +## Decision + +Add three experiment-core contracts: + +- `experiment-capture-spec-v1`: a declarative capture specification. It names + the task/run/apparatus scope, capture windows, capture requirements, channels, + media types, sensitivity, integrity requirements, retention policy, and loss + disclosure expectations. It records what should be captured, not whether a + backend captured it. +- `experiment-evidence-record-v1`: a raw evidence record. It binds a capture + specification and requirement to a run, source references, capture time, + capture window, raw content reference or bounded payload summary, sensitivity, + redaction state, and provenance. It does not carry metric values or evaluation + outcomes. +- `experiment-derived-measure-v1`: a derived measure or evaluation output. It + binds a metric reference, derivation method, source evidence records, + generation time, value status, reported value when present, uncertainty, + limitations, and provenance. It cannot stand in for raw evidence. + +Add an optional `capabilities.observation` block to `backend-manifest-v2` for +EXP-715. This block declares supported capture kinds, channel kinds, evidence +contracts, media types, sealing modes, redaction support, loss-disclosure +support, chain-of-custody support, and constraints. Its governed vocabularies +live in the concept-authority catalog under: + +- `capabilities.observation.supported_capture_kinds` +- `capabilities.observation.supported_channel_kinds` +- `capabilities.observation.supported_sealing_modes` + +A backend that declares `capabilities.observation` must also declare the +published experiment evidence contracts that make the claim falsifiable: +`experiment-capture-spec-v1`, `experiment-evidence-record-v1`, and +`experiment-derived-measure-v1`. + +## Consequences + +**Positive** + +- Experiment evidence intent, raw observations, and derived measures now have + separate closed-world artifacts, making provenance and interpretation chains + reviewable. +- Backend manifests can advertise observation capability without overloading + orchestrator, evaluator, or participant-runtime capability blocks. +- Conformance can reject observation capability claims that lack the published + evidence contracts needed to inspect them. + +**Negative / costs** + +- Existing experiment schemas include the extended experiment reference + vocabulary, so their published hashes change even though their primary + contract shape remains intact. +- Backends that choose to claim observation capability must keep concept + bindings, supported contract versions, fixtures, and conformance evidence in + sync. + +**Risks** + +- Implementers may treat a capture specification as proof that capture occurred. + The contract names and validators intentionally separate + `capture-spec`, `evidence-record`, and `derived-measure` references to keep + this distinction visible. +- A derived measure can be misread as raw evidence unless review tooling follows + `source_evidence_refs`. The derived-measure contract therefore requires at + least one evidence-record reference. + +## Alternatives Considered + +- **Embed capture requirements into `experiment-task-v1`.** Rejected: tasks + already express protocol intent; capture plans evolve by apparatus and run, + and forcing them into tasks would make task publication imply backend + collection behavior. +- **Use run evidence artifacts for raw evidence and result summaries for + measures.** Rejected: those fields remain useful summaries, but they are too + coarse to publish capture requirement bindings, redaction/loss disclosure, and + source-to-measure provenance as first-class artifacts. +- **Declare observation support only through evaluator capability.** Rejected: + evidence collection can be backend, participant-runtime, network, or service + mediated and is not identical to scoring or objective evaluation. +- **Implement capture storage and APIs in the same change.** Rejected: issue + #88 is the contract-boundary decision. Runtime capture, storage, API, and + processor behavior are intentionally left to the spawned implementation + issues that can consume these contracts. diff --git a/docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md b/docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md new file mode 100644 index 000000000..3d45ccea6 --- /dev/null +++ b/docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md @@ -0,0 +1,129 @@ +# ADR-065: Experiment Run Provenance Contract Boundary + +## Status + +accepted + +## Date + +2026-06-22 + +## Classification + +Classification: FM2 +Required artifacts: ADR, formal spec, schema update, fixtures, contract tests +Waivers: none + +Issue #89 publishes the joint design for EXP-710, EXP-720, and EXP-722. The +work extends the existing `experiment-run-v1` archival run contract; it does +not create a second run-provenance root schema. + +## Context + +ADR-055 established the experiment-core family for tasks, apparatus contexts, +runs, and studies. ADR-064 added evidence capture specifications, raw evidence +records, derived measures, and backend observation capability declarations. + +The remaining joint provenance requirements need the run record to act as the +canonical archival join point: + +- EXP-710 requires traceability from task and run context through captured + evidence to derived measures, evaluations, and experiment claims. +- EXP-720 requires a canonical run provenance record distinct from live + execution state, with task, scenario/module digest, processor, backend, + manifest, configuration, parameter, stochastic-control, timestamp, result, + and evidence pointers. +- EXP-722 requires realized forms chosen by processors and backends for + underspecified concerns to be preserved separately from authored scenario + material and derived results. + +The pre-existing `ExperimentRunModel` already carries most EXP-720 context. A +new root schema would split the authoritative record and force consumers to +reconcile two provenance shapes for one run. The correct boundary is therefore +to extend `experiment-run-v1` with the missing traceability and realized-form +surfaces. + +## Decision + +`experiment-run-v1` is the canonical run provenance record. It remains the +authoritative archival record for one task execution and now includes two +additional surfaces: + +1. `traceability` - a required block that links the run to capture + specifications, raw evidence records, derived measures, and claim/report + references. Claims require at least one derived-measure reference so a result + claim cannot float free of interpreted evidence. +2. `realized_form_disclosures` - optional disclosures for underspecified + concerns whose concrete form was chosen by a processor, backend, operator, or + observation. Each disclosure names the concern, realization basis, + realization authority, authored reference when present, realized reference or + value summary, disclosure text, and supporting evidence-record refs. + +The existing fields continue to carry the rest of the canonical provenance: +`task_ref`, `scenario_snapshot_ref`, embedded `apparatus_context`, participant +implementation provenance, parameter set, stochastic controls, clocks, +timestamps, result summaries, run evidence artifacts, and used/generated/ +derived reference lists. + +The contract deliberately keeps these concepts separate: + +- authored scenario material remains `scenario` or `scenario-snapshot` + references; +- realized choices are disclosures under the run record; +- raw observations are `experiment-evidence-record-v1`; +- interpreted outputs are `experiment-derived-measure-v1` and run result + summaries; +- live control-plane snapshots and operation statuses are not canonical run + provenance. + +The published JSON Schema remains the portable structural contract. Semantic +constraints that standard JSON Schema cannot fully express are declared through +the existing `x-aces-invariants` profile and enforced by the Pydantic contract +models. + +## Consequences + +### Positive + +- There is one canonical run provenance artifact for EXP-710, EXP-720, and + EXP-722 rather than parallel provenance and realized-form schemas. +- Consumers can follow a durable chain from task/run context to capture specs, + evidence records, derived measures, and claims. +- Realized choices for underspecified concerns are reviewable without treating + them as authored scenario meaning or as metric results. + +### Negative / Costs + +- The `experiment-run-v1` schema hash changes because a required traceability + block is added to the existing draft contract. +- Fixtures and producers of run records must now publish capture/evidence + traceability alongside result summaries. + +### Risks + +- Implementers may try to use `realized_form_disclosures` as a free-form log + field. The contract requires a concern id, basis, realization authority, and a + realized reference or value summary to keep disclosures inspectable. +- Traceability references can identify artifacts but do not fetch or validate + external payloads. Future storage/API work must reuse the existing + control-plane authorization, redaction, request-size, audit, and idempotency + patterns before dereferencing artifact URIs. + +## Alternatives Considered + +- **Add `experiment-run-provenance-v1` as a new root schema.** Rejected because + `experiment-run-v1` is already the archival run record; a second root would + duplicate authority and make traceability reconciliation ambiguous. +- **Put realized-form disclosure into apparatus context only.** Rejected + because realized choices can involve processor resolution, backend defaults, + participant implementations, capture windows, and parameter defaults. The run + is the only artifact that sees all of those choices together. +- **Use only `used_refs`, `generated_refs`, and `derived_from_refs`.** Rejected + because generic lineage lists do not state the EXP-710 path from capture spec + to raw evidence to derived measure to claim, and they cannot express + realized-form disclosure semantics. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| diff --git a/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md b/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md new file mode 100644 index 000000000..107b4ef4a --- /dev/null +++ b/docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md @@ -0,0 +1,239 @@ +# ADR-066: Observability and Evidence Plane Separation + +## Status + +accepted + +## Date + +2026-06-23 + +## Classification + +Classification: FM2 +Required artifacts: ADR, formal spec, SDL authoring catalog, clause matrix +Waivers: Executable contracts, fixtures, and tests are owned by the spawned +implementation issues #334, #335, #336, and #337. + +## Context + +Issue #127 is the joint design surface for: + +- SEM-224, observability plane separation semantics; +- SEM-225, processor/backend realization augmentation semantics; +- DSL-123, scenario-native observability and telemetry systems; and +- DSL-124, authored data and evidence requirements. + +Existing ACES artifacts already define adjacent boundaries: + +- ADR-022 and ADR-054 separate participant-visible observations from hidden + truth, scoring state, centralized-training state, archival evidence, and + participant action-observation history. +- ADR-055, ADR-064, and ADR-065 separate experiment tasks, apparatus contexts, + capture specifications, raw evidence records, derived measures, run + traceability, and realized-form disclosures. +- `specs/sdl/runtime-inventory.md` defines node-scoped runtime-family + inventory as the SDL pattern for in-world logical services. +- ADR-056 and ADR-057 define redaction and observed-value boundaries for + runtime facts that can carry secrets. + +The missing design is the cross-plane rule set. Authors need in-world +observability systems to be scenario elements. Experiment designers need +capture requirements that say what data must be collected. Processors and +backends need operational telemetry and may add instrumentation. Runs need raw +evidence records and derived analysis outputs. Those are related but not +interchangeable. + +Without an explicit split, implementers can accidentally treat backend logs as +participant observations, treat the presence of raw capture as satisfaction of +an authored evidence requirement, leak hidden adjudication assets through +analysis outputs, or make an environment-visible augmentation without +disclosing its effect on comparability. + +## Decision + +Define five named planes and one cross-cutting augmentation classification. + +### 1. Scenario-native observability plane + +Scenario-native observability systems are in-world systems that the authored +scenario makes part of the environment: telemetry systems, logs, tracing +backends, monitoring dashboards, sensors, detection engines, SIEM-like managers, +or comparable resources that participants or scenario relationships may depend +on, interact with, or target. + +They belong in SDL authoring space. Implementations must prefer the existing +runtime-family model under `nodes..runtime.*`. A new runtime family is +appropriate only when the system has a distinct, product-neutral logical +service identity that cannot fit existing families without distorting their +meaning. This decision does not add a universal top-level `observability` +section. + +### 2. Authored evidence-requirement plane + +An authored evidence requirement records what data, evidence, output, or +capture product must exist from a declared source, scope, window, channel, or +boundary. It is independent of participant objectives and distinct from +scenario-native observability systems. + +Evidence requirements are authoring obligations. They are not proof that +capture occurred and they are not raw captured payloads. When an executable +artifact is needed, the requirement maps to experiment-core capture +specification concepts such as source refs, capture windows, capture +requirements, sensitivity, integrity, retention, and loss disclosure. + +### 3. Processor/backend operational observability plane + +Processor and backend operational observability covers apparatus logs, +diagnostics, traces, audit records, setup attestations, health checks, +measurement-channel facts, and capability disclosures used to operate or verify +the apparatus. + +These facts are not participant-visible observations and are not authored +scenario meaning unless an explicit SDL or runtime contract projects them into +that plane. They must use existing diagnostics, manifests, control-plane +security, audit, idempotency, request-size, and redacted-error patterns. + +### 4. Captured evidence plane + +Captured evidence is a concrete raw evidence artifact or record produced for a +run. It must carry provenance, source refs, capture time or window, raw-content +reference or bounded summary, sensitivity, redaction state, checksum or +integrity metadata where applicable, and the authored requirement or capture +specification it claims to satisfy. + +Captured evidence does not carry metric values, scores, or evaluation +decisions. Those belong to derived analysis. + +### 5. Derived analysis plane + +Derived analysis outputs are interpreted outputs over evidence: derived +measures, result summaries, outcome interpretations, studies, reports, exports, +analysis artifacts, and claims. + +Derived analysis must cite its source evidence. It must not stand in for raw +evidence, and it must not reveal hidden state, hidden answer keys, private +traces, or adjudication assets unless an explicit marking, redaction, and +authorization boundary permits that disclosure. + +### 6. Realization augmentation classification + +Processor/backend augmentation is apparatus-added behavior or instrumentation +used to satisfy evidence, evaluation, operational, or comparability needs. An +augmentation may carry one or more of these classifications: + +- `apparatus_only`: visible only to the processor, backend, operator, or + control apparatus; +- `environment_visible`: changes or adds behavior inside the realized + environment; +- `participant_visible`: can affect what a participant sees, receives, or can + infer through a visibility projection; and +- `comparability_relevant`: can affect whether runs, participants, backends, or + conditions can be compared. + +Participant-visible augmentation must pass the participant visibility, +marking, and redaction gates from ADR-022 and ADR-054. Environment-visible or +comparability-relevant augmentation must be represented through first-class +runtime, evidence, or provenance carriers. It must not hide in +`RuntimeSnapshot.metadata`, evaluator details, diagnostics, audit blobs, +backend DTOs, or raw logs. + +## Required Boundaries + +- A backend log is not a participant observation unless a participant + observation envelope or SDL visibility rule projects it. +- A capture specification or authored evidence requirement is not proof that + evidence was captured. +- A raw evidence record is not a metric, score, result summary, or derived + analysis output. +- A scenario-native observability system may be a source for evidence capture, + but its existence does not satisfy the capture requirement by itself. +- Processor/backend operational telemetry may support apparatus audit or + setup evidence, but it is not authored scenario meaning. +- Hidden adjudication assets, evaluator state, answer keys, private traces, + prompts, and secrets remain outside portable public surfaces unless a + governed disclosure rule, marking, redaction policy, and authorization scope + apply. +- Loss, redaction, latency, observer effects, and weaker capability guarantees + must be explicit when a claim depends on them. + +## Implementation Mapping + +This ADR is design coverage for issue #127. The spawned implementation issues +own executable work: + +- #334 / SEM-224: plane classifier, validation, and traceability over the five + named planes. +- #335 / SEM-225: augmentation disclosure carriers and validators. +- #336 / DSL-123: scenario-native observability authoring surfaces, using the + runtime-family extension model. +- #337 / DSL-124: authored evidence-requirement authoring surfaces and their + mapping to experiment-core capture concepts. + +The formal criteria and source-to-contract-to-test matrix live in +`specs/formal/observability-evidence-plane.md`. The SDL authoring rules live in +`specs/sdl/observability-and-evidence.md`. + +## Alternatives Considered + +### Add one generic observability model + +Rejected. A single catch-all model would collapse in-world observability +systems, backend diagnostics, evidence requirements, raw evidence, and analysis +outputs. It would also bypass the existing runtime-family, experiment-core, +participant-runtime, and control-plane seams. + +### Put authored evidence requirements only in experiment-core contracts + +Rejected. Experiment-core capture specifications are the portable capture +contract boundary, but DSL-124 asks for an authored language surface. The SDL +surface can map to capture specifications, but the authored obligation must not +be replaced by archival run artifacts. + +### Treat backend operational telemetry as the canonical evidence surface + +Rejected. Backend telemetry is apparatus operational data. It can support +audit, setup, or capture claims when projected through the right contracts, but +it cannot become participant-visible state or requirement satisfaction by +existence. + +### Model all augmentation through SEM-218 explicitness + +Rejected. SEM-218 distinguishes authored binding from processor/backend +realization. SEM-225 needs a more specific disclosure classification for +instrumentation and apparatus behavior that can be environment-visible, +participant-visible, or comparability-relevant. + +## Consequences + +### Positive + +- The four requirements have one shared vocabulary for plane separation. +- Future SDL and contract work can add executable surfaces without inventing + parallel schema, validation, persistence, diagnostic, or audit stacks. +- Reviewers can reject cross-plane mistakes with concrete criteria instead of + relying on prose intuition. +- Augmentation effects that can change participant visibility or comparability + become explicit review objects. + +### Negative / costs + +- Implementers must classify plane ownership and augmentation visibility before + adding executable surfaces. +- Some follow-on issues must touch multiple roots together: SDL specs, + contracts, schema manifests, fixtures, validation helpers, and tests. + +### Risks + +- If future implementation skips the classifier and matrix, the same words + (`logs`, `telemetry`, `evidence`, `observation`) can drift across planes. +- If augmentation is treated as apparatus-only by default, environment-visible + or comparability-relevant changes can become invisible to reviewers. +- If captured evidence and derived analysis are not linked by explicit source + refs, result claims can float free of the raw evidence they interpret. + +## Amendments + +| Date | Commit/PR | Summary | +|------|-----------|---------| +| 2026-06-23 | #335 | Implemented SEM-225 run-level augmentation disclosures in `experiment-run-v1`, including separate environment-visible, participant-visible, and comparability-relevant validation. | diff --git a/docs/decisions/adrs/adr-067-participant-behavior-model.md b/docs/decisions/adrs/adr-067-participant-behavior-model.md new file mode 100644 index 000000000..9b3d44847 --- /dev/null +++ b/docs/decisions/adrs/adr-067-participant-behavior-model.md @@ -0,0 +1,288 @@ +# ADR-067: Participant Behavior Model + +## Status + +proposed + +## Date + +2026-06-23 + +## Classification + +Classification: FM2 +Required artifacts: ADR, formal spec, clause matrix +Waivers: Executable SDL fields, contract schemas, fixtures, runtime emission, +and tests are owned by spawned implementation issues #204, #205, #206, #207, +and #208. + +## Context + +Issue #77 is the joint design surface for: + +- `ACT-602`, executable participant behavior model; +- `ACT-603`, abstract participant interaction model; +- `ACT-606`, first-class participant behavior specifications; +- `ACT-607`, participant authority and scope boundaries; and +- `ACT-608`, participant behavior modes. + +These requirements sit on top of already-published participant semantics: + +- ADR-020 defines authored participant framing in SDL: identity, role, + starting conditions, authority anchors, and operating scope. +- ADR-022 and `specs/formal/participant-semantics/` define the semantic model + for participant actions, observations, visibility, failures, temporal + ordering, attribution, and outcome interpretation. +- ADR-041 defines participant implementation manifests and run-level + provenance for the apparatus that makes or relays decisions. +- ADR-054 and `specs/formal/participant-runtime/` define the observable + runtime lifecycle, shared-state records, observation envelopes, behavior + history, and concurrency boundaries. +- ADR-060 and `specs/formal/runtime-contracts/participant-backend-contracts.md` + define the backend-facing carrier and retrieval contract surface. + +The missing design is the joint behavior-model layer that tells child +implementation work how those pieces compose. Without one model, implementers +can accidentally create a parallel `participants` schema tree, treat action +names as portable behavior, place authority in credentials or bearer tokens, +use backend logs as observations, or add a second behavior-mode taxonomy. + +## Decision + +Adopt one participant behavior model that composes the existing participant +semantics, SDL framing, runtime evidence, backend contract, and implementation +provenance surfaces. + +### 1. Participant behavior is a composed model, not a new stack + +The participant behavior model is the composition of: + +- authored participant framing from SDL `agents.*`; +- governed participant action contracts; +- participant observation boundaries and visibility transitions; +- participant-local outcome interpretation rules; +- declared authority, trust, access, control, and operating-scope boundaries; +- selected participant behavior mode; +- participant implementation manifest and provenance refs; +- backend realization and feature-support declarations; and +- runtime behavior-history, observation, shared-state, attribution, temporal, + evidence, and outcome records. + +No new top-level `participants` model, participant-specific persistence store, +exception hierarchy, audit stack, schema publication path, or backend-native +behavior abstraction is introduced by this design. + +### 2. ACT-603 abstract interaction model is the semantic center + +The abstract interaction model reuses ADR-022. A portable participant +interaction is defined over: + +- actor identity and participant address; +- action contract and action attempt; +- participant-visible observation; +- participant-local and shared state references; +- preconditions, effects, side effects, and failure classes; +- authority and scope facts consulted by those preconditions; +- temporal context and ordering relation; +- joint-action, coordination, contention, interference, and shared-state + relationships; +- attribution and evidence labels; and +- participant-local outcome interpretation. + +Action names, tool names, ATT&CK/CVE labels, backend commands, reward values, +timestamps, scheduler order, and raw logs are not portable interaction +semantics unless they are bound through the governed ACES contracts above. + +### 3. ACT-602 executable model means machine-checkable ACES contracts + +The executable participant behavior model is executable because processors, +backends, conformance tools, and validators can check it, not because ACES +standardizes one participant runtime loop or one external agent API. + +Executable behavior must flow through existing gates: + +- parser and closed SDL model validation; +- semantic validation of action, observation, outcome, authority, scope, and + named references; +- compiler output with canonical participant addresses; +- closed `ContractModel` payloads and generated schemas when a portable + contract is published; +- runtime behavior-history and observation validation; +- backend capability and feature-support disclosures; and +- conformance diagnostics and evidence requirements. + +Backends may realize behavior with humans, scripts, policies, LLM agents, RL +policies, emulators, simulators, services, or mixed controllers. The portable +claim is the ACES contract and evidence record, not the backend's private +implementation. + +### 4. ACT-606 behavior specifications are first-class aggregates + +A participant behavior specification is a named, versioned aggregate over the +existing behavior surfaces. It may bind: + +- participant or participant-role refs; +- action contract refs; +- observation boundary refs; +- outcome interpretation rule refs; +- authority, trust, access, control, and scope refs; +- behavior mode; +- realization profile and fidelity/disclosure claims; +- required backend feature support and evidence contracts; and +- lifecycle, semantic version, and extension-policy metadata. + +The aggregate is a specification artifact. It does not replace action +contracts, observation boundaries, outcome rules, participant implementation +manifests, backend capability declarations, or runtime evidence records. + +### 5. ACT-607 authority and scope remain authored semantics + +Participant authority and scope are scenario meaning. They are separate from +transport authentication, control-plane identity, OS users, credentials, +backend capability, participant implementation identity, and episode lifecycle +state. + +The model keeps these facets distinct: + +- starting accounts and initial access anchors; +- initial knowledge and starting conditions; +- authority anchors and trust bases; +- operating scope; +- action preconditions and failure classes; +- observation boundaries and visibility projections; +- implementation capability declarations; and +- control-plane authorization. + +Credentials, tokens, hidden prompts, answer keys, private traces, backend +private configuration, and adjudication assets are never portable authority +evidence inline. Use refs, digests, markings, redaction policies, and explicit +evidence boundaries. + +### 6. ACT-608 behavior modes reuse the controlled vocabulary + +Behavior modes are declared through the existing +`participant-decision-surface-modes` controlled vocabulary: + +- `autonomous`; +- `scripted`; +- `policy-directed`; +- `replayed`; +- `human-supervised`; and +- `mixed-control`. + +The ACT-608 word "supervised" maps to `human-supervised` for the current +surface. A broader non-human supervision concept must enter through the +governed vocabulary process rather than a casual `supervised` alias. + +Behavior mode is not implementation kind, participant role, backend feature +support, control-plane authorization, or multi-participant interaction class. +It says how decisions are selected or controlled at the participant +decision-surface boundary. + +### 7. Child implementation boundaries are fixed + +This joint design establishes the model shape. Spawned issues own executable +work: + +- #204 / ACT-602: executable behavior model gates, contract bindings, and + conformance evidence. +- #205 / ACT-603: abstract interaction model implementation coverage. +- #206 / ACT-606: first-class behavior specification authoring and validation + surface. +- #207 / ACT-607: authority/scope boundary authoring and validation surface. +- #208 / ACT-608: behavior-mode declaration, selection, and validation surface. + +Those issues must reuse the seams named in this ADR. They must not publish +parallel semantics to avoid the composition constraints. + +## Required Boundaries + +- `agents.*.actions` is an authoring affordance; action contracts carry + portable action semantics. +- Participant-visible observation is not hidden world truth, scoring state, + centralized training state, or archival evidence. +- Authority is not possession of a credential, bearer token, OS account, + backend handle, or control-plane caller identity. +- Behavior mode is not implementation kind, participant role, backend support + strength, or control-plane authorization. +- Runtime behavior history is evidence of realized behavior; it is not the + authored behavior specification. +- Backend capability declarations are support claims; they are not proof that a + particular participant implementation ran. +- Schema validity is necessary but insufficient for semantic conformance. + Runtime and conformance claims require evidence refs, negative fixtures, and + diagnostics at the owning implementation issue. +- Hidden truth, answer keys, private prompts, credentials, raw command output, + and backend-private objects must not be placed in portable specs, schemas, + diagnostics, fixtures, logs, snapshots, or changelog text. + +## Alternatives Considered + +### Add a new top-level `participants` behavior model + +Rejected. ADR-020 already establishes `agents.*` as the participant authoring +home unless a distinct concept requires another surface. A second top-level +model would split identity, role, authority, action, observation, and outcome +meaning across two SDL surfaces. + +### Treat backend or agent-framework APIs as the executable model + +Rejected. Gym-like, PettingZoo-like, CybORG-like, service, script, human, and +LLM-agent interfaces can all be useful realizations. None is the portable ACES +semantic authority. ACES claims must be expressed through its own contracts, +evidence, capability, and conformance surfaces. + +### Treat behavior modes as free-form strings + +Rejected. ACT-608 mode values affect comparability and evidence claims. They +must resolve through the controlled vocabulary and governed extension rules. + +### Put authority and scope only in runtime enforcement + +Rejected. ACT-607 is authored scenario meaning. Runtime enforcement may prove +or realize it, but the boundary must be declared and reviewable before a +backend acts. + +## Consequences + +### Positive + +- The five ACT requirements share one vocabulary and boundary model. +- Implementation issues get concrete seams instead of negotiating behavior + semantics independently. +- Existing parser, semantic validation, schema, runtime, backend, conformance, + and controlled-vocabulary machinery stays canonical. +- Security-sensitive concepts stay separated: authority, credentials, control + auth, observation, hidden truth, implementation identity, and backend support + are not collapsed. + +### Negative / costs + +- Implementers must carry more references through the behavior model instead + of adding local strings or metadata blobs. +- Behavior specifications need lifecycle, versioning, evidence, and extension + discipline even before a backend can execute every behavior class. +- Reviewers must distinguish design coverage from executable implementation + evidence for the spawned issues. + +### Risks + +- If a child issue treats action names or tool labels as action contracts, + ACES behavior portability will be overstated. +- If behavior modes are duplicated outside the controlled vocabulary, run + comparability and conformance will drift. +- If authority or scope is enforced only by credentials or backend sandboxing, + scenario meaning will be tied to deployment apparatus rather than authored + semantics. +- If runtime evidence is treated as the behavior specification, replay and + audit records can be mistaken for authoring intent. + +## References + +- Participant behavior model formal design: + `specs/formal/participant-behavior-model/README.md` +- [ADR-020: Declarative Participant Framing Boundaries](adr-020-declarative-participant-framing-boundaries.md) +- [ADR-022: Participant Behavior and Interaction Semantics](adr-022-participant-behavior-and-interaction-semantics.md) +- [ADR-041: Participant Implementation Manifest and Provenance Surface](adr-041-participant-implementation-manifest-and-provenance.md) +- [ADR-054: Participant Runtime Observable Lifecycle](adr-054-participant-runtime-observable-lifecycle.md) +- [ADR-060: Participant Backend-Facing Contract Surface](adr-060-participant-backend-facing-contract-surface.md) diff --git a/docs/decisions/adrs/adr-index.yaml b/docs/decisions/adrs/adr-index.yaml index 2b542880e..75d469f73 100644 --- a/docs/decisions/adrs/adr-index.yaml +++ b/docs/decisions/adrs/adr-index.yaml @@ -256,3 +256,19 @@ adrs: - id: ADR-062 path: docs/decisions/adrs/adr-062-concept-authority-catalog-governance-gate.md pin: b5a15b467977e433fbfff55a1de2b4744faada7dd54ea4806496e8293bedae91 + - id: ADR-063 + path: docs/decisions/adrs/adr-063-reference-emulation-backend.md + pin: 7c82b434119b802f8acc7c23148811316b21754fac39798b5790ff5e65bb2415 + - id: ADR-064 + path: docs/decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary.md + pin: 8bd179ba5c14c82b5998f9269d96a0c981cccb870c5b42bd27eb4b636b9c908b + - id: ADR-065 + path: docs/decisions/adrs/adr-065-experiment-run-provenance-contract-boundary.md + pin: 69f90581b2bcedc12bfb7ed8aa688787d7a0d9aa4cc49a90f8c631ecbae8a356 + - id: ADR-066 + path: docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md + pin: c2a8cf0bafdf53007df8a6c40f60bd63114656887b3c2feaca6f678ef6ae6d65 + amendments: + - date: 2026-06-23 + ref: "#335" + summary: "Recorded SEM-225 run-level augmentation disclosure implementation coverage." diff --git a/docs/decisions/issue-13-oci-tar-extraction-preflight.md b/docs/decisions/issue-13-oci-tar-extraction-preflight.md new file mode 100644 index 000000000..1a46fee54 --- /dev/null +++ b/docs/decisions/issue-13-oci-tar-extraction-preflight.md @@ -0,0 +1,156 @@ +# Issue 13 OCI Tar Extraction Preflight + +Date: 2026-06-21 + +Issue: #13. + +Requirement: none. The issue title, body, and acceptance criteria are the +contract. + +This note records architecture preflight guardrails for closing the unsafe OCI +module bundle extraction path on supported Python runtimes. It is guidance for +implementation only: it does not change resolver behavior, tests, changelog, or +published SDL documentation. + +## Binding Sources + +- ADR-053 owns SDL module composition: remote modules are resolved through the + module registry, checked against trust/digest/version/export policy, expanded + before semantic validation, and then compiled as one canonical scenario. +- `aces_sdl.module_registry` owns OCI source parsing, trust policy loading, + registry fetches, digest verification, signature verification, cache + placement, bundle extraction, `root_file` resolution, lock records, and + resolved import identity. +- `ImportDecl`, `ModuleDescriptor`, `TrustPolicy`, `RegistryTrustPolicy`, + `Lockfile`, and `ResolvedModule` are the canonical model surfaces. Do not add + a second OCI bundle schema or resolver DTO for this bug. +- `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, and + `implementations/python/pyproject.toml` define the verification graph and + Python support floor (`requires-python = ">=3.11"`). + +## Architecture Decisions + +- Fail closed on every supported runtime. Python 3.11 cannot call + `TarFile.extractall(..., filter="data")`, so the design must not rely on + catching `TypeError` and then calling unfiltered `extractall(...)`. +- Keep one private OCI bundle extraction policy in `aces_sdl.module_registry`. + The resolver, CLI, parser, and composition layer should consume that policy + through `resolve_import()` / `_extract_bundle_to_cache()`, not duplicate tar + validation. +- Treat remote OCI bundle bytes as attacker-controlled even after registry + allowlist, digest pinning, and signature checks. Those checks prove identity + and integrity; they do not make tar member paths safe to apply to the local + filesystem. +- Validate the complete member list before writing any member. Reject absolute + paths, parent-directory segments, host-dependent drive/root spellings, + symlinks, hard links, device nodes, FIFOs, and any member type outside + regular files and directories. +- Validate `root_file` with the same cache-containment policy after + normalization. The resolved root must remain inside + `.aces/module-cache//` and must be an extracted regular file. +- Preserve existing OCI lock and trust behavior. The fix is an extraction + hardening change, not a change to source syntax, lockfile identity, + signature policy, digest semantics, or module descriptor validation. + +## Required Incumbents + +- Resolver and supply-chain checks: `_parse_oci_source()`, + `_registry_base_url()`, `_json_request()`, `_bytes_request()`, + `_select_tag()`, `_validate_digest_pin()`, `_verify_signatures()`, + `_verify_allowed_parameters()`, `_descriptor_digest()`, and + `_oci_cache_dir()`. +- Models and validation: `SDLModel(extra="forbid")`, `ImportDecl`, + `ModuleDescriptor`, `TrustPolicy`, `RegistryTrustPolicy`, `Lockfile`, + `LockRecord`, and `ResolvedModule`. +- Parse and composition flow: `_load_normalized_data()`, + `parse_sdl_file()`, `aces_sdl.composition.expand_sdl_modules()`, import cycle + detection, namespace rewriting, and whole-scenario `SemanticValidator` + validation. +- Error handling: use `SDLParseError` for resolver failures and keep CLI + exposure through the existing Typer command envelopes. Do not add a tar- or + registry-specific exception hierarchy. +- Tests and workflow: extend `implementations/python/tests/test_sdl_module_registry.py` + for malicious tar members and `root_file` escapes; run the canonical + `nox -s tests`, `nox -s lint`, `nox -s hygiene`, and `nox -s verify` graph as + appropriate for the implementation. +- Security lint posture: `module_registry.py` already carries the narrow Ruff + Bandit ignores for explicit OCI URL fetch and tar extraction. Do not broaden + ignores globally; the extraction call must remain justified by local policy + tests. + +## Cross-Cutting Layers + +- YAML/config parsing: imported `root_file`, module descriptors, trust policy, + and lockfiles still enter through existing Pydantic models and parser + helpers. No ad hoc YAML or JSON parser should be introduced. +- Registry trust policy: preserve registry allowlists, insecure-HTTP opt-in, + signature requirement, trusted signer matching, version selection, digest + pins, lockfile digest checks, and export-hash checks before parsing the + extracted module. +- Filesystem boundary: all writes are confined to the digest-keyed cache + directory under the SDL base directory. Path checks must be based on resolved + containment, not string prefix comparisons. +- OS/process exposure: this issue should add no shell commands, subprocesses, + environment variables, tokens, private keys, or process-argv secrets. It only + consumes already-fetched bundle bytes in memory. +- Error envelope: failures should name the unsafe member or invalid + `root_file`, but should not print bundle contents, config payloads, private + key material, registry credentials, environment values, or tracebacks. +- Runtime/backend layers: compiled scenarios and runtime managers see only the + already-expanded canonical scenario. They must not learn about tar members, + cache internals, or module source-file layout. +- Repository policy: implementation belongs under + `implementations/python/packages/aces_sdl/` with focused tests under + `implementations/python/tests/`; user-visible security behavior needs a + `changelog.d/13.security.md` fragment when the code fix lands. + +## Extension Boundary + +The extensibility seam is the private bundle extraction policy, parameterized by +destination cache directory and declared root file. Future changes such as +alternate archive formats, stricter member metadata policy, cache atomicity, or +additional remote source classes should plug into that seam without changing +CLI comparison logic, parser behavior, lockfile serialization, or runtime +contracts. + +Keep the seam about extraction safety only. Source identity remains +`resolved_source`; runtime reads use `ResolvedModule.root_file`; cache keying +uses the manifest digest; content integrity uses the bundle digest. + +## Gotchas And Anti-Patterns + +Avoid: + +- retaining any path that calls `tar.extractall(cache_dir)` on Python 3.11; +- treating `members=` alone as equivalent to Python 3.12 `filter="data"` if the + implementation still accepts links, special files, ownership, or dangerous + permission metadata; +- validating only the final resolved path while allowing literal `..` segments + in member names or `root_file`; +- using host-dependent `Path` parsing before accounting for tar's POSIX path + format and Windows drive/backslash edge cases; +- checking `root_path.exists()` as the only proof that the cached extraction is + safe across different manifest or bundle identities; +- extracting members one by one before the full archive has passed policy; +- conflating OCI module bundles with the reference backend OCI container + driver or Docker/Podman runtime behavior; +- changing `manifest_digest`, `content_digest`, `resolved_source`, `root_file`, + or `module.id` meanings while fixing extraction; +- adding duplicate schemas, duplicate validators, duplicate resolver services, + duplicate exception types, or compatibility-wrapper logic under + `implementations/python/src/aces/`; +- widening the bug into registry operations, cache eviction, signer + distribution, lockfile migration, module publishing redesign, or runtime + planning changes. + +## Non-Goals + +- Implementing the extraction fix, tests, changelog, or docs updates in this + preflight. +- Changing SDL import source classes, module descriptor semantics, trust policy + defaults, lockfile schema, OCI publishing layout, or CLI command names. +- Changing parser normalization, semantic validation, instantiation, compiler, + runtime, control-plane, backend conformance, MCP, or reference backend OCI + behavior. +- Raising the Python support floor as the primary fix unless the project makes + a separate explicit runtime-support decision. diff --git a/docs/decisions/issue-196-run-313-reference-processor-preflight.md b/docs/decisions/issue-196-run-313-reference-processor-preflight.md new file mode 100644 index 000000000..7115caf4e --- /dev/null +++ b/docs/decisions/issue-196-run-313-reference-processor-preflight.md @@ -0,0 +1,213 @@ +# Issue 196 RUN-313 Reference Processor Preflight + +Date: 2026-06-20 + +Issue: #196. + +Requirement: RUN-313. + +This note records architecture preflight guardrails for the repository-owned +reference processor implementation. It is guidance for implementation only: it +does not implement a processor facade, change manifests, add conformance cases, +change schemas, or alter runtime behavior. + +## Binding Sources + +- ADR-008 defines the processor as the semantics-bearing layer between SDL + authoring and backend realization. Runtime is live execution state, not the + whole processor. +- ADR-036 defines package ownership: `aces_sdl` parses and instantiates, + `aces_processor` compiles, plans, and owns processor declarations, + `aces_runtime` owns live control, `aces_contracts` owns neutral DTOs, and + backends stay behind `aces_backend_protocols`. +- ADR-009 and ADR-061 define the authority boundary: published schemas, + fixtures, profiles, and specs are authority; reference implementation code + consumes them and proves compatibility. +- ADR-014, `.ground-control.yaml`, `.gc/plan-rules.md`, and `noxfile.py` define + the canonical verification graph and policy gates. +- `docs/explain/sdl/runtime-architecture.md` is the current end-to-end + processor/runtime path: instantiate, compile, plan, apply through a runtime + target, and validate portable envelopes at runtime boundaries. +- `docs/explain/reference/backend-conformance.md` is the nearest conformance + pattern: conformance is artifact-driven, schema-first, uses `Diagnostic` + envelopes, and avoids runner-local schema or profile authority. + +## Architecture Decisions + +- Treat the reference processor as an implementation-side orchestration surface + over existing public seams. It may assemble parse/instantiate, compile, plan, + manifest publication, and runtime/control-plane execution, but it must not + create a second compiler, planner, contract model, address scheme, or + manifest authority. +- Keep implementation ownership aligned with ADR-036. Processor-facing + assembly belongs in `aces_processor`; live apply/control-plane behavior stays + in `aces_runtime`; Typer commands stay in `aces_cli`; conformance runners stay + in `aces_conformance`; neutral DTOs stay in `aces_contracts`. +- The published processor manifest remains the declaration surface. Use + `create_reference_processor_manifest()` and + `reference_processor_manifest_payload()` as the single manifest rendering + path, and validate the result through `ProcessorManifestV2Model` and the + checked-in fixture. +- `supported_contract_versions` must be evidence-backed. Do not add a contract + id to the reference processor manifest unless the implementation actually + emits, consumes, or validates that contract through the shared model and a + test or conformance case exercises the path. +- End-to-end execution should drive the existing path: + `parse_sdl_file()` or `parse_sdl()`, `instantiate_scenario()`, + `compile_runtime_model()` / `compile_scenario_runtime_model()`, `plan()`, + `RuntimeManager` or `RuntimeControlPlane`, and a `RuntimeTarget` supplied by + `BackendRegistry`. +- The in-memory stub backend is a non-normative backend target for exercising + the processor/runtime path. Do not turn `aces_backend_stubs` into processor + authority, backend conformance authority, or a production backend. +- Any processor conformance addition should follow the backend conformance + model: published fixture/profile artifacts plus one registered validator + seam. Do not hard-code a second profile table, fixture loader, or schema + registry in processor implementation code. + +## Required Incumbents + +- SDL ingress and validation: `aces_sdl.parser.parse_sdl_file`, + `parse_sdl`, YAML safe loading, `_load_normalized_data`, + `SemanticValidator`, `instantiate_scenario`, `InstantiatedScenario`, + `SDLParseError`, `SDLValidationError`, and `SDLInstantiationError`. +- Processor path: `aces_processor.compiler.compile_runtime_model`, + `compile_scenario_runtime_model`, `aces_processor.planner.plan`, + `snapshot_delete_order`, `RuntimeModel`, `ExecutionPlan`, + `resource_payload(...)`, and `aces_processor.semantics.planner`. +- Processor declarations: `ProcessorManifest`, `ProcessorCapabilitySet`, + `ProcessorFeature`, `REFERENCE_PROCESSOR_NAME`, + `REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2`, + `create_reference_processor_manifest()`, and + `reference_processor_manifest_payload()`. +- Contract authority: `ContractModel(extra="forbid")`, + `ProcessorManifestV2Model`, `BackendManifestV2Model`, plan/result/status + models, `schema_bundle()`, `manifest_authority`, + `contracts/schema-publication-manifest.json`, and + `tools/check_generated_schemas.py`. +- Apparatus compatibility and provenance: + `validate_experiment_apparatus_context_against_manifests()`, + `ExperimentApparatusContextModel`, `ExperimentRunModel`, + `ApparatusIdentityModel`, `ConceptBindingEntryModel`, and + `aces_contracts.apparatus`. +- Runtime and backend boundaries: `BackendRegistry`, `RuntimeTarget`, + `_validate_runtime_target_shape`, `RuntimeManager`, + `RuntimeControlPlane`, `_call_backend_apply`, `_call_backend_diagnostics`, + `ApplyResult`, `RuntimeSnapshot`, `OperationReceipt`, and + `OperationStatus`. +- Observability and error shape: `aces_contracts.diagnostics.Diagnostic`, + `Severity`, runtime diagnostic helpers, operation records, control-plane + audit events, and existing conformance report envelopes. +- CLI and workflow: `aces_cli.main`, `aces_cli.processor`, `aces_cli.conformance`, + `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `implementations/python/pyproject.toml`, and compatibility-only wrappers + under `implementations/python/src/aces/`. + +## Cross-Cutting Layers + +- SDL/config parsing: authored input must pass through existing YAML parsing, + top-level shape checks, import expansion, variable substitution, + closed-world Pydantic models, and semantic validation. Do not build runtime + models directly from dictionaries to skip parser or validator behavior. +- Contract validation: portable payloads must use `aces_contracts` models and + `schema_bundle()`. Published schema edits require + `contracts/schema-publication-manifest.json` ledger updates and the generated + schema drift gate; implementation-only model edits are not schema authority. +- Manifest authority: processor and backend manifest claims must pass + `manifest_authority` allowlists, controlled vocabulary checks, concept + binding validation, and mutual compatibility checks. Manifest compatibility + is processor/backend apparatus compatibility, not SDL scenario meaning. +- Runtime target validation: any executable target must be constructed as a + `RuntimeTarget` through the registry/factory seam so manifest component + presence and method call shapes are checked before execution. +- Runtime apply boundary: backend calls must go through `_call_backend_apply` + or `_call_backend_diagnostics`, which deep-copy snapshots, convert backend + exceptions to diagnostics, validate `ApplyResult` shape, validate runtime + result contracts, and revert to the baseline snapshot on contract failure. +- Control-plane security: if the reference processor exposes HTTP/JSON control, + use `create_control_plane_app()` with `ControlPlaneSecurityConfig` rather + than a new adapter. Defaults must remain fail-closed: no trusted header + identities, no bearer tokens, verified proxy headers required when enabled, + role checks on mutating/read operations, target binding, request-size limits, + redacted internal 500s, idempotency keys, and audit records. +- Secret handling: manifests, fixtures, examples, diagnostics, and audit events + must not carry live secrets. Experiment parameters that cannot be published + use existing redaction/withheld shapes; backend exception text should not + contain tokens because runtime diagnostics can surface exception messages. +- Environment and OS exposure: avoid hidden environment configuration for + processor behavior. If a CLI or test needs subprocesses, use fixed argv, + `sys.executable` or the nox/uv invocation, no `shell=True`, no tokens in + argv, and no ambient home-directory, network, or package-install assumptions. +- Persistence: durable live state belongs behind `ControlPlaneStore`, + `InMemoryControlPlaneStore`, or `LocalControlPlaneStore`. Do not create a + second operation store or ad hoc snapshot JSON format for the processor path. +- Error envelopes: public execution and conformance failures should remain + structured `Diagnostic` values, operation statuses, or existing SDL errors. + Do not add processor-local exception hierarchies, log channels, raw + tracebacks, rejected payload dumps, or backend-native object reprs. +- Import and source policy: package imports must satisfy ADR-036 and + `tools/policy/adr_policy.yaml`; no new implementation logic belongs under + `implementations/python/src/aces/`; non-test package files must stay within + the ADR-015 line-cap policy. + +## Extension Boundary + +The main extension seam is a small reference-processor assembly API +parameterized by scenario input, instantiation parameters/profile, target name +or registry descriptor, target config, optional base snapshot, and optional +control-plane store. Future backend variations should add or select +`BackendRegistry` descriptors and manifest payloads, not edit processor control +flow. + +The manifest extension seam is +`REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2` plus the contract models, fixtures, +and conformance tests that prove each claim. Adding a future processor-facing +contract should require one authority update, one validator/fixture seam, and +one manifest test update, not local string checks across compiler, CLI, and +conformance code. + +The conformance extension seam is the published contract id. Processor +conformance, if added, should mirror the backend runner shape: load the +published fixture/profile corpus, validate through shared contract models, and +return structured diagnostics. Fixture-only validation can support unknown +future profile ids; live target certification cannot certify a runtime surface +the implementation does not understand. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating the Python reference processor as normative authority instead of a + consumer and executable proof of `specs/` and `contracts/`; +- adding a second compiled-runtime schema, manifest renderer, contract-id + allowlist, profile table, fixture loader, diagnostic class, or exception + hierarchy; +- bypassing `instantiate_scenario()`, `compile_runtime_model()`, `plan()`, + `RuntimeTarget`, `RuntimeManager`, or `_call_backend_apply()` for convenience; +- using backend-native state, object identities, or ad hoc dictionaries as + portable envelopes; +- broadening `aces_cli` or `aces_mcp` into semantic owners or runtime-internal + callers; +- making `aces_backend_stubs` production-like or normative; +- claiming support in `processor-manifest-v2` before the code path and tests + exercise the corresponding contract; +- changing published schemas without the schema publication manifest and + generated-schema parity gate; +- leaking secrets through command argv, environment dumps, backend exception + strings, diagnostics, audit records, fixture payloads, or CI logs; +- adding implementation logic under the legacy `aces.*` compatibility tree; +- editing `CHANGELOG.md` directly instead of adding the required fragment when + the later implementation is user-visible. + +## Non-Goals + +- No new processor implementation code in this preflight. +- No schema, fixture, manifest, conformance, CLI, runtime, or backend behavior + changes in this preflight. +- No production Docker/cloud/simulation backend and no managed cyber range + behavior. +- No new authentication mechanism, secret store, persistence backend, logging + stack, network fetch path, or OS process manager. +- No migration of legacy compatibility wrappers or owning-package public + surfaces beyond what the eventual RUN-313 implementation explicitly needs. +- No implementation plan, task breakdown, or requirement status transition. diff --git a/docs/decisions/issue-197-run-314-reference-emulation-backend-preflight.md b/docs/decisions/issue-197-run-314-reference-emulation-backend-preflight.md new file mode 100644 index 000000000..819e5ae41 --- /dev/null +++ b/docs/decisions/issue-197-run-314-reference-emulation-backend-preflight.md @@ -0,0 +1,237 @@ +# Issue 197 RUN-314 Reference Emulation Backend Preflight + +Date: 2026-06-20 + +Issue: #197. + +Requirement: RUN-314. + +This note records architecture preflight guardrails for a repository-owned +reference emulation backend. It is guidance for implementation only: it does +not implement a backend, add a profile, change manifests, publish schemas, +change conformance, or alter runtime behavior. + +## Binding Sources + +- ADR-004 defines the compile, plan, execute runtime architecture and requires + backends to provide explicit domain protocols plus a `BackendManifest`. +- ADR-008 separates processor apparatus, backend apparatus, live runtime state, + and archival run provenance. +- ADR-009, ADR-012, ADR-019, and ADR-061 define the authority boundary: + contracts, fixtures, profiles, concept authority, and published schemas are + authority; implementation code consumes and proves compatibility. +- ADR-036 defines package ownership: `aces_runtime` owns live control, + `aces_backend_protocols` owns backend declarations, `aces_backend_stubs` owns + non-normative stubs, `aces_contracts` owns neutral DTOs, and compatibility + wrappers under `implementations/python/src/aces/` must stay thin. +- ADR-041 and ADR-055 keep participant implementation identity and experiment + apparatus context separate from backend identity, authored SDL, and mutable + runtime snapshots. +- ADR-060 and `docs/research/participant-backend-contracts/preflight-guardrails.md` + govern participant-runtime declarations and retrieval surfaces. +- `docs/explain/reference/backend-conformance.md` governs backend conformance: + profiles and fixtures are artifact-driven, schema-first, and diagnostic-based. +- `docs/explain/reference/explicitness-realization-semantics.md` governs the + SEM-218 backend-realization boundary and provenance disclosure. +- `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, and the policy + tools define the verification graph and repository guardrails. + +## Architecture Decisions + +- Treat the reference emulation backend as an implementation-side backend that + realizes plans through `Provisioner`, `Orchestrator`, `Evaluator`, and, if + claimed, `ParticipantRuntime` protocol components. It must not become a new + processor, runtime manager, conformance authority, schema authority, or + experiment archive. +- Publish backend identity and capability through `BackendManifest` and + `backend_manifest_payload()`. Do not create an emulation-specific manifest + schema, local contract-id allowlist, or duplicate profile table. +- Construct executable targets through `BackendRegistry` and `RuntimeTarget`. + Target creation must keep manifest introspection and component construction + on the existing descriptor seam, and the component presence must match the + manifest. +- Route apply/control operations through `RuntimeManager`, + `RuntimeControlPlane`, `execute_operation()`, `execute_participant_action()`, + `_call_backend_apply()`, and `_call_backend_diagnostics()`. Do not call + backend components directly from CLI, tests, conformance, or HTTP adapters. +- Preserve the distinction between emulated infrastructure facts and portable + ACES runtime facts. Docker, Podman, libvirt, VM, virtual-network, or container + IDs are backend-native evidence unless mapped into existing + `RuntimeSnapshot`, result, history, or SEM-218 provenance surfaces. +- Keep the in-memory stub backend non-normative. RUN-314 may use stub behavior + as a test comparison point, but must not turn `aces_backend_stubs` into the + reference emulation backend or into backend contract authority. +- A claim in `supported_contract_versions`, `realization_support`, or + `capabilities.participant_runtime` is evidence-backed. The backend may only + claim contracts and capability terms it actually emits, consumes, or validates + through shared models and conformance/live tests. + +## Required Incumbents + +Reuse these repo surfaces before adding anything new: + +- Backend protocols and declarations: `aces_backend_protocols.protocols`, + `BackendManifest`, `BackendCapabilitySet`, `ProvisionerCapabilities`, + `OrchestratorCapabilities`, `EvaluatorCapabilities`, + `ParticipantRuntimeCapabilities`, `participant_runtime_capability_contract_gaps()`, + and `backend_manifest_payload()`. +- Runtime construction and execution: `BackendRegistry`, `RuntimeTarget`, + `RuntimeTargetComponents`, `_validate_runtime_target_shape`, + `RuntimeManager`, `RuntimeControlPlane`, `_call_backend_apply()`, + `_call_backend_diagnostics()`, `execute_operation()`, and + `execute_participant_action()`. +- Contract DTOs and validation: `ContractModel(extra="forbid")`, + `BackendManifestV2Model`, plan models, operation receipt/status models, + `RuntimeSnapshotEnvelopeModel`, workflow/evaluation/participant result + models, `schema_bundle()`, and `contracts/schema-publication-manifest.json`. +- Capability/profile authority: `BACKEND_SUPPORTED_CONTRACT_IDS`, + `validate_backend_supported_contract_versions()`, + `contracts/profiles/backend/*.json`, `BackendProfileModel`, + `load_backend_profile_from_path()`, and the `fixtures_root` / + `profiles_root` override seams in conformance. +- Vocabulary and concept authority: `validate_controlled_vocabulary_scope_values()`, + `ConceptBindingEntryModel`, `RealizationSupportDeclarationModel`, + `RealizationSupportMode`, governed `x-:` extension syntax, and + the concept-authority catalogs. +- Observability and error shape: `aces_contracts.diagnostics.Diagnostic`, + `Severity`, runtime diagnostic helpers, operation records, audit events, and + conformance report envelopes. +- Persistence: `ControlPlaneStore`, `InMemoryControlPlaneStore`, + `LocalControlPlaneStore`, `_snapshot_payload()`, `_record_payload()`, and + append-only audit records. +- HTTP/security: `create_control_plane_app()`, + `ControlPlaneSecurityConfig.strict_defaults()`, `ControlPlaneIdentity`, + `ControlPlaneRole`, request-size guards, idempotency fingerprints, audited + authorization, and redacted FastAPI internal errors. +- Apparatus and provenance: `ExperimentApparatusContextModel`, + `ExperimentRunModel`, `ParticipantImplementationManifestModel`, + `ParticipantImplementationProvenanceModel`, and + `validate_experiment_apparatus_context_against_manifests()`. +- Verification: `.ground-control.yaml`, `.gc/plan-rules.md`, `noxfile.py`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/check_generated_schemas.py`, `tools/check_json_artifacts.py`, and + `tools/verify_all.py`. + +## Cross-Cutting Layers + +The intended design must pass every layer it touches: + +- SDL/config ingress: scenario input still flows through `parse_sdl()` / + `parse_sdl_file()`, `instantiate_scenario()`, `compile_runtime_model()` or + `compile_scenario_runtime_model()`, and `plan()`. Backend configuration must + be explicit data passed through `BackendRegistry` factories, not hidden SDL + keys or ambient YAML side channels. +- Manifest authority gate: `supported_contract_versions`, concept bindings, + realization-support declarations, participant-runtime role/feature terms, and + backend compatibility must validate through the existing manifest models and + authority helpers. +- Runtime target shape gate: manifest component claims must match actual + provisioner, orchestrator, evaluator, and participant-runtime components, and + each method must be invokable with the runtime call shape. +- Backend apply gate: backend methods must return `ApplyResult` with a + `RuntimeSnapshot`; `_call_backend_apply()` deep-copies snapshots, converts + backend exceptions to diagnostics, validates result shape, validates runtime + result contracts, runs SEM-218 non-approximation/provenance checks where + applicable, and reverts to the baseline snapshot on contract failure. +- Runtime snapshot gate: portable observation belongs in first-class snapshot, + result, history, participant episode, shared-state, and + `realization_provenance` fields. Do not smuggle emulation state through + `RuntimeSnapshot.metadata` or generic `details` when a first-class carrier + exists. +- Control-plane security gate: any HTTP/JSON exercise surface must use + `create_control_plane_app()` and explicit `ControlPlaneSecurityConfig`. + Defaults remain fail-closed: no trusted header identities, no bearer tokens, + verified proxy headers only when enabled, target-bound identities, role + checks for read/mutating operations, request-size limits, idempotency + fingerprints, audit records, and redacted internal 500s. +- Secret and OS exposure gate: daemon endpoints, API tokens, registry + credentials, VM credentials, SSH keys, passwords, private host paths, raw + environment dumps, process argv, backend inspect payloads, and backend-native + object reprs must not appear in manifests, fixtures, diagnostics, audit + details, snapshots, conformance reports, or examples. References, digests, + sensitivity labels, and redaction classifications are the portable surface. +- Host process boundary: if a backend must invoke local emulation tools, use + fixed argv, no `shell=True`, no tokens in process arguments, bounded timeouts, + controlled working directories, and structured stdout/stderr handling that + cannot leak secrets through diagnostics. +- Persistence gate: durable live state goes through `ControlPlaneStore`; local + backend working directories or emulator state are backend-private caches, not + a second ACES operation store or snapshot schema. +- Error-envelope gate: public failures are `Diagnostic` values, + `OperationReceipt`, `OperationStatus`, existing SDL exceptions, or existing + HTTP error envelopes. Do not add a backend-specific public exception + hierarchy, raw tracebacks, log channel, or unredacted payload dump. +- Conformance gate: fixture validation and live target certification use + `aces_conformance.conformance`, published fixtures, published backend + profiles, and structured diagnostics. Do not certify an emulation backend by + local smoke tests alone. +- Package/import gate: new backend implementation code, if needed, belongs in + an implementation package that consumes `aces_backend_protocols`, + `aces_contracts`, and the public runtime registry seam. Core packages must + not import a concrete backend, and no implementation logic belongs in the + compatibility-only `implementations/python/src/aces/` tree. + +## Extension Boundary + +The primary extension seam is the backend registry descriptor: +`manifest_factory(**config)` plus `components_factory(manifest=manifest, +**config)`. The next reasonable variation should select or configure an +emulation provider, workspace, network namespace, image source, and resource +limits through that seam without rewriting `RuntimeManager`, +`RuntimeControlPlane`, conformance, or manifest rendering. + +The manifest extension seam is the existing backend manifest fields: +`supported_contract_versions`, `realization_support`, +`capabilities.provisioner`, optional `orchestrator`, optional `evaluator`, and +optional `participant_runtime`. Future claims require authority-backed +contract ids, governed vocabulary terms, fixtures, and conformance evidence. + +The conformance extension seam is the published backend profile artifact and +contract id. If a new profile is genuinely needed, add it under +`contracts/profiles/backend/` and load it through the existing profile loader; +do not edit a Python-only profile map. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating "reference emulation backend" as a new authority surface instead of + a concrete implementation of existing backend contracts; +- adding an emulation-specific manifest schema, schema registry, fixture loader, + profile table, vocabulary table, exception hierarchy, operation store, audit + log, or HTTP adapter; +- bypassing `BackendRegistry`, `RuntimeTarget`, `RuntimeManager`, + `RuntimeControlPlane`, `_call_backend_apply()`, or conformance runners for + convenience; +- using backend-native IDs, daemon inspect payloads, container/VM names, + scheduler order, timestamps, or tool labels as portable ACES semantics + without typed mapping and provenance; +- collapsing backend capability, processor capability, participant + implementation identity, control-plane identity, and experiment apparatus + context into one concept; +- claiming participant runtime support without publishing state/history through + the snapshot fields that conformance checks; +- putting raw emulator command output, credentials, tokens, SSH keys, private + paths, host environment, or raw tracebacks into portable artifacts; +- adding Docker/Podman/libvirt-specific SDL syntax, top-level runtime fields, or + public contracts when existing runtime surfaces already carry the portable + fact; +- editing published schemas without the schema publication manifest and + generated-schema parity gate; +- adding implementation logic under `implementations/python/src/aces/` or + weakening ADR policy to make imports pass. + +## Non-Goals + +- No backend implementation code in this preflight. +- No new schema, fixture, manifest, profile, conformance, CLI, runtime, or + package-metadata changes in this preflight. +- No production cloud, managed cyber range, orchestration platform, or + privileged host daemon policy. +- No new authentication mechanism, secret store, persistence backend, logging + stack, process manager, or emulator abstraction. +- No redesign of SDL authoring syntax, processor planning, runtime snapshots, + participant implementation manifests, experiment-run provenance, or backend + conformance. +- No implementation plan, task breakdown, requirement status transition, or PR + merge guidance. diff --git a/docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md b/docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md new file mode 100644 index 000000000..2113b8ba8 --- /dev/null +++ b/docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md @@ -0,0 +1,211 @@ +# Issue 248 SEM-216 Boundary Semantics Preflight + +Date: 2026-06-23 + +Issue: #248. + +Requirement: SEM-216. + +This note records architecture preflight guardrails for distinguishing +runtime-observable state, captured evidence, derived evaluations, analysis +outputs, and audience-specific views. It is implementation guidance only: it +does not add schemas, validators, runtime behavior, APIs, storage, or coverage +claims. + +## Binding Sources + +- ADR-016 and `docs/explain/reference/shared-semantic-integrity.md` define the + SEM lifecycle model and list SEM-216 as planned coverage. +- ADR-054 defines runtime-observable participant lifecycle and observation + boundaries. +- ADR-060 and `specs/formal/runtime-contracts/participant-backend-contracts.md` + define participant carriers and retrieval projections. +- ADR-055, ADR-064, ADR-065, and `specs/formal/experiment-core/README.md` + define experiment tasks, archival runs, capture specs, evidence records, + derived measures, run traceability, and realized-form disclosures. +- ADR-009, ADR-019, ADR-061, `contracts/schema-publication-manifest.json`, and + `.gc/plan-rules.md` define schema authority and publication governance. +- ADR-012 and ADR-062 define concept-authority, controlled-vocabulary, and + extension discipline. +- ADR-021 defines falsification-first evidence expectations for architecture + and maturity claims. + +## Architecture Decisions + +- Do not create a universal "state/evidence/result/view" super-schema. SEM-216 + is a boundary-semantics requirement over existing contract families. +- Runtime-observable state is live control-plane/runtime material: + `RuntimeSnapshot`, snapshot entries, workflow/evaluation results and history, + participant episode/behavior/shared-state/joint-action records, operation + status, and audit metadata. It is operational and mutable until captured or + sealed; it is not archival run provenance by itself. +- Captured evidence is the EXP-708 evidence surface: + `experiment-evidence-record-v1`, run evidence artifacts, content URI plus + checksum, bounded payload summary, sensitivity, redaction state, loss + disclosure, and provenance. A capture spec declares intent; it is not proof + that evidence exists. +- Derived evaluations and interpreted outputs are distinct from raw evidence. + Live evaluator state uses compiled evaluation result/history contracts; + archival interpreted measures use `experiment-derived-measure-v1`; run result + summaries are compact run-local summaries, not the canonical derivation + record when a first-class derived measure is needed. +- Analysis outputs are study/report/analysis artifacts or derived measures with + `measure_kind: analysis-output`. Claim-bearing analysis must remain grounded + through run traceability and at least one derived-measure reference; it must + not float from raw runtime state or evaluator `detail`. +- Audience-specific views are projections over recorded carriers, not sources + of truth. Reuse `participant-status-view-v1`, + `participant-history-view-v1`, and `participant-context-view-v1` patterns: + source refs, source layers, transformation refs, evidence/provenance refs, + audience scope, visibility projection, markings, redaction policy, + completeness, comparability, limitations, and optional payload refs. +- Cross-boundary movement must be by typed references, source layers, + traceability blocks, checksums, and provenance refs. Do not copy backend + payloads into `metadata`, `detail`, `details`, audit details, or view payloads + to avoid modeling the boundary. + +## Required Incumbents + +- Runtime and backend boundary: + `aces_contracts.runtime_state.RuntimeSnapshot`, + `RuntimeSnapshotEnvelopeModel`, `ApplyResult`, `_call_backend_apply()`, + `_snapshot_contract_diagnostics()`, + `workflow_result_contract_diagnostics()`, + `evaluation_result_contract_diagnostics()`, + `participant_runtime_state_contract_diagnostics()`, and + `participant_runtime_history_transition_diagnostics()`. +- Control-plane security, API, and persistence: + `RuntimeControlPlane`, `ControlPlaneStore`, `ControlPlaneSecurityConfig`, + `ControlPlaneIdentity`, `ControlPlaneRole`, request-size guards, + idempotency keys, request fingerprints, audit events, response models, and + redacted FastAPI 500 envelopes. +- View contracts and retrieval: + `ParticipantStatusViewModel`, `ParticipantHistoryViewModel`, + `ParticipantContextViewModel`, `ParticipantContextSourceLayerModel`, + `ParticipantContextTransformationModel`, + `ParticipantContextComparabilityModel`, and + `aces_runtime.participant_retrieval`. +- Experiment contracts: + `ExperimentCaptureSpecModel`, `ExperimentEvidenceRecordModel`, + `ExperimentRawEvidenceContentModel`, `ExperimentDerivedMeasureModel`, + `ExperimentRunModel`, `ExperimentRunTraceabilityModel`, + `ExperimentResultSummaryModel`, `ExperimentStudyModel`, constrained + `ExperimentReferenceModel` subclasses, and + `validate_experiment_run_against_task()`. +- Manifest/capability authority: + `BackendManifestV2Model`, `ObservationCapabilitiesModel`, + backend observation capability gap checks, supported-contract allowlists, and + governed observation vocabulary scopes. +- Schema and concept authority: + `ContractModel`, `schema_bundle()`, `tools/generate_contract_schemas.py`, + `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication-manifest.json`, + `contracts/concept-authority/`, controlled-vocabulary validators, reference + models, and semantic profiles. +- Verification: + `.ground-control.yaml`, `.gc/plan-rules.md`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/check_schema_publication.py`, `tools/check_generated_schemas.py`, + `tools/check_json_artifacts.py`, `tools/check_semantic_coverage.py`, and + `tools/verify_all.py`. + +## Cross-Cutting Layers + +- SDL/config layer: if an implementation touches authoring or configuration, + use the existing safe YAML parser, closed `SDLModel` shapes, variable-key + rejection, `SemanticValidator`, and `instantiate_scenario()` revalidation. + Do not infer boundary meaning from raw YAML strings. +- Contract shape layer: externally visible payloads must remain closed-world + `ContractModel` descendants, generated/published JSON Schemas, fixtures, and + semantic-invariant annotations. Schema changes must update the publication + manifest and keep `schema_bundle()` byte-compatible with published schemas. +- Runtime adapter layer: backend-returned live state must fail closed through + `_call_backend_apply()` and existing `runtime.backend-contract-invalid` + diagnostics before entering snapshots or persistence. +- Evaluation layer: evaluator outputs must correspond to observable compiled + evaluation entries and pass `EvaluationResultContract`, + `EvaluationExecutionContract`, `EvaluationExecutionState`, + `EvaluationHistoryEvent`, and `validate_evaluation_result()` semantics. +- Evidence and archival layer: evidence, derived measures, run traceability, + and run summaries must pass the existing experiment model validators, + timestamp parsing, reference constraints, reported-value rules, redaction/loss + disclosure rules, and task/run cross-artifact validation. +- View layer: retrieval views must pass source binding, nested scope binding, + completeness-basis, comparability, audience-scope, visibility-projection, + marking, and redaction-policy validation. Views must not invent state absent + from recorded carriers. +- Manifest/profile layer: observation capability, supported contract versions, + concept bindings, controlled vocabulary scopes, and semantic-profile + assumptions must resolve through existing authority helpers. +- API/auth layer: every future read or mutation surface must reuse + control-plane authentication, backend/operator/auditor role checks, + request-size limits, idempotency, request fingerprints, audit events, and + redacted error envelopes. +- Persistence layer: live state remains in `ControlPlaneStore` plain-data + envelopes; archival evidence/run/measure records require their own + schema-versioned contract records when implemented. Do not use + `RuntimeSnapshot.metadata`, operation records, participant histories, or audit + details as archival storage. +- Error-envelope and OS exposure layer: diagnostics, HTTP errors, logs, + fixtures, audit records, helper commands, and process argv must not expose + credentials, bearer tokens, private keys, hidden truth, raw evidence payloads, + environment dumps, backend-private object reprs, or full tracebacks. + +## Extension Boundary + +The extensibility seam is the existing set of typed references and governed +dimensions: + +- `ParticipantContextSourceLayerModel.source_layer`, `audience_scope`, + transformation refs, comparability refs, evidence refs, and provenance refs + for view semantics; +- `ExperimentReferenceModel.ref_kind`, `ExperimentArtifactRefModel.role`, + `ExperimentDerivedMeasureModel.measure_kind`, method metadata, + `ExperimentRunTraceabilityModel`, and `ExperimentRealizedFormDisclosureModel` + for experiment and analysis semantics; +- backend `capabilities.observation.supported_*` vocabularies for portable + observation capability claims. + +Future variation should parameterize producer source, seal point, content +locator/checksum policy, view audience, source layer, transformation, and +comparability basis through those fields. Add concept-authority or controlled +vocabulary terms only when portable comparison requires them; otherwise keep +backend- or study-specific details as refs, limitations, or method parameters. + +## Gotchas And Anti-Patterns + +Avoid: + +- treating `RuntimeSnapshot`, operation status, workflow/evaluation history, + participant history, or audit events as archival run provenance; +- treating capture specs as proof of capture; +- treating raw evidence records as metric values, scores, derived measures, or + analysis outputs; +- treating derived measures, run result summaries, participant outcome reports, + evaluator `detail`, or study analysis plans as interchangeable result blobs; +- treating views as sources of truth or letting a `payload_ref` hide a + backend-native object model; +- exposing hidden world truth, centralized-training state, scoring state, + private answer keys, prompts, credentials, or raw configuration in + participant-visible or audience-specific views; +- adding duplicate schemas, reference models, validators, exception + hierarchies, logging/audit paths, persistence stores, fixture loaders, + manifest renderers, or workflow logic; +- hand-editing `contracts/schemas/` or skipping schema publication manifest + updates when published schemas change; +- weakening accepted ADRs in place rather than using ADR-059 amendment or + supersedure rules. + +## Non-Goals + +- Implementing SEM-216 behavior, schemas, validators, endpoints, storage, + capture, analysis, or tests in this preflight note. +- Updating the SEM-200 coverage row or transitioning SEM-216 from DRAFT. +- Adding SDL authoring syntax, a new universal boundary taxonomy, a new + archival provenance root, a new evidence store, a new view service, or a new + analysis engine. +- Redesigning participant semantics, evaluator semantics, experiment-core + contracts, backend manifests, control-plane security, schema authority, or + concept authority. +- Publishing secrets, hidden truth, backend-private payloads, raw evidence, or + raw process/environment state as portable contract data. diff --git a/docs/decisions/issue-334-sem-224-observability-plane-preflight.md b/docs/decisions/issue-334-sem-224-observability-plane-preflight.md new file mode 100644 index 000000000..b9a64771f --- /dev/null +++ b/docs/decisions/issue-334-sem-224-observability-plane-preflight.md @@ -0,0 +1,230 @@ +# Issue 334 SEM-224 Observability Plane Preflight + +Date: 2026-06-23 + +Issue: #334. + +Requirement: SEM-224. + +This note records architecture preflight guardrails for implementing +observability plane separation semantics. It is implementation guidance only: +it does not add SDL syntax, schemas, validators, runtime behavior, APIs, +storage, fixtures, tests, or coverage claims. + +## Binding Sources + +- ADR-066 is the semantic authority for the five observability/evidence planes + and the augmentation classification boundary. +- `specs/formal/observability-evidence-plane.md` defines the invariant set, + source-to-contract-to-test matrix, and negative probe set for SEM-224. +- `specs/sdl/observability-and-evidence.md` defines SDL authoring rules for + scenario-native observability systems and authored evidence requirements. +- ADR-022 and ADR-054 define participant-visible observations, visibility + projection, markings, redaction, loss, and information guarantees. +- ADR-055, ADR-064, and ADR-065 define experiment tasks, capture specs, raw + evidence records, derived measures, apparatus context, run traceability, and + realized-form disclosures. +- ADR-036 defines package ownership: SDL language logic belongs in + `aces_sdl`, runtime live control belongs in `aces_runtime`, and neutral + boundary DTOs belong in `aces_contracts`. +- ADR-056 and ADR-057 define explicit-redaction behavior for runtime observed + values and keep scenario credentials distinct from operator secrets. +- ADR-009, ADR-019, ADR-061, `contracts/schema-publication-manifest.json`, and + `.gc/plan-rules.md` define schema authority and publication governance. +- ADR-012 and ADR-062 define concept-authority, controlled-vocabulary, and + extension discipline. + +## Architecture Decisions + +- SEM-224 is a plane-classification and boundary-validation requirement over + existing carriers. Do not create a generic top-level `observability` model, + a universal evidence super-schema, or a second runtime telemetry hierarchy. +- A claim-bearing artifact has one primary plane by carrier and contract role, + not by words such as `log`, `trace`, `telemetry`, `observation`, or + `evidence`. +- Scenario-native observability stays in SDL authoring space. Node-scoped + logical services must use the existing `nodes..runtime.*` + runtime-family registry and reference model when a current family fits. +- Authored evidence requirements are declarative obligations. They may bind to + `experiment-capture-spec-v1`, but they are not raw evidence, not participant + objectives, and not proof that capture occurred. +- Processor/backend operational observability is apparatus data. It may support + audit, setup evidence, or capability claims only after it is projected + through existing manifest, diagnostics, control-plane, apparatus-context, or + evidence contracts. +- Captured evidence is raw evidence material or an + `experiment-evidence-record-v1` record. It must not carry metric values, + scores, or interpreted conclusions. +- Derived analysis is interpreted output over evidence. It must cite source + evidence and must not disclose hidden truth, prompts, private traces, + answer keys, secrets, or adjudication assets without governed marking, + redaction, and authorization. +- The classifier seam should be data-only and carrier-oriented. It may accept + an extensible carrier kind or contract id plus optional source-layer/ref-kind + parameters, but it must not import runtime internals, inspect backend-native + DTOs, or infer plane ownership from arbitrary strings. +- Any executable validation must compose with existing validators: SDL semantic + validation for authored surfaces, Pydantic contract validators for external + contracts, runtime adapter diagnostics for backend-returned state, and + control-plane API guards for HTTP exposure. + +## Required Incumbents + +- SDL authoring and semantic validation: + `SDLModel`, `parse_sdl()`, `parse_sdl_file()`, `_HASHMAP_SECTIONS`, + variable-key rejection, `SemanticValidator`, `instantiate_scenario()` full + revalidation, `specs/sdl/sections.md`, `specs/sdl/references.md`, and + `specs/sdl/runtime-inventory.md`. +- Scenario-native runtime families: + `RuntimeConfiguration`, `RUNTIME_SERVICE_FAMILIES`, + `RuntimeServiceFamily`, `collect_qualified_runtime_family_refs()`, and + current families such as network sensors, network detection engines, + security monitoring managers, forwarding agents, service listeners, + platform applications, and datastore services. +- Experiment evidence and analysis contracts: + `ExperimentReferenceModel` and constrained subclasses, + `ExperimentCaptureSpecModel`, `ExperimentCaptureRequirementModel`, + `ExperimentCaptureWindowModel`, `ExperimentEvidenceRecordModel`, + `ExperimentRawEvidenceContentModel`, `ExperimentDerivedMeasureModel`, + `ExperimentRunTraceabilityModel`, `ExperimentRealizedFormDisclosureModel`, + `ExperimentRunModel`, `ExperimentStudyModel`, and + `validate_experiment_run_against_task()`. +- Participant-visible observation and audience-view contracts: + `ParticipantObservationEnvelopeModel`, + `ParticipantContextViewModel`, `ParticipantHistoryViewModel`, + `ParticipantStatusViewModel`, participant runtime base-envelope fields, + visibility projection, source-layer, transformation, marking, redaction, and + comparability validation. +- Apparatus and operational observability: + `BackendManifestV2Model`, `ProcessorManifestV2Model`, + backend observation capabilities, `ExperimentApparatusContextModel`, + selected manifest validation, `Diagnostic`, `Severity`, + `RuntimeSnapshot`, `OperationReceipt`, `OperationStatus`, and audit records. +- Control-plane security and exposure: + `ControlPlaneSecurityConfig`, `ControlPlaneIdentity`, `ControlPlaneRole`, + bearer/proxy authentication, backend/operator/auditor role gates, + request-size guards, idempotency keys, request fingerprints, audit events, + response models, and redacted FastAPI 500 envelopes. +- Schema and concept authority: + `ContractModel`, `schema_bundle()`, `tools/generate_contract_schemas.py`, + `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication-manifest.json`, + `contracts/concept-authority/`, controlled-vocabulary validators, reference + models, semantic profiles, and `tools/check_generated_schemas.py`. +- Repo workflow and policy: + `.ground-control.yaml`, `.gc/plan-rules.md`, + `tools/check_repo_policy.py`, `tools/check_requirement_governance.py`, + `tools/check_schema_publication.py`, `tools/check_json_artifacts.py`, + `tools/check_semantic_coverage.py`, and `tools/verify_all.py`. + +## Cross-Cutting Layers + +- SDL/config layer: new authoring claims must pass safe YAML loading, closed + `SDLModel` shapes, normalized keys, symbol-key variable rejection, semantic + reference resolution, and post-instantiation revalidation. A plane decision + must come from the carrier and resolved refs, not from raw YAML text. +- Runtime-family layer: scenario-native observability must use registered + runtime families, stable ids, same-node service refs, child-ref catalogs, and + family-specific validators. Add a new family only with an owning ADR, + `runtime-inventory.md` row, schema/model updates, validators, fixtures, and + tests. +- Contract shape layer: external payloads must remain closed-world + `ContractModel` descendants with generated/published JSON Schemas. Published + schema changes require `schema_bundle()` parity, fixtures, and a + `contracts/schema-publication-manifest.json` last-change ledger entry. +- Experiment-core layer: capture intent, raw evidence, derived measures, run + traceability, realized-form disclosure, and studies must pass the existing + experiment model validators, timestamp parsing, reference constraints, + reported-value rules, redaction/loss disclosure rules, and task/run + cross-artifact validation. +- Participant visibility layer: any participant-visible projection must pass + ADR-022/ADR-054 observation-envelope and audience-view rules for source + layer, transformation, visibility, marking, redaction policy, delivery basis, + information guarantee, loss, and comparability. +- Apparatus/control-plane layer: backend logs, traces, health, setup evidence, + and diagnostics remain operational until projected through manifests, + apparatus context, diagnostics, evidence records, or run traceability. + Control-plane exposure must reuse auth, role checks, request-size limits, + idempotency, request fingerprints, audit events, response models, and + redacted error envelopes. +- Persistence layer: live state remains in `RuntimeSnapshot` and + `ControlPlaneStore` envelopes. Archival evidence/run/measure artifacts must + use their schema-versioned contracts. Do not use `RuntimeSnapshot.metadata`, + operation details, audit blobs, backend DTOs, or raw logs as portable plane + carriers. +- Error-envelope and OS exposure layer: diagnostics, HTTP errors, audit + records, logs, fixtures, process argv, and helper command examples must not + expose bearer tokens, private keys, operator secrets, hidden truth, raw + evidence payloads, environment dumps, backend-private object reprs, or full + tracebacks. +- Policy layer: implementation must satisfy Ground Control policy checks, + module-boundary policy, concept-authority governance, generated-schema parity, + schema-publication governance, semantic coverage, and requirement + traceability. Do not add authority-bearing artifacts outside approved roots. + +## Extension Boundary + +The extensibility seam is a carrier-oriented plane classifier plus existing +typed references: + +- carrier or contract id identifies the primary plane; +- `ExperimentReferenceModel.ref_kind`, capture requirement ids, measurement + channel refs, evidence-record refs, derived-measure refs, and run + traceability express cross-plane links; +- `ParticipantContextSourceLayerModel.source_layer`, transformation refs, + evidence refs, provenance refs, audience scope, and comparability refs + express audience-view projections; +- `RUNTIME_SERVICE_FAMILIES` and child-ref metadata express + scenario-native observability targets; +- backend observation capability vocabularies express apparatus support + claims. + +Future variation should parameterize carrier kind, source ref, capture window, +channel, source layer, audience, transformation, evidence ref, provenance ref, +redaction/loss expectation, and comparability basis through those existing +fields. Add concept-authority or controlled-vocabulary terms only when portable +comparison requires them. + +## Gotchas And Anti-Patterns + +Avoid: + +- classifying planes by string labels instead of carrier type and resolved + contract role; +- treating backend logs, traces, health checks, audit records, or stack traces + as participant observations or authored scenario meaning; +- treating a capture spec or authored evidence requirement as proof of capture; +- treating a scenario-native observability system as satisfying an evidence + requirement merely because it exists; +- putting evidence, analysis, augmentation, or comparability semantics only in + `metadata`, `details`, diagnostics, audit blobs, backend-native DTOs, raw + logs, or free-form tags; +- mixing raw evidence with metric values, scores, evaluator decisions, result + summaries, or claims; +- leaking hidden adjudication assets, prompts, answer keys, private traces, + operator secrets, or backend-private ids through participant-visible + observations or public analysis outputs; +- duplicating schemas, validation helpers, exception hierarchies, logging/audit + paths, persistence stores, fixture loaders, manifest renderers, concept + vocabularies, or workflow logic; +- bypassing `contracts/schema-publication-manifest.json` or hand-editing + published schemas without reference-implementation parity; +- weakening accepted ADRs in place instead of following ADR-059 amendment or + supersedure rules. + +## Non-Goals + +- Implementing SEM-224 behavior, schemas, validators, endpoints, storage, + capture scheduling, telemetry collection, analysis engines, fixtures, or + tests in this preflight note. +- Updating SEM-224 status or claiming implementation coverage. +- Adding SDL syntax for DSL-123 or DSL-124, which belong to their own + implementation issues. +- Implementing SEM-225 augmentation carriers or validators beyond preserving + the boundary needed for SEM-224. +- Creating a generic observability bag, a universal evidence taxonomy, a new + archival provenance root, a new backend telemetry API, or a new persistence + store. +- Redesigning participant semantics, experiment-core contracts, + runtime-family schemas, control-plane security, schema authority, or concept + authority. diff --git a/docs/decisions/issue-336-dsl-123-scenario-native-observability-preflight.md b/docs/decisions/issue-336-dsl-123-scenario-native-observability-preflight.md new file mode 100644 index 000000000..44f30c41c --- /dev/null +++ b/docs/decisions/issue-336-dsl-123-scenario-native-observability-preflight.md @@ -0,0 +1,226 @@ +# Issue 336 DSL-123 Scenario-Native Observability Preflight + +Date: 2026-06-24 + +Issue: #336. + +Requirement: DSL-123. + +This note records architecture preflight guardrails for implementing +scenario-native observability, telemetry, logging, tracing, monitoring, and +comparable in-world data systems. It is implementation guidance only: it does +not add SDL syntax, schemas, validators, runtime behavior, APIs, storage, +fixtures, tests, or coverage claims. + +## Binding Sources + +- ADR-066 is the semantic authority for observability/evidence plane + separation. +- `specs/formal/observability-evidence-plane.md` defines the DSL-123 matrix + rows and negative probes. +- `specs/sdl/observability-and-evidence.md` defines the SDL authoring rules for + scenario-native observability systems. +- `docs/decisions/issue-334-sem-224-observability-plane-preflight.md` and + `aces_sdl.observability_plane_semantics` define the carrier-oriented plane + classifier that DSL-123 must extend rather than replace. +- `specs/sdl/runtime-inventory.md`, `specs/sdl/sections.md`, and + `specs/sdl/references.md` define the runtime-family, top-level-section, and + reference-resolution extension boundaries. +- ADR-022 and ADR-054 define participant-visible observation projection, + markings, redaction, loss, and information guarantees. +- ADR-055, ADR-064, and ADR-065 define experiment-core task, capture, raw + evidence, derived measure, run traceability, realized-form, and augmentation + carriers. +- ADR-036 defines package ownership: SDL language logic belongs in `aces_sdl`, + runtime live control belongs in `aces_runtime`, and neutral boundary DTOs + belong in `aces_contracts`. +- ADR-056 and ADR-057 define redaction behavior for observed runtime values and + the boundary between scenario values and operator secrets. +- ADR-009, ADR-019, ADR-061, ADR-062, + `contracts/schema-publication-manifest.json`, and `.gc/plan-rules.md` define + schema authority, publication governance, concept authority, and workflow + gates. + +## Architecture Decisions + +- DSL-123 is an SDL authoring and reference-targeting requirement. First-class + means stable SDL identity plus typed or qualified references; it does not mean + a generic top-level `observability` bag. +- Node-scoped observability systems must use the existing + `nodes..runtime.` runtime-family model when the service is + logical runtime state. +- Reuse current runtime families when they fit the product-neutral service + identity: `network_sensors`, `network_detection_engines`, + `security_monitoring_managers`, `forwarding_agents`, `service_listeners`, + `platform_applications`, and `datastore_services`. +- Add a new runtime family only when the modeled system has a distinct + product-neutral logical service identity that cannot fit an existing family + without changing that family's meaning. +- Targeting and interaction must route through fail-closed references: + qualified runtime-family refs from `collect_qualified_runtime_family_refs()`, + existing objective `target` resolution, and typed relationship subtypes. Bare + ambiguous refs must never resolve by first match. +- Plane ownership remains carrier-oriented. If DSL-123 adds or reclassifies a + scenario-native family, update `SCENARIO_NATIVE_OBSERVABILITY_FAMILIES` and + its validation against `RUNTIME_SERVICE_FAMILIES`; do not infer from words + such as `log`, `trace`, `telemetry`, `monitoring`, or `evidence`. +- A scenario-native observability system may be an evidence source, a + relationship endpoint, an objective/action target, or an affected asset. Its + existence is not an authored evidence requirement, proof of capture, raw + evidence, derived analysis, or backend operational telemetry. +- Processor/backend logs, traces, health checks, and diagnostics stay in the + processor/backend operational plane unless projected through an existing SDL, + participant-runtime, apparatus, evidence, or run-provenance carrier. +- Participant-visible dashboards, alerts, logs, traces, or monitoring outputs + must pass participant visibility projection, markings, redaction, loss, and + authorization gates. They must not be exposed by raw backend DTOs, stack + traces, diagnostics, or `RuntimeSnapshot.metadata`. + +## Required Incumbents + +- SDL parser/model closure: `parse_sdl()`, `parse_sdl_file()`, `SDLModel`, + `_HASHMAP_SECTIONS`, variable-key rejection, `SemanticValidator`, + `instantiate_scenario()`, and post-instantiation semantic revalidation. +- Runtime-family registry and refs: `RuntimeConfiguration`, + `RUNTIME_SERVICE_FAMILIES`, `RuntimeServiceFamily`, + `RuntimeReferenceChild`, `collect_qualified_runtime_family_refs()`, + `nested_node_runtime_family_aliases()`, and family-specific validators. +- Runtime-family docs and catalogs: `specs/sdl/runtime-inventory.md`, + `specs/sdl/references.md`, `specs/sdl/sections.md`, and + `specs/sdl/diagnostics.md`. +- Plane classifier: `ObservabilityEvidencePlane`, + `SCENARIO_NATIVE_OBSERVABILITY_FAMILIES`, `classify_runtime_family()`, + `classify_contract_plane()`, and `token_decides_plane()`. +- Existing runtime families likely to carry DSL-123 use cases: + network sensors, network detection engines, security monitoring managers, + forwarding agents, service listeners, platform applications, and datastore + services. +- Experiment-core boundaries: `ExperimentReferenceModel`, + `ExperimentCaptureSpecModel`, `ExperimentCaptureRequirementModel`, + `ExperimentEvidenceRecordModel`, `ExperimentDerivedMeasureModel`, + `ExperimentRunTraceabilityModel`, `ExperimentAugmentationDisclosureModel`, + `ExperimentRunModel`, and `validate_experiment_run_against_task()`. +- Participant-visible contracts: `ParticipantObservationEnvelopeModel`, + `ParticipantContextViewModel`, `ParticipantHistoryViewModel`, + `ParticipantStatusViewModel`, source-layer, transformation, marking, + redaction, loss, and comparability validators. +- Apparatus/control-plane surfaces: `BackendManifestV2Model`, + `ProcessorManifestV2Model`, `ExperimentApparatusContextModel`, + `Diagnostic`, `Severity`, `RuntimeSnapshot`, `OperationReceipt`, + `OperationStatus`, `ControlPlaneSecurityConfig`, `ControlPlaneIdentity`, + `ControlPlaneRole`, `ControlPlaneStore`, audit events, request-size guards, + idempotency keys, request fingerprints, response models, and redacted FastAPI + 500 envelopes. +- Schema and concept authority: `ContractModel`, `schema_bundle()`, + `tools/generate_contract_schemas.py`, `tools/check_generated_schemas.py`, + `contracts/schemas/`, `contracts/fixtures/`, + `contracts/schema-publication-manifest.json`, concept-authority catalogs, and + controlled-vocabulary validators. + +## Cross-Cutting Layers + +- SDL/config layer: authored observability elements must pass safe YAML loading, + normalized field keys, closed `SDLModel` shapes, symbol-key variable + rejection, semantic validation, and instantiated revalidation. New fields must + be represented in `sections.md`, `references.md`, published SDL schemas, and + the reference implementation together. +- Runtime-family layer: node-scoped systems must have stable `_id` + identity, duplicate-id rejection, registered child-ref metadata, and + family-local semantic validators. A new family must register once in + `RUNTIME_SERVICE_FAMILIES`; no second registry is allowed. +- Reference layer: objective targets, relationship endpoints, evidence sources, + and child records must resolve fail-closed through the existing reference + catalog and validator. Ambiguous or dangling refs are fatal. +- Plane-classifier layer: scenario-native observability families must be + registered data, not token-matched strings. Any family change must keep + `classify_runtime_family()` and the SEM-224 tests fail-closed. +- Contract shape layer: any external payload must remain a closed + `ContractModel` descendant with `schema_bundle()` parity. Published schema + changes require fixtures and a `contracts/schema-publication-manifest.json` + last-change ledger entry. +- Experiment-core layer: capture intent, raw evidence, derived measures, run + traceability, and augmentation disclosures must use existing experiment-core + carriers. Observability elements can be referenced by those carriers, but + must not replace them. +- Participant visibility layer: any participant-visible observability output + must route through participant observation/context/history/status contracts + with source layer, transformation, marking, redaction, loss, and comparability + metadata. +- Apparatus/control-plane layer: backend operational telemetry remains + apparatus data unless explicitly projected. HTTP exposure must reuse + authentication, role checks, request-size limits, idempotency, request + fingerprints, audit events, response models, and redacted error envelopes. +- Persistence layer: live operational state remains in `RuntimeSnapshot` and + `ControlPlaneStore` envelopes. Portable scenario meaning, evidence, analysis, + and augmentation claims must not live only in `RuntimeSnapshot.metadata`, + operation details, audit blobs, backend DTOs, raw logs, or free-form tags. +- Secret and OS-exposure layer: runtime fields, examples, diagnostics, logs, + audit details, fixtures, command examples, process argv, environment captures, + and backend inspect payloads must not expose bearer tokens, private keys, + operator secrets, hidden truth, raw trace payloads, raw evidence payloads, + full stack traces, or backend-private object representations. +- Policy layer: implementation must satisfy Ground Control policy checks, + module-boundary policy, concept-authority governance, generated-schema + parity, schema-publication governance, semantic coverage, and requirement + traceability. + +## Extension Boundary + +The extensibility seam is runtime-family identity plus typed references: + +- runtime family key, collection name, primary id field, and child-ref tree + identify targetable scenario-native observability assets; +- existing objective `target`, typed relationship refs, experiment + `ExperimentReferenceModel.ref_kind`, capture channel refs, evidence-record + refs, derived-measure refs, and run traceability express cross-plane links; +- participant source layers, transformations, audience scope, markings, + redaction policy, evidence refs, provenance refs, and comparability refs + express any participant-visible projection; +- concept-authority or controlled-vocabulary terms belong only where portable + comparison needs a shared term, not where a family-local enum is enough. + +Future telemetry, logging, tracing, metrics, or dashboard variants should be +parameterized as product-neutral runtime-family fields or children such as +source refs, output streams, control channels, collection windows, channels, +formats, transports, markings, loss/redaction expectations, and comparability +basis. Do not hard-code a vendor, protocol, or backend adapter as the DSL +concept boundary. + +## Gotchas And Anti-Patterns + +Avoid: + +- adding a top-level `observability`, `telemetry`, `logs`, or `traces` bag; +- adding a second runtime-family registry, target resolver, or plane classifier; +- deciding plane ownership from string tokens instead of carrier role and + registered runtime family; +- resolving ambiguous observability targets by first match; +- using backend logs, traces, health checks, diagnostics, audit records, stack + traces, process argv, or environment dumps as participant observations or + authored scenario meaning; +- treating an observability system declaration as an authored evidence + requirement or proof that capture occurred; +- duplicating experiment-core capture, evidence-record, derived-measure, run, + apparatus-context, or augmentation schemas; +- storing portable meaning only in metadata, diagnostic details, audit blobs, + backend-native DTOs, raw logs, or free-form tags; +- leaking hidden truth, answer keys, evaluator state, prompts, private traces, + bearer tokens, credentials, or operator secrets through SDL, contracts, + fixtures, generated schemas, diagnostics, logs, audit details, or examples; +- weakening accepted ADRs in place instead of following ADR-059 amendment or + supersedure rules. + +## Non-Goals + +- Implementing DSL-123 behavior, SDL syntax, schemas, validators, compiler + addresses, endpoints, storage, telemetry collection, log parsing, trace + collection, fixtures, or tests in this preflight note. +- Updating DSL-123 status or claiming implementation coverage. +- Implementing DSL-124 authored evidence requirements, evidence capture + scheduling, raw evidence records, derived analysis, or run-level satisfaction + logic. +- Implementing SEM-225 augmentation behavior beyond preserving the disclosure + boundary that DSL-123 must not bypass. +- Replacing participant visibility semantics, experiment-core contracts, + runtime-family schemas, control-plane security, schema authority, concept + authority, diagnostics, audit, persistence, or workflow policy. diff --git a/docs/explain/reference/reference-emulation-backend.md b/docs/explain/reference/reference-emulation-backend.md new file mode 100644 index 000000000..71f538ed4 --- /dev/null +++ b/docs/explain/reference/reference-emulation-backend.md @@ -0,0 +1,146 @@ +# Reference Emulation Backend + +The reference emulation backend (RUN-314, +[ADR-063](../../decisions/adrs/adr-063-reference-emulation-backend.md)) is a +repository-owned, concrete implementation of the four backend protocol roles — +Provisioner, Orchestrator, Evaluator, and ParticipantRuntime. It lives in the +implementation package `aces_reference_backend` and is **not** a normative +authority surface: it consumes the existing manifest, registry, conformance, and +SEM-218 realization seams and proves compatibility against the published +contracts. + +Unlike the non-normative in-memory stub (`aces_backend_stubs`), the reference +backend realizes provisioning plans against a pluggable deployment driver. The +default driver is hermetic; an opt-in OCI driver realizes against a real +container runtime (docker/podman). Either way, only portable ACES facts reach +snapshots, diagnostics, and conformance reports. + +## Constructing and registering a target + +The backend registers on the standard `BackendRegistry` descriptor seam under +the name `reference-emulation`. Construct a target directly or through the +registry: + +```python +from aces_reference_backend import ( + create_reference_backend_target, + register_reference_backend, +) +from aces.core.runtime.registry import BackendRegistry + +# Direct: default hermetic in-process driver. +target = create_reference_backend_target() + +# Via the registry; driver config flows through the descriptor seam. +registry = BackendRegistry() +register_reference_backend(registry) +target = registry.create("reference-emulation") +``` + +Config kwargs flow to both the manifest factory and the components factory, so a +`driver=` (or any other extra) passes through; the manifest factory accepts and +ignores extras it does not use. + +## Drivers + +- **In-process (default).** `InProcessDriver` records the realize/destroy + operations it is asked to perform and synthesizes portable handles. It runs no + subprocess and needs no container runtime, so it is safe in CI and in the + default conformance/apply path. +- **OCI (opt-in).** `OciDeploymentDriver` realizes against docker or podman + through fixed-argv subprocess calls (never a shell string), a closed runtime + allowlist, and bounded timeouts. Backend-native output (container ids, daemon + inspect payloads, raw stderr) is consumed privately and never reaches the + returned handles or diagnostics. It enforces an operator **image-trust + policy** — only the configured `default_image`, an `allowed_images` entry, or a + digest-pinned ref is realized, so a plan-pinned `node.source` cannot turn plan + submission into arbitrary-image code execution. Realization is transactional + (a partial failure rolls back what succeeded), and containers are attached to + every network their plan declares. + +```python +from aces_reference_backend import create_reference_backend_target +from aces_reference_backend.drivers.oci import OciDeploymentDriver + +driver = OciDeploymentDriver(runtime="docker", workspace="aces-ref") +target = create_reference_backend_target(driver=driver) +``` + +## Running it: apply and provenance + +Drive apply and control through `RuntimeManager` / `RuntimeControlPlane`, never +by calling backend components directly. Apply records SEM-218 realization +provenance through the existing apply gate: + +```python +import textwrap +from aces.core.runtime.manager import RuntimeManager +from aces.core.sdl import parse_sdl +from aces_reference_backend import create_reference_backend_target + +manager = RuntimeManager(create_reference_backend_target()) +plan = manager.plan(parse_sdl(textwrap.dedent(""" + name: demo + nodes: + web: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} +"""))) +result = manager.apply(plan) + +assert result.success +# The realized snapshot entry preserves the planned (portable) payload; real +# container realization is a driver side effect, not a snapshot mutation. +assert result.snapshot.entries["provision.node.web"].payload["os_family"] == "linux" +# SEM-218 provenance is filled by the apply gate. +provenance = {e.field_path: e for e in result.snapshot.realization_provenance} +assert provenance["nodes.web.os"].requirement_kind == "os-family" +``` + +## Conformance + +The reference target passes `run_target_conformance` at the +`FULL_REMOTE_CONTROL_PLANE` profile — the same case set the stub passes, +including the full RUN-311 participant-episode probe: + +```python +from aces.core.runtime.conformance import ( + BackendCapabilityProfile, + run_target_conformance, +) +from aces_reference_backend import create_reference_backend_target + +report = run_target_conformance(create_reference_backend_target()) +assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE +assert report.passed +``` + +## Opt-in container integration + +Real-container realization is verified by a `docker`-marked integration test +(`implementations/python/tests/test_reference_backend_docker_integration.py`). +The `docker` marker is excluded from the default hermetic test suite, and the +test self-skips cleanly when no container runtime is available. Run it +explicitly: + +```bash +# from the repo root +nox -s integration_docker +# or directly +cd implementations/python && uv run --frozen python -m pytest -m docker -q +``` + +A non-blocking, runtime-gated CI job runs the same session when a container +runtime is present; the canonical `verify` graph stays hermetic and never +depends on a runtime. + +## Portable-fact boundary + +Container, VM, and network ids, daemon inspect payloads, host paths, +environment, process argv, tokens, credentials, SSH keys, and backend-native +object reprs never appear in manifests, snapshots, diagnostics, conformance +reports, or examples. The portable surface is references, digests, sensitivity +labels, and redaction classifications. Public failures are `Diagnostic`, +`OperationReceipt`, or `OperationStatus` values; the backend defines no +backend-specific exception hierarchy, log channel, or raw traceback. diff --git a/docs/explain/reference/shared-concept-model.md b/docs/explain/reference/shared-concept-model.md index f9586c68f..cd47cd5f2 100644 --- a/docs/explain/reference/shared-concept-model.md +++ b/docs/explain/reference/shared-concept-model.md @@ -167,6 +167,39 @@ against the authoritative catalog at model time, and scope paths must resolve to governed manifest vocabulary surfaces that are actually declared in the artifact. +## External Knowledge Binding Effects (SEM-217) + +`SEM-217` fixes what an external knowledge binding is allowed to do to native +ACES meaning. The effect is explicit and surface-owned; it is never inferred +from a label, a URL, or the fact that an external source uses similar words. + +The current effect vocabulary is closed: + +- `annotates`: the external reference adds reviewable context or evidence and + does not change native validation, planning, runtime, or conformance meaning + by itself. +- `aligns`: the ACES concept family is adopted from the external authority with + equivalent meaning. For the current UCO slice, adopted families align and + carry an empty divergence list. +- `refines`: the ACES concept family is adapted from the external authority. + It preserves a reviewed correspondence while narrowing or diverging in an + explicitly recorded way. +- `constrains`: a governed profile, manifest, or vocabulary surface restricts + which family or term a field may use. A constraint is enforceable validation + behavior, not descriptive metadata. + +Implementation guidance: + +- resolve effects from existing contract data: concept-family provenance, + `uco-alignment-v1`, semantic-profile `required_bindings`, manifest + `concept_bindings`, and controlled vocabulary governed scopes; +- do not add live ontology fetches, authority-specific runtime calls, or + token-bearing process arguments; +- do not treat UCO, ATT&CK, OCSF, CACAO, STIX, OpenC2, CVE, exploit modules, + or benchmark milestones as SDL syntax or as automatic schema inheritance; +- do not overload `ConceptBinding` into a general external term-mapping model; + it remains the manifest vocabulary-to-family binding surface. + ## ACES Extension Discipline (GOV-919) `GOV-919` implements the ACES concept layer by making native extension metadata diff --git a/docs/explain/reference/shared-semantic-integrity.md b/docs/explain/reference/shared-semantic-integrity.md index 300c9a55a..17a372dfa 100644 --- a/docs/explain/reference/shared-semantic-integrity.md +++ b/docs/explain/reference/shared-semantic-integrity.md @@ -249,8 +249,9 @@ so they are tracked by their own requirements, not here. | Participant temporal, tool/affordance, and decision-surface semantics | SEM-213, SEM-219, SEM-220 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | partial | | Participant reference trajectories, demonstrations, budgets, and quota/exhaustion semantics | SEM-221, SEM-223 | — | — | planned | | Participant outcome interpretation | SEM-215 | authoring, validation, compilation, planning, execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/adrs/adr-022-participant-behavior-and-interaction-semantics.md`, `implementations/python/packages/aces_sdl/participant_outcome_semantics.py`, `implementations/python/packages/aces_sdl/semantics/participant_outcome.py`, `implementations/python/packages/aces_processor/models.py`, `implementations/python/packages/aces_processor/compiler.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/tests/test_sem_215_participant_outcome_interpretation.py`, `implementations/python/tests/test_participant_semantics_invariant_oracle.py` | active | -| Derived operational context views (portable meaning and comparability) | SEM-214 | — | — | planned | -| Evidence, evaluation, view-boundary, and observability-plane semantics | SEM-216, SEM-224, SEM-225 | — | — | planned | -| External knowledge bindings semantics | SEM-217 | — | — | planned | +| Derived operational context views (portable meaning and comparability) | SEM-214 | execution, observation | `specs/formal/participant-semantics/README.md`, `specs/formal/runtime-contracts/participant-backend-contracts.md`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/packages/aces_runtime/participant_retrieval.py`, `implementations/python/packages/aces_runtime/control_plane_api_participant_retrieval.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_control_plane.py`, `implementations/python/tests/test_runtime_control_plane_api.py` | active | +| Boundary semantics for runtime-observable state, captured evidence, derived evaluations, analysis outputs, and audience-specific views | SEM-216 | execution, observation | `specs/formal/participant-semantics/README.md`, `docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md`, `implementations/python/packages/aces_contracts/contracts.py`, `contracts/schemas/control-plane/participant-context-view-v1.json`, `contracts/schemas/experiment-core/experiment-evidence-record-v1.json`, `implementations/python/tests/test_sem_216_boundary_semantics.py`, `implementations/python/tests/test_participant_backend_contracts.py`, `implementations/python/tests/test_runtime_contracts.py` | active | +| Evidence, evaluation, view-boundary, and observability-plane semantics | SEM-224, SEM-225, DSL-123, DSL-124 | authoring, validation, execution, observation | `docs/decisions/adrs/adr-066-observability-evidence-plane-separation.md`, `specs/formal/observability-evidence-plane.md`, `specs/sdl/observability-and-evidence.md` | partial | +| External knowledge bindings semantics | SEM-217 | validation, execution | `specs/formal/participant-semantics/README.md`, `docs/explain/reference/shared-concept-model.md`, `implementations/python/packages/aces_contracts/semantic_binding_effects.py`, `implementations/python/tests/test_sem_217_knowledge_bindings.py` | active | | Explicitness and realization semantics (binding declarations vs processor/backend realization) | SEM-218 | validation, instantiation, compilation, planning, execution, observation | `specs/formal/realization/explicitness-and-realization.md`, `specs/formal/realization/README.md`, `docs/explain/reference/explicitness-realization-semantics.md`, `implementations/python/packages/aces_sdl/explicitness.py`, `implementations/python/packages/aces_sdl/validator/__init__.py`, `implementations/python/packages/aces_sdl/instantiate.py`, `implementations/python/packages/aces_contracts/apparatus.py`, `implementations/python/packages/aces_contracts/vocabulary.py`, `implementations/python/packages/aces_contracts/contracts.py`, `implementations/python/packages/aces_contracts/runtime_state.py`, `implementations/python/packages/aces_backend_protocols/manifest.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_processor/semantics/realization.py`, `implementations/python/packages/aces_runtime/backend_calls.py`, `implementations/python/packages/aces_runtime/manager.py`, `implementations/python/packages/aces_runtime/control_plane_store.py`, `implementations/python/tests/test_sem_218_explicitness.py`, `implementations/python/tests/test_sem_218_realization.py`, `implementations/python/tests/test_sem_218_runtime_realization.py`, `implementations/python/tests/test_runtime_planner.py`, `implementations/python/tests/test_backend_manifest.py`, `implementations/python/tests/test_processor_manifest.py`, `implementations/python/tests/test_runtime_contracts.py` | active | | Clock, time-domain, advancement/pacing/synchronization, and temporal ordering/causality semantics | SEM-227, SEM-228, SEM-229 | — | — | planned | diff --git a/docs/explain/sdl/limitations.md b/docs/explain/sdl/limitations.md index a3c711516..db427b801 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. SDL now covers node-scoped network-sensor monitoring posture and network detection-engine inventory, but the broader authored evidence-requirements model remains incomplete | OpenRange, OCSF-informed telemetry models | +| **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 | | **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/runtime-architecture.md b/docs/explain/sdl/runtime-architecture.md index a5003c469..548381eb8 100644 --- a/docs/explain/sdl/runtime-architecture.md +++ b/docs/explain/sdl/runtime-architecture.md @@ -171,6 +171,27 @@ honestly react to upstream changes. Ordering graphs must remain acyclic within each domain; the planner emits error diagnostics and invalidates the plan if a cycle survives into runtime planning. +### Reference processor + +`aces_processor.reference.run_reference_processor(scenario, backend_manifest)` +(and the `ReferenceProcessor` class) is the repository-owned reference +implementation of the processing model. It assembles the stages above into one +call — accepting SDL text, a file path, or an already-parsed scenario — and +returns a `ReferenceProcessorResult` carrying the compiled `RuntimeModel`, the +`ExecutionPlan`, and the combined compilation + planning diagnostics +(`is_valid` is false when any are errors). `ReferenceProcessor.manifest_payload()` +exposes the published processor manifest through the canonical renderer. + +Per ADR-008 the processor is the semantics-bearing layer between SDL authoring +and backend realization, so the reference processor's responsibility ends at the +`ExecutionPlan`: it imports only the SDL/processor/contract layers and never +`aces_runtime` (the one-directional boundary enforced by +`tools/policy/adr_policy.yaml`). End-to-end execution is realized by composing +its plan with the reference runtime (`RuntimeManager` / `RuntimeControlPlane`); +the backend-conformance live probe drives exactly this composition, and the +RUN-313 tests use it to prove every contract version the processor manifest +declares is exercised end to end. + ## Runtime Snapshot `RuntimeSnapshot` is the typed state model used by the planner and manager. Each diff --git a/docs/index.md b/docs/index.md index 2319a2b54..e16b76881 100644 --- a/docs/index.md +++ b/docs/index.md @@ -164,6 +164,12 @@ 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/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 @@ -181,6 +187,7 @@ explain/reference/glossary explain/reference/shared-concept-model explain/reference/shared-semantic-integrity explain/reference/backend-conformance +explain/reference/reference-emulation-backend explain/reference/normative-artifact-authority explain/reference/assessment-semantics explain/reference/objective-semantics diff --git a/docs/research/experiment-core/index.md b/docs/research/experiment-core/index.md index aabdb3fec..f02a1bdc3 100644 --- a/docs/research/experiment-core/index.md +++ b/docs/research/experiment-core/index.md @@ -1,7 +1,8 @@ # Experiment Core Research Notes -These notes support the issue #87 experiment-core design. They are research and -pre-flight evidence, not normative contract authority. +These notes support the issue #87 experiment-core design and the issue #88 +evidence/measure extension. They are research and pre-flight evidence, not +normative contract authority. ```{toctree} :maxdepth: 1 @@ -13,4 +14,10 @@ cyber-range-scientific-instrument design-criteria-for-exp-701-705 traceability-matrix-exp-701-705 preflight-guardrails +issue-88-evidence-measure-preflight-guardrails +issue-233-exp-707-capture-spec-preflight-guardrails +issue-234-exp-708-evidence-record-preflight-guardrails +issue-235-exp-709-derived-measure-preflight-guardrails +issue-238-exp-720-run-provenance-preflight-guardrails +issue-239-exp-722-realized-form-preflight-guardrails ``` diff --git a/docs/research/experiment-core/issue-233-exp-707-capture-spec-preflight-guardrails.md b/docs/research/experiment-core/issue-233-exp-707-capture-spec-preflight-guardrails.md new file mode 100644 index 000000000..63c51c3d8 --- /dev/null +++ b/docs/research/experiment-core/issue-233-exp-707-capture-spec-preflight-guardrails.md @@ -0,0 +1,133 @@ +# Issue #233 EXP-707 Capture Specification Preflight Guardrails + +Date: 2026-06-22 + +Issue: #233. + +Requirement: EXP-707. + +This preflight narrows the issue #88 evidence and measure boundary to the +capture-specification work for EXP-707. ADR-064 and +`specs/formal/experiment-core/README.md` remain the normative design authority. +This note is guidance for implementation only. + +## Architecture Decisions + +- Treat `experiment-capture-spec-v1` as declarative intent: it records what + evidence should be captured, over what scope and window, with sensitivity, + integrity, retention, redaction, and loss-disclosure expectations. +- Keep capture specifications distinct from scenario-internal logging, + monitoring setup, raw evidence records, derived measures, run summaries, and + backend observation capability claims. +- Reuse the existing schema-first contract surface in + `aces_contracts.contracts`; do not add a second capture-spec schema, parser, + registry, or validation stack. +- Keep EXP-707 out of live runtime state. A capture spec may reference a task, + run, apparatus context, participant, backend, processor, network, or service + scope, but it must not mutate `RuntimeSnapshot`, `ControlPlaneStore`, or + scenario runtime configuration. +- Future capture backends, retention stores, APIs, and capture success records + are separate work. EXP-707 is only the specification surface. + +## Required Incumbents + +- Contract source: + `implementations/python/packages/aces_contracts/contracts.py`, especially + `ContractModel`, `ExperimentCaptureSpecModel`, + `ExperimentCaptureRequirementModel`, `ExperimentCaptureWindowModel`, + `ExperimentMeasurementChannelReferenceModel`, and + `ExperimentReferenceModel`. +- Published schemas: + `schema_bundle()`, `tools/generate_contract_schemas.py`, + `contracts/schemas/experiment-core/experiment-capture-spec-v1.json`, and + `contracts/schema-publication-manifest.json`. +- Fixture and conformance corpus: + `contracts/fixtures/experiment-core/experiment-capture-spec-v1/` and the + experiment-core schema tests in `implementations/python/tests/`. +- Semantic invariants: + `x-aces-invariants` entries for capture requirement key equality, capture + window resolution, and capture window time ordering. +- Adjacent concept boundaries: + `experiment-evidence-record-v1` for raw captured evidence, + `experiment-derived-measure-v1` for interpreted outputs, + `experiment-run-v1` evidence artifacts for archival run summaries, and + `backend-manifest-v2` `capabilities.observation` for backend support claims. +- If any API surface is added later: + `aces_runtime.control_plane_api`, `control_plane_api_guards`, + `control_plane_security`, request fingerprints, audit events, response + models, and redacted error handling. + +## Cross-Cutting Layers + +- Structural validation: every external payload must pass closed-world + Pydantic `ContractModel` validation and the generated draft 2020-12 JSON + Schema. Unknown fields remain errors. +- Semantic validation: capture requirement map keys must match embedded + `requirement_id` values; every `window_refs` value must resolve to a + declared capture window; each window must declare a start, end, or trigger; + and windows with both timestamps must not end before they start. +- Reference shape: use typed `ExperimentReferenceModel` variants and existing + reference-kind constraints. Do not encode scope or channel meaning in + free-form strings when a typed reference already exists. +- Security and redaction: capture specs may declare sensitivity, redaction + policy, retention policy, and loss-disclosure expectations, but must not + carry real credentials, bearer tokens, private keys, environment dumps, raw + process argv, backend-private payloads, or full tracebacks in fields, + fixtures, diagnostics, logs, or examples. +- API/auth surface: if a capture-spec endpoint is later introduced, mutating + requests must use the existing control-plane identity and role checks, + request-size guard, idempotency fingerprint, audit logging, closed DTOs, and + redacted FastAPI error envelope. Do not create a capture-specific auth or + error stack. +- Persistence: do not put capture specs in `RuntimeSnapshot.metadata`, operation + records, participant histories, or backend-private logs. Any future durable + store must preserve schema-versioned artifacts and avoid treating the current + in-memory control-plane store as archival experiment persistence. +- OS-level exposure: command-line helpers and examples must not pass secrets or + captured raw payloads through process arguments. Use checked-in fixtures with + synthetic data and content references with checksums. +- Governance: schema changes must update the contract source, regenerate + schemas through `schema_bundle()`, preserve fixture coverage, and update the + schema publication manifest when published schema hashes change. + +## Extensibility Guardrail + +The extension point belongs in declared capture dimensions, not in new +parallel surfaces. Preserve and extend the existing parameters: +`capture_kind`, `capture_scope`, `channel_ref`, `window_refs`, +`expected_media_types`, `required_artifact_roles`, `sensitivity`, +`redaction_policy`, `integrity_requirements`, `retention_policy`, and +`loss_disclosure_required`. New portable vocabulary should go through concept +authority only when comparison or backend capability claims depend on bounded +terms. + +## Gotchas And Anti-Patterns + +- Do not treat a capture spec as proof that evidence was captured. +- Do not put metric values, scores, evaluation decisions, or derived summaries + in a capture spec. +- Do not collapse capture specs, evidence records, and derived measures into a + single blob. +- Do not duplicate task observation requirements or run evidence artifacts as a + second source of truth. Capture specs may reference those artifacts or + requirements but must not redefine their contracts. +- Do not infer capture scope from scenario logging configuration, monitoring + declarations, backend defaults, or participant observation history. +- Do not add new SDL root sections or scenario syntax for EXP-707 without a new + ADR. +- Do not hand-edit `contracts/schemas/`. +- Do not create new exception hierarchies, logging stacks, audit formats, + schema registries, persistence stores, or workflow logic for this issue. + +## Non-Goals + +- Runtime evidence capture, packet/log collection, telemetry streaming, and + storage retention implementation. +- HTTP APIs, schedulers, workers, or background capture orchestration. +- Raw evidence publication, redaction execution, loss accounting, chain of + custody, and derived-measure computation. +- Statistical analysis, evaluator behavior, score calculation, or study + comparison logic. +- Scenario-internal monitoring or logging configuration. +- Backend capability declaration beyond consuming the existing + `capabilities.observation` boundary where relevant. diff --git a/docs/research/experiment-core/issue-234-exp-708-evidence-record-preflight-guardrails.md b/docs/research/experiment-core/issue-234-exp-708-evidence-record-preflight-guardrails.md new file mode 100644 index 000000000..befd0cb1a --- /dev/null +++ b/docs/research/experiment-core/issue-234-exp-708-evidence-record-preflight-guardrails.md @@ -0,0 +1,178 @@ +# Issue #234 EXP-708 Evidence Record Preflight Guardrails + +Date: 2026-06-22 + +Issue: #234. + +Requirement: EXP-708. + +This preflight narrows the issue #88 evidence and measure boundary to raw +captured evidence records for EXP-708. ADR-064 and +`specs/formal/experiment-core/README.md` remain the normative design authority. +This note is guidance for implementation only. + +## Architecture Decisions + +- Treat `experiment-evidence-record-v1` as the first-class raw captured + evidence surface for observations, traces, telemetry, artifacts, logs, + packet captures, and other run evidence. +- Keep raw evidence records distinct from `experiment-capture-spec-v1` + capture intent, `experiment-derived-measure-v1` interpreted outputs, + `experiment-run-v1` evidence-artifact summaries, participant-runtime + observation envelopes, backend-private logs, and backend observation + capability claims. +- Every evidence record must cite a capture specification, a capture + requirement, a run, source references, evidence kind, capture timestamp, + capture window, raw content, sensitivity, redaction state, and provenance. +- Raw content must use the existing `raw_content` choices: an artifact + reference, a content URI with checksum, or a bounded payload summary. Do not + add backend-specific inline payload shapes or opaque result blobs. +- EXP-708 does not implement capture execution, storage, retention, redaction + execution, APIs, schedulers, workers, statistical analysis, or evaluator + behavior. Later work may publish or retrieve evidence records, but must do so + through the existing contract and control-plane gates. + +## Required Incumbents + +- Contract source: + `implementations/python/packages/aces_contracts/contracts.py`, especially + `ContractModel`, `ExperimentEvidenceRecordModel`, + `ExperimentRawEvidenceContentModel`, `ExperimentCaptureSpecReferenceModel`, + `ExperimentEvidenceRecordReferenceModel`, `ExperimentReferenceModel`, + `ExperimentArtifactRefModel`, `ExperimentChecksumModel`, RFC 3339 date-time + parsing, and `schema_bundle()`. +- Published contract surface: + `contracts/schemas/experiment-core/experiment-evidence-record-v1.json`, + `contracts/fixtures/experiment-core/experiment-evidence-record-v1/`, + `contracts/schema-publication-manifest.json`, and + `tools/generate_contract_schemas.py`. +- Validation and conformance: + `implementations/python/tests/test_runtime_contracts.py`, + `tools/check_generated_schemas.py`, `tools/check_schema_publication.py`, + `tools/check_json_artifacts.py`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, and `tools/verify_all.py`. +- Adjacent evidence boundaries: + `experiment-capture-spec-v1` for capture intent, + `experiment-derived-measure-v1` for values and analysis outputs, + `experiment-run-v1` for archival run summaries, and + participant-runtime observation contracts for live participant-facing + history. +- Backend capability declarations: + `ObservationCapabilities`, `ObservationCapabilitiesModel`, + `backend_manifest_payload()`, `observation_capability_contract_gaps()`, + `BACKEND_SUPPORTED_CONTRACT_IDS`, + `OBSERVATION_CAPABILITY_REQUIRED_CONTRACTS`, and the governed observation + vocabularies in concept authority. +- If an API or retrieval surface is added later: + `aces_runtime.control_plane_api`, `control_plane_api_guards`, + `control_plane_api_models`, `control_plane_security`, + `control_plane_store`, request fingerprints, audit events, structured + `Diagnostic` values, and redacted FastAPI error handling. + +## Whole-Repo Scope + +- Repo workflow policy: `.ground-control.yaml`, `.gc/plan-rules.md`, and the + repo policy and verification scripts. +- Normative design authority: ADR-064, ADR-055, and + `specs/formal/experiment-core/README.md`. +- Contract publication authority: contract source, generated schemas, fixtures, + schema publication manifest, and schema drift/publication checks. +- Concept and capability authority: concept-authority vocabularies, backend + manifest authority lists, backend protocol capability dataclasses, manifest + renderers, and observation capability gap checks. +- Runtime/API boundary for future work: control-plane auth, request guards, + idempotency, audit store, diagnostics, redacted error envelopes, and existing + runtime redaction/config validators. + +## Cross-Cutting Layers + +- Structural validation: external evidence payloads must pass closed-world + `ContractModel` validation and the generated draft 2020-12 JSON Schema. + Unknown fields remain errors. +- Semantic validation: captured timestamps must parse as RFC 3339; content URI + evidence must include a checksum; raw evidence must carry artifact content, + URI content, or a bounded summary; redacted or withheld records must disclose + loss in `raw_content.loss_disclosure`. +- Reference shape: use typed experiment references for capture specs, evidence + records, tasks, runs, apparatus context, source references, and provenance. + Do not encode reference kind or binding semantics in free-form strings. +- Manifest and vocabulary authority: backend observation claims must remain in + `capabilities.observation`, must use governed vocabulary scopes, and must be + falsifiable against the published experiment evidence contracts. +- API/auth surface: any future HTTP mutation or read path must use the existing + control-plane identity and role checks. Mutating requests require backend or + operator authority; read requests require backend, operator, or auditor + authority. +- Request and idempotency surface: future HTTP paths must keep request-size + guards, closed DTOs, idempotency keys, request fingerprints, and audit + recording. Do not create an evidence-specific request pipeline. +- Secret-handling surface: evidence records carry sensitivity, redaction state, + loss disclosure, content references, checksums, and provenance. They must not + carry credentials, bearer tokens, private keys, hidden answer keys, + environment dumps, backend-private object reprs, full tracebacks, or raw + process argv. Runtime observed-value inputs must preserve the existing + redaction helpers instead of bypassing them. +- Config/env-binding surface: EXP-708 must not introduce a new environment, + config, or secret-binding shape. If evidence is derived from SDL/runtime + configuration, use the existing runtime configuration, image provenance, and + observed-value redaction validators; never serialize raw `os.environ`, + process argv, or backend-local config objects into evidence records. +- OS-level exposure: command helpers and examples must not pass secrets, + tokens, or large raw capture payloads through process arguments. Use content + files or checked-in synthetic fixtures referenced by URI and checksum. +- Error-envelope surface: validation and runtime failures must use existing + `Diagnostic` values or the existing redacted HTTP error pattern. Error + details must not echo captured payloads, credentials, tracebacks, or backend + internals. +- Persistence surface: do not place evidence records or raw captured payloads + in `RuntimeSnapshot.metadata`, operation records, participant histories, + audit details, or backend-private logs. Any future durable store must preserve + schema-versioned evidence records and their checksum/redaction metadata. + +## Extensibility Guardrail + +The extension seam belongs in the evidence record's declared dimensions and in +the adjacent capture/capability contracts, not in a parallel evidence schema. +Preserve and extend `evidence_kind`, `source_refs`, `raw_content`, +`sensitivity`, `redaction_state`, and `provenance_refs`; use capture +specifications for capture windows and requirements; use +`capabilities.observation.supported_*` vocabularies for portable backend +capability claims. New portable evidence kinds or channel kinds require +contract-source and concept-authority changes, not ad hoc strings in API DTOs. +Future publication APIs should parameterize the content locator/sealing policy +that produces `content_uri` plus `content_checksum` or an `artifact_ref`; +storage backend details must not leak into the evidence-record contract. + +## Gotchas And Anti-Patterns + +- Do not treat a capture specification as proof that evidence was captured. +- Do not use participant observation envelopes, workflow histories, runtime + snapshots, audit events, or backend log entries as substitutes for + `experiment-evidence-record-v1`. +- Do not put metric ids, computed values, scores, evaluation decisions, or + analysis summaries in raw evidence records. +- Do not use run evidence artifacts alone as the raw-evidence authority when a + first-class evidence record is needed. +- Do not duplicate `raw_content`, checksum, redaction, reference, schema, + validation, exception, logging, audit, or persistence logic. +- Do not hand-edit `contracts/schemas/`; update contract sources, regenerate, + update the schema publication manifest when hashes change, and keep fixtures + and tests aligned. +- Do not log or audit raw evidence payloads, backend-private objects, secrets, + full tracebacks, or command lines used to collect evidence. +- Do not add new SDL root sections or scenario syntax for EXP-708 without a + new ADR. + +## Non-Goals + +- Runtime capture, packet/log collection, telemetry streaming, retention, or + chain-of-custody implementation. +- HTTP APIs, CLI ingestion, schedulers, workers, or background capture + orchestration. +- Redaction execution, access-control policy engines, immutable object-store + integration, or evidence deletion workflows. +- Derived-measure computation, evaluator behavior, score calculation, + statistical analysis, or study comparison logic. +- Scenario-internal monitoring or logging configuration. +- New schemas, validators, exception hierarchies, persistence stores, or + workflow logic beyond consuming the existing EXP-708 contract boundary. diff --git a/docs/research/experiment-core/issue-235-exp-709-derived-measure-preflight-guardrails.md b/docs/research/experiment-core/issue-235-exp-709-derived-measure-preflight-guardrails.md new file mode 100644 index 000000000..b44f34336 --- /dev/null +++ b/docs/research/experiment-core/issue-235-exp-709-derived-measure-preflight-guardrails.md @@ -0,0 +1,179 @@ +# Issue #235 EXP-709 Derived Measure Preflight Guardrails + +Date: 2026-06-22 + +Issue: #235. + +Requirement: EXP-709. + +This preflight narrows the issue #88 evidence and measure boundary to derived +measures, evaluations, scores, summaries, and comparable analysis outputs for +EXP-709. ADR-064 and `specs/formal/experiment-core/README.md` remain the +normative design authority. This note is guidance for implementation only. + +## Architecture Decisions + +- Treat `experiment-derived-measure-v1` as the first-class interpreted output + surface computed from raw evidence records. +- Keep derived measures distinct from `experiment-evidence-record-v1` raw + captured evidence, `experiment-capture-spec-v1` capture intent, + `experiment-run-v1` result summaries, study analysis plans, live evaluation + result envelopes, participant outcome reports, and backend-private evaluator + details. +- Every derived measure must cite one or more evidence-record refs, a metric or + evaluation reference, derivation method metadata, generation time, value + status, optional reported value, uncertainty, limitations, and provenance. +- The reported value is the interpreted result, not a raw payload container. Do + not use `value`, `limitations`, or `provenance_refs` to inline raw evidence, + backend logs, tracebacks, hidden answer keys, or large analysis artifacts. +- EXP-709 does not implement measure computation, statistical analysis, + evaluator behavior, storage, APIs, schedulers, workers, or study comparison + workflows. Later work may compute or publish measures, but must use the + existing contract and control-plane gates. + +## Required Incumbents + +- Contract source: + `implementations/python/packages/aces_contracts/contracts.py`, especially + `ContractModel`, `ExperimentDerivedMeasureModel`, + `ExperimentDerivedMeasureMethodModel`, `ExperimentParameterModel`, + `ExperimentEvidenceRecordReferenceModel`, + `ExperimentDerivedMeasureReferenceModel`, `ExperimentReferenceModel`, RFC + 3339 date-time parsing, reported-value-status validation, and + `schema_bundle()`. +- Published contract surface: + `contracts/schemas/experiment-core/experiment-derived-measure-v1.json`, + `contracts/fixtures/experiment-core/experiment-derived-measure-v1/`, + `contracts/schema-publication-manifest.json`, and + `tools/generate_contract_schemas.py`. +- Validation and conformance: + `implementations/python/tests/test_runtime_contracts.py`, + `tools/check_generated_schemas.py`, `tools/check_schema_publication.py`, + `tools/check_json_artifacts.py`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, and `tools/verify_all.py`. +- Adjacent boundaries: + `experiment-evidence-record-v1` for source observations, + `experiment-capture-spec-v1` for capture requirements, + `experiment-run-v1` traceability for capture/evidence/measure/claim links, + `ExperimentResultSummaryModel` for compact run-level result summaries, and + `experiment-study-v1` for analysis plans and comparison grouping. +- Backend capability declarations: + `ObservationCapabilities`, `ObservationCapabilitiesModel`, + `backend_manifest_payload()`, `observation_capability_contract_gaps()`, + `BACKEND_SUPPORTED_CONTRACT_IDS`, + `OBSERVATION_CAPABILITY_REQUIRED_CONTRACTS`, and the governed observation + vocabularies in concept authority. +- If an API or retrieval surface is added later: + `aces_runtime.control_plane_api`, `control_plane_api_guards`, + `control_plane_api_models`, `control_plane_security`, + `control_plane_store`, request fingerprints, audit events, structured + `Diagnostic` values, and redacted FastAPI error handling. + +## Whole-Repo Scope + +- Repo workflow policy: `.ground-control.yaml`, `.gc/plan-rules.md`, and the + repo policy and verification scripts. +- Normative design authority: ADR-064, ADR-055, ADR-065, and + `specs/formal/experiment-core/README.md`. +- Contract publication authority: contract source, generated schemas, fixtures, + schema publication manifest, and schema drift/publication checks. +- Concept and capability authority: concept-authority vocabularies, backend + manifest authority lists, backend protocol capability dataclasses, manifest + renderers, and observation capability gap checks. +- Runtime/API boundary for future work: control-plane auth, request guards, + idempotency, audit store, diagnostics, redacted error envelopes, existing + runtime redaction/config validators, and OS-level command exposure rules. + +## Cross-Cutting Layers + +- Structural validation: external derived-measure payloads must pass + closed-world `ContractModel` validation and the generated draft 2020-12 JSON + Schema. Unknown fields remain errors. +- Semantic validation: `source_evidence_refs` must contain at least one + `evidence-record` ref; `generated_at` must parse as RFC 3339; reported + measures must include `value`; missing, withheld, and not-applicable measures + must not include `value`. +- Reference shape: use typed experiment references for metric/evaluation refs, + source evidence refs, derived-measure refs, and provenance. Do not encode + reference kind, source binding, or derivation identity in free-form strings + when a typed reference or method field exists. +- Manifest and vocabulary authority: backend observation claims must remain in + `capabilities.observation`, must use governed vocabulary scopes, and must be + falsifiable against the published experiment capture, evidence, and derived + measure contracts. +- API/auth surface: any future HTTP mutation or read path must use the existing + control-plane identity and role checks. Mutating requests require backend or + operator authority; read requests require backend, operator, or auditor + authority. +- Request and idempotency surface: future HTTP paths must keep request-size + guards, closed DTOs, idempotency keys, request fingerprints, and audit + recording. Do not create a measure-specific request pipeline. +- Secret-handling surface: derived measures carry interpreted values, method + metadata, uncertainty, limitations, source evidence refs, and provenance. + They must not carry credentials, bearer tokens, private keys, hidden answer + keys, environment dumps, backend-private object reprs, raw evidence payloads, + full tracebacks, or raw process argv. If a source value is sensitive, keep it + in the evidence-record path with sensitivity/redaction metadata and publish + only a bounded, reviewable derived result. +- Config/env-binding surface: EXP-709 must not introduce a new environment, + config, or secret-binding shape. Method parameters use the existing + redaction-aware `ExperimentParameterModel`; never serialize raw `os.environ`, + process argv, or backend-local config objects into derived measures. +- OS-level exposure: command helpers and examples must not pass secrets, + tokens, raw evidence payloads, or large analysis outputs through process + arguments. Use content references, checked-in synthetic fixtures, and + checksums where artifacts are needed. +- Error-envelope surface: validation and runtime failures must use existing + `Diagnostic` values or the existing redacted HTTP error pattern. Error + details must not echo raw evidence, credentials, computed hidden truth, + tracebacks, or backend internals. +- Persistence surface: do not place derived measures or raw analysis outputs in + `RuntimeSnapshot.metadata`, operation records, participant histories, audit + details, or backend-private logs. Any future durable store must preserve + schema-versioned derived-measure records and their source-evidence links. + +## Extensibility Guardrail + +The extension seam belongs in the existing derived-measure dimensions, not in a +parallel score, evaluation, summary, or analysis-output schema. Preserve and +extend `measure_kind`, `metric_ref`, `method.method_id`, +`method.method_version`, `method.parameters`, `source_evidence_refs`, +`value_status`, `value`, `uncertainty`, `limitations`, and `provenance_refs`. +New portable measure kinds, value envelopes, sensitivity markings, or method +taxonomies require contract-source and schema-publication changes. Backend or +study-specific variation should be parameterized through method metadata and +typed refs rather than by adding ad hoc API DTO fields or evaluator-private +payloads. + +## Gotchas And Anti-Patterns + +- Do not treat a derived measure as raw evidence or proof of capture. +- Do not let a run result summary become the canonical derived-measure record; + it may summarize a result, but the reviewable derivation chain belongs in + `experiment-derived-measure-v1` plus run traceability. +- Do not put raw observations, logs, packet captures, traces, screenshots, + backend-native evaluator payloads, or participant-private data in the + derived-measure `value`. +- Do not collapse capture specs, evidence records, derived measures, run + summaries, participant outcome reports, and study analysis plans into a + single result blob. +- Do not duplicate reported-value-status validation, reference validation, + timestamp parsing, schema generation, fixture loading, exception handling, + logging, audit, or persistence logic. +- Do not hand-edit `contracts/schemas/`; update contract sources, regenerate, + update the schema publication manifest when hashes change, and keep fixtures + and tests aligned. +- Do not add new SDL root sections, evaluator APIs, analysis services, storage + backends, or workflow steps for EXP-709 without a separate design decision. + +## Non-Goals + +- Measure computation, evaluator behavior, score calculation, statistical + analysis, model evaluation engines, or study comparison logic. +- Runtime capture, packet/log collection, telemetry streaming, retention, + redaction execution, or chain-of-custody implementation. +- HTTP APIs, CLI ingestion, schedulers, workers, background analysis jobs, or + durable publication stores. +- Scenario-internal monitoring, logging, scoring, objective, or task syntax. +- New schemas, validators, exception hierarchies, persistence stores, or + workflow logic beyond consuming the existing EXP-709 contract boundary. diff --git a/docs/research/experiment-core/issue-238-exp-720-run-provenance-preflight-guardrails.md b/docs/research/experiment-core/issue-238-exp-720-run-provenance-preflight-guardrails.md new file mode 100644 index 000000000..4a0f35760 --- /dev/null +++ b/docs/research/experiment-core/issue-238-exp-720-run-provenance-preflight-guardrails.md @@ -0,0 +1,200 @@ +# Issue #238 EXP-720 Run Provenance Preflight Guardrails + +Date: 2026-06-22 + +Issue: #238. + +Requirement: EXP-720. + +This preflight narrows ADR-065 to the canonical run provenance record for +EXP-720. ADR-065 and `specs/formal/experiment-core/README.md` remain the +design authority. This note is implementation guidance only. + +## Architecture Decisions + +- Treat `experiment-run-v1` as the only canonical archival run provenance + record. Do not add `experiment-run-provenance-v1` or a second run root. +- Keep archival run provenance distinct from live execution state: + `RuntimeSnapshot`, `ControlPlaneStore`, operation status, workflow/evaluation + histories, participant episode state, audit events, and backend-private logs + are evidence inputs or operational views, not the authoritative run record. +- Preserve the existing run record split: task and scenario references, + apparatus context, participant implementation provenance, parameter set, + stochastic controls, timestamps, result summaries, evidence artifacts, + traceability, realized-form disclosures, and lineage references each keep + their current meanings. +- Manifest references and digests must use `ExperimentManifestReferenceModel` + and apparatus-context manifest validation. Digest-bound manifest refs are + limited to processor/backend manifests whose payloads can be checked. +- Configuration, parameter, and stochastic-control fields are bounded archival + declarations. They must not serialize raw environments, process argv, + backend-native config objects, bearer tokens, credentials, private keys, or + unredacted secret material. +- EXP-720 does not implement capture execution, storage, HTTP APIs, schedulers, + runtime state reconstruction, replay, statistical analysis, or a provenance + graph service. Later producer or API work must emit the existing + `ExperimentRunModel` shape through the existing gates. + +## Required Incumbents + +- Contract source: + `implementations/python/packages/aces_contracts/contracts.py`, especially + `ContractModel`, `ExperimentRunModel`, `ExperimentRunTraceabilityModel`, + `ExperimentRealizedFormDisclosureModel`, `ExperimentApparatusContextModel`, + `ParticipantImplementationProvenanceModel`, `ExperimentReferenceModel`, + constrained reference models, artifact refs, checksums, parameters, + stochastic controls, clock context, RFC 3339 parsing, and + `validate_experiment_run_against_task()`. +- Published contract surface: + `contracts/schemas/experiment-core/experiment-run-v1.json`, + `contracts/fixtures/experiment-core/experiment-run-v1/`, + `contracts/schema-publication-manifest.json`, + `implementations/python/packages/aces_contracts/versions.py`, and + `tools/generate_contract_schemas.py`. +- Adjacent experiment artifacts: + `experiment-task-v1`, `experiment-apparatus-context-v1`, + `experiment-capture-spec-v1`, `experiment-evidence-record-v1`, + `experiment-derived-measure-v1`, `experiment-study-v1`, and + participant implementation manifest/provenance contracts. +- Manifest and concept authority: + `aces_contracts.manifest_authority`, processor/backend manifest models, + backend observation capability declarations, controlled vocabularies, + concept families `tasks-runs-studies`, `apparatus-declarations`, + `provenance-and-evidence`, `realization-and-disclosure`, and + `time-and-apparatus`. +- Validation and conformance: + `implementations/python/tests/test_runtime_contracts.py`, + `tools/check_generated_schemas.py`, `tools/check_schema_publication.py`, + `tools/check_json_artifacts.py`, `tools/check_repo_policy.py`, + `tools/check_requirement_governance.py`, and `tools/verify_all.py`. +- If a future producer reads live runtime surfaces: + `aces_runtime.control_plane`, `control_plane_store`, result/history models, + structured `Diagnostic` values, and the runtime snapshot + `realization_provenance` ledger are inputs only. They do not become archival + persistence or schema authority. +- If a future API publishes or retrieves run records: + `aces_runtime.control_plane_api`, `control_plane_api_guards`, + `control_plane_security`, request fingerprints, idempotency keys, audit + events, response models, and redacted FastAPI error handling must be reused. +- If run records are derived from runtime configuration: + `RuntimeEnvironmentVariable`, runtime sensitivity classifications, and the + observed-value redaction helpers in `aces_sdl.runtime_values` remain the + secret-handling gate. + +## Whole-Repo Scope + +- Repo workflow policy: `.ground-control.yaml`, `.gc/plan-rules.md`, and the + repo policy and verification scripts. +- Normative design authority: ADR-055, ADR-064, ADR-065, ADR-008, and + `specs/formal/experiment-core/README.md`. +- Contract publication authority: contract source, generated schemas, fixtures, + schema publication manifest, semantic invariant annotations, and schema drift + checks. +- Concept and capability authority: concept-authority catalogs, manifest + authority lists, backend/processor manifest payloads, backend protocol + capability dataclasses, and observation capability gap checks. +- Runtime/API boundary for future work: control-plane auth, request-size guard, + idempotency, audit store, diagnostics, redacted error envelopes, and existing + runtime redaction/config validators. + +## Cross-Cutting Layers + +- Structural validation: external run payloads must pass closed-world + `ContractModel` validation and the generated draft 2020-12 JSON Schema. + Unknown fields remain errors. +- Semantic validation: `ExperimentRunModel._validate_archival_run()` enforces + time ordering, invalidation details, succeeded-result reporting, participant + implementation provenance resolution, result evidence resolution, and + realized-form evidence tracing. +- Cross-artifact validation: `validate_experiment_run_against_task()` checks + task identity/version, scenario snapshot compatibility, apparatus + constraints, task-declared metric ids, and task/metric evidence requirements. +- Traceability validation: `ExperimentRunTraceabilityModel` requires + duplicate-free capture-spec, evidence-record, derived-measure, and claim + refs; claim refs must be grounded by at least one derived-measure ref. +- Realized-form validation: `ExperimentRealizedFormDisclosureModel` requires a + realized reference or value summary, enforces processor/backend realization + authority for matching bases, and keeps disclosure evidence refs unique. +- Manifest and digest validation: `ExperimentManifestReferenceModel` constrains + digest-bound manifest refs to processor/backend subject refs with supported + manifest schema versions. Apparatus context validators must bind selected + manifests to concrete processor/backend payloads. +- Concept and vocabulary validation: portable comparison terms must come from + existing concept-authority and manifest-authority helpers, not ad hoc local + strings. +- API/auth surface: any future HTTP mutation or read path must use existing + control-plane identity and role checks. Mutating requests require backend or + operator authority; read requests require backend, operator, or auditor + authority. +- Request and idempotency surface: future HTTP paths must keep request-size + guards, closed DTOs, idempotency keys, request fingerprints, and audit + recording. Do not create a run-provenance-specific request pipeline. +- Secret-handling surface: run records may carry sensitivity-aware artifact + references, checksums, bounded summaries, and redaction disclosures. They must + not carry credentials, bearer tokens, private keys, hidden answer keys, raw + prompt contents, environment dumps, backend-private object reprs, full + tracebacks, raw process argv, or raw backend payloads. +- Config/env-binding surface: configuration provenance must use existing + runtime configuration and observed-value redaction shapes. Never serialize + raw `os.environ`, CLI argv, process tables, or backend-local config objects + into `parameter_set`, `apparatus_context`, evidence artifacts, diagnostics, + audit details, fixtures, or examples. +- OS-level exposure: collection and publication helpers must not pass tokens, + credentials, or large raw capture payloads through command-line arguments. + Use content files or synthetic fixtures referenced by URI and checksum. +- Error-envelope surface: validation and runtime failures must use existing + `Diagnostic` values, Pydantic validation errors, or the existing redacted HTTP + error pattern. Error details must not echo record payloads, secrets, + tracebacks, or backend internals. +- Persistence surface: archival run records must not be stored in + `RuntimeSnapshot.metadata`, operation records, participant histories, audit + details, or backend-private logs. Any future durable store must persist + schema-versioned `experiment-run-v1` records with their evidence and + traceability references intact. + +## Extensibility Guardrail + +The extension seam is inside the existing run contract and adjacent evidence +contracts, not in a parallel provenance schema. Future variation should extend +governed dimensions such as `traceability`, `realized_form_disclosures`, +`ExperimentReferenceModel.ref_kind`, `ExperimentArtifactRefModel.role`, +manifest/capability vocabularies, or capture/evidence/derived-measure +contracts. Publication or retrieval code should parameterize the producer +source and artifact locator/sealing policy that produce refs, URIs, checksums, +and redaction disclosures; storage or backend details must not leak into the +canonical run record. + +## Gotchas And Anti-Patterns + +- Do not reconstruct the archival run lazily from mutable control-plane state. +- Do not treat `run_id`, operation id, workflow run id, participant episode id, + snapshot address, or backend-native execution id as interchangeable. +- Do not put archival run provenance into `RuntimeSnapshot.metadata`, + operation status, workflow history, participant history, audit logs, or + backend-private logs. +- Do not create duplicate provenance schemas, schema registries, validators, + exception hierarchies, workflow logic, manifest renderers, storage stacks, + logging stacks, or audit stacks. +- Do not use `realized_form_disclosures` as an unstructured log field. +- Do not treat traceability refs as proof that external artifacts exist or are + authorized for dereference. Dereference belongs to future storage/API work + using existing auth, redaction, request-size, audit, and idempotency gates. +- Do not hand-edit `contracts/schemas/`; update contract sources, regenerate, + update the schema publication manifest when hashes change, and keep fixtures + and tests aligned. +- Do not make processor identity explicit while backend or participant + implementation identity stays implicit. +- Do not store secrets or backend-private payloads in parameters, + configuration, evidence summaries, diagnostics, logs, fixtures, or examples. + +## Non-Goals + +- New root schema or alternative canonical provenance artifact. +- Runtime capture, replay, storage, retention, API, or query implementation. +- Live-state store changes, control-plane workflow changes, schedulers, workers, + or background capture orchestration. +- Statistical analysis, derived-measure computation, evaluator behavior, or + study comparison logic. +- Scenario syntax, SDL root sections, or task model changes. +- New exception hierarchy, logging/audit pipeline, persistence stack, manifest + renderer, or validation stack. diff --git a/docs/research/experiment-core/issue-239-exp-722-realized-form-preflight-guardrails.md b/docs/research/experiment-core/issue-239-exp-722-realized-form-preflight-guardrails.md new file mode 100644 index 000000000..926c91fd2 --- /dev/null +++ b/docs/research/experiment-core/issue-239-exp-722-realized-form-preflight-guardrails.md @@ -0,0 +1,137 @@ +# Issue #239 EXP-722 Realized Form Disclosure Preflight Guardrails + +Date: 2026-06-22 + +Issue: #239. + +Requirement: EXP-722. + +This preflight narrows ADR-065 to realized-form disclosure. ADR-065 and +`specs/formal/experiment-core/README.md` remain the design authority. This note +is implementation guidance only. + +## Architecture Decisions + +- Preserve realized forms inside `experiment-run-v1` + `realized_form_disclosures`; do not create a parallel realized-form, + apparatus-realization, or run-provenance root schema. +- Treat each disclosure as run provenance for an underspecified concern, not as + authored SDL meaning, apparatus context alone, raw evidence, derived measure + output, result summary, runtime snapshot metadata, or backend-private log + data. +- Keep the existing disclosure shape: stable concern id, governed concern kind, + realization basis, realizing authority reference, optional authored reference, + realized reference or bounded value summary, disclosure text, and optional + evidence-record refs. +- Realization evidence must flow through `experiment-evidence-record-v1` and + the run's `traceability.evidence_record_refs`; disclosure refs are not proof + of external artifact existence or authorization to dereference content. +- Free text in `realized_value_summary` and `disclosure` is review context only. + It must not carry secrets, backend-native object dumps, process argv, + environment dumps, raw tracebacks, hidden answers, or unredacted capture + payloads. + +## Required Incumbents + +- Contract authority: + `implementations/python/packages/aces_contracts/contracts.py`, especially + `ContractModel`, `ExperimentRunModel`, + `ExperimentRealizedFormDisclosureModel`, + `ExperimentRunTraceabilityModel`, `ExperimentReferenceModel`, + constrained experiment references, artifact references, checksums, + redaction-aware experiment parameters, RFC 3339 helpers, and + `validate_experiment_run_against_task()`. +- Published schema and fixtures: + `contracts/schemas/experiment-core/experiment-run-v1.json`, + `contracts/fixtures/experiment-core/experiment-run-v1/`, + `contracts/schema-publication-manifest.json`, + `tools/generate_contract_schemas.py`, `tools/check_generated_schemas.py`, + and `tools/check_schema_publication.py`. +- Adjacent experiment contracts: + `experiment-task-v1`, `experiment-apparatus-context-v1`, + `experiment-capture-spec-v1`, `experiment-evidence-record-v1`, + `experiment-derived-measure-v1`, `experiment-study-v1`, and participant + implementation manifest/provenance contracts. +- Realization inputs: + runtime snapshot `realization_provenance`, backend manifest + `realization_support`, reference backend realization checks, processor/backend + manifest identity, and concept-authority families such as + `realization-and-disclosure`, `apparatus-declarations`, and + `provenance-and-evidence`. These are inputs or capability declarations, not + replacement archival records. +- Future producer/API work must reuse existing control-plane identity, + authorization, request-size, idempotency, audit, diagnostics, and redacted + error-envelope patterns. Do not add a disclosure-specific API/security stack. + +## Cross-Cutting Layers + +- Structural validation: external payloads must pass the generated draft + 2020-12 schema and the closed-world `ContractModel` source. Unknown fields + remain errors. +- Semantic validation: `ExperimentRealizedFormDisclosureModel` requires + `realized_ref` or `realized_value_summary`, enforces processor/backend + authority for matching bases, and keeps disclosure evidence refs unique. +- Run-level validation: `ExperimentRunModel._validate_archival_run()` requires + disclosure evidence refs to be present in + `traceability.evidence_record_refs`. +- Cross-artifact validation: `validate_experiment_run_against_task()` remains + the task/run protocol gate. Realized forms must not bypass task apparatus, + metric, scenario snapshot, or evidence requirements. +- Manifest and capability validation: processor/backend realization claims must + build on existing processor/backend manifest identity, backend + `realization_support`, manifest-authority, and concept-authority validators + instead of local strings. +- Auth surface: any future HTTP create/read path must use control-plane roles + and identity checks. Disclosure publication is a run-provenance mutation; + dereference of evidence content is a separate authorized read. +- Secret-handling surface: summaries and disclosure text may describe a choice, + but sensitive content belongs behind redacted or restricted evidence artifact + refs with sensitivity metadata. Do not serialize raw env, argv, credentials, + private keys, bearer tokens, prompts, or backend-private payloads. +- OS-level exposure: producers and validation helpers must not pass secrets or + raw evidence payloads through command-line arguments. Use bounded fixture + files, content-addressed artifacts, URIs, and checksums. +- Error-envelope surface: validation failures must use Pydantic errors, + existing `Diagnostic` values, or existing redacted HTTP error envelopes. Do + not echo full run records, evidence payloads, tracebacks, or backend internals. + +## Extensibility Guardrail + +The extension seam is the existing disclosure model and governed vocabulary, +not a new workflow or schema stack. Add future variation by extending +`concern_kind`, `basis`, `ExperimentReferenceModel.ref_kind`, evidence record +kinds, manifest capability declarations, or concept-authority terms through the +normal contract/schema publication path. Producer code should be parameterized +by realization source and artifact sealing policy so a later backend, processor, +or operator source can emit the same canonical disclosure shape. + +## Gotchas And Anti-Patterns + +- Do not convert runtime snapshot `realization_provenance` directly into the + archival run record without sealing it as `experiment-run-v1` and preserving + traceability refs. +- Do not treat backend manifest `realization_support` as evidence that a run + disclosed all realized choices. It is a capability claim, not a run fact. +- Do not use `realized_form_disclosures` as an unstructured log list. +- Do not use authored scenario refs, apparatus component ids, operation ids, + workflow ids, participant episode ids, or backend-native execution ids as + interchangeable realization identities. +- Do not duplicate schema registries, validation helpers, exception + hierarchies, logging, auditing, persistence, manifest rendering, or workflow + code for EXP-722. +- Do not hand-edit `contracts/schemas/`; update contract sources, regenerate, + update the publication manifest when hashes change, and keep fixtures/tests in + sync. + +## Non-Goals + +- New root schema, new provenance graph service, or alternative canonical run + record. +- Runtime capture, replay, storage, retention, query, scheduling, or HTTP API + implementation. +- Derived-measure computation, evaluator behavior, statistical analysis, or + study comparison logic. +- SDL syntax changes, task model changes, or apparatus-context-only + realization records. +- New exception hierarchy, security model, persistence stack, logging stack, + audit stack, or workflow pipeline. diff --git a/docs/research/experiment-core/issue-88-evidence-measure-preflight-guardrails.md b/docs/research/experiment-core/issue-88-evidence-measure-preflight-guardrails.md new file mode 100644 index 000000000..753708dba --- /dev/null +++ b/docs/research/experiment-core/issue-88-evidence-measure-preflight-guardrails.md @@ -0,0 +1,44 @@ +# Issue #88 Evidence And Measure Preflight Guardrails + +Date: 2026-06-21 + +Issue: #88. + +Requirements: EXP-707, EXP-708, EXP-709, EXP-715. + +The architecture preflight confirmed that ADR-055, +`specs/formal/experiment-core/README.md`, and +`docs/research/experiment-core/preflight-guardrails.md` already cover the +EXP-701 through EXP-705 boundary. Issue #88 adds only the evidence and measure +contract extension described below. + +## Binding Guardrails + +- Keep EXP-707 as a declarative capture specification: it records what evidence + should be captured, not runtime capture, storage, or collection success. +- Keep EXP-708 as raw captured observations/artifacts: evidence records carry + raw content references or bounded summaries, sensitivity, redaction state, + loss disclosure, and provenance. +- Keep EXP-709 as derived measures/evaluations: derived measures cite source + evidence records and carry method, value status, value, uncertainty, + limitations, and provenance. +- Keep EXP-715 as a backend capability declaration in the existing manifest, + profile, concept-authority, and conformance stack. +- Do not implement runtime capture, retention, persistence, HTTP APIs, + schedulers, statistical engines, packet/log parsers, or new SDL syntax in + issue #88. +- Do not collapse capture specs, raw evidence, and derived measures into one + result blob or into existing run result summaries. + +## Required Incumbents + +- `implementations/python/packages/aces_contracts/contracts.py`, + `versions.py`, `schema_bundle()`, and + `tools/generate_contract_schemas.py`. +- `contracts/schemas/`, `contracts/fixtures/`, and + `contracts/schema-publication-manifest.json`. +- `contracts/concept-authority/controlled-vocabularies-v1.json` and its + fixture corpus for governed observation capability vocabularies. +- `aces_backend_protocols.capabilities`, `aces_backend_protocols.manifest`, + backend profiles, and conformance checks. +- Existing ACES semantic invariant annotations and fixture validation tests. diff --git a/docs/specs/formal.md b/docs/specs/formal.md index fb6626a33..2105bca74 100644 --- a/docs/specs/formal.md +++ b/docs/specs/formal.md @@ -16,12 +16,14 @@ formal artifacts are warranted. - **Runtime Contracts** (`specs/formal/runtime-contracts/`) -- Result/evaluation contracts - **Participant Semantics** (`specs/formal/participant-semantics/`) -- Participant action, observation, interaction, visibility, causality, temporal behavior, - and outcome-interpretation semantics + derived context-view, and outcome-interpretation semantics - **Participant Runtime** (`specs/formal/participant-runtime/`) -- Participant runtime state/history, observable action lifecycle, shared operational state, and concurrent execution semantics - **Experiment Core** (`specs/formal/experiment-core/`) -- Task, run, - apparatus-context, study/collection, and archival provenance contracts + apparatus-context, study/collection, capture specification, raw evidence, + derived measure, backend observation capability, and archival provenance + contracts ## FM Classification diff --git a/implementations/python/packages/aces_backend_protocols/capabilities.py b/implementations/python/packages/aces_backend_protocols/capabilities.py index ab2660792..17d122b12 100644 --- a/implementations/python/packages/aces_backend_protocols/capabilities.py +++ b/implementations/python/packages/aces_backend_protocols/capabilities.py @@ -11,11 +11,14 @@ ) 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 WorkflowFeature, WorkflowStatePredicateFeature +from aces_contracts.vocabulary import ParticipantFeatureSupportLevel, WorkflowFeature, WorkflowStatePredicateFeature 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" +OBSERVATION_CAPABILITY_CAPTURE_KIND_SCOPE = "capabilities.observation.supported_capture_kinds" +OBSERVATION_CAPABILITY_CHANNEL_KIND_SCOPE = "capabilities.observation.supported_channel_kinds" +OBSERVATION_CAPABILITY_SEALING_MODE_SCOPE = "capabilities.observation.supported_sealing_modes" _PARTICIPANT_EPISODE_CONTRACTS = frozenset( { @@ -30,6 +33,15 @@ "runtime-snapshot-v1", } ) +_PARTICIPANT_INTERACTION_CONTRACTS = frozenset( + { + "participant-behavior-history-event-stream-v1", + "participant-shared-state-record-v1", + "participant-joint-action-record-v1", + "participant-time-management-context-v1", + "runtime-snapshot-v1", + } +) PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS = { PARTICIPANT_RUNTIME_ROLE_SCOPE: { @@ -51,10 +63,10 @@ "temporal_contracts": _PARTICIPANT_BEHAVIOR_CONTRACTS, }, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE: { - "contention": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "coordination": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "interference": _PARTICIPANT_BEHAVIOR_CONTRACTS, - "shared_state_change": _PARTICIPANT_BEHAVIOR_CONTRACTS, + "contention": _PARTICIPANT_INTERACTION_CONTRACTS, + "coordination": _PARTICIPANT_INTERACTION_CONTRACTS, + "interference": _PARTICIPANT_INTERACTION_CONTRACTS, + "shared_state_change": _PARTICIPANT_INTERACTION_CONTRACTS, }, } """Minimum published contract surfaces needed to make API-405 claims checkable. @@ -65,11 +77,17 @@ while omitting the contracts that carry the corresponding runtime evidence. """ +OBSERVATION_CAPABILITY_REQUIRED_CONTRACTS = frozenset( + { + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", + } +) + @dataclass(frozen=True) class ProvisionerCapabilities: - """Provisioning support declaration.""" - name: str supported_node_types: frozenset[str] = frozenset() supported_os_families: frozenset[str] = frozenset() @@ -186,6 +204,66 @@ def __post_init__(self) -> None: raise ValueError("EvaluatorCapabilities must support scoring, objectives, or both") +def _validate_unique_non_empty_strings(field_name: str, values: tuple[str, ...]) -> None: + if any(not value.strip() for value in values): + raise ValueError(f"{field_name} must not contain empty strings") + if len(set(values)) != len(values): + raise ValueError(f"{field_name} must not contain duplicate values") + + +def _validate_participant_feature_support_term(feature: str) -> None: + errors: list[str] = [] + for scope in (PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE): + try: + validate_controlled_vocabulary_scope_values(scope, (feature,)) + except ValueError as exc: + errors.append(str(exc)) + continue + return + raise ValueError( + "ParticipantFeatureSupport.feature must be a governed participant behavior or interaction feature " + f"term, or match the governed extension pattern; got {feature!r}; " + f"validation details: {'; '.join(errors)}" + ) + + +@dataclass(frozen=True) +class ParticipantFeatureSupport: + """API-407 per-feature participant runtime support declaration.""" + + feature: str + support_level: ParticipantFeatureSupportLevel | str + constraint_refs: tuple[str, ...] = () + disclosure_refs: tuple[str, ...] = () + + def __post_init__(self) -> None: + if not self.feature.strip(): + raise ValueError("ParticipantFeatureSupport.feature must be non-empty") + _validate_participant_feature_support_term(self.feature) + + try: + support_level = ( + self.support_level + if isinstance(self.support_level, ParticipantFeatureSupportLevel) + else ParticipantFeatureSupportLevel(str(self.support_level)) + ) + except ValueError as exc: + raise ValueError("ParticipantFeatureSupport.support_level must be a valid support level") from exc + + constraint_refs = tuple(self.constraint_refs) + disclosure_refs = tuple(self.disclosure_refs) + _validate_unique_non_empty_strings("ParticipantFeatureSupport.constraint_refs", constraint_refs) + _validate_unique_non_empty_strings("ParticipantFeatureSupport.disclosure_refs", disclosure_refs) + if support_level != ParticipantFeatureSupportLevel.EXACT and not disclosure_refs: + raise ValueError( + "ParticipantFeatureSupport disclosure_refs must be non-empty when support_level is below exact" + ) + + object.__setattr__(self, "support_level", support_level) + object.__setattr__(self, "constraint_refs", constraint_refs) + object.__setattr__(self, "disclosure_refs", disclosure_refs) + + @dataclass(frozen=True) class ParticipantRuntimeCapabilities: """Participant-episode lifecycle support declaration. @@ -208,6 +286,7 @@ class ParticipantRuntimeCapabilities: supported_participant_roles: frozenset[str] = frozenset() supported_behavior_features: frozenset[str] = frozenset() supported_interaction_features: frozenset[str] = frozenset() + feature_support: tuple[ParticipantFeatureSupport, ...] = () constraints: dict[str, str] = field(default_factory=dict) def __post_init__(self) -> None: @@ -243,6 +322,82 @@ def __post_init__(self) -> None: PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, self.supported_interaction_features, ) + feature_support = tuple( + entry if isinstance(entry, ParticipantFeatureSupport) else ParticipantFeatureSupport(**entry) + for entry in self.feature_support + ) + feature_names = tuple(entry.feature for entry in feature_support) + _validate_unique_non_empty_strings("ParticipantRuntimeCapabilities.feature_support", feature_names) + supported_features = self.supported_behavior_features | self.supported_interaction_features + for entry in feature_support: + if ( + entry.support_level == ParticipantFeatureSupportLevel.UNSUPPORTED + and entry.feature in supported_features + ): + raise ValueError( + "ParticipantRuntimeCapabilities.feature_support cannot declare a supported feature unsupported" + ) + object.__setattr__(self, "feature_support", feature_support) + + +@dataclass(frozen=True) +class ObservationCapabilities: + """Backend observation and evidence-collection support declaration (EXP-715).""" + + name: str + supported_capture_kinds: frozenset[str] = frozenset() + supported_channel_kinds: frozenset[str] = frozenset() + supported_evidence_contracts: frozenset[str] = frozenset() + supported_media_types: frozenset[str] = frozenset() + supported_sealing_modes: frozenset[str] = frozenset() + supports_redaction: bool = False + supports_loss_disclosure: bool = False + supports_chain_of_custody: bool = False + constraints: dict[str, str] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.name.strip(): + raise ValueError("ObservationCapabilities.name must be non-empty") + _validate_unique_non_empty_strings( + "ObservationCapabilities.supported_capture_kinds", self.supported_capture_kinds + ) + _validate_unique_non_empty_strings( + "ObservationCapabilities.supported_channel_kinds", self.supported_channel_kinds + ) + _validate_unique_non_empty_strings( + "ObservationCapabilities.supported_evidence_contracts", + self.supported_evidence_contracts, + ) + _validate_unique_non_empty_strings("ObservationCapabilities.supported_media_types", self.supported_media_types) + _validate_unique_non_empty_strings( + "ObservationCapabilities.supported_sealing_modes", self.supported_sealing_modes + ) + if not self.supported_capture_kinds: + raise ValueError("ObservationCapabilities.supported_capture_kinds must not be empty") + if not self.supported_channel_kinds: + raise ValueError("ObservationCapabilities.supported_channel_kinds must not be empty") + if not self.supported_evidence_contracts: + raise ValueError("ObservationCapabilities.supported_evidence_contracts must not be empty") + if not self.supported_media_types: + raise ValueError("ObservationCapabilities.supported_media_types must not be empty") + if not self.supported_sealing_modes: + raise ValueError("ObservationCapabilities.supported_sealing_modes must not be empty") + validate_controlled_vocabulary_scope_values( + OBSERVATION_CAPABILITY_CAPTURE_KIND_SCOPE, + self.supported_capture_kinds, + ) + validate_controlled_vocabulary_scope_values( + OBSERVATION_CAPABILITY_CHANNEL_KIND_SCOPE, + self.supported_channel_kinds, + ) + validate_controlled_vocabulary_scope_values( + OBSERVATION_CAPABILITY_SEALING_MODE_SCOPE, + self.supported_sealing_modes, + ) + validate_backend_supported_contract_versions(self.supported_evidence_contracts) + for contract_id in self.supported_evidence_contracts: + if not contract_id.startswith("experiment-"): + raise ValueError("ObservationCapabilities.supported_evidence_contracts must be experiment contracts") @dataclass(frozen=True) @@ -253,6 +408,7 @@ class BackendCapabilitySet: orchestrator: OrchestratorCapabilities | None = None evaluator: EvaluatorCapabilities | None = None participant_runtime: ParticipantRuntimeCapabilities | None = None + observation: ObservationCapabilities | None = None @dataclass(frozen=True) @@ -297,6 +453,7 @@ def __init__( 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: @@ -312,6 +469,7 @@ def __init__( orchestrator=orchestrator, evaluator=evaluator, participant_runtime=participant_runtime, + observation=observation, ) supported_contract_versions = frozenset(supported_contract_versions) if not supported_contract_versions: @@ -361,6 +519,10 @@ def evaluator(self) -> EvaluatorCapabilities | None: 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 @@ -373,6 +535,10 @@ def has_evaluator(self) -> bool: 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: @@ -417,3 +583,18 @@ def participant_runtime_capability_contract_gaps(manifest: BackendManifest) -> t if missing: gaps.append(f"{scope}.{term} missing required contracts: {', '.join(missing)}") return tuple(gaps) + + +def observation_capability_contract_gaps(manifest: BackendManifest) -> tuple[str, ...]: + """Return missing contract surfaces for declared EXP-715 observation claims.""" + + observation = manifest.observation + if observation is None: + return () + + required_contracts = set(observation.supported_evidence_contracts) | set(OBSERVATION_CAPABILITY_REQUIRED_CONTRACTS) + missing = sorted(required_contracts - manifest.supported_contract_versions) + gaps: list[str] = [] + if missing: + gaps.append(f"capabilities.observation missing required contracts: {', '.join(missing)}") + return tuple(gaps) diff --git a/implementations/python/packages/aces_backend_protocols/manifest.py b/implementations/python/packages/aces_backend_protocols/manifest.py index d0f07d645..77839ff68 100644 --- a/implementations/python/packages/aces_backend_protocols/manifest.py +++ b/implementations/python/packages/aces_backend_protocols/manifest.py @@ -9,6 +9,8 @@ BackendCompatibilityModel, BackendManifestV2Model, ConceptBindingEntryModel, + ObservationCapabilitiesModel, + ParticipantFeatureSupportModel, RealizationSupportDeclarationModel, ) from aces_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS @@ -97,11 +99,36 @@ def backend_manifest_v2_model(manifest: BackendManifest) -> BackendManifestV2Mod "supported_interaction_features": sorted( manifest.participant_runtime.supported_interaction_features ), + "feature_support": [ + ParticipantFeatureSupportModel( + feature=entry.feature, + support_level=entry.support_level, + constraint_refs=list(entry.constraint_refs), + disclosure_refs=list(entry.disclosure_refs), + ) + for entry in manifest.participant_runtime.feature_support + ], "constraints": dict(manifest.participant_runtime.constraints), } if manifest.participant_runtime is not None else None ), + "observation": ( + ObservationCapabilitiesModel( + name=manifest.observation.name, + supported_capture_kinds=sorted(manifest.observation.supported_capture_kinds), + supported_channel_kinds=sorted(manifest.observation.supported_channel_kinds), + supported_evidence_contracts=sorted(manifest.observation.supported_evidence_contracts), + supported_media_types=sorted(manifest.observation.supported_media_types), + supported_sealing_modes=sorted(manifest.observation.supported_sealing_modes), + supports_redaction=manifest.observation.supports_redaction, + supports_loss_disclosure=manifest.observation.supports_loss_disclosure, + supports_chain_of_custody=manifest.observation.supports_chain_of_custody, + constraints=dict(manifest.observation.constraints), + ).model_dump(mode="json") + if manifest.observation is not None + else None + ), }, ) diff --git a/implementations/python/packages/aces_backend_stubs/stubs.py b/implementations/python/packages/aces_backend_stubs/stubs.py index e2ec36e7b..057185735 100644 --- a/implementations/python/packages/aces_backend_stubs/stubs.py +++ b/implementations/python/packages/aces_backend_stubs/stubs.py @@ -12,6 +12,7 @@ BackendCapabilitySet, BackendManifest, EvaluatorCapabilities, + ObservationCapabilities, OrchestratorCapabilities, ParticipantRuntimeCapabilities, ProvisionerCapabilities, @@ -61,6 +62,7 @@ def _current_backend_version() -> str: def create_stub_manifest( *, with_participant_runtime: bool = True, + with_observation: bool = True, **config, ) -> BackendManifest: """Return the fully capable stub manifest. @@ -81,7 +83,13 @@ def create_stub_manifest( supported_contract_versions.discard("participant-lifecycle-event-v1") supported_contract_versions.discard("participant-observation-envelope-v1") supported_contract_versions.discard("participant-shared-state-record-v1") + supported_contract_versions.discard("participant-joint-action-record-v1") + supported_contract_versions.discard("participant-time-management-context-v1") supported_contract_versions.discard("participant-outcome-report-v1") + if not with_observation: + supported_contract_versions.discard("experiment-capture-spec-v1") + supported_contract_versions.discard("experiment-evidence-record-v1") + supported_contract_versions.discard("experiment-derived-measure-v1") concept_bindings = ( ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"), ConceptBinding(scope="capabilities.provisioner.supported_os_families", family="assets"), @@ -105,6 +113,21 @@ def create_stub_manifest( family="relationships", ), ) + if with_observation: + concept_bindings += ( + ConceptBinding( + scope="capabilities.observation.supported_capture_kinds", + family="provenance-and-evidence", + ), + ConceptBinding( + scope="capabilities.observation.supported_channel_kinds", + family="apparatus-declarations", + ), + ConceptBinding( + scope="capabilities.observation.supported_sealing_modes", + family="provenance-and-evidence", + ), + ) return BackendManifest( name="stub", version=_current_backend_version(), @@ -190,6 +213,36 @@ def create_stub_manifest( if with_participant_runtime else None ), + observation=( + ObservationCapabilities( + name="stub-observation", + supported_capture_kinds=frozenset({"artifact", "log", "observation", "telemetry", "trace"}), + supported_channel_kinds=frozenset( + { + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history", + } + ), + supported_evidence_contracts=frozenset( + { + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", + } + ), + supported_media_types=frozenset({"application/json", "text/plain"}), + supported_sealing_modes=frozenset({"digest", "immutable-store"}), + supports_redaction=True, + supports_loss_disclosure=True, + supports_chain_of_custody=False, + ) + if with_observation + else None + ), ), ) diff --git a/implementations/python/packages/aces_conformance/conformance.py b/implementations/python/packages/aces_conformance/conformance.py index e8b75c85c..18e2522fa 100644 --- a/implementations/python/packages/aces_conformance/conformance.py +++ b/implementations/python/packages/aces_conformance/conformance.py @@ -10,7 +10,11 @@ from textwrap import dedent from typing import Any -from aces_backend_protocols.capabilities import BackendManifest, participant_runtime_capability_contract_gaps +from aces_backend_protocols.capabilities import ( + BackendManifest, + observation_capability_contract_gaps, + participant_runtime_capability_contract_gaps, +) from aces_backend_protocols.manifest import backend_manifest_payload from aces_contracts.backend_profiles import ( BackendProfileModel, @@ -23,6 +27,9 @@ EvaluationHistoryEventModel, EvaluationPlanModel, EvaluationResultStateModel, + ExperimentCaptureSpecModel, + ExperimentDerivedMeasureModel, + ExperimentEvidenceRecordModel, OperationReceiptModel, OperationStatusModel, OrchestrationPlanModel, @@ -31,6 +38,9 @@ ParticipantEpisodeStateModel, ParticipantImplementationManifestModel, ParticipantImplementationProvenanceModel, + ParticipantLifecycleEventModel, + ParticipantObservationEnvelopeModel, + ParticipantSharedStateRecordModel, ProvisioningPlanModel, RuntimeSnapshotEnvelopeModel, WorkflowExecutionStateModel, @@ -40,16 +50,17 @@ from aces_contracts.corpus import FIXTURES, corpus_family_root from aces_contracts.diagnostics import Diagnostic, Severity from aces_contracts.evaluation import EvaluationExecutionState +from aces_contracts.participant_concurrency import iter_participant_concurrency_snapshot_violations from aces_contracts.participant_episode import ( ParticipantEpisodeExecutionState, ParticipantEpisodeHistoryEvent, ParticipantEpisodeTerminalReason, iter_participant_episode_snapshot_violations, ) +from aces_contracts.participant_shared_state import iter_participant_shared_state_snapshot_violations from aces_contracts.planning import RuntimeDomain from aces_contracts.runtime_state import RuntimeSnapshot, RuntimeSnapshotEnvelope, SnapshotEntry from aces_contracts.workflow import WorkflowExecutionState -from aces_processor.compiler import compile_runtime_model from aces_processor.models import ( ParticipantActionContractRuntime, ParticipantBehaviorHistoryEvent, @@ -57,7 +68,7 @@ iter_participant_behavior_history_violations, iter_participant_behavior_joint_action_violations, ) -from aces_processor.planner import plan +from aces_processor.reference import run_reference_processor from aces_runtime.control_plane import RuntimeControlPlane from aces_runtime.registry import RuntimeTarget from aces_runtime.result_contracts import ( @@ -161,6 +172,12 @@ class BackendConformanceReport: "workflow-result-envelope-v1": WorkflowExecutionStateModel.model_validate, "evaluation-result-envelope-v1": EvaluationResultStateModel.model_validate, "participant-episode-state-envelope-v1": ParticipantEpisodeStateModel.model_validate, + "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_validate, + "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_validate, + "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_validate, + "experiment-capture-spec-v1": ExperimentCaptureSpecModel.model_validate, + "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_validate, + "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_validate, } @@ -433,6 +450,21 @@ def _snapshot_from_envelope(payload: dict[str, Any]) -> RuntimeSnapshot: participant_address: [event.model_dump(mode="json") for event in history] for participant_address, history in validated.participant_behavior_history.items() }, + shared_state_records={ + state_address: record.model_dump(mode="json") + for state_address, record in validated.shared_state_records.items() + }, + shared_state_history={ + state_address: [record.model_dump(mode="json") for record in records] + for state_address, records in validated.shared_state_history.items() + }, + joint_action_records={ + record_id: record.model_dump(mode="json") for record_id, record in validated.joint_action_records.items() + }, + time_management_contexts={ + context_id: context.model_dump(mode="json") + for context_id, context in validated.time_management_contexts.items() + }, metadata=dict(validated.metadata), ) @@ -587,6 +619,29 @@ def _participant_behavior_history_diagnostics( return diagnostics +def _participant_behavior_binding_diagnostics( + participant_address: str, + history: object, + *, + has_participant_action_binding: bool, + has_participant_boundary_binding: bool, +) -> list[Diagnostic]: + if not isinstance(history, list) or not history: + return [] + if has_participant_action_binding and has_participant_boundary_binding: + return [] + return [ + _diagnostic( + _SEMANTIC_INVALID_DIAGNOSTIC_CODE, + f"runtime.snapshot.participant-behavior-history.{participant_address}", + ( + "participant behavior history requires a participant.behavior snapshot entry " + "with action_contract_addresses and observation_boundary_addresses" + ), + ) + ] + + def _participant_behavior_snapshot_diagnostics( snapshot: RuntimeSnapshot, ) -> list[Diagnostic]: @@ -602,6 +657,14 @@ def _participant_behavior_snapshot_diagnostics( for participant_address, history in snapshot.participant_behavior_history.items(): has_participant_action_binding = participant_address in participant_action_addresses has_participant_boundary_binding = participant_address in participant_observation_boundary_addresses + diagnostics.extend( + _participant_behavior_binding_diagnostics( + participant_address, + history, + has_participant_action_binding=has_participant_action_binding, + has_participant_boundary_binding=has_participant_boundary_binding, + ) + ) participant_boundary_addresses = participant_observation_boundary_addresses.get(participant_address) if participant_boundary_addresses is None: participant_boundary_addresses = _participant_history_observation_boundary_addresses(history) @@ -708,6 +771,31 @@ def _participant_behavior_stream_diagnostics(contract_name: str, payload: Any) - return diagnostics +def _shared_state_snapshot_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: + return [ + _diagnostic(_SEMANTIC_INVALID_DIAGNOSTIC_CODE, address, message) + for address, message in iter_participant_shared_state_snapshot_violations( + snapshot.shared_state_records, + snapshot.shared_state_history, + participant_behavior_history=snapshot.participant_behavior_history, + metadata=snapshot.metadata, + ) + ] + + +def _participant_concurrency_snapshot_diagnostics(snapshot: RuntimeSnapshot) -> list[Diagnostic]: + return [ + _diagnostic(_SEMANTIC_INVALID_DIAGNOSTIC_CODE, address, message) + for address, message in iter_participant_concurrency_snapshot_violations( + snapshot.joint_action_records, + snapshot.time_management_contexts, + participant_behavior_history=snapshot.participant_behavior_history, + shared_state_records=snapshot.shared_state_records, + shared_state_history=snapshot.shared_state_history, + ) + ] + + def _runtime_snapshot_semantic_diagnostics(payload: Any) -> list[Diagnostic]: snapshot = _snapshot_from_envelope(payload) return [ @@ -715,6 +803,8 @@ def _runtime_snapshot_semantic_diagnostics(payload: Any) -> list[Diagnostic]: *evaluation_result_contract_diagnostics(snapshot), *_participant_episode_snapshot_diagnostics(snapshot), *_participant_behavior_snapshot_diagnostics(snapshot), + *_shared_state_snapshot_diagnostics(snapshot), + *_participant_concurrency_snapshot_diagnostics(snapshot), ] @@ -984,7 +1074,9 @@ def run_target_conformance( ) contract_gaps = _declared_contract_gaps(effective_profile, target.manifest, profiles_root=profiles_root) surface_gaps = _capability_gaps(effective_profile, target) - claim_gaps = participant_runtime_capability_contract_gaps(target.manifest) + 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)) passed = fixture_report.passed and not contract_gaps and not capability_gaps diagnostics = list(fixture_report.diagnostics) @@ -1215,7 +1307,7 @@ def _live_target_cases( """ ) ) - execution_plan = plan(compile_runtime_model(scenario), target.manifest) + execution_plan = run_reference_processor(scenario, target.manifest).execution_plan control_plane = RuntimeControlPlane(target) control_plane.submit_provisioning(execution_plan.provisioning) if target.orchestrator is not None: @@ -1252,6 +1344,17 @@ def _live_target_cases( participant_address: list(events) for participant_address, events in control_plane.snapshot.participant_episode_history.items() }, + "participant_behavior_history": { + participant_address: list(events) + for participant_address, events in control_plane.snapshot.participant_behavior_history.items() + }, + "shared_state_records": dict(control_plane.snapshot.shared_state_records), + "shared_state_history": { + state_address: list(records) + for state_address, records in control_plane.snapshot.shared_state_history.items() + }, + "joint_action_records": dict(control_plane.snapshot.joint_action_records), + "time_management_contexts": dict(control_plane.snapshot.time_management_contexts), "metadata": dict(control_plane.snapshot.metadata), } snapshot_diags = [ diff --git a/implementations/python/packages/aces_contracts/_participant_behavior_types.py b/implementations/python/packages/aces_contracts/_participant_behavior_types.py index 313c477de..04ddcd921 100644 --- a/implementations/python/packages/aces_contracts/_participant_behavior_types.py +++ b/implementations/python/packages/aces_contracts/_participant_behavior_types.py @@ -100,6 +100,8 @@ class ParticipantLifecycleOperationState(str, Enum): "participant_episode_results", "participant_episode_history", "participant_behavior_history", + "shared_state_records", + "shared_state_history", } ) _REQUIRED_BEHAVIOR_EVENT_FIELDS = ( diff --git a/implementations/python/packages/aces_contracts/contracts.py b/implementations/python/packages/aces_contracts/contracts.py index 2c9ae0eba..1aed78f4a 100644 --- a/implementations/python/packages/aces_contracts/contracts.py +++ b/implementations/python/packages/aces_contracts/contracts.py @@ -12,6 +12,7 @@ from aces_sdl import VARIABLE_TOKEN_PATTERN from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance +from aces_sdl.observability_plane_semantics import classify_contract_plane from aces_sdl.participant_attribution_semantics import ( ParticipantAttributionCandidateKind, ParticipantAttributionOrderingBasisKind, @@ -63,6 +64,9 @@ CONTROLLED_VOCABULARIES_SCHEMA_VERSION, EVALUATION_STATE_SCHEMA_VERSION, EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION, + EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION, + EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION, + EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION, EXPERIMENT_RUN_SCHEMA_VERSION, EXPERIMENT_STUDY_SCHEMA_VERSION, EXPERIMENT_TASK_SCHEMA_VERSION, @@ -139,6 +143,9 @@ class ContractModel(BaseModel): "capabilities.provisioner.supported_account_features", "capabilities.orchestrator.supported_sections", "capabilities.evaluator.supported_sections", + "capabilities.observation.supported_capture_kinds", + "capabilities.observation.supported_channel_kinds", + "capabilities.observation.supported_sealing_modes", "capabilities.participant_runtime.supported_participant_roles", "capabilities.participant_runtime.supported_behavior_features", "capabilities.participant_runtime.supported_interaction_features", @@ -291,6 +298,17 @@ def _add_aces_invariant( ) +def _add_aces_plane(json_schema: JsonSchemaValue, contract_id: str) -> None: + """Publish the carrier's single SEM-224 observability/evidence plane. + + Plane ownership is sourced from the carrier-oriented classifier so the + portable ``x-aces-plane`` annotation cannot drift from + ``aces_sdl.observability_plane_semantics`` (ADR-066 / SEM-224). + """ + + json_schema["x-aces-plane"] = classify_contract_plane(contract_id).value + + def _schema_contains_aces_invariants(schema_node: Any) -> bool: if isinstance(schema_node, dict): if "x-aces-invariants" in schema_node: @@ -369,6 +387,40 @@ def _attach_experiment_datetime_invariants(contract_id: str, json_schema: dict[s ) +def _validate_reported_value_status( + value_status: str, + value: object | None, + *, + reported_message: str, + non_reported_message: str, +) -> None: + if value_status == "reported" and value is None: + raise ValueError(reported_message) + if value_status != "reported" and value is not None: + raise ValueError(non_reported_message) + + +def _extend_reported_value_status_schema(json_schema: JsonSchemaValue) -> None: + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"value_status": {"const": "reported"}}, + "required": ["value_status"], + }, + "then": {"required": ["value"], "properties": {"value": {"not": {"type": "null"}}}}, + }, + { + "if": { + "properties": {"value_status": {"enum": ["missing", "withheld", "not-applicable"]}}, + "required": ["value_status"], + }, + "then": {"properties": {"value": {"type": "null"}}}, + }, + ] + ) + + _DEFS_KEY = "$defs" _INSTANTIATION_INVARIANT_CONTRACT_ID = "instantiated-scenario-v1" _SCHEMA_MAP_KEYS = ("properties", "patternProperties", _DEFS_KEY) @@ -914,6 +966,32 @@ def _validate_lifecycle_fields(self) -> ParticipantBehaviorHistoryEventModel: "disclose_weak_guarantee", "unsupported", ] +ParticipantRuntimeConflictClass = Literal["none", "read_write", "write_write", "unsupported"] +ParticipantRuntimeJointActionConflictPolicy = Literal[ + "none", + "coordinate", + "serialize", + "reject", + "retry", + "withhold", + "merge", + "rollback", + "disclose_weak_guarantee", + "unsupported", +] +ParticipantRuntimeIsolationGuarantee = Literal["none", "serializable", "snapshot", "causal", "unsupported"] +ParticipantRuntimeAtomicityScope = Literal["single_object", "multi_object", "coordination_interval", "unsupported"] +ParticipantRuntimeTimeManagementMode = Literal[ + "display", + "pacing", + "lookahead", + "rollback", + "devs", + "fmi", + "backend_serialized", + "unsupported", +] +ParticipantRuntimeTimeClaimStrength = Literal["display", "bounded", "exact", "unsupported"] class EventClassificationModel(ContractModel): @@ -1091,6 +1169,52 @@ class ParticipantSharedStateAccessModel(ContractModel): atomic_group_ref: NonEmptyString | None = None evidence_refs: list[NonEmptyString] = Field(default_factory=list) + @model_validator(mode="after") + def _validate_revision_markers(self) -> ParticipantSharedStateAccessModel: + if self.access_kind in {"read", "read_write"} and self.read_revision is None and self.read_digest is None: + raise ValueError("shared state read access requires read_revision or read_digest") + if self.access_kind in {"write", "read_write"} and self.write_revision is None and self.write_digest is None: + raise ValueError("shared state write access requires write_revision or write_digest") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": { + "properties": {"access_kind": {"enum": ["read", "read_write"]}}, + "required": ["access_kind"], + }, + "then": { + "anyOf": [ + {"required": ["read_revision"], "properties": {"read_revision": {"type": "string"}}}, + {"required": ["read_digest"], "properties": {"read_digest": {"type": "string"}}}, + ] + }, + }, + { + "if": { + "properties": {"access_kind": {"enum": ["write", "read_write"]}}, + "required": ["access_kind"], + }, + "then": { + "anyOf": [ + {"required": ["write_revision"], "properties": {"write_revision": {"type": "string"}}}, + {"required": ["write_digest"], "properties": {"write_digest": {"type": "string"}}}, + ] + }, + }, + ] + ) + return json_schema + class ParticipantSharedStateRecordModel(ParticipantRuntimeBaseEnvelopeModel): """RUN-307 versioned shared operational state-change report.""" @@ -1107,6 +1231,201 @@ class ParticipantSharedStateRecordModel(ParticipantRuntimeBaseEnvelopeModel): value_ref: NonEmptyString | None = None accesses: list[ParticipantSharedStateAccessModel] = Field(default_factory=list) + @model_validator(mode="after") + def _validate_revision_marker(self) -> ParticipantSharedStateRecordModel: + if self.revision is None and self.digest is None: + raise ValueError("participant shared state record requires revision or digest") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("anyOf", []).extend( + [ + {"required": ["revision"], "properties": {"revision": {"type": "string"}}}, + {"required": ["digest"], "properties": {"digest": {"type": "string"}}}, + ] + ) + return json_schema + + +class ParticipantJointActionAccessSetModel(ContractModel): + """Read/write footprint for one member event in a joint action record.""" + + member_event_ref: NonEmptyString + shared_state_read_refs: list[NonEmptyString] = Field(default_factory=list) + shared_state_write_refs: list[NonEmptyString] = Field(default_factory=list) + exclusive_resource_refs: list[NonEmptyString] = Field(default_factory=list) + visibility_effect_refs: list[NonEmptyString] = Field(default_factory=list) + evidence_stream_refs: list[NonEmptyString] = Field(default_factory=list) + + +def _exact_string_permutation(values: list[str], expected: set[str]) -> bool: + return len(values) == len(expected) and set(values) == expected + + +def _joint_action_actual_conflict(access_sets: list[ParticipantJointActionAccessSetModel]) -> str: + read_write_conflict = False + for left_index, left in enumerate(access_sets): + left_reads = set(left.shared_state_read_refs) + left_writes = set(left.shared_state_write_refs) | {f"resource:{ref}" for ref in left.exclusive_resource_refs} + for right in access_sets[left_index + 1 :]: + right_reads = set(right.shared_state_read_refs) + right_writes = set(right.shared_state_write_refs) | { + f"resource:{ref}" for ref in right.exclusive_resource_refs + } + if left_writes & right_writes: + return "write_write" + if (left_writes & right_reads) or (left_reads & right_writes): + read_write_conflict = True + return "read_write" if read_write_conflict else "none" + + +def _joint_action_member_ref_set(member_event_refs: list[str]) -> set[str]: + member_refs = list(member_event_refs) + member_ref_set = set(member_refs) + if len(member_refs) != len(member_ref_set): + raise ValueError("joint action member_event_refs must be unique") + return member_ref_set + + +def _validate_joint_action_members(record: Any, member_ref_set: set[str]) -> None: + access_event_refs = [access.member_event_ref for access in record.access_sets] + if not _exact_string_permutation(access_event_refs, member_ref_set): + raise ValueError("joint action access_sets must cover member_event_refs exactly once") + if record.realized_order and not _exact_string_permutation(record.realized_order, member_ref_set): + raise ValueError("joint action realized_order must be an exact permutation of member_event_refs") + + +def _validate_joint_action_disclosure(record: Any) -> None: + if record.unsupported_disclosure and record.exact_concurrency_claim: + raise ValueError("unsupported concurrency disclosure cannot carry an exact concurrency claim") + if record.exact_concurrency_claim and record.time_management_context_ref is None: + raise ValueError("exact concurrency claims require time_management_context_ref") + + +def _joint_action_unsupported_policy_applies(record: Any) -> bool: + if record.conflict_policy != "unsupported": + return False + if not record.unsupported_disclosure or record.exact_concurrency_claim: + raise ValueError("unsupported conflict_policy requires unsupported_disclosure and no exact claim") + return True + + +def _validate_joint_action_conflict(record: Any, actual_conflict: str) -> None: + if not record.unsupported_disclosure and record.conflict_class != actual_conflict: + raise ValueError("joint action conflict_class must match declared access-set conflicts") + if record.conflict_class == "none" and actual_conflict != "none": + raise ValueError("joint action conflict_class cannot be none when access sets conflict") + _validate_joint_action_conflict_policy(record, actual_conflict) + _validate_joint_action_atomicity(record, actual_conflict) + + +def _validate_joint_action_conflict_policy(record: Any, actual_conflict: str) -> None: + if record.isolation_guarantee == "serializable" and not record.realized_order: + raise ValueError("serializable joint action isolation requires realized_order") + if record.conflict_policy == "serialize" and not record.realized_order: + raise ValueError("serialize conflict_policy requires realized_order") + if record.conflict_policy == "retry" and (record.retry_limit is None or not record.rollback_event_refs): + raise ValueError("retry conflict_policy requires retry_limit and rollback_event_refs") + if record.conflict_policy == "none" and actual_conflict != "none": + raise ValueError("none conflict_policy is only valid when access sets do not conflict") + + +def _validate_joint_action_atomicity(record: Any, actual_conflict: str) -> None: + has_recovery_evidence = bool(record.realized_order or record.rollback_event_refs) + if record.atomicity_scope == "multi_object" and actual_conflict != "none" and not has_recovery_evidence: + raise ValueError("multi_object conflicting joint actions require realized_order or rollback_event_refs") + + +class ParticipantJointActionRecordModel(ParticipantRuntimeBaseEnvelopeModel): + """RUN-308 joint action / concurrency record over behavior events.""" + + joint_action_set_id: NonEmptyString + member_event_refs: list[NonEmptyString] = Field(min_length=1) + access_sets: list[ParticipantJointActionAccessSetModel] = Field(min_length=1) + conflict_class: ParticipantRuntimeConflictClass + conflict_policy: ParticipantRuntimeJointActionConflictPolicy + isolation_guarantee: ParticipantRuntimeIsolationGuarantee + atomicity_scope: ParticipantRuntimeAtomicityScope + realized_order: list[NonEmptyString] = Field(default_factory=list) + simultaneity_group_ref: NonEmptyString | None = None + time_management_context_ref: NonEmptyString | None = None + participant_observation_refs: list[NonEmptyString] = Field(default_factory=list) + rollback_event_refs: list[NonEmptyString] = Field(default_factory=list) + retry_limit: NonNegativeInteger | None = None + timeout_policy_ref: NonEmptyString | None = None + fairness_policy_ref: NonEmptyString | None = None + unsupported_disclosure: bool = False + exact_concurrency_claim: bool = False + + @model_validator(mode="after") + def _validate_joint_action_record(self) -> ParticipantJointActionRecordModel: + member_ref_set = _joint_action_member_ref_set(self.member_event_refs) + _validate_joint_action_members(self, member_ref_set) + _validate_joint_action_disclosure(self) + if _joint_action_unsupported_policy_applies(self): + return self + + actual_conflict = _joint_action_actual_conflict(self.access_sets) + _validate_joint_action_conflict(self, actual_conflict) + return self + + +def _validate_time_management_claim(context: Any) -> None: + if context.unsupported_disclosure and context.claim_strength == "exact": + raise ValueError("unsupported time-management disclosure cannot carry an exact claim") + if context.basis == "wall_clock_only" and context.claim_strength != "display": + raise ValueError("wall_clock_only time basis supports display claims only") + if context.claim_strength in {"bounded", "exact"} and context.clock_ref is None: + raise ValueError("bounded or exact time-management claims require clock_ref") + + +def _validate_time_management_mode(context: Any) -> None: + if context.mode == "backend_serialized": + _validate_backend_serialized_time_management(context) + if context.mode == "lookahead" and context.lookahead is None: + raise ValueError("lookahead mode requires lookahead") + if context.mode == "pacing" and context.advance_by is None: + raise ValueError("pacing mode requires advance_by") + if context.mode == "rollback" and not context.rollback_event_refs: + raise ValueError("rollback mode requires rollback_event_refs") + if context.mode in {"devs", "fmi"} and (context.clock_ref is None or context.basis == "wall_clock_only"): + raise ValueError("devs and fmi modes require a non-wall-clock basis and clock_ref") + if context.mode == "unsupported" and not context.unsupported_disclosure: + raise ValueError("unsupported time-management mode requires unsupported_disclosure") + + +def _validate_backend_serialized_time_management(context: Any) -> None: + if not context.backend_serialized or context.basis != "serialized_backend_order" or context.clock_ref is None: + raise ValueError("backend_serialized mode requires serialized_backend_order basis and clock_ref") + + +class ParticipantTimeManagementContextModel(ParticipantRuntimeBaseEnvelopeModel): + """RUN-308 time-management basis for concurrent or distributed runtime claims.""" + + context_id: NonEmptyString + mode: ParticipantRuntimeTimeManagementMode + claim_strength: ParticipantRuntimeTimeClaimStrength + basis: ParticipantRuntimeOrderingBasis + clock_ref: NonEmptyString | None = None + lookahead: NonNegativeInteger | None = None + advance_by: PositiveInteger | None = None + rollback_event_refs: list[NonEmptyString] = Field(default_factory=list) + unsupported_disclosure: bool = False + backend_serialized: bool = False + + @model_validator(mode="after") + def _validate_time_management_context(self) -> ParticipantTimeManagementContextModel: + _validate_time_management_claim(self) + _validate_time_management_mode(self) + return self + class ParticipantOutcomeReportSourceModel(ContractModel): """SEM-215 grounding source for one participant outcome report.""" @@ -1134,7 +1453,7 @@ class ParticipantOutcomeReportModel(ParticipantRuntimeBaseEnvelopeModel): outcome_id: NonEmptyString interpretation_rule_ref: NonEmptyString outcome_sources: list[ParticipantOutcomeReportSourceModel] = Field(min_length=1) - state_relationships: list[ParticipantOutcomeReportStateRelationshipModel] = Field(default_factory=list) + state_relationships: list[ParticipantOutcomeReportStateRelationshipModel] = Field(min_length=1) class ParticipantStatusViewEpisodeStateModel(ContractModel): @@ -1346,8 +1665,135 @@ def __get_pydantic_json_schema__( return json_schema +ParticipantContextAudienceScope = Literal[ + "participant_visible", + "operator_visible", + "evaluator_visible", + "auditor_visible", +] +ParticipantContextParticipantScope = Literal["participant_local"] +ParticipantContextSourceLayer = Literal[ + "source_snapshot", + "participant_observation", + "participant_behavior_history", + "participant_episode_state", + "participant_status_view", + "participant_history_view", + "evidence_record", + "derived_measure", + "control_plane_operation", +] +ParticipantContextTemporalRelation = Literal[ + "same_observation_point", + "bounded_staleness", + "historical_replay", +] +ParticipantContextComparabilityClass = Literal[ + "portable_equivalent", + "portable_with_disclosed_weakening", + "backend_specific_non_comparable", +] + + +class ParticipantContextSourceLayerModel(ContractModel): + """One governed source layer consumed by a SEM-214 context view.""" + + source_id: NonEmptyString + source_layer: ParticipantContextSourceLayer + ref: NonEmptyString + temporal_relation: ParticipantContextTemporalRelation + observation_point: NonEmptyString | None = None + freshness_basis_ref: NonEmptyString | None = None + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_temporal_basis(self) -> ParticipantContextSourceLayerModel: + if self.temporal_relation == "bounded_staleness" and self.freshness_basis_ref is None: + raise ValueError("freshness_basis_ref is required for bounded_staleness source layers") + 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", []).append( + { + "if": { + "properties": {"temporal_relation": {"const": "bounded_staleness"}}, + "required": ["temporal_relation"], + }, + "then": { + "required": ["freshness_basis_ref"], + "properties": {"freshness_basis_ref": {"type": "string", "minLength": 1}}, + }, + } + ) + return json_schema + + +class ParticipantContextTransformationModel(ContractModel): + """Governed transformation relation for a SEM-214 context view.""" + + transformation_rule_ref: NonEmptyString + description: NonEmptyString + input_source_ids: list[NonEmptyString] = Field(min_length=1) + output_semantics_ref: NonEmptyString | None = None + + +class ParticipantContextComparabilityModel(ContractModel): + """Explicit comparability claim for a SEM-214 context view.""" + + comparability_class: ParticipantContextComparabilityClass + comparison_basis_ref: NonEmptyString + backend_disclosure_refs: list[NonEmptyString] = Field(default_factory=list) + limitations: list[NonEmptyString] = Field(min_length=1) + + @model_validator(mode="after") + def _validate_disclosed_weakening(self) -> ParticipantContextComparabilityModel: + if ( + self.comparability_class in {"portable_with_disclosed_weakening", "backend_specific_non_comparable"} + and not self.backend_disclosure_refs + ): + raise ValueError("backend_disclosure_refs are required when comparability is weakened or backend-specific") + 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", []).append( + { + "if": { + "properties": { + "comparability_class": { + "enum": [ + "portable_with_disclosed_weakening", + "backend_specific_non_comparable", + ] + } + }, + "required": ["comparability_class"], + }, + "then": { + "required": ["backend_disclosure_refs"], + "properties": {"backend_disclosure_refs": {"minItems": 1}}, + }, + } + ) + return json_schema + + class ParticipantContextViewModel(ContractModel): - """API-408 derived operational context view (reference-and-provenance only).""" + """API-408 derived operational context view with SEM-214 semantics.""" view_id: NonEmptyString participant_address: NonEmptyString @@ -1355,13 +1801,142 @@ class ParticipantContextViewModel(ContractModel): generated_at: Rfc3339DateTimeString source_snapshot_ref: NonEmptyString view_ref: NonEmptyString + meaning_ref: NonEmptyString + participant_scope: ParticipantContextParticipantScope + audience_scope: ParticipantContextAudienceScope + observation_point: NonEmptyString derived_from_refs: list[NonEmptyString] = Field(min_length=1) + source_layers: list[ParticipantContextSourceLayerModel] = Field(min_length=1) + transformation: ParticipantContextTransformationModel + comparability: ParticipantContextComparabilityModel + evidence_refs: list[NonEmptyString] = Field(min_length=1) + provenance_refs: list[NonEmptyString] = Field(min_length=1) + semantic_limitations: list[NonEmptyString] = Field(min_length=1) derivation_basis_ref: NonEmptyString | None = None payload_ref: NonEmptyString | None = None visibility_projection_ref: NonEmptyString marking_definition_refs: list[NonEmptyString] = Field(default_factory=list) redaction_policy_ref: NonEmptyString | None = None + @model_validator(mode="after") + def _validate_sem214_source_binding(self) -> ParticipantContextViewModel: + source_ids = [source.source_id for source in self.source_layers] + if len(set(source_ids)) != len(source_ids): + raise ValueError("context view source_layers source_id values must be unique") + unknown_inputs = sorted(set(self.transformation.input_source_ids) - set(source_ids)) + if unknown_inputs: + raise ValueError( + "context view transformation input_source_ids must reference source_layers: " + + ", ".join(unknown_inputs) + ) + available_refs = set(self.derived_from_refs) | {self.source_snapshot_ref} + missing_refs = sorted(source.ref for source in self.source_layers if source.ref not in available_refs) + if missing_refs: + raise ValueError( + "context view source layer refs must be listed in derived_from_refs or source_snapshot_ref: " + + ", ".join(missing_refs) + ) + return self + + @model_validator(mode="after") + def _validate_sem216_audience_boundary(self) -> ParticipantContextViewModel: + # SEM-216 B1/B2: archived evidence and derived evaluation/adjudication outputs are + # distinct strata from an audience-specific view. A participant-visible view may only + # draw on an evidence_record or derived_measure source layer through a governed view + # rule (derivation_basis_ref), under a redaction policy (redaction_policy_ref); the + # archival source must be consumed by the transformation rather than passed through raw; + # and the disclosed payload must be the transformed output, never the raw archival ref. + # + # The required-ref clauses are also published as a schema allOf so schema-only consumers + # enforce them; the relational mediation and payload-aliasing clauses cannot be expressed + # in JSON Schema and are published as x-aces-invariants (see __get_pydantic_json_schema__). + if self.audience_scope != "participant_visible": + return self + archival_layers = [ + source for source in self.source_layers if source.source_layer in {"evidence_record", "derived_measure"} + ] + if not archival_layers: + return self + if self.derivation_basis_ref is None: + raise ValueError( + "participant-visible context views that draw on archival evidence_record or derived_measure " + "source layers must declare a derivation_basis_ref governed view rule" + ) + if self.redaction_policy_ref is None: + raise ValueError( + "participant-visible context views that draw on archival evidence_record or derived_measure " + "source layers must declare a redaction_policy_ref" + ) + mediated = set(self.transformation.input_source_ids) + unmediated = sorted(source.source_id for source in archival_layers if source.source_id not in mediated) + if unmediated: + raise ValueError( + "participant-visible archival source layers must be mediated by the transformation view rule; " + "unmediated source ids: " + ", ".join(unmediated) + ) + if self.payload_ref is not None: + raw_archival_refs = {source.ref for source in archival_layers} + raw_archival_refs.update(ref for source in archival_layers for ref in source.evidence_refs) + if self.payload_ref in raw_archival_refs: + raise ValueError( + "participant-visible context views must not set payload_ref to a raw archival " + "evidence_record/derived_measure source ref; payload_ref must identify the transformed, " + "redacted view output produced under the governed view rule" + ) + 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", []).append( + { + "if": { + "properties": { + "audience_scope": {"const": "participant_visible"}, + "source_layers": { + "contains": { + "properties": {"source_layer": {"enum": ["evidence_record", "derived_measure"]}}, + "required": ["source_layer"], + } + }, + }, + "required": ["audience_scope", "source_layers"], + }, + "then": { + "required": ["derivation_basis_ref", "redaction_policy_ref"], + "properties": { + "derivation_basis_ref": {"type": "string", "minLength": 1}, + "redaction_policy_ref": {"type": "string", "minLength": 1}, + }, + }, + } + ) + # SEM-216 relational obligations that standard JSON Schema cannot express are published + # as ACES semantic invariants so schema-only consumers see the full portable contract and + # the validator that enforces it (mirrors the experiment-core x-aces-invariants pattern). + _add_aces_invariant( + json_schema, + "context-view-sem216-archival-source-mediated", + "Participant-visible context views drawing on an archival evidence_record or derived_measure " + "source layer must mediate that source through transformation.input_source_ids.", + validator="aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary", + inputs=[{"contract_id": "participant-context-view-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "context-view-sem216-payload-not-raw-archival", + "Participant-visible context views must not set payload_ref to a raw archival evidence_record or " + "derived_measure source ref; payload_ref must identify the transformed, redacted view output.", + validator="aces_contracts.contracts.ParticipantContextViewModel._validate_sem216_audience_boundary", + inputs=[{"contract_id": "participant-context-view-v1", "instance_path": "#/payload_ref"}], + ) + return json_schema + class PlanOperationModel(ContractModel): action: str @@ -1440,6 +2015,10 @@ class RuntimeSnapshotEnvelopeModel(ContractModel): participant_episode_results: dict[str, ParticipantEpisodeStateModel] = Field(default_factory=dict) participant_episode_history: dict[str, list[ParticipantEpisodeHistoryEventModel]] = Field(default_factory=dict) participant_behavior_history: dict[str, list[ParticipantBehaviorHistoryEventModel]] = Field(default_factory=dict) + shared_state_records: dict[str, ParticipantSharedStateRecordModel] = Field(default_factory=dict) + shared_state_history: dict[str, list[ParticipantSharedStateRecordModel]] = Field(default_factory=dict) + joint_action_records: dict[str, ParticipantJointActionRecordModel] = Field(default_factory=dict) + time_management_contexts: dict[str, ParticipantTimeManagementContextModel] = Field(default_factory=dict) realization_provenance: list[RealizationProvenanceEntryModel] = Field(default_factory=list) metadata: dict[str, Any] = Field(default_factory=dict) @@ -1884,11 +2463,52 @@ def _validate_api_407_feature_support(self) -> ParticipantRuntimeCapabilitiesMod return self +class ObservationCapabilitiesModel(ContractModel): + """EXP-715 backend observation and evidence-collection capability declaration.""" + + name: NonEmptyString + supported_capture_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_channel_kinds: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_evidence_contracts: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_media_types: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supported_sealing_modes: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + supports_redaction: bool = False + supports_loss_disclosure: bool = False + supports_chain_of_custody: bool = False + constraints: dict[str, str] = Field(default_factory=dict) + + @model_validator(mode="after") + def _validate_observation_capability(self) -> ObservationCapabilitiesModel: + _validate_unique_string_values("supported_capture_kinds", self.supported_capture_kinds) + _validate_unique_string_values("supported_channel_kinds", self.supported_channel_kinds) + _validate_unique_string_values("supported_evidence_contracts", self.supported_evidence_contracts) + _validate_unique_string_values("supported_media_types", self.supported_media_types) + _validate_unique_string_values("supported_sealing_modes", self.supported_sealing_modes) + _validate_controlled_vocabulary_terms( + "capabilities.observation.supported_capture_kinds", + self.supported_capture_kinds, + ) + _validate_controlled_vocabulary_terms( + "capabilities.observation.supported_channel_kinds", + self.supported_channel_kinds, + ) + _validate_controlled_vocabulary_terms( + "capabilities.observation.supported_sealing_modes", + self.supported_sealing_modes, + ) + validate_backend_supported_contract_versions(self.supported_evidence_contracts) + for contract_id in self.supported_evidence_contracts: + if not contract_id.startswith("experiment-"): + raise ValueError("observation supported_evidence_contracts must be experiment contract ids") + return self + + class BackendCapabilitiesV2Model(ContractModel): provisioner: ProvisionerCapabilitiesModel orchestrator: OrchestratorCapabilitiesModel | None = None evaluator: EvaluatorCapabilitiesModel | None = None participant_runtime: ParticipantRuntimeCapabilitiesModel | None = None + observation: ObservationCapabilitiesModel | None = None class ProcessorManifestV2Model(ContractModel): @@ -2148,12 +2768,16 @@ class ExperimentReferenceModel(ContractModel): "protocol", "apparatus-context", "run", + "metric-definition", "result", "study", "manifest", "profile", "capability", + "capture-spec", "evidence", + "evidence-record", + "derived-measure", "measurement-channel", "analysis-artifact", "other", @@ -2463,15 +3087,15 @@ def __get_pydantic_json_schema__( return json_schema -class ExperimentMeasurementChannelReferenceModel(ExperimentReferenceModel): - """Reference constrained to a declared measurement channel.""" +class ExperimentCaptureSpecReferenceModel(ExperimentReferenceModel): + """Reference constrained to a declarative capture specification.""" - ref_kind: Literal["measurement-channel"] + ref_kind: Literal["capture-spec"] @model_validator(mode="after") - def _validate_measurement_channel_reference_scope(self) -> ExperimentMeasurementChannelReferenceModel: + def _validate_capture_spec_reference_scope(self) -> ExperimentCaptureSpecReferenceModel: if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set: - raise ValueError("measurement-channel references must not carry ref_digest or ref_path") + raise ValueError("capture-spec references must not carry ref_digest or ref_path") return self @classmethod @@ -2489,8 +3113,86 @@ def __get_pydantic_json_schema__( return json_schema -class ExperimentApparatusCompatibilityReferenceModel(ExperimentReferenceModel): - """Profile or capability reference declared by apparatus compatibility metadata.""" +class ExperimentEvidenceRecordReferenceModel(ExperimentReferenceModel): + """Reference constrained to a raw captured evidence record.""" + + ref_kind: Literal["evidence-record"] + + @model_validator(mode="after") + def _validate_evidence_record_reference_scope(self) -> ExperimentEvidenceRecordReferenceModel: + if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set: + raise ValueError("evidence-record references must not carry ref_digest or ref_path") + 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) + properties = json_schema.get("properties") + if isinstance(properties, dict): + properties.pop("ref_digest", None) + properties.pop("ref_path", None) + return json_schema + + +class ExperimentDerivedMeasureReferenceModel(ExperimentReferenceModel): + """Reference constrained to a derived measure or analysis output.""" + + ref_kind: Literal["derived-measure"] + + @model_validator(mode="after") + def _validate_derived_measure_reference_scope(self) -> ExperimentDerivedMeasureReferenceModel: + if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set: + raise ValueError("derived-measure references must not carry ref_digest or ref_path") + 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) + properties = json_schema.get("properties") + if isinstance(properties, dict): + properties.pop("ref_digest", None) + properties.pop("ref_path", None) + return json_schema + + +class ExperimentMeasurementChannelReferenceModel(ExperimentReferenceModel): + """Reference constrained to a declared measurement channel.""" + + ref_kind: Literal["measurement-channel"] + + @model_validator(mode="after") + def _validate_measurement_channel_reference_scope(self) -> ExperimentMeasurementChannelReferenceModel: + if "ref_digest" in self.model_fields_set or "ref_path" in self.model_fields_set: + raise ValueError("measurement-channel references must not carry ref_digest or ref_path") + 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) + properties = json_schema.get("properties") + if isinstance(properties, dict): + properties.pop("ref_digest", None) + properties.pop("ref_path", None) + return json_schema + + +class ExperimentApparatusCompatibilityReferenceModel(ExperimentReferenceModel): + """Profile or capability reference declared by apparatus compatibility metadata.""" ref_kind: Literal["profile", "capability"] @@ -2626,6 +3328,36 @@ def _reference_identity_satisfies_requirement( return _reference_identity_satisfies_requirement(candidate_subject, requirement_subject) +def _experiment_reference_key( + reference: ExperimentReferenceModel, +) -> tuple[Any, ...]: + subject_ref = getattr(reference, "subject_ref", None) + return ( + reference.ref_kind, + reference.ref_id, + reference.ref_version, + _canonical_digest(getattr(reference, "ref_digest", None)), + getattr(reference, "ref_path", None), + _experiment_reference_key(subject_ref) if subject_ref is not None else None, + ) + + +def _validate_unique_experiment_references( + field_name: str, + references: list[ExperimentReferenceModel], +) -> None: + seen: set[tuple[Any, ...]] = set() + duplicates: list[str] = [] + for reference in references: + key = _experiment_reference_key(reference) + if key in seen: + duplicates.append(_format_reference(reference)) + seen.add(key) + if duplicates: + joined = ", ".join(sorted(set(duplicates))) + raise ValueError(f"{field_name} must not contain duplicates: {joined}") + + def _identity_matches_reference(identity: ApparatusIdentityModel, reference: ExperimentReferenceModel) -> bool: if reference.ref_digest is not None or reference.ref_path is not None: return False @@ -2732,6 +3464,624 @@ class ExperimentValidityNoteModel(ContractModel): mitigation: NonEmptyString | None = None +class ExperimentCaptureWindowModel(ContractModel): + """Declarative scope/window over which evidence must be captured.""" + + window_id: NonEmptyString + window_kind: Literal["task", "run", "apparatus", "event", "interval", "manual"] + starts_at: Rfc3339DateTimeString | None = None + ends_at: Rfc3339DateTimeString | None = None + trigger_ref: ExperimentReferenceModel | None = None + description: NonEmptyString | None = None + + @model_validator(mode="after") + def _validate_capture_window(self) -> ExperimentCaptureWindowModel: + if self.starts_at is None and self.ends_at is None and self.trigger_ref is None: + raise ValueError("capture windows must declare starts_at, ends_at, or trigger_ref") + if self.starts_at is not None and self.ends_at is not None: + starts_at = _parse_rfc3339_datetime("starts_at", self.starts_at) + ends_at = _parse_rfc3339_datetime("ends_at", self.ends_at) + if ends_at < starts_at: + raise ValueError("capture window ends_at must be greater than or equal to starts_at") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("anyOf", []).extend( + [ + {"required": ["starts_at"], "properties": {"starts_at": {"not": {"type": "null"}}}}, + {"required": ["ends_at"], "properties": {"ends_at": {"not": {"type": "null"}}}}, + {"required": ["trigger_ref"], "properties": {"trigger_ref": {"not": {"type": "null"}}}}, + ] + ) + _add_aces_invariant( + json_schema, + "capture-window-interval-valid", + "Capture window ends_at must not precede starts_at when both timestamps are present.", + validator="aces_contracts.contracts.ExperimentCaptureWindowModel._validate_capture_window", + inputs=[{"contract_id": "experiment-capture-spec-v1", "instance_path": "#/capture_windows"}], + ) + return json_schema + + +class ExperimentCaptureRequirementModel(ContractModel): + """One evidence capture requirement inside a capture specification.""" + + requirement_id: NonEmptyString + title: NonEmptyString + capture_kind: Literal["artifact", "observation", "trace", "telemetry", "log", "packet-capture", "other"] + capture_scope: Literal["task", "run", "apparatus", "participant", "backend", "processor", "network", "service"] + channel_ref: ExperimentMeasurementChannelReferenceModel + window_refs: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + expected_media_types: list[NonEmptyString] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + required_artifact_roles: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + sensitivity: Literal["public", "internal", "restricted", "redacted"] + redaction_policy: NonEmptyString | None = None + integrity_requirements: list[NonEmptyString] = Field(min_length=1) + retention_policy: NonEmptyString | None = None + loss_disclosure_required: bool = True + notes: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_capture_requirement(self) -> ExperimentCaptureRequirementModel: + _validate_unique_string_values("window_refs", self.window_refs) + _validate_unique_string_values("expected_media_types", self.expected_media_types) + _validate_unique_string_values("required_artifact_roles", self.required_artifact_roles) + return self + + +class ExperimentCaptureSpecModel(ContractModel): + """Declarative EXP-707 specification of what experiment evidence to capture.""" + + schema_version: Literal[EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION] + capture_spec_id: NonEmptyString + spec_version: NonEmptyString + title: NonEmptyString + description: NonEmptyString + scope_refs: list[ExperimentReferenceModel] = Field(min_length=1) + capture_windows: list[ExperimentCaptureWindowModel] = Field(min_length=1) + capture_requirements: dict[NonEmptyString, ExperimentCaptureRequirementModel] = Field(min_length=1) + validity_notes: list[ExperimentValidityNoteModel] = Field(default_factory=list) + artifact_refs: list[ExperimentArtifactRefModel] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_capture_spec(self) -> ExperimentCaptureSpecModel: + mismatches = [ + requirement_key + for requirement_key, requirement in self.capture_requirements.items() + if requirement.requirement_id != requirement_key + ] + if mismatches: + joined = ", ".join(sorted(mismatches)) + raise ValueError(f"capture_requirements keys must match embedded requirement_id: {joined}") + window_ids = {window.window_id for window in self.capture_windows} + missing_window_refs = sorted( + { + window_ref + for requirement in self.capture_requirements.values() + for window_ref in requirement.window_refs + if window_ref not in window_ids + } + ) + if missing_window_refs: + joined = ", ".join(missing_window_refs) + raise ValueError(f"capture requirement window_refs must resolve to capture_windows: {joined}") + 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, + "capture-requirement-key-matches-requirement-id", + "Every capture_requirements object key must match the embedded requirement_id value, and window_refs " + "must resolve to declared capture_windows.", + validator="aces_contracts.contracts.ExperimentCaptureSpecModel._validate_capture_spec", + inputs=[{"contract_id": "experiment-capture-spec-v1", "instance_path": "#"}], + ) + _add_aces_plane(json_schema, "experiment-capture-spec-v1") + return json_schema + + +class ExperimentRawEvidenceContentModel(ContractModel): + """Raw captured payload reference or bounded summary for EXP-708 records.""" + + artifact_ref: ExperimentArtifactRefModel | None = None + content_uri: NonEmptyString | None = None + content_checksum: ExperimentChecksumModel | None = None + payload_summary: NonEmptyString | None = None + loss_disclosure: NonEmptyString | None = None + + @model_validator(mode="after") + def _validate_raw_content(self) -> ExperimentRawEvidenceContentModel: + if self.artifact_ref is None and self.content_uri is None and self.payload_summary is None: + raise ValueError("raw evidence content must include artifact_ref, content_uri, or payload_summary") + if self.content_uri is not None and self.content_checksum is None: + raise ValueError("content_uri raw evidence must include content_checksum") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("anyOf", []).extend( + [ + {"required": ["artifact_ref"], "properties": {"artifact_ref": {"not": {"type": "null"}}}}, + {"required": ["content_uri"], "properties": {"content_uri": {"not": {"type": "null"}}}}, + {"required": ["payload_summary"], "properties": {"payload_summary": {"not": {"type": "null"}}}}, + ] + ) + json_schema.setdefault("allOf", []).append( + { + "if": {"required": ["content_uri"], "properties": {"content_uri": {"not": {"type": "null"}}}}, + "then": { + "required": ["content_checksum"], + "properties": {"content_checksum": {"not": {"type": "null"}}}, + }, + } + ) + return json_schema + + +class ExperimentEvidenceRecordModel(ContractModel): + """Raw captured EXP-708 evidence record, distinct from derived measures.""" + + schema_version: Literal[EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION] + evidence_record_id: NonEmptyString + record_version: NonEmptyString + capture_spec_ref: ExperimentCaptureSpecReferenceModel + capture_requirement_ref: NonEmptyString + run_ref: ExperimentReferenceModel + task_ref: ExperimentTaskReferenceModel | None = None + apparatus_context_ref: ExperimentReferenceModel | None = None + source_refs: list[ExperimentReferenceModel] = Field(min_length=1) + evidence_kind: Literal["artifact", "observation", "trace", "telemetry", "log", "packet-capture", "other"] + captured_at: Rfc3339DateTimeString + capture_window_ref: NonEmptyString + raw_content: ExperimentRawEvidenceContentModel + sensitivity: Literal["public", "internal", "restricted", "redacted"] + redaction_state: Literal["none", "redacted", "withheld"] + provenance_refs: list[ExperimentReferenceModel] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_evidence_record(self) -> ExperimentEvidenceRecordModel: + _parse_rfc3339_datetime("captured_at", self.captured_at) + if self.redaction_state != "none" and self.raw_content.loss_disclosure is None: + raise ValueError("redacted or withheld evidence records must include raw_content.loss_disclosure") + 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, + "evidence-record-raw-content-present", + "Evidence records must carry raw content as an artifact reference, content URI with checksum, or bounded " + "payload summary; redacted/withheld records must disclose loss.", + validator="aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record", + inputs=[{"contract_id": "experiment-evidence-record-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "evidence-record-captured-at-valid", + "captured_at must be a valid RFC 3339 date-time.", + validator="aces_contracts.contracts.ExperimentEvidenceRecordModel._validate_evidence_record", + inputs=[{"contract_id": "experiment-evidence-record-v1", "instance_path": "#/captured_at"}], + ) + # SEM-216 B4: redacted or withheld evidence records must disclose redaction/loss at the + # evidence boundary. Publish the model rule as a portable schema constraint so any + # consumer validating against the JSON Schema enforces the disclosure, not just the model. + json_schema.setdefault("allOf", []).append( + { + "if": { + "properties": {"redaction_state": {"enum": ["redacted", "withheld"]}}, + "required": ["redaction_state"], + }, + "then": { + "required": ["raw_content"], + "properties": { + "raw_content": { + "required": ["loss_disclosure"], + "properties": {"loss_disclosure": {"type": "string", "minLength": 1}}, + } + }, + }, + } + ) + _add_aces_plane(json_schema, "experiment-evidence-record-v1") + return json_schema + + +class ExperimentDerivedMeasureMethodModel(ContractModel): + """Method metadata for deriving measures from raw evidence.""" + + method_id: NonEmptyString + method_version: NonEmptyString + name: NonEmptyString + description: NonEmptyString | None = None + parameters: list[ExperimentParameterModel] = Field(default_factory=list) + + +class ExperimentDerivedMeasureModel(ContractModel): + """EXP-709 derived measure/evaluation output computed from raw evidence.""" + + schema_version: Literal[EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION] + derived_measure_id: NonEmptyString + measure_version: NonEmptyString + measure_kind: Literal["metric", "evaluation", "score", "summary", "analysis-output", "other"] + metric_ref: ExperimentReferenceModel + method: ExperimentDerivedMeasureMethodModel + source_evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(min_length=1) + generated_at: Rfc3339DateTimeString + value_status: Literal["reported", "missing", "withheld", "not-applicable"] + value: str | int | float | bool | None = None + uncertainty: NonEmptyString | None = None + limitations: list[NonEmptyString] = Field(default_factory=list) + provenance_refs: list[ExperimentReferenceModel] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_derived_measure(self) -> ExperimentDerivedMeasureModel: + _parse_rfc3339_datetime("generated_at", self.generated_at) + _validate_reported_value_status( + self.value_status, + self.value, + reported_message="reported derived measures must include value", + non_reported_message="non-reported derived measures must not include value", + ) + 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) + _extend_reported_value_status_schema(json_schema) + _add_aces_invariant( + json_schema, + "derived-measure-reported-value-present", + "Reported derived measures must include a value; missing/withheld/not-applicable measures must not.", + validator="aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure", + inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "derived-measure-generated-at-valid", + "generated_at must be a valid RFC 3339 date-time.", + validator="aces_contracts.contracts.ExperimentDerivedMeasureModel._validate_derived_measure", + inputs=[{"contract_id": "experiment-derived-measure-v1", "instance_path": "#/generated_at"}], + ) + _add_aces_plane(json_schema, "experiment-derived-measure-v1") + return json_schema + + +class ExperimentRunTraceabilityModel(ContractModel): + """Canonical run provenance links across capture, evidence, measures, and claims.""" + + capture_spec_refs: list[ExperimentCaptureSpecReferenceModel] = Field(min_length=1) + evidence_record_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(min_length=1) + derived_measure_refs: list[ExperimentDerivedMeasureReferenceModel] = Field(default_factory=list) + claim_refs: list[ExperimentReferenceModel] = Field(default_factory=list) + notes: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_run_traceability(self) -> ExperimentRunTraceabilityModel: + _validate_unique_experiment_references("traceability capture_spec_refs", self.capture_spec_refs) + _validate_unique_experiment_references("traceability evidence_record_refs", self.evidence_record_refs) + _validate_unique_experiment_references("traceability derived_measure_refs", self.derived_measure_refs) + _validate_unique_experiment_references("traceability claim_refs", self.claim_refs) + if self.claim_refs and not self.derived_measure_refs: + raise ValueError("traceability claim_refs require at least one derived_measure_refs entry") + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + _add_aces_invariant( + json_schema, + "run-traceability-refs-unique", + "Run provenance traceability references must be duplicate-free, and claim refs must be grounded by " + "at least one derived measure ref.", + validator="aces_contracts.contracts.ExperimentRunTraceabilityModel._validate_run_traceability", + inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/traceability"}], + ) + return json_schema + + +class ExperimentRealizedFormDisclosureModel(ContractModel): + """Disclosure of one realized form chosen for an underspecified run concern.""" + + concern_id: NonEmptyString + concern_kind: Literal[ + "scenario-module", + "processor-selection", + "backend-selection", + "participant-implementation", + "apparatus-configuration", + "parameter-default", + "stochastic-control", + "measurement-channel", + "capture-window", + "other", + ] + basis: Literal["author-declared", "processor-realized", "backend-realized", "operator-supplied", "observed"] + realized_by_ref: ExperimentReferenceModel + authored_ref: ExperimentReferenceModel | None = None + realized_ref: ExperimentReferenceModel | None = None + realized_value_summary: NonEmptyString | None = None + disclosure: NonEmptyString + evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_realized_form_disclosure(self) -> ExperimentRealizedFormDisclosureModel: + if self.realized_ref is None and self.realized_value_summary is None: + raise ValueError("realized form disclosures must include realized_ref or realized_value_summary") + if self.basis == "processor-realized" and self.realized_by_ref.ref_kind != "processor": + raise ValueError("processor-realized disclosures must use a processor realized_by_ref") + if self.basis == "backend-realized" and self.realized_by_ref.ref_kind != "backend": + raise ValueError("backend-realized disclosures must use a backend realized_by_ref") + _validate_unique_experiment_references("realized form disclosure evidence_refs", self.evidence_refs) + return self + + @classmethod + def __get_pydantic_json_schema__( + cls, + core_schema: CoreSchema, + handler: GetJsonSchemaHandler, + ) -> JsonSchemaValue: + json_schema = handler(core_schema) + json_schema = handler.resolve_ref_schema(json_schema) + json_schema.setdefault("anyOf", []).extend( + [ + {"required": ["realized_ref"], "properties": {"realized_ref": {"not": {"type": "null"}}}}, + { + "required": ["realized_value_summary"], + "properties": {"realized_value_summary": {"not": {"type": "null"}}}, + }, + ] + ) + json_schema.setdefault("allOf", []).extend( + [ + { + "if": {"properties": {"basis": {"const": "processor-realized"}}, "required": ["basis"]}, + "then": { + "properties": { + "realized_by_ref": { + "required": ["ref_kind"], + "properties": {"ref_kind": {"const": "processor"}}, + } + } + }, + }, + { + "if": {"properties": {"basis": {"const": "backend-realized"}}, "required": ["basis"]}, + "then": { + "properties": { + "realized_by_ref": { + "required": ["ref_kind"], + "properties": {"ref_kind": {"const": "backend"}}, + } + } + }, + }, + ] + ) + _add_aces_invariant( + json_schema, + "realized-form-disclosure-substantive", + "Every realized-form disclosure must name a realized reference or value summary and use the right " + "processor/backend realization authority for processor-realized and backend-realized concerns.", + validator=( + "aces_contracts.contracts.ExperimentRealizedFormDisclosureModel._validate_realized_form_disclosure" + ), + inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/realized_form_disclosures"}], + ) + return json_schema + + +_SEM_225_PORTABLE_CARRIER_KINDS = frozenset( + { + "apparatus-context", + "capture-spec", + "derived-measure", + "evidence-record", + "manifest", + "measurement-channel", + "profile", + "run", + "scenario-snapshot", + } +) + + +def _validate_sem_225_claim_evidence( + classifications: set[str], + evidence_refs: list[ExperimentEvidenceRecordReferenceModel], +) -> None: + if classifications - {"apparatus_only"} and not evidence_refs: + raise ValueError("environment, participant, or comparability augmentations require evidence_refs") + + +def _validate_sem_225_environment_visible(disclosure: ExperimentAugmentationDisclosureModel) -> None: + if disclosure.environment_effect is None: + raise ValueError("environment_visible augmentation disclosures require environment_effect") + if not any(ref.ref_kind in _SEM_225_PORTABLE_CARRIER_KINDS for ref in disclosure.carrier_refs): + raise ValueError("environment_visible augmentation disclosures require a portable carrier_ref") + + +def _validate_sem_225_participant_visible(disclosure: ExperimentAugmentationDisclosureModel) -> None: + if disclosure.participant_visibility is None: + raise ValueError("participant_visible augmentation disclosures require participant_visibility") + if not disclosure.markings: + raise ValueError("participant_visible augmentation disclosures require markings") + + +def _validate_sem_225_comparability_relevant(disclosure: ExperimentAugmentationDisclosureModel) -> None: + if disclosure.comparability_effect is None: + raise ValueError("comparability_relevant augmentation disclosures require comparability_effect") + if disclosure.observer_effect is None: + raise ValueError("comparability_relevant augmentation disclosures require observer_effect") + + +class ExperimentAugmentationDisclosureModel(ContractModel): + """Disclosure for processor/backend augmentation used by a run.""" + + augmentation_id: NonEmptyString + purpose: Literal["evidence", "evaluation", "operational", "comparability", "other"] + realization_layer: Literal[ + "processor", + "backend", + "apparatus", + "runtime-environment", + "participant-runtime", + "measurement-channel", + "analysis", + "other", + ] + classifications: list[ + Literal["apparatus_only", "environment_visible", "participant_visible", "comparability_relevant"] + ] = Field(min_length=1, json_schema_extra={"uniqueItems": True}) + augmented_by_ref: ExperimentReferenceModel + carrier_refs: list[ExperimentReferenceModel] = Field(min_length=1) + affected_refs: list[ExperimentReferenceModel] = Field(default_factory=list) + evidence_refs: list[ExperimentEvidenceRecordReferenceModel] = Field(default_factory=list) + disclosure_policy: NonEmptyString + markings: list[NonEmptyString] = Field(default_factory=list, json_schema_extra={"uniqueItems": True}) + observer_effect: NonEmptyString | None = None + environment_effect: NonEmptyString | None = None + participant_visibility: NonEmptyString | None = None + comparability_effect: NonEmptyString | None = None + notes: list[NonEmptyString] = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_augmentation_disclosure(self) -> ExperimentAugmentationDisclosureModel: + _validate_unique_string_values("augmentation classifications", self.classifications) + _validate_unique_string_values("augmentation disclosure markings", self.markings) + _validate_unique_string_values("augmentation disclosure notes", self.notes) + _validate_unique_experiment_references("augmentation disclosure carrier_refs", self.carrier_refs) + _validate_unique_experiment_references("augmentation disclosure affected_refs", self.affected_refs) + _validate_unique_experiment_references("augmentation disclosure evidence_refs", self.evidence_refs) + + if self.augmented_by_ref.ref_kind not in {"processor", "backend"}: + raise ValueError("augmentation disclosures must use a processor or backend augmented_by_ref") + + classification_set = set(self.classifications) + _validate_sem_225_claim_evidence(classification_set, self.evidence_refs) + if "environment_visible" in classification_set: + _validate_sem_225_environment_visible(self) + if "participant_visible" in classification_set: + _validate_sem_225_participant_visible(self) + if "comparability_relevant" in classification_set: + _validate_sem_225_comparability_relevant(self) + 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( + [ + { + "properties": { + "augmented_by_ref": { + "required": ["ref_kind"], + "properties": {"ref_kind": {"enum": ["processor", "backend"]}}, + } + } + }, + { + "if": { + "properties": {"classifications": {"contains": {"const": "environment_visible"}}}, + "required": ["classifications"], + }, + "then": { + "required": ["carrier_refs", "environment_effect", "evidence_refs"], + "properties": { + "environment_effect": {"type": "string", "minLength": 1}, + "carrier_refs": { + "contains": { + "required": ["ref_kind"], + "properties": {"ref_kind": {"enum": sorted(_SEM_225_PORTABLE_CARRIER_KINDS)}}, + } + }, + "evidence_refs": {"minItems": 1}, + }, + }, + }, + { + "if": { + "properties": {"classifications": {"contains": {"const": "participant_visible"}}}, + "required": ["classifications"], + }, + "then": { + "required": ["participant_visibility", "markings", "evidence_refs"], + "properties": { + "participant_visibility": {"type": "string", "minLength": 1}, + "markings": {"minItems": 1}, + "evidence_refs": {"minItems": 1}, + }, + }, + }, + { + "if": { + "properties": {"classifications": {"contains": {"const": "comparability_relevant"}}}, + "required": ["classifications"], + }, + "then": { + "required": ["comparability_effect", "observer_effect", "evidence_refs"], + "properties": { + "comparability_effect": {"type": "string", "minLength": 1}, + "observer_effect": {"type": "string", "minLength": 1}, + "evidence_refs": {"minItems": 1}, + }, + }, + }, + ] + ) + _add_aces_invariant( + json_schema, + "augmentation-disclosure-semantics-valid", + "Augmentation disclosures must keep environment-visible, participant-visible, and " + "comparability-relevant semantics explicit and must use processor/backend authority.", + validator=( + "aces_contracts.contracts.ExperimentAugmentationDisclosureModel._validate_augmentation_disclosure" + ), + inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#/augmentation_disclosures"}], + ) + return json_schema + + class ExperimentMetricDefinitionModel(ContractModel): """Metric definition bound to a measured construct and unit of analysis.""" @@ -3394,10 +4744,12 @@ class ExperimentResultSummaryModel(ContractModel): @model_validator(mode="after") def _validate_reported_value(self) -> ExperimentResultSummaryModel: - if self.value_status == "reported" and self.value is None: - raise ValueError("reported result summaries must include value") - if self.value_status != "reported" and self.value is not None: - raise ValueError("non-reported result summaries must not include value") + _validate_reported_value_status( + self.value_status, + self.value, + reported_message="reported result summaries must include value", + non_reported_message="non-reported result summaries must not include value", + ) return self @classmethod @@ -3408,24 +4760,7 @@ def __get_pydantic_json_schema__( ) -> JsonSchemaValue: json_schema = handler(core_schema) json_schema = handler.resolve_ref_schema(json_schema) - json_schema.setdefault("allOf", []).extend( - [ - { - "if": { - "properties": {"value_status": {"const": "reported"}}, - "required": ["value_status"], - }, - "then": {"required": ["value"], "properties": {"value": {"not": {"type": "null"}}}}, - }, - { - "if": { - "properties": {"value_status": {"enum": ["missing", "withheld", "not-applicable"]}}, - "required": ["value_status"], - }, - "then": {"properties": {"value": {"type": "null"}}}, - }, - ] - ) + _extend_reported_value_status_schema(json_schema) return json_schema @@ -3459,6 +4794,9 @@ class ExperimentRunModel(ContractModel): clock_context: ExperimentClockContextModel run_status: Literal["sealed", "completed", "failed", "aborted", "invalidated", "superseded"] outcome_status: Literal["succeeded", "failed", "partial", "inconclusive", "not-evaluated"] + traceability: ExperimentRunTraceabilityModel + realized_form_disclosures: list[ExperimentRealizedFormDisclosureModel] = Field(default_factory=list) + augmentation_disclosures: list[ExperimentAugmentationDisclosureModel] = Field(default_factory=list) evidence_artifacts: list[ExperimentArtifactRefModel] = Field(min_length=1) result_summaries: dict[NonEmptyString, ExperimentResultSummaryModel] = Field(min_length=1) deviations: list[NonEmptyString] = Field(default_factory=list) @@ -3519,6 +4857,35 @@ def _validate_archival_run(self) -> ExperimentRunModel: if missing_evidence_refs: joined = ", ".join(missing_evidence_refs) raise ValueError(f"result_summaries evidence_refs must resolve to evidence_artifacts: {joined}") + traced_evidence_record_refs = { + _experiment_reference_key(evidence_ref) for evidence_ref in self.traceability.evidence_record_refs + } + missing_disclosure_evidence_refs = sorted( + _format_reference(evidence_ref) + for disclosure in self.realized_form_disclosures + for evidence_ref in disclosure.evidence_refs + if _experiment_reference_key(evidence_ref) not in traced_evidence_record_refs + ) + if missing_disclosure_evidence_refs: + joined = ", ".join(missing_disclosure_evidence_refs) + raise ValueError( + f"realized_form_disclosures evidence_refs must be listed in traceability evidence_record_refs: {joined}" + ) + _validate_unique_string_values( + "augmentation_disclosures augmentation_id", + [disclosure.augmentation_id for disclosure in self.augmentation_disclosures], + ) + missing_augmentation_evidence_refs = sorted( + _format_reference(evidence_ref) + for disclosure in self.augmentation_disclosures + for evidence_ref in disclosure.evidence_refs + if _experiment_reference_key(evidence_ref) not in traced_evidence_record_refs + ) + if missing_augmentation_evidence_refs: + joined = ", ".join(missing_augmentation_evidence_refs) + raise ValueError( + f"augmentation_disclosures evidence_refs must be listed in traceability evidence_record_refs: {joined}" + ) return self @classmethod @@ -3562,6 +4929,21 @@ def __get_pydantic_json_schema__( validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run", inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], ) + _add_aces_invariant( + json_schema, + "realized-form-evidence-refs-traced", + "Every realized-form disclosure evidence ref must also appear in the run traceability evidence refs.", + validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run", + inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], + ) + _add_aces_invariant( + json_schema, + "augmentation-disclosure-evidence-refs-traced", + "Every augmentation disclosure evidence ref must also appear in the run traceability evidence refs, " + "and augmentation_id values must be unique within the run.", + validator="aces_contracts.contracts.ExperimentRunModel._validate_archival_run", + inputs=[{"contract_id": "experiment-run-v1", "instance_path": "#"}], + ) _add_aces_invariant( json_schema, "task-run-protocol-binding-valid", @@ -5223,6 +6605,9 @@ 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-capture-spec-v1": ExperimentCaptureSpecModel.model_json_schema(), + "experiment-derived-measure-v1": ExperimentDerivedMeasureModel.model_json_schema(), + "experiment-evidence-record-v1": ExperimentEvidenceRecordModel.model_json_schema(), "experiment-run-v1": ExperimentRunModel.model_json_schema(), "experiment-study-v1": ExperimentStudyModel.model_json_schema(), "experiment-task-v1": ExperimentTaskModel.model_json_schema(), @@ -5253,6 +6638,8 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "participant-lifecycle-event-v1": ParticipantLifecycleEventModel.model_json_schema(), "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel.model_json_schema(), "participant-shared-state-record-v1": ParticipantSharedStateRecordModel.model_json_schema(), + "participant-joint-action-record-v1": ParticipantJointActionRecordModel.model_json_schema(), + "participant-time-management-context-v1": ParticipantTimeManagementContextModel.model_json_schema(), "participant-outcome-report-v1": ParticipantOutcomeReportModel.model_json_schema(), "participant-status-view-v1": ParticipantStatusViewModel.model_json_schema(), "participant-history-view-v1": ParticipantHistoryViewModel.model_json_schema(), @@ -5303,11 +6690,21 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentApparatusConstraintModel", "ExperimentApparatusContextModel", "ExperimentArtifactRefModel", + "ExperimentAugmentationDisclosureModel", "ExperimentBackendReferenceModel", + "ExperimentCaptureRequirementModel", + "ExperimentCaptureSpecModel", + "ExperimentCaptureSpecReferenceModel", + "ExperimentCaptureWindowModel", "ExperimentChecksumModel", "ExperimentClockContextModel", "ExperimentConditionAssignmentParameterModel", "ExperimentConditionAssignmentReferenceModel", + "ExperimentDerivedMeasureMethodModel", + "ExperimentDerivedMeasureModel", + "ExperimentDerivedMeasureReferenceModel", + "ExperimentEvidenceRecordModel", + "ExperimentEvidenceRecordReferenceModel", "ExperimentEvidenceReferenceModel", "ExperimentEvaluationProtocolModel", "ExperimentInvalidationModel", @@ -5318,10 +6715,12 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentMultipleComparisonPolicyModel", "ExperimentParameterModel", "ExperimentProcessorReferenceModel", + "ExperimentRealizedFormDisclosureModel", "ExperimentReferenceModel", "ExperimentResultSummaryModel", "ExperimentRunAllocationPlanModel", "ExperimentRunModel", + "ExperimentRunTraceabilityModel", "ExperimentScenarioReferenceModel", "ExperimentScenarioSnapshotReferenceModel", "ExperimentSplitAndLeakageControlsModel", @@ -5335,6 +6734,9 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ExperimentUncertaintyMethodModel", "ExperimentValidityNoteModel", "EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION", + "EXPERIMENT_CAPTURE_SPEC_SCHEMA_VERSION", + "EXPERIMENT_DERIVED_MEASURE_SCHEMA_VERSION", + "EXPERIMENT_EVIDENCE_RECORD_SCHEMA_VERSION", "EXPERIMENT_RUN_SCHEMA_VERSION", "EXPERIMENT_STUDY_SCHEMA_VERSION", "EXPERIMENT_TASK_SCHEMA_VERSION", @@ -5348,6 +6750,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "OPERATION_SCHEMA_VERSION", "OperationReceiptModel", "OperationStatusModel", + "ObservationCapabilitiesModel", "OrchestrationPlanModel", "OrchestratorCapabilitiesModel", "PARTICIPANT_EPISODE_STATE_SCHEMA_VERSION", @@ -5375,6 +6778,8 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ParticipantImplementationManifestModel", "ParticipantImplementationProvenanceModel", "ParticipantImplementationSelectionModel", + "ParticipantJointActionAccessSetModel", + "ParticipantJointActionRecordModel", "ParticipantLifecycleEventModel", "ParticipantObservationEnvelopeModel", "ParticipantObservationLossDescriptorModel", @@ -5392,6 +6797,7 @@ def schema_bundle() -> dict[str, dict[str, Any]]: "ParticipantStatusViewEpisodeStateModel", "ParticipantStatusViewModel", "ParticipantTemporalRuntimeContextModel", + "ParticipantTimeManagementContextModel", "VIEW_SCOPE_PROJECTED_FIELDS", "PlanOperationModel", "ProcessorFeature", diff --git a/implementations/python/packages/aces_contracts/manifest_authority.py b/implementations/python/packages/aces_contracts/manifest_authority.py index 635be87e4..df331d7c7 100644 --- a/implementations/python/packages/aces_contracts/manifest_authority.py +++ b/implementations/python/packages/aces_contracts/manifest_authority.py @@ -50,7 +50,12 @@ "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", ) PARTICIPANT_IMPLEMENTATION_SUPPORTED_CONTRACT_IDS = ( diff --git a/implementations/python/packages/aces_contracts/participant_concurrency.py b/implementations/python/packages/aces_contracts/participant_concurrency.py new file mode 100644 index 000000000..c3ac686ab --- /dev/null +++ b/implementations/python/packages/aces_contracts/participant_concurrency.py @@ -0,0 +1,475 @@ +"""RUN-308 participant concurrency runtime validators.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping +from dataclasses import dataclass + +from .participant_concurrency_time import TIME_CONTEXTS_KEY as _TIME_CONTEXTS_KEY +from .participant_concurrency_time import time_contexts_violations as _time_contexts_violations + +Violation = tuple[str, str] + +_JOINT_ACTION_RECORDS_KEY = "runtime.snapshot.joint-action-records" + + +def iter_participant_concurrency_snapshot_violations( + joint_action_records: object, + time_management_contexts: object, + *, + participant_behavior_history: object = None, + shared_state_records: object = None, + shared_state_history: object = None, +) -> Iterator[Violation]: + """Yield RUN-308 snapshot violations for joint action/time records.""" + + known_event_refs = _known_behavior_event_refs(participant_behavior_history) + known_state_refs = _known_shared_state_refs(shared_state_records, shared_state_history) + known_time_context_refs = _known_mapping_keys(time_management_contexts) + known_time_contexts = time_management_contexts if isinstance(time_management_contexts, Mapping) else {} + violations: list[Violation] = [] + violations.extend( + _joint_action_records_violations( + joint_action_records, + known_event_refs=known_event_refs, + known_state_refs=known_state_refs, + known_time_context_refs=known_time_context_refs, + known_time_contexts=known_time_contexts, + ) + ) + violations.extend(_time_contexts_violations(time_management_contexts, known_event_refs=known_event_refs)) + return iter(violations) + + +def iter_participant_concurrency_transition_violations( + previous_joint_action_records: object, + next_joint_action_records: object, + previous_time_management_contexts: object, + next_time_management_contexts: object, +) -> Iterator[Violation]: + """Yield append-only violations for RUN-308 concurrency records.""" + + yield from _append_only_mapping_violations( + "joint_action_records", + _JOINT_ACTION_RECORDS_KEY, + previous_joint_action_records, + next_joint_action_records, + ) + yield from _append_only_mapping_violations( + "time_management_contexts", + _TIME_CONTEXTS_KEY, + previous_time_management_contexts, + next_time_management_contexts, + ) + + +def _known_behavior_event_refs(participant_behavior_history: object) -> set[str]: + if not isinstance(participant_behavior_history, Mapping): + return set() + return {ref for history in participant_behavior_history.values() for ref in _behavior_history_refs(history)} + + +def _behavior_history_refs(history: object) -> Iterator[str]: + if not isinstance(history, list): + return + for event in history: + if isinstance(event, Mapping): + yield from _behavior_event_refs(event) + + +def _behavior_event_refs(event: Mapping[object, object]) -> Iterator[str]: + for field_name in ("event_id", "action_instance_id"): + ref = event.get(field_name) + if isinstance(ref, str) and ref: + yield ref + + +def _known_shared_state_refs(shared_state_records: object, shared_state_history: object) -> set[str]: + refs: set[str] = set() + for candidate in (shared_state_records, shared_state_history): + if isinstance(candidate, Mapping): + refs.update(str(key) for key in candidate if isinstance(key, str) and key) + return refs + + +def _known_mapping_keys(candidate: object) -> set[str]: + if not isinstance(candidate, Mapping): + return set() + return {str(key) for key in candidate if isinstance(key, str) and key} + + +def _joint_action_records_violations( + records: object, + *, + known_event_refs: set[str], + known_state_refs: set[str], + known_time_context_refs: set[str], + known_time_contexts: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + if not isinstance(records, Mapping): + return [(_JOINT_ACTION_RECORDS_KEY, "joint_action_records must be a mapping")] + for outer_key, record in records.items(): + locator = f"{_JOINT_ACTION_RECORDS_KEY}.{outer_key}" + if not isinstance(outer_key, str) or not outer_key: + violations.append((_JOINT_ACTION_RECORDS_KEY, "joint_action_records keys must be non-empty strings")) + elif not isinstance(record, Mapping): + violations.append((locator, "joint action record must be a mapping")) + else: + violations.extend( + _joint_action_record_violations( + locator, + outer_key, + record, + known_event_refs=known_event_refs, + known_state_refs=known_state_refs, + known_time_context_refs=known_time_context_refs, + known_time_contexts=known_time_contexts, + ) + ) + return violations + + +def _joint_action_record_violations( + locator: str, + outer_key: str, + record: Mapping[object, object], + *, + known_event_refs: set[str], + known_state_refs: set[str], + known_time_context_refs: set[str], + known_time_contexts: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + violations.extend(_joint_action_identity_violations(locator, outer_key, record.get("joint_action_set_id"))) + + member_refs = record.get("member_event_refs") + access_sets = record.get("access_sets") + member_ref_set, member_violations = _joint_action_member_ref_violations( + locator, + member_refs, + known_event_refs=known_event_refs, + ) + violations.extend(member_violations) + + access_event_refs, access_violations = _joint_action_access_violations( + locator, + access_sets, + known_state_refs=known_state_refs, + ) + violations.extend(access_violations) + if member_ref_set and not _exact_string_set(access_event_refs, member_ref_set): + violations.append((locator, "joint action access_sets must cover member_event_refs exactly once")) + + realized_order = record.get("realized_order", []) + violations.extend(_joint_action_realized_order_violations(locator, realized_order, member_ref_set)) + + conflict_policy = record.get("conflict_policy") + conflict_class = record.get("conflict_class") + unsupported = record.get("unsupported_disclosure") is True + exact_claim = record.get("exact_concurrency_claim") is True + actual_conflict = _actual_conflict(access_sets) + violations.extend( + _joint_action_conflict_violations( + locator, + _JointActionConflictCheck( + conflict_class=conflict_class, + conflict_policy=conflict_policy, + actual_conflict=actual_conflict, + realized_order=realized_order, + unsupported=unsupported, + exact_claim=exact_claim, + retry_limit=record.get("retry_limit"), + rollback_event_refs=record.get("rollback_event_refs", []), + atomicity_scope=record.get("atomicity_scope"), + isolation_guarantee=record.get("isolation_guarantee"), + ), + ) + ) + + violations.extend( + _joint_action_time_context_violations( + locator, + record.get("time_management_context_ref"), + exact_claim=exact_claim, + known_time_context_refs=known_time_context_refs, + known_time_contexts=known_time_contexts, + ) + ) + return violations + + +def _joint_action_identity_violations(locator: str, outer_key: str, joint_id: object) -> list[Violation]: + if not isinstance(joint_id, str) or not joint_id: + return [(locator, "joint action record requires joint_action_set_id")] + if joint_id != outer_key: + return [(locator, f"joint action record key {outer_key!r} does not match joint_action_set_id")] + return [] + + +def _joint_action_member_ref_violations( + locator: str, + member_refs: object, + *, + known_event_refs: set[str], +) -> tuple[set[str], list[Violation]]: + if not isinstance(member_refs, list) or not member_refs: + return set(), [(locator, "joint action member_event_refs must be a non-empty list")] + + valid_refs = [ref for ref in member_refs if isinstance(ref, str) and ref] + member_ref_set = set(valid_refs) + if len(valid_refs) != len(member_refs): + return member_ref_set, [(locator, "joint action member_event_refs entries must be non-empty strings")] + if len(member_ref_set) != len(valid_refs): + return member_ref_set, [(locator, "joint action member_event_refs must be unique")] + return member_ref_set, [ + (locator, f"joint action member_event_ref {ref!r} does not resolve to behavior history") + for ref in sorted(member_ref_set - known_event_refs) + ] + + +def _joint_action_realized_order_violations( + locator: str, realized_order: object, member_ref_set: set[str] +) -> list[Violation]: + if not isinstance(realized_order, list): + return [(locator, "joint action realized_order must be a list")] + if member_ref_set and realized_order and not _exact_string_set(realized_order, member_ref_set): + return [(locator, "joint action realized_order must be an exact permutation of member_event_refs")] + return [] + + +def _joint_action_time_context_violations( + locator: str, + time_context_ref: object, + *, + exact_claim: bool, + known_time_context_refs: set[str], + known_time_contexts: Mapping[object, object], +) -> list[Violation]: + if not isinstance(time_context_ref, str) or not time_context_ref: + if exact_claim: + return [(locator, "exact concurrency claims require time_management_context_ref")] + return [] + + if time_context_ref not in known_time_context_refs: + return [(locator, f"time_management_context_ref {time_context_ref!r} does not resolve")] + time_context = known_time_contexts.get(time_context_ref) + if exact_claim and isinstance(time_context, Mapping) and time_context.get("claim_strength") != "exact": + return [(locator, "exact concurrency claims require exact time-management context")] + return [] + + +def _joint_action_access_violations( + locator: str, + access_sets: object, + *, + known_state_refs: set[str], +) -> tuple[list[str], list[Violation]]: + event_refs: list[str] = [] + violations: list[Violation] = [] + if not isinstance(access_sets, list) or not access_sets: + return event_refs, [(locator, "joint action access_sets must be a non-empty list")] + for index, access in enumerate(access_sets): + access_locator = f"{locator}.access_sets[{index}]" + access_event_refs, access_violations = _single_access_set_violations( + access_locator, + access, + known_state_refs=known_state_refs, + ) + event_refs.extend(access_event_refs) + violations.extend(access_violations) + return event_refs, violations + + +def _single_access_set_violations( + access_locator: str, + access: object, + *, + known_state_refs: set[str], +) -> tuple[list[str], list[Violation]]: + if not isinstance(access, Mapping): + return [], [(access_locator, "joint action access set must be a mapping")] + + event_ref = access.get("member_event_ref") + event_refs: list[str] = [] + violations: list[Violation] = [] + if not isinstance(event_ref, str) or not event_ref: + violations.append((access_locator, "joint action access set requires member_event_ref")) + else: + event_refs.append(event_ref) + + for field_name in ("shared_state_read_refs", "shared_state_write_refs"): + violations.extend( + _access_state_ref_violations( + access_locator, + field_name, + access.get(field_name, []), + known_state_refs=known_state_refs, + ) + ) + return event_refs, violations + + +def _access_state_ref_violations( + access_locator: str, + field_name: str, + values: object, + *, + known_state_refs: set[str], +) -> list[Violation]: + if not isinstance(values, list): + return [(access_locator, f"{field_name} must be a list")] + return [ + violation + for state_ref in values + for violation in _access_state_ref_violation(access_locator, field_name, state_ref, known_state_refs) + ] + + +def _access_state_ref_violation( + access_locator: str, + field_name: str, + state_ref: object, + known_state_refs: set[str], +) -> list[Violation]: + if not isinstance(state_ref, str) or not state_ref: + return [(access_locator, f"{field_name} entries must be non-empty strings")] + if state_ref not in known_state_refs: + return [(access_locator, f"{field_name} entry {state_ref!r} does not resolve")] + return [] + + +@dataclass(frozen=True) +class _JointActionConflictCheck: + conflict_class: object + conflict_policy: object + actual_conflict: str + realized_order: object + unsupported: bool + exact_claim: bool + retry_limit: object + rollback_event_refs: object + atomicity_scope: object + isolation_guarantee: object + + +def _joint_action_conflict_violations(locator: str, check: _JointActionConflictCheck) -> list[Violation]: + violations: list[Violation] = [] + if check.unsupported and check.exact_claim: + violations.append((locator, "unsupported concurrency disclosure cannot carry an exact concurrency claim")) + if check.conflict_policy == "unsupported": + violations.extend(_unsupported_conflict_policy_violations(locator, check)) + return violations + + violations.extend(_conflict_class_violations(locator, check)) + violations.extend(_conflict_policy_violations(locator, check)) + violations.extend(_conflict_atomicity_violations(locator, check)) + return violations + + +def _unsupported_conflict_policy_violations(locator: str, check: _JointActionConflictCheck) -> list[Violation]: + if not check.unsupported or check.exact_claim: + return [(locator, "unsupported conflict_policy requires unsupported_disclosure and no exact claim")] + return [] + + +def _conflict_class_violations(locator: str, check: _JointActionConflictCheck) -> list[Violation]: + violations: list[Violation] = [] + if not check.unsupported and check.conflict_class != check.actual_conflict: + violations.append((locator, "joint action conflict_class must match declared access-set conflicts")) + if check.conflict_class == "none" and check.actual_conflict != "none": + violations.append((locator, "joint action conflict_class cannot be none when access sets conflict")) + return violations + + +def _conflict_policy_violations(locator: str, check: _JointActionConflictCheck) -> list[Violation]: + violations: list[Violation] = [] + has_realized_order = _has_realized_order(check.realized_order) + if check.isolation_guarantee == "serializable" and not has_realized_order: + violations.append((locator, "serializable joint action isolation requires realized_order")) + if check.conflict_policy == "serialize" and not has_realized_order: + violations.append((locator, "serialize conflict_policy requires realized_order")) + if check.conflict_policy == "retry" and (not isinstance(check.retry_limit, int) or not check.rollback_event_refs): + violations.append((locator, "retry conflict_policy requires retry_limit and rollback_event_refs")) + if check.conflict_policy == "none" and check.actual_conflict != "none": + violations.append((locator, "none conflict_policy is only valid when access sets do not conflict")) + return violations + + +def _conflict_atomicity_violations(locator: str, check: _JointActionConflictCheck) -> list[Violation]: + has_recovery_evidence = _has_realized_order(check.realized_order) or bool(check.rollback_event_refs) + if check.atomicity_scope == "multi_object" and check.actual_conflict != "none" and not has_recovery_evidence: + return [(locator, "multi_object conflicting joint actions require realized_order or rollback_event_refs")] + return [] + + +def _has_realized_order(realized_order: object) -> bool: + return isinstance(realized_order, list) and bool(realized_order) + + +def _actual_conflict(access_sets: object) -> str: + if not isinstance(access_sets, list): + return "none" + return _classify_access_conflict(access for access in access_sets if isinstance(access, Mapping)) + + +def _classify_access_conflict(access_sets: Iterator[Mapping[object, object]]) -> str: + mapped_access_sets = list(access_sets) + read_write_conflict = False + for left_index, left in enumerate(mapped_access_sets): + for right in mapped_access_sets[left_index + 1 :]: + conflict = _access_pair_conflict(left, right) + if conflict == "write_write": + return "write_write" + if conflict == "read_write": + read_write_conflict = True + return "read_write" if read_write_conflict else "none" + + +def _access_pair_conflict(left: Mapping[object, object], right: Mapping[object, object]) -> str: + left_reads = _string_set(left.get("shared_state_read_refs", [])) + left_writes = _write_refs(left) + right_reads = _string_set(right.get("shared_state_read_refs", [])) + right_writes = _write_refs(right) + if left_writes & right_writes: + return "write_write" + if (left_writes & right_reads) or (left_reads & right_writes): + return "read_write" + return "none" + + +def _write_refs(access: Mapping[object, object]) -> set[str]: + refs = _string_set(access.get("shared_state_write_refs", [])) + refs.update(f"resource:{ref}" for ref in _string_set(access.get("exclusive_resource_refs", []))) + return refs + + +def _string_set(values: object) -> set[str]: + if not isinstance(values, list): + return set() + return {value for value in values if isinstance(value, str) and value} + + +def _exact_string_set(values: object, expected: set[str]) -> bool: + return isinstance(values, list) and len(values) == len(expected) and set(values) == expected + + +def _append_only_mapping_violations( + field_name: str, + address_prefix: str, + previous_records: object, + next_records: object, +) -> Iterator[Violation]: + if not isinstance(previous_records, Mapping) or not isinstance(next_records, Mapping): + return + for record_id, previous_record in previous_records.items(): + if not isinstance(record_id, str) or not record_id: + continue + locator = f"{address_prefix}.{record_id}" + if record_id not in next_records: + yield (locator, f"{field_name} must be append-only; record {record_id!r} was removed") + elif next_records[record_id] != previous_record: + yield (locator, f"{field_name} must be append-only; record {record_id!r} changed") + + +__all__ = ("iter_participant_concurrency_snapshot_violations", "iter_participant_concurrency_transition_violations") diff --git a/implementations/python/packages/aces_contracts/participant_concurrency_time.py b/implementations/python/packages/aces_contracts/participant_concurrency_time.py new file mode 100644 index 000000000..4892b6018 --- /dev/null +++ b/implementations/python/packages/aces_contracts/participant_concurrency_time.py @@ -0,0 +1,148 @@ +"""RUN-308 participant concurrency time-management validators.""" + +from __future__ import annotations + +from collections.abc import Mapping + +Violation = tuple[str, str] + +TIME_CONTEXTS_KEY = "runtime.snapshot.time-management-contexts" + + +def time_contexts_violations(contexts: object, *, known_event_refs: set[str]) -> list[Violation]: + violations: list[Violation] = [] + if not isinstance(contexts, Mapping): + return [(TIME_CONTEXTS_KEY, "time_management_contexts must be a mapping")] + for outer_key, context in contexts.items(): + locator = f"{TIME_CONTEXTS_KEY}.{outer_key}" + if not isinstance(outer_key, str) or not outer_key: + violations.append((TIME_CONTEXTS_KEY, "time_management_contexts keys must be non-empty strings")) + elif not isinstance(context, Mapping): + violations.append((locator, "time management context must be a mapping")) + else: + violations.extend(_time_context_violations(locator, outer_key, context, known_event_refs=known_event_refs)) + return violations + + +def _time_context_violations( + locator: str, + outer_key: str, + context: Mapping[object, object], + *, + known_event_refs: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + violations.extend(_time_context_identity_violations(locator, outer_key, context.get("context_id"))) + + mode = context.get("mode") + claim_strength = context.get("claim_strength") + basis = context.get("basis") + clock_ref = context.get("clock_ref") + unsupported = context.get("unsupported_disclosure") is True + violations.extend( + _time_context_claim_violations( + locator, + claim_strength=claim_strength, + basis=basis, + clock_ref=clock_ref, + unsupported=unsupported, + ) + ) + violations.extend( + _time_context_mode_violations( + locator, + context, + mode=mode, + basis=basis, + clock_ref=clock_ref, + unsupported=unsupported, + known_event_refs=known_event_refs, + ) + ) + return violations + + +def _time_context_identity_violations(locator: str, outer_key: str, context_id: object) -> list[Violation]: + if not isinstance(context_id, str) or not context_id: + return [(locator, "time management context requires context_id")] + if context_id != outer_key: + return [(locator, f"time management context key {outer_key!r} does not match context_id")] + return [] + + +def _time_context_claim_violations( + locator: str, + *, + claim_strength: object, + basis: object, + clock_ref: object, + unsupported: bool, +) -> list[Violation]: + violations: list[Violation] = [] + if unsupported and claim_strength == "exact": + violations.append((locator, "unsupported time-management disclosure cannot carry an exact claim")) + if basis == "wall_clock_only" and claim_strength != "display": + violations.append((locator, "wall_clock_only time basis supports display claims only")) + if claim_strength in {"bounded", "exact"} and not isinstance(clock_ref, str): + violations.append((locator, "bounded or exact time-management claims require clock_ref")) + return violations + + +def _time_context_mode_violations( + locator: str, + context: Mapping[object, object], + *, + mode: object, + basis: object, + clock_ref: object, + unsupported: bool, + known_event_refs: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + if mode == "backend_serialized" and _invalid_backend_serialized_context(context, basis, clock_ref): + violations.append((locator, "backend_serialized mode requires serialized_backend_order basis and clock_ref")) + if mode == "lookahead" and not isinstance(context.get("lookahead"), int): + violations.append((locator, "lookahead mode requires lookahead")) + if mode == "pacing" and not isinstance(context.get("advance_by"), int): + violations.append((locator, "pacing mode requires advance_by")) + if mode == "rollback": + violations.extend( + _rollback_time_context_violations( + locator, + context.get("rollback_event_refs", []), + known_event_refs=known_event_refs, + ) + ) + if mode in {"devs", "fmi"} and (not isinstance(clock_ref, str) or basis == "wall_clock_only"): + violations.append((locator, "devs and fmi modes require a non-wall-clock basis and clock_ref")) + if mode == "unsupported" and not unsupported: + violations.append((locator, "unsupported time-management mode requires unsupported_disclosure")) + return violations + + +def _invalid_backend_serialized_context( + context: Mapping[object, object], + basis: object, + clock_ref: object, +) -> bool: + return not ( + context.get("backend_serialized") is True and basis == "serialized_backend_order" and isinstance(clock_ref, str) + ) + + +def _rollback_time_context_violations( + locator: str, + rollback_refs: object, + *, + known_event_refs: set[str], +) -> list[Violation]: + if not isinstance(rollback_refs, list) or not rollback_refs: + return [(locator, "rollback mode requires rollback_event_refs")] + return [ + (locator, f"rollback_event_ref {ref!r} does not resolve") + for ref in rollback_refs + if isinstance(ref, str) and ref and ref not in known_event_refs + ] + + +__all__ = ("TIME_CONTEXTS_KEY", "time_contexts_violations") diff --git a/implementations/python/packages/aces_contracts/participant_shared_state.py b/implementations/python/packages/aces_contracts/participant_shared_state.py new file mode 100644 index 000000000..c860d645c --- /dev/null +++ b/implementations/python/packages/aces_contracts/participant_shared_state.py @@ -0,0 +1,356 @@ +"""Shared operational state runtime validators for RUN-307.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping + +from ._participant_behavior_types import _RESERVED_RUNTIME_STATE_KEYS + +Violation = tuple[str, str] + +_SHARED_STATE_RECORDS_KEY = "runtime.snapshot.shared-state-records" +_SHARED_STATE_HISTORY_KEY = "runtime.snapshot.shared-state-history" +_METADATA_KEY = "runtime.snapshot.metadata" +_BEHAVIOR_HISTORY_KEY = "runtime.snapshot.participant-behavior-history" +_VALID_ACCESS_KINDS = frozenset({"read", "write", "read_write"}) +_REQUIRED_RECORD_FIELDS = ( + "state_address", + "state_scope", + "state_kind", + "ordering_basis", + "conflict_policy", + "provenance", +) + + +def iter_participant_shared_state_snapshot_violations( + shared_state_records: object, + shared_state_history: object, + *, + participant_behavior_history: object = None, + metadata: object = None, +) -> Iterator[tuple[str, str]]: + """Yield RUN-307 shared-state snapshot violations.""" + + known_addresses: set[str] = set() + violations: list[Violation] = [] + violations.extend(_reserved_metadata_key_violations(metadata)) + violations.extend(_shared_state_records_violations(shared_state_records, known_addresses)) + violations.extend(_shared_state_history_violations(shared_state_history, known_addresses)) + violations.extend(_behavior_shared_state_ref_violations(participant_behavior_history, known_addresses)) + return iter(violations) + + +def iter_participant_shared_state_history_transition_violations( + previous_shared_state_history: object, + next_shared_state_history: object, +) -> Iterator[tuple[str, str]]: + """Yield append-only violations for shared-state history transitions.""" + + violations: list[Violation] = [] + if isinstance(previous_shared_state_history, Mapping) and isinstance(next_shared_state_history, Mapping): + for state_address, previous_records in previous_shared_state_history.items(): + violations.extend( + _history_transition_state_violations(state_address, previous_records, next_shared_state_history) + ) + return iter(violations) + + +def _history_transition_state_violations( + state_address: object, + previous_records: object, + next_shared_state_history: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + if isinstance(state_address, str) and state_address and isinstance(previous_records, list): + next_records = next_shared_state_history.get(state_address) + locator = f"{_SHARED_STATE_HISTORY_KEY}.{state_address}" + if not isinstance(next_records, list): + violations.append( + (locator, f"shared_state_history must be append-only; state {state_address!r} history was removed") + ) + elif len(next_records) < len(previous_records): + violations.append( + ( + locator, + ( + f"shared_state_history must be append-only; state {state_address!r} history shrank " + f"from {len(previous_records)} to {len(next_records)} records" + ), + ) + ) + else: + violations.extend(_history_record_rewrite_violations(locator, previous_records, next_records)) + return violations + + +def _history_record_rewrite_violations( + locator: str, + previous_records: list[object], + next_records: list[object], +) -> list[Violation]: + violations: list[Violation] = [] + for index, previous_record in enumerate(previous_records): + if next_records[index] != previous_record: + violations.append( + ( + f"{locator}[{index}]", + f"shared_state_history must be append-only; existing record at index {index} changed", + ) + ) + return violations + + +def _reserved_metadata_key_violations(metadata: object) -> list[Violation]: + violations: list[Violation] = [] + if metadata is not None and not isinstance(metadata, Mapping): + violations.append((_METADATA_KEY, "RuntimeSnapshot.metadata must be a mapping")) + elif isinstance(metadata, Mapping): + for key in sorted(_RESERVED_RUNTIME_STATE_KEYS.intersection(str(item) for item in metadata)): + violations.append( + ( + f"{_METADATA_KEY}.{key}", + ( + f"RuntimeSnapshot.metadata must not contain {key!r}; " + "participant runtime state/history must use first-class snapshot fields" + ), + ) + ) + return violations + + +def _shared_state_records_violations(records: object, known_addresses: set[str]) -> list[Violation]: + violations: list[Violation] = [] + if not isinstance(records, Mapping): + violations.append((_SHARED_STATE_RECORDS_KEY, "shared_state_records must be a mapping")) + else: + for outer_key, record in records.items(): + violations.extend(_shared_state_record_entry_violations(outer_key, record, known_addresses)) + return violations + + +def _shared_state_record_entry_violations( + outer_key: object, + record: object, + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + locator = f"{_SHARED_STATE_RECORDS_KEY}.{outer_key}" + if not isinstance(outer_key, str) or not outer_key: + violations.append((_SHARED_STATE_RECORDS_KEY, "shared_state_records keys must be non-empty strings")) + elif not isinstance(record, Mapping): + violations.append((locator, "shared state record must be a mapping")) + else: + violations.extend(_shared_state_record_violations(locator, outer_key, record)) + _add_known_state_addresses(known_addresses, outer_key, record) + return violations + + +def _shared_state_history_violations(history: object, known_addresses: set[str]) -> list[Violation]: + violations: list[Violation] = [] + if not isinstance(history, Mapping): + violations.append((_SHARED_STATE_HISTORY_KEY, "shared_state_history must be a mapping")) + else: + for outer_key, records in history.items(): + violations.extend(_shared_state_history_entry_violations(outer_key, records, known_addresses)) + return violations + + +def _shared_state_history_entry_violations( + outer_key: object, + records: object, + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + locator = f"{_SHARED_STATE_HISTORY_KEY}.{outer_key}" + if not isinstance(outer_key, str) or not outer_key: + violations.append((_SHARED_STATE_HISTORY_KEY, "shared_state_history keys must be non-empty strings")) + elif not isinstance(records, list): + violations.append((locator, "shared_state_history entries must be lists")) + else: + known_addresses.add(outer_key) + violations.extend(_shared_state_history_records_violations(locator, outer_key, records, known_addresses)) + return violations + + +def _shared_state_history_records_violations( + locator: str, + outer_key: str, + records: list[object], + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + for index, record in enumerate(records): + record_locator = f"{locator}[{index}]" + if not isinstance(record, Mapping): + violations.append((record_locator, "shared state history record must be a mapping")) + else: + violations.extend(_shared_state_record_violations(record_locator, outer_key, record)) + _add_known_state_addresses(known_addresses, outer_key, record) + return violations + + +def _shared_state_record_violations( + locator: str, + expected_address: str, + record: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + missing = [field for field in _REQUIRED_RECORD_FIELDS if not _non_empty_string(record.get(field))] + if missing: + violations.append((locator, "shared state record is missing required fields: " + ", ".join(missing))) + else: + state_address = record["state_address"] + if state_address != expected_address: + violations.append( + ( + locator, + ( + f"shared state record outer key {expected_address!r} " + f"does not match state_address {state_address!r}" + ), + ) + ) + + if not (_non_empty_string(record.get("revision")) or _non_empty_string(record.get("digest"))): + violations.append((locator, "shared state record requires revision or digest")) + + accesses = record.get("accesses", []) + if not isinstance(accesses, list): + violations.append((locator, "shared state record accesses must be a list")) + else: + violations.extend(_shared_state_accesses_violations(locator, state_address, accesses)) + return violations + + +def _shared_state_accesses_violations( + locator: str, + state_address: object, + accesses: list[object], +) -> list[Violation]: + violations: list[Violation] = [] + for index, access in enumerate(accesses): + access_locator = f"{locator}.accesses[{index}]" + if not isinstance(access, Mapping): + violations.append((access_locator, "shared state access must be a mapping")) + else: + violations.extend(_shared_state_access_violations(access_locator, state_address, access)) + return violations + + +def _shared_state_access_violations( + locator: str, + record_address: object, + access: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + state_address = access.get("state_address") + if not _non_empty_string(state_address): + violations.append((locator, "shared state access state_address must be a non-empty string")) + elif state_address != record_address: + violations.append( + (locator, f"shared state access state_address {state_address!r} does not match record state_address") + ) + + access_kind = access.get("access_kind") + if access_kind not in _VALID_ACCESS_KINDS: + violations.append((locator, f"shared state access_kind {access_kind!r} is not supported")) + else: + violations.extend(_shared_state_access_version_violations(locator, access_kind, access)) + return violations + + +def _shared_state_access_version_violations( + locator: str, + access_kind: object, + access: Mapping[object, object], +) -> list[Violation]: + violations: list[Violation] = [] + if access_kind in {"read", "read_write"} and not ( + _non_empty_string(access.get("read_revision")) or _non_empty_string(access.get("read_digest")) + ): + violations.append((locator, "shared state read access requires read_revision or read_digest")) + if access_kind in {"write", "read_write"} and not ( + _non_empty_string(access.get("write_revision")) or _non_empty_string(access.get("write_digest")) + ): + violations.append((locator, "shared state write access requires write_revision or write_digest")) + return violations + + +def _behavior_shared_state_ref_violations( + participant_behavior_history: object, + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + if isinstance(participant_behavior_history, Mapping): + for participant_address, history in participant_behavior_history.items(): + violations.extend(_participant_behavior_ref_violations(participant_address, history, known_addresses)) + return violations + + +def _participant_behavior_ref_violations( + participant_address: object, + history: object, + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + if isinstance(participant_address, str) and isinstance(history, list): + for index, event in enumerate(history): + violations.extend(_behavior_event_ref_violations(participant_address, index, event, known_addresses)) + return violations + + +def _behavior_event_ref_violations( + participant_address: str, + index: int, + event: object, + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + if isinstance(event, Mapping): + refs = event.get("shared_state_refs", []) + if isinstance(refs, list): + violations.extend(_unresolved_behavior_ref_violations(participant_address, index, refs, known_addresses)) + return violations + + +def _unresolved_behavior_ref_violations( + participant_address: str, + index: int, + refs: list[object], + known_addresses: set[str], +) -> list[Violation]: + violations: list[Violation] = [] + for ref in refs: + if isinstance(ref, str) and ref and ref not in known_addresses: + violations.append( + ( + f"{_BEHAVIOR_HISTORY_KEY}.{participant_address}[{index}].shared_state_refs", + ( + f"participant behavior shared_state_refs entry {ref!r} does not resolve to " + "shared_state_records or shared_state_history" + ), + ) + ) + return violations + + +def _add_known_state_addresses( + known_addresses: set[str], + outer_key: str, + record: Mapping[object, object], +) -> None: + known_addresses.add(outer_key) + state_address = record.get("state_address") + if isinstance(state_address, str) and state_address: + known_addresses.add(state_address) + + +def _non_empty_string(value: object) -> bool: + return isinstance(value, str) and bool(value) + + +__all__ = ( + "iter_participant_shared_state_history_transition_violations", + "iter_participant_shared_state_snapshot_violations", +) diff --git a/implementations/python/packages/aces_contracts/runtime_state.py b/implementations/python/packages/aces_contracts/runtime_state.py index 177fdb3f0..0d7e45155 100644 --- a/implementations/python/packages/aces_contracts/runtime_state.py +++ b/implementations/python/packages/aces_contracts/runtime_state.py @@ -71,6 +71,10 @@ class RuntimeSnapshot: participant_episode_results: dict[str, dict[str, Any]] = field(default_factory=dict) participant_episode_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) participant_behavior_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) + shared_state_records: dict[str, dict[str, Any]] = field(default_factory=dict) + shared_state_history: dict[str, list[dict[str, Any]]] = field(default_factory=dict) + joint_action_records: dict[str, dict[str, Any]] = field(default_factory=dict) + time_management_contexts: dict[str, dict[str, Any]] = field(default_factory=dict) # SEM-218 invariant I5: per-concern provenance for realized realization # concerns recorded across this snapshot's result / history surfaces. realization_provenance: tuple[RealizationProvenanceEntry, ...] = () @@ -117,6 +121,26 @@ def with_entries( "participant_behavior_history", self.participant_behavior_history, ), + shared_state_records=_mapping_update( + updates, + "shared_state_records", + self.shared_state_records, + ), + shared_state_history=_history_update( + updates, + "shared_state_history", + self.shared_state_history, + ), + joint_action_records=_mapping_update( + updates, + "joint_action_records", + self.joint_action_records, + ), + time_management_contexts=_mapping_update( + updates, + "time_management_contexts", + self.time_management_contexts, + ), realization_provenance=_provenance_update( updates, "realization_provenance", @@ -134,6 +158,10 @@ def with_entries( "participant_episode_results", "participant_episode_history", "participant_behavior_history", + "shared_state_records", + "shared_state_history", + "joint_action_records", + "time_management_contexts", "realization_provenance", "metadata", } diff --git a/implementations/python/packages/aces_contracts/semantic_binding_effects.py b/implementations/python/packages/aces_contracts/semantic_binding_effects.py new file mode 100644 index 000000000..47e130e3f --- /dev/null +++ b/implementations/python/packages/aces_contracts/semantic_binding_effects.py @@ -0,0 +1,115 @@ +"""SEM-217 semantic effects for external knowledge bindings.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Literal + +from .contracts import ( + ConceptFamilyCatalogModel, + SemanticProfileModel, + UcoAlignmentCatalogModel, +) +from .vocabulary import ConceptProvenanceCategory + +SemanticProfilePhase = Literal["authoring", "exchange", "processing", "execution"] +_SEMANTIC_PROFILE_PHASES = frozenset(("authoring", "exchange", "processing", "execution")) + + +class ExternalKnowledgeBindingEffect(str, Enum): + """Portable SEM-217 effects a binding may have on native ACES meaning.""" + + ANNOTATES = "annotates" + CONSTRAINS = "constrains" + REFINES = "refines" + ALIGNS = "aligns" + + +@dataclass(frozen=True, slots=True) +class SemanticBindingEffectRecord: + """Resolved SEM-217 effect for one governed binding surface.""" + + surface: str + family: str + effects: frozenset[ExternalKnowledgeBindingEffect] + provenance: str | None = None + scope: str | None = None + authority: str | None = None + authority_reference: str | None = None + external_types: tuple[str, ...] = () + divergences: tuple[str, ...] = () + review_scope: str | None = None + + +def uco_alignment_binding_effects( + concept_catalog: ConceptFamilyCatalogModel, + alignment_catalog: UcoAlignmentCatalogModel, +) -> dict[str, SemanticBindingEffectRecord]: + """Resolve SEM-217 effects for the checked-in UCO alignment catalog. + + UCO class links always annotate the native ACES family with reviewed + external evidence. Adopted families additionally align with UCO meaning; + adapted families refine it and must carry explicit divergences. + """ + + records: dict[str, SemanticBindingEffectRecord] = {} + for family_id, alignment in alignment_catalog.alignments.items(): + family = concept_catalog.families.get(family_id) + if family is None: + raise ValueError(f"uco alignment references unknown concept family {family_id!r}") + if family.provenance != alignment.provenance: + raise ValueError( + f"uco alignment provenance for {family_id!r} is {alignment.provenance.value!r}, " + f"but the concept catalog declares {family.provenance.value!r}" + ) + + effects = {ExternalKnowledgeBindingEffect.ANNOTATES} + if alignment.provenance == ConceptProvenanceCategory.ADOPTED: + effects.add(ExternalKnowledgeBindingEffect.ALIGNS) + elif alignment.provenance == ConceptProvenanceCategory.ADAPTED: + effects.add(ExternalKnowledgeBindingEffect.REFINES) + else: + raise ValueError(f"uco alignment family {family_id!r} must not be native") + + records[family_id] = SemanticBindingEffectRecord( + surface="uco-alignment", + family=family_id, + effects=frozenset(effects), + provenance=alignment.provenance.value, + authority=family.authority, + authority_reference=family.authority_reference, + external_types=tuple(uco_type.uco_class for uco_type in alignment.uco_types), + divergences=tuple(alignment.divergences), + review_scope=alignment_catalog.review_scope, + ) + return records + + +def semantic_profile_required_binding_effects( + profile: SemanticProfileModel, + phase_name: SemanticProfilePhase, +) -> tuple[SemanticBindingEffectRecord, ...]: + """Resolve SEM-217 constraint effects declared by a semantic profile phase.""" + + if phase_name not in _SEMANTIC_PROFILE_PHASES: + allowed = ", ".join(sorted(_SEMANTIC_PROFILE_PHASES)) + raise ValueError(f"semantic profile phase must be one of: {allowed}") + phase = getattr(profile, phase_name) + return tuple( + SemanticBindingEffectRecord( + surface=f"semantic-profile:{profile.profile_id}:{phase_name}", + scope=binding.scope, + family=binding.family, + effects=frozenset({ExternalKnowledgeBindingEffect.CONSTRAINS}), + ) + for binding in phase.required_bindings + ) + + +__all__ = [ + "ExternalKnowledgeBindingEffect", + "SemanticBindingEffectRecord", + "semantic_profile_required_binding_effects", + "uco_alignment_binding_effects", +] diff --git a/implementations/python/packages/aces_contracts/versions.py b/implementations/python/packages/aces_contracts/versions.py index 7b0c48829..1a48cf68d 100644 --- a/implementations/python/packages/aces_contracts/versions.py +++ b/implementations/python/packages/aces_contracts/versions.py @@ -28,3 +28,6 @@ EXPERIMENT_APPARATUS_CONTEXT_SCHEMA_VERSION = "experiment-apparatus-context/v1" EXPERIMENT_RUN_SCHEMA_VERSION = "experiment-run/v1" EXPERIMENT_STUDY_SCHEMA_VERSION = "experiment-study/v1" +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" diff --git a/implementations/python/packages/aces_processor/reference.py b/implementations/python/packages/aces_processor/reference.py new file mode 100644 index 000000000..c89a9edc9 --- /dev/null +++ b/implementations/python/packages/aces_processor/reference.py @@ -0,0 +1,142 @@ +"""Repository-owned reference processor (RUN-313). + +The reference processor realizes the normative processing model: it carries SDL +authoring input through instantiation, compilation, and planning to a portable +:class:`~aces_processor.models.ExecutionPlan`, and exposes the published +processor manifest. Per ADR-008 the processor is the semantics-bearing layer +between SDL authoring and backend realization, so its responsibility ends at the +execution plan; live backend realization (apply) is the runtime's job. End-to-end +execution is realized by composing this plan with the reference runtime +(``aces_runtime``) and is proven by ``aces_conformance`` and the RUN-313 tests. + +Import layering (ADR-036 / ``tools/policy/adr_policy.yaml``): this module imports +only the lower SDL/processor/contract layers. It must not import ``aces_runtime``; +that one-directional boundary is why the reference processor stops at the plan. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +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_processor.compiler import compile_scenario_runtime_model +from aces_processor.manifest import ( + REFERENCE_PROCESSOR_NAME, + REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2, + reference_processor_manifest_payload, +) +from aces_processor.models import ExecutionPlan, RuntimeModel, RuntimeSnapshot +from aces_processor.planner import plan as _plan + +__all__ = [ + "ReferenceProcessor", + "ReferenceProcessorResult", + "ScenarioInput", + "run_reference_processor", +] + +# 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 + + +@dataclass(frozen=True) +class ReferenceProcessorResult: + """Portable outcome of a reference-processor realization run.""" + + scenario_name: str + runtime_model: RuntimeModel + execution_plan: ExecutionPlan + diagnostics: tuple[Diagnostic, ...] + + @property + def is_valid(self) -> bool: + """True when neither compilation nor planning produced an error.""" + + 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): + return scenario + if isinstance(scenario, Path): + return parse_sdl_file(scenario) + if isinstance(scenario, str): + 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__}" + ) + + +class ReferenceProcessor: + """Repository-owned reference processor over the SDL -> ExecutionPlan path.""" + + name: str = REFERENCE_PROCESSOR_NAME + supported_contract_versions: tuple[str, ...] = REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2 + + @staticmethod + def manifest_payload(*, version: str | None = None) -> dict[str, Any]: + """Return the published reference processor manifest as JSON-ready data.""" + + return reference_processor_manifest_payload(version=version) + + @staticmethod + def realize( + scenario: ScenarioInput, + backend_manifest: BackendManifest, + *, + parameters: Mapping[str, object] | None = None, + profile: str | None = None, + base_snapshot: RuntimeSnapshot | None = None, + target_name: str | None = None, + ) -> ReferenceProcessorResult: + """Realize an SDL scenario into a portable execution plan. + + Drives the canonical processing path: parse (when needed) -> + instantiate -> compile -> plan, against ``backend_manifest`` (the + backend the plan targets). Compilation and planning diagnostics are + surfaced on the result rather than raised, so capability gaps and + ordering conflicts are reported as data. + """ + + raw = _resolve_scenario(scenario) + model = compile_scenario_runtime_model(raw, parameters=parameters, profile=profile) + execution_plan = _plan(model, backend_manifest, base_snapshot, target_name=target_name) + diagnostics = (*model.diagnostics, *execution_plan.diagnostics) + return ReferenceProcessorResult( + scenario_name=model.scenario_name, + runtime_model=model, + execution_plan=execution_plan, + diagnostics=diagnostics, + ) + + +def run_reference_processor( + scenario: ScenarioInput, + backend_manifest: BackendManifest, + *, + parameters: Mapping[str, object] | None = None, + profile: str | None = None, + base_snapshot: RuntimeSnapshot | None = None, + target_name: str | None = None, +) -> ReferenceProcessorResult: + """Convenience wrapper around :meth:`ReferenceProcessor.realize`.""" + + return ReferenceProcessor.realize( + scenario, + backend_manifest, + parameters=parameters, + profile=profile, + base_snapshot=base_snapshot, + target_name=target_name, + ) diff --git a/implementations/python/packages/aces_reference_backend/__init__.py b/implementations/python/packages/aces_reference_backend/__init__.py new file mode 100644 index 000000000..3b92d6608 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/__init__.py @@ -0,0 +1,46 @@ +"""Reference emulation backend (RUN-314). + +A concrete, container-backed reference backend implementing the four +``aces_backend_protocols.protocols`` roles (Provisioner, Orchestrator, +Evaluator, ParticipantRuntime). It publishes identity/capability through +the standard ``BackendManifest`` and registers on the existing +``BackendRegistry`` descriptor seam under the name ``reference-emulation``. + +The default driver is the hermetic in-process driver; an opt-in OCI driver +realizes plans against a real container runtime (docker/podman). Only +portable ACES facts ever reach manifests, snapshots, diagnostics, or +conformance reports -- never container/VM/network ids, host paths, +environment, credentials, argv, or backend-native reprs. +""" + +from __future__ import annotations + +from .driver import ( + ContainerHandle, + ContainerSpec, + DeploymentDriver, + NetworkHandle, + NetworkSpec, +) +from .manifest import REFERENCE_BACKEND_NAME, create_reference_backend_manifest +from .realization import Realization, interpret_provisioning_plan +from .target import ( + create_reference_backend_components, + create_reference_backend_target, + register_reference_backend, +) + +__all__ = [ + "REFERENCE_BACKEND_NAME", + "ContainerHandle", + "ContainerSpec", + "DeploymentDriver", + "NetworkHandle", + "NetworkSpec", + "Realization", + "create_reference_backend_components", + "create_reference_backend_manifest", + "create_reference_backend_target", + "interpret_provisioning_plan", + "register_reference_backend", +] diff --git a/implementations/python/packages/aces_reference_backend/driver.py b/implementations/python/packages/aces_reference_backend/driver.py new file mode 100644 index 000000000..189d1a16b --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/driver.py @@ -0,0 +1,115 @@ +"""Portable driver boundary for the reference emulation backend (RUN-314). + +The ``DeploymentDriver`` protocol is the host-process / emulator boundary. +Specs describe what to realize in portable ACES terms; handles describe +what was realized in portable ACES terms. No spec or handle carries a +container/VM/network id, host path, environment, credential, argv, or any +backend-native repr -- only references, digests, and labels that are safe +to surface in a snapshot, diagnostic, or conformance report. + +Drivers return ``Diagnostic`` values and handles; they never raise a +backend-specific exception hierarchy. Programmer errors (a malformed call +from inside this package) may raise ordinary exceptions, which the runtime +adapter (`_call_backend_apply`) converts into diagnostics at the boundary. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Protocol + +from aces_contracts.diagnostics import Diagnostic + + +@dataclass(frozen=True) +class NetworkSpec: + """Portable description of a network to realize. + + ``address`` is the ACES resource address. ``labels`` carries only + non-sensitive classification labels (e.g. ``{"internal": "true"}``). + """ + + address: str + name: str + labels: dict[str, str] = field(default_factory=dict) + + +@dataclass(frozen=True) +class ContainerSpec: + """Portable description of a container to realize. + + ``image_ref`` is a portable image reference (a name/tag or digest), not + a pulled local image id. ``networks`` are ACES network resource + addresses. ``labels`` carries only non-sensitive classification labels. + """ + + address: str + name: str + image_ref: str + networks: tuple[str, ...] = () + labels: dict[str, str] = field(default_factory=dict) + + +@dataclass(frozen=True) +class NetworkHandle: + """Portable result of realizing a network. + + Carries the ACES ``address`` and a ``realized`` flag only. A real driver + must NOT place the backend-native network id here. + """ + + address: str + realized: bool = True + + +@dataclass(frozen=True) +class ContainerHandle: + """Portable result of realizing a container. + + Carries the ACES ``address`` and a ``realized`` flag only. A real driver + must NOT place the backend-native container id, inspect payload, or host + path here. + """ + + address: str + realized: bool = True + + +@dataclass(frozen=True) +class DriverResult: + """Aggregate portable result of a realize/destroy call.""" + + networks: tuple[NetworkHandle, ...] = () + containers: tuple[ContainerHandle, ...] = () + diagnostics: tuple[Diagnostic, ...] = () + + +class DeploymentDriver(Protocol): + """Host-process boundary for realizing a portable deployment. + + Implementations realize networks then containers, and destroy + containers then networks. All inputs and outputs are portable; the + concrete implementation owns any backend-native bookkeeping privately. + """ + + def realize( + self, + *, + networks: tuple[NetworkSpec, ...], + containers: tuple[ContainerSpec, ...], + ) -> DriverResult: + """Realize the given networks and containers; return portable handles.""" + ... + + def destroy( + self, + *, + networks: tuple[str, ...], + containers: tuple[str, ...], + ) -> DriverResult: + """Destroy the realized resources for the given ACES addresses.""" + ... + + def realized_addresses(self) -> frozenset[str]: + """Return the set of ACES addresses currently realized by this driver.""" + ... diff --git a/implementations/python/packages/aces_reference_backend/drivers/__init__.py b/implementations/python/packages/aces_reference_backend/drivers/__init__.py new file mode 100644 index 000000000..b0e1acb01 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/drivers/__init__.py @@ -0,0 +1,8 @@ +"""Deployment drivers for the reference emulation backend.""" + +from __future__ import annotations + +from .inprocess import InProcessDriver +from .oci import OciDeploymentDriver + +__all__ = ["InProcessDriver", "OciDeploymentDriver"] diff --git a/implementations/python/packages/aces_reference_backend/drivers/inprocess.py b/implementations/python/packages/aces_reference_backend/drivers/inprocess.py new file mode 100644 index 000000000..93fe6c00b --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/drivers/inprocess.py @@ -0,0 +1,82 @@ +"""Hermetic in-process driver (default) for the reference backend. + +Records the realize/destroy operations it is asked to perform and +synthesizes portable handles. No subprocess, no container runtime, no IO -- +so it is safe in CI and in the default conformance/apply path. It keeps a +private ledger of recorded ops (for test assertions) and a private set of +realized ACES addresses; neither leaks into any portable artifact. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from aces_reference_backend.driver import ( + ContainerHandle, + ContainerSpec, + DriverResult, + NetworkHandle, + NetworkSpec, +) + + +@dataclass(frozen=True) +class RecordedOp: + """A recorded driver operation (test/inspection only, not portable).""" + + verb: str + kind: str + address: str + + +@dataclass +class InProcessDriver: + """Hermetic driver that records ops and synthesizes portable handles.""" + + recorded_ops: list[RecordedOp] = field(default_factory=list) + _realized: set[str] = field(default_factory=set) + + def realize( + self, + *, + networks: tuple[NetworkSpec, ...], + containers: tuple[ContainerSpec, ...], + ) -> DriverResult: + network_handles: list[NetworkHandle] = [] + for spec in networks: + self.recorded_ops.append(RecordedOp(verb="realize", kind="network", address=spec.address)) + self._realized.add(spec.address) + network_handles.append(NetworkHandle(address=spec.address, realized=True)) + container_handles: list[ContainerHandle] = [] + for spec in containers: + self.recorded_ops.append(RecordedOp(verb="realize", kind="container", address=spec.address)) + self._realized.add(spec.address) + container_handles.append(ContainerHandle(address=spec.address, realized=True)) + return DriverResult( + networks=tuple(network_handles), + containers=tuple(container_handles), + ) + + def destroy( + self, + *, + networks: tuple[str, ...], + containers: tuple[str, ...], + ) -> DriverResult: + container_handles: list[ContainerHandle] = [] + for address in containers: + self.recorded_ops.append(RecordedOp(verb="destroy", kind="container", address=address)) + self._realized.discard(address) + container_handles.append(ContainerHandle(address=address, realized=False)) + network_handles: list[NetworkHandle] = [] + for address in networks: + self.recorded_ops.append(RecordedOp(verb="destroy", kind="network", address=address)) + self._realized.discard(address) + network_handles.append(NetworkHandle(address=address, realized=False)) + return DriverResult( + networks=tuple(network_handles), + containers=tuple(container_handles), + ) + + def realized_addresses(self) -> frozenset[str]: + return frozenset(self._realized) diff --git a/implementations/python/packages/aces_reference_backend/drivers/oci.py b/implementations/python/packages/aces_reference_backend/drivers/oci.py new file mode 100644 index 000000000..4ec654187 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/drivers/oci.py @@ -0,0 +1,296 @@ +"""OCI deployment driver (docker/podman) for the reference backend. + +Security boundary (binding guardrails, issue #197 preflight): + +- Every host-process invocation uses a FIXED argv list -- never a shell + string, never ``shell=True``. The container runtime name is validated + against a closed allowlist so it can never carry an injected command. +- Every invocation has a bounded ``timeout``; a timeout becomes a portable + diagnostic, not an escaped exception. +- No tokens, passwords, credentials, or host environment ever appear in + argv. +- Backend-native output (container ids, daemon inspect payloads, raw + stderr) is consumed privately and NEVER placed into the returned portable + handles or diagnostics. Diagnostics carry the ACES address and a fixed + message only. + +The actual subprocess call is the only impure leaf; it is injected as +``runner`` so tests exercise the full argv/timeout/redaction logic without +a real daemon, and the real default runner line is marked ``# pragma: no +cover``. +""" + +from __future__ import annotations + +import subprocess +from collections.abc import Callable +from dataclasses import dataclass + +from aces_contracts.diagnostics import Diagnostic, Severity + +from aces_reference_backend.driver import ( + ContainerHandle, + ContainerSpec, + DriverResult, + NetworkHandle, + NetworkSpec, +) + +_DOMAIN = "runtime" +_ALLOWED_RUNTIMES = frozenset({"docker", "podman"}) +_DEFAULT_TIMEOUT_SECONDS = 120 + +_CODE_TIMEOUT = "reference-backend.driver.timeout" +_CODE_RUNTIME_UNAVAILABLE = "reference-backend.driver.runtime-unavailable" +_CODE_COMMAND_FAILED = "reference-backend.driver.command-failed" +_CODE_IMAGE_NOT_ALLOWED = "reference-backend.driver.image-not-allowed" + +_KIND_TO_CODE = { + "timeout": _CODE_TIMEOUT, + "runtime-missing": _CODE_RUNTIME_UNAVAILABLE, + "command-failed": _CODE_COMMAND_FAILED, +} + +Runner = Callable[..., subprocess.CompletedProcess] + + +def _default_runner(argv: list[str], **kwargs) -> subprocess.CompletedProcess: + # The real subprocess call is the impure IO leaf; tests inject a fake + # runner, and coverage excludes this function (see pyproject exclude_also). + return subprocess.run(argv, **kwargs) + + +@dataclass(frozen=True) +class ImageTrustPolicy: + """Operator policy deciding which container images may be realized. + + A plan author controls ``spec.image_ref`` (via ``node.source``) and ``run`` + pulls+executes it; fixed argv stops shell injection but is not an image + trust boundary. Only the operator ``default_image``, an explicit + ``allowed_images`` entry, or a digest-pinned ref (``...@sha256:...``) is + permitted, so plan submission cannot become arbitrary-image code execution. + """ + + default_image: str | None = None + allowed_images: tuple[str, ...] = () + allow_digest_pinned: bool = True + + def image_for(self, image_ref: str) -> str: + # A configured default overrides the synthesized ``aces-reference/*`` + # placeholder so an image-less plan can still realize against a registry. + if self.default_image and image_ref.startswith("aces-reference/"): + return self.default_image + return image_ref + + def permits(self, image: str) -> bool: + if self.default_image is not None and image == self.default_image: + return True + if image in self.allowed_images: + return True + return self.allow_digest_pinned and "@sha256:" in image + + +_DEFAULT_IMAGE_POLICY = ImageTrustPolicy() + + +class OciDeploymentDriver: + """Realize portable specs against a real container runtime.""" + + def __init__( + self, + *, + runtime: str = "docker", + workspace: str, + runner: Runner | None = None, + timeout_seconds: int = _DEFAULT_TIMEOUT_SECONDS, + keep_alive: tuple[str, ...] = ("sleep", "3600"), + image_policy: ImageTrustPolicy = _DEFAULT_IMAGE_POLICY, + ) -> None: + if runtime not in _ALLOWED_RUNTIMES: + raise ValueError(f"Unsupported container runtime; allowed: {sorted(_ALLOWED_RUNTIMES)}.") + if not workspace or not workspace.strip(): + raise ValueError("OciDeploymentDriver requires a non-empty workspace label.") + if not 0 < timeout_seconds <= 600: + raise ValueError("OciDeploymentDriver timeout_seconds must be in (0, 600].") + self._runtime = runtime + self._workspace = workspace + self._runner = runner or _default_runner + self._timeout = timeout_seconds + self._keep_alive = tuple(keep_alive) + self._image_policy = image_policy + self._realized: set[str] = set() + # ACES address -> the runtime object name realize() used, so destroy() + # removes exactly what was created even when a payload pinned an + # explicit name that differs from the address's last segment. + self._names: dict[str, str] = {} + + def _label_args(self) -> list[str]: + return ["--label", f"aces.workspace={self._workspace}"] + + def _run(self, argv: list[str]) -> tuple[bool, str | None]: + """Run a fixed argv; return (success, failure_kind). + + Native stdout/stderr is consumed but never returned to the caller -- + only a coarse, fixed failure-kind string used to pick a diagnostic. + """ + + try: + completed = self._runner( + argv, + capture_output=True, + text=True, + timeout=self._timeout, + check=False, + ) + except subprocess.TimeoutExpired: + return False, "timeout" + except FileNotFoundError: + return False, "runtime-missing" + kind = None if completed.returncode == 0 else "command-failed" + return kind is None, kind + + def realize( + self, + *, + networks: tuple[NetworkSpec, ...], + containers: tuple[ContainerSpec, ...], + ) -> DriverResult: + diagnostics: list[Diagnostic] = [] + network_handles: list[NetworkHandle] = [] + for spec in networks: + argv = [self._runtime, "network", "create", *self._label_args(), spec.name] + ok, kind = self._run(argv) + if ok: + self._realized.add(spec.address) + self._names[spec.address] = spec.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: + image = self._image_policy.image_for(spec.image_ref) + if not self._image_policy.permits(image): + diagnostics.append(self._image_rejected(spec.address)) + continue + argv = [ + self._runtime, + "run", + "--detach", + "--rm", + *self._label_args(), + "--name", + spec.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 + # carries network resource addresses; resolve each to the runtime + # name this driver actually created. + *self._network_args(spec.networks), + image, + # Fixed keep-alive so generic images do not exit immediately. + # Pure argv tokens -- never a shell string. + *self._keep_alive, + ] + ok, kind = self._run(argv) + if ok: + self._realized.add(spec.address) + self._names[spec.address] = spec.name + container_handles.append(ContainerHandle(address=spec.address, realized=True)) + else: + diagnostics.append(self._failure(spec.address, kind)) + result = DriverResult( + networks=tuple(network_handles), + containers=tuple(container_handles), + diagnostics=tuple(diagnostics), + ) + # Transactional boundary: if any resource failed, roll back the ones + # that succeeded so a partial realize never leaves an orphan runtime + # resource behind a failed operation. + if result.diagnostics: + self._rollback(network_handles, container_handles) + return DriverResult(diagnostics=result.diagnostics) + return result + + def _network_args(self, network_addresses: tuple[str, ...]) -> list[str]: + args: list[str] = [] + for address in network_addresses: + args.extend(("--network", self._name_for(address))) + return args + + def _rollback( + self, + networks: list[NetworkHandle], + containers: list[ContainerHandle], + ) -> None: + realized_containers = tuple(handle.address for handle in containers if handle.realized) + realized_networks = tuple(handle.address for handle in networks if handle.realized) + if realized_containers or realized_networks: + 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]) + + def destroy( + self, + *, + networks: tuple[str, ...], + containers: tuple[str, ...], + ) -> DriverResult: + diagnostics: list[Diagnostic] = [] + container_handles: list[ContainerHandle] = [] + for address in containers: + argv = [self._runtime, "rm", "--force", self._name_for(address)] + ok, kind = self._run(argv) + if ok: + # Only forget the resource once it is actually gone; a failed + # teardown stays tracked so a retry can reconcile it. + self._realized.discard(address) + self._names.pop(address, None) + else: + diagnostics.append(self._failure(address, kind)) + container_handles.append(ContainerHandle(address=address, realized=not ok)) + network_handles: list[NetworkHandle] = [] + for address in networks: + argv = [self._runtime, "network", "rm", self._name_for(address)] + ok, kind = self._run(argv) + if ok: + self._realized.discard(address) + self._names.pop(address, None) + else: + diagnostics.append(self._failure(address, kind)) + network_handles.append(NetworkHandle(address=address, realized=not ok)) + return DriverResult( + networks=tuple(network_handles), + containers=tuple(container_handles), + diagnostics=tuple(diagnostics), + ) + + def realized_addresses(self) -> frozenset[str]: + return frozenset(self._realized) + + @staticmethod + def _failure(address: str, kind: str | None) -> Diagnostic: + code = _KIND_TO_CODE.get(kind or "command-failed", _CODE_COMMAND_FAILED) + message = { + _CODE_TIMEOUT: f"Container runtime operation for '{address}' exceeded the bounded timeout.", + _CODE_RUNTIME_UNAVAILABLE: f"Container runtime is unavailable for '{address}'.", + _CODE_COMMAND_FAILED: f"Container runtime operation for '{address}' did not succeed.", + }[code] + return Diagnostic(code=code, domain=_DOMAIN, address=address, message=message, severity=Severity.ERROR) + + @staticmethod + def _image_rejected(address: str) -> Diagnostic: + # The rejected image ref is plan-controlled; keep it out of the message + # so the diagnostic never echoes attacker-chosen content. + return Diagnostic( + code=_CODE_IMAGE_NOT_ALLOWED, + domain=_DOMAIN, + address=address, + message=( + f"Container image for '{address}' is not permitted by the driver image-trust policy; " + "configure default_image, allowed_images, or a digest-pinned ref." + ), + severity=Severity.ERROR, + ) diff --git a/implementations/python/packages/aces_reference_backend/evaluator.py b/implementations/python/packages/aces_reference_backend/evaluator.py new file mode 100644 index 000000000..f0342a6a4 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/evaluator.py @@ -0,0 +1,146 @@ +"""Reference evaluator: portable evaluation result/history envelopes.""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from aces_contracts.planning import ChangeAction, EvaluationOp, EvaluationPlan, RuntimeDomain +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry +from aces_contracts.versions import EVALUATION_STATE_SCHEMA_VERSION + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +class ReferenceEvaluator: + """In-process evaluator over portable runtime snapshots.""" + + def __init__(self) -> None: + self._running = False + self._startup_order: list[str] = [] + self._results: dict[str, dict[str, object]] = {} + self._history: dict[str, list[dict[str, object]]] = {} + + def start(self, plan: EvaluationPlan, snapshot: RuntimeSnapshot) -> ApplyResult: + entries = dict(snapshot.entries) + changed_addresses: list[str] = [] + results = dict(snapshot.evaluation_results) + history = {address: list(events) for address, events in snapshot.evaluation_history.items()} + now = _now_iso() + 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_payload = self._result_payload(op, now) + results[op.address] = result_payload + history[op.address] = self._history_events(result_payload, now) + if op.action != ChangeAction.UNCHANGED: + changed_addresses.append(op.address) + self._running = bool(plan.resources) + self._startup_order = list(plan.startup_order) + self._results = results + self._history = history + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + entries, + evaluation_results=results, + evaluation_history=history, + ), + changed_addresses=changed_addresses, + ) + + @staticmethod + def _result_payload(op: EvaluationOp, now: str) -> dict[str, object]: + result_contract = op.payload.get("result_contract", {}) + if not isinstance(result_contract, dict): + 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"reference 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 + + @staticmethod + def _history_events(result_payload: 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_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": {}, + }, + ] + + def status(self) -> dict[str, object]: + return { + "running": self._running, + "startup_order": list(self._startup_order), + "results": len(self._results), + } + + def results(self) -> dict[str, dict[str, object]]: + return dict(self._results) + + def history(self) -> dict[str, list[dict[str, object]]]: + return {address: list(events) for address, events in self._history.items()} + + def stop(self, snapshot: RuntimeSnapshot) -> ApplyResult: + entries = { + address: entry for address, entry in snapshot.entries.items() if entry.domain != RuntimeDomain.EVALUATION + } + removed = [address for address, entry in snapshot.entries.items() if entry.domain == RuntimeDomain.EVALUATION] + self._running = False + self._startup_order = [] + self._results = {} + self._history = {} + return ApplyResult( + success=True, + snapshot=snapshot.with_entries(entries, evaluation_results={}, evaluation_history={}), + changed_addresses=removed, + ) diff --git a/implementations/python/packages/aces_reference_backend/manifest.py b/implementations/python/packages/aces_reference_backend/manifest.py new file mode 100644 index 000000000..c517fdbc4 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/manifest.py @@ -0,0 +1,212 @@ +"""Backend manifest for the reference emulation backend (RUN-314). + +The manifest declares the same evidence-backed contract ids, concept +bindings, realization-support declaration, and capability terms the +non-normative stub declares -- the reference backend emits/validates the +identical portable surface, so its claims are backed by the same shared +models and conformance evidence. The identity (``reference-emulation``) +and capability component names differ; nothing here imports or subclasses +the stub. +""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as distribution_version + +from aces_backend_protocols.capabilities import ( + PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, + PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, + PARTICIPANT_RUNTIME_ROLE_SCOPE, + BackendCapabilitySet, + BackendManifest, + EvaluatorCapabilities, + ObservationCapabilities, + OrchestratorCapabilities, + ParticipantRuntimeCapabilities, + ProvisionerCapabilities, + WorkflowFeature, + WorkflowStatePredicateFeature, +) +from aces_contracts.apparatus import ConceptBinding, RealizationSupportDeclaration +from aces_contracts.manifest_authority import BACKEND_SUPPORTED_CONTRACT_IDS +from aces_contracts.vocabulary import RealizationSupportMode + +REFERENCE_BACKEND_NAME = "reference-emulation" + +_PARTICIPANT_ROLES = frozenset(PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_ROLE_SCOPE]) +_PARTICIPANT_BEHAVIOR_FEATURES = frozenset( + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE] +) +_PARTICIPANT_INTERACTION_FEATURES = frozenset( + PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS[PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE] +) + + +def _current_backend_version() -> str: + try: + return distribution_version("aces-sdl") + except PackageNotFoundError: + return "0.0.0+unknown" + + +def _concept_bindings() -> tuple[ConceptBinding, ...]: + return ( + ConceptBinding(scope="capabilities.provisioner.supported_node_types", family="assets"), + ConceptBinding(scope="capabilities.provisioner.supported_os_families", family="assets"), + ConceptBinding(scope="capabilities.provisioner.supported_content_types", family="tools-and-artifacts"), + ConceptBinding(scope="capabilities.provisioner.supported_account_features", family="identities"), + ConceptBinding(scope="capabilities.orchestrator.supported_sections", family="actions-and-events"), + ConceptBinding(scope="capabilities.evaluator.supported_sections", family="observables"), + ConceptBinding( + scope="capabilities.participant_runtime.supported_participant_roles", + family="identities", + ), + ConceptBinding( + scope="capabilities.participant_runtime.supported_behavior_features", + family="actions-and-events", + ), + ConceptBinding( + scope="capabilities.participant_runtime.supported_interaction_features", + family="relationships", + ), + ConceptBinding( + scope="capabilities.observation.supported_capture_kinds", + family="provenance-and-evidence", + ), + ConceptBinding( + scope="capabilities.observation.supported_channel_kinds", + family="apparatus-declarations", + ), + ConceptBinding( + scope="capabilities.observation.supported_sealing_modes", + family="provenance-and-evidence", + ), + ) + + +def _realization_support() -> tuple[RealizationSupportDeclaration, ...]: + return ( + RealizationSupportDeclaration( + domain="runtime-realization", + support_mode=RealizationSupportMode.CONSTRAINED, + supported_constraint_kinds=frozenset( + { + "node-type", + "os-family", + "content-type", + "account-feature", + "workflow-feature", + "workflow-state-predicate", + } + ), + supported_exact_requirement_kinds=frozenset({"declared-capability-match"}), + disclosure_kinds=frozenset( + { + "backend-manifest-v2", + "runtime-snapshot-v1", + "operation-status-v1", + } + ), + ), + ) + + +def _capabilities() -> BackendCapabilitySet: + return BackendCapabilitySet( + provisioner=ProvisionerCapabilities( + name="reference-emulation-provisioner", + supported_node_types=frozenset({"vm", "switch"}), + 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, + ), + orchestrator=OrchestratorCapabilities( + name="reference-emulation-orchestrator", + supported_sections=frozenset({"injects", "events", "scripts", "stories", "workflows"}), + supports_workflows=True, + supports_condition_refs=True, + supports_inject_bindings=True, + supported_workflow_features=frozenset( + { + WorkflowFeature.DECISION, + WorkflowFeature.SWITCH, + WorkflowFeature.CALL, + WorkflowFeature.PARALLEL_BARRIER, + WorkflowFeature.RETRY, + WorkflowFeature.FAILURE_TRANSITIONS, + WorkflowFeature.CANCELLATION, + WorkflowFeature.TIMEOUTS, + WorkflowFeature.COMPENSATION, + } + ), + supported_workflow_state_predicates=frozenset( + { + WorkflowStatePredicateFeature.OUTCOME_MATCHING, + WorkflowStatePredicateFeature.ATTEMPT_COUNTS, + } + ), + ), + evaluator=EvaluatorCapabilities( + name="reference-emulation-evaluator", + supported_sections=frozenset({"conditions", "metrics", "evaluations", "tlos", "goals", "objectives"}), + supports_scoring=True, + supports_objectives=True, + ), + participant_runtime=ParticipantRuntimeCapabilities( + name="reference-emulation-participant-runtime", + supported_participant_roles=_PARTICIPANT_ROLES, + supported_behavior_features=_PARTICIPANT_BEHAVIOR_FEATURES, + supported_interaction_features=_PARTICIPANT_INTERACTION_FEATURES, + ), + observation=ObservationCapabilities( + name="reference-emulation-observation", + supported_capture_kinds=frozenset({"artifact", "log", "observation", "telemetry", "trace"}), + supported_channel_kinds=frozenset( + { + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history", + } + ), + supported_evidence_contracts=frozenset( + { + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", + } + ), + supported_media_types=frozenset({"application/json", "text/plain"}), + supported_sealing_modes=frozenset({"digest", "immutable-store"}), + supports_redaction=True, + supports_loss_disclosure=True, + supports_chain_of_custody=False, + ), + ) + + +def create_reference_backend_manifest(**config) -> BackendManifest: + """Return the fully capable reference emulation backend manifest. + + Extra ``config`` kwargs (e.g. ``driver``, ``workspace``) flow through + the registry descriptor to both factories; the manifest factory accepts + and ignores them. + """ + + del config + return BackendManifest( + name=REFERENCE_BACKEND_NAME, + version=_current_backend_version(), + supported_contract_versions=frozenset(BACKEND_SUPPORTED_CONTRACT_IDS), + compatible_processors=frozenset({"aces-reference-processor"}), + concept_bindings=_concept_bindings(), + realization_support=_realization_support(), + capabilities=_capabilities(), + ) diff --git a/implementations/python/packages/aces_reference_backend/orchestrator.py b/implementations/python/packages/aces_reference_backend/orchestrator.py new file mode 100644 index 000000000..3e3d4dbba --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/orchestrator.py @@ -0,0 +1,147 @@ +"""Reference orchestrator: portable workflow result/history envelopes. + +Mirrors the portable orchestration result/history shape the conformance +contracts require. State lives in the snapshot's first-class +``orchestration_results`` / ``orchestration_history`` carriers; this object +keeps a small private mirror only for the status/results/history probe +methods the runtime call shape requires. +""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from aces_contracts.planning import ChangeAction, OrchestrationPlan, RuntimeDomain +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry + +_QUEUED_RESOURCE_TYPES = frozenset({"event", "script", "story", "workflow"}) + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +class ReferenceOrchestrator: + """In-process orchestrator over portable runtime snapshots.""" + + def __init__(self) -> None: + self._running = False + self._startup_order: list[str] = [] + self._results: dict[str, dict[str, object]] = {} + self._history: dict[str, list[dict[str, object]]] = {} + + def start(self, plan: OrchestrationPlan, snapshot: RuntimeSnapshot) -> ApplyResult: + entries = dict(snapshot.entries) + results = dict(snapshot.orchestration_results) + history = {address: list(events) for address, events in snapshot.orchestration_history.items()} + changed_addresses: list[str] = [] + now = _now_iso() + 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 + status = "queued" if op.resource_type in _QUEUED_RESOURCE_TYPES else "bound" + entries[op.address] = SnapshotEntry( + address=op.address, + domain=RuntimeDomain.ORCHESTRATION, + resource_type=op.resource_type, + payload=op.payload, + ordering_dependencies=op.ordering_dependencies, + refresh_dependencies=op.refresh_dependencies, + status=status, + ) + if op.resource_type == "workflow": + results[op.address] = self._workflow_result(op.payload, now) + history[op.address] = [self._workflow_started_event(op.payload, now)] + if op.action != ChangeAction.UNCHANGED: + changed_addresses.append(op.address) + self._running = bool(plan.resources) + self._startup_order = list(plan.startup_order) + self._results = results + self._history = history + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + entries, + orchestration_results=results, + orchestration_history=history, + ), + changed_addresses=changed_addresses, + ) + + @staticmethod + def _workflow_result(payload: dict[str, object], now: str) -> dict[str, object]: + result_contract = payload.get("result_contract", {}) + if not isinstance(result_contract, dict): + result_contract = {} + observable_steps_raw = result_contract.get("observable_steps", {}) + observable_steps = { + step_name: {"lifecycle": "pending", "outcome": None, "attempts": 0} + for step_name, step_payload in ( + observable_steps_raw.items() if isinstance(observable_steps_raw, dict) else [] + ) + if isinstance(step_payload, dict) + } + return { + "state_schema_version": result_contract.get( + "state_schema_version", + payload.get("state_schema_version", "workflow-step-state/v1"), + ), + "workflow_status": "running", + "run_id": f"{payload.get('name', 'workflow')}-run", + "started_at": now, + "updated_at": now, + "terminal_reason": None, + "compensation_status": "not_required", + "compensation_started_at": None, + "compensation_updated_at": None, + "compensation_failures": [], + "steps": observable_steps, + } + + @staticmethod + def _workflow_started_event(payload: dict[str, object], now: str) -> dict[str, object]: + execution_contract = payload.get("execution_contract", {}) + start_step = execution_contract.get("start_step") if isinstance(execution_contract, dict) else None + return { + "event_type": "workflow_started", + "timestamp": now, + "step_name": start_step, + "branch_name": None, + "join_step": None, + "outcome": None, + "details": {}, + } + + def status(self) -> dict[str, object]: + return { + "running": self._running, + "startup_order": list(self._startup_order), + "results": len(self._results), + } + + def results(self) -> dict[str, dict[str, object]]: + return dict(self._results) + + def history(self) -> dict[str, list[dict[str, object]]]: + return {address: list(events) for address, events in self._history.items()} + + def stop(self, snapshot: RuntimeSnapshot) -> ApplyResult: + entries = { + address: entry for address, entry in snapshot.entries.items() if entry.domain != RuntimeDomain.ORCHESTRATION + } + removed = [ + address for address, entry in snapshot.entries.items() if entry.domain == RuntimeDomain.ORCHESTRATION + ] + self._running = False + self._startup_order = [] + self._results = {} + self._history = {} + return ApplyResult( + success=True, + snapshot=snapshot.with_entries(entries, orchestration_results={}, orchestration_history={}), + changed_addresses=removed, + ) diff --git a/implementations/python/packages/aces_reference_backend/participant_runtime.py b/implementations/python/packages/aces_reference_backend/participant_runtime.py new file mode 100644 index 000000000..5f7672253 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/participant_runtime.py @@ -0,0 +1,294 @@ +"""Reference participant runtime: RUN-311 episode lifecycle transitions. + +Each control method advances the current ``participant_episode_results`` +entry in lockstep with append-only history events, so the resulting +snapshot always satisfies ``iter_participant_episode_snapshot_violations``: +identity is stable across resets/restarts, history is append-only, and the +current result is the head of the history chain. Independent of the stub. +""" + +from __future__ import annotations + +from datetime import UTC, datetime + +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.participant_episode import ( + ParticipantEpisodeControlAction, + ParticipantEpisodeExecutionState, + ParticipantEpisodeHistoryEvent, + ParticipantEpisodeHistoryEventType, + ParticipantEpisodeInitializeRequest, + ParticipantEpisodeResetRequest, + ParticipantEpisodeRestartRequest, + ParticipantEpisodeStatus, + ParticipantEpisodeTerminalReason, + ParticipantEpisodeTerminateRequest, +) +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot + +_EMPTY_ADDRESS_MSG = "participant_address must be non-empty" + +_TERMINAL_EVENT_FOR_REASON: dict[ParticipantEpisodeTerminalReason, ParticipantEpisodeHistoryEventType] = { + ParticipantEpisodeTerminalReason.COMPLETED: ParticipantEpisodeHistoryEventType.EPISODE_COMPLETED, + ParticipantEpisodeTerminalReason.TIMED_OUT: ParticipantEpisodeHistoryEventType.EPISODE_TIMED_OUT, + ParticipantEpisodeTerminalReason.TRUNCATED: ParticipantEpisodeHistoryEventType.EPISODE_TRUNCATED, + ParticipantEpisodeTerminalReason.INTERRUPTED: ParticipantEpisodeHistoryEventType.EPISODE_INTERRUPTED, +} + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +class ReferenceParticipantRuntime: + """In-process participant runtime driving RUN-311 transitions.""" + + def __init__(self) -> None: + self._results: dict[str, dict[str, object]] = {} + self._history: dict[str, list[dict[str, object]]] = {} + self._episode_counter: dict[str, int] = {} + + def initialize( + self, + request: ParticipantEpisodeInitializeRequest, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + address = request.participant_address + if not address: + return self._reject(snapshot, _EMPTY_ADDRESS_MSG, address) + if address in snapshot.participant_episode_results: + return self._reject( + snapshot, + f"participant {address!r} already has a live episode; use reset or restart", + address, + ) + now = _now_iso() + episode_id = request.episode_id or self._allocate_episode_id(address) + state = ParticipantEpisodeExecutionState( + participant_address=address, + episode_id=episode_id, + sequence_number=0, + status=ParticipantEpisodeStatus.RUNNING, + initialized_at=now, + updated_at=now, + last_control_action=ParticipantEpisodeControlAction.INITIALIZE, + ) + events = [ + ParticipantEpisodeHistoryEvent( + event_type=ParticipantEpisodeHistoryEventType.EPISODE_INITIALIZED, + timestamp=now, + participant_address=address, + episode_id=episode_id, + sequence_number=0, + control_action=ParticipantEpisodeControlAction.INITIALIZE, + ), + ParticipantEpisodeHistoryEvent( + event_type=ParticipantEpisodeHistoryEventType.EPISODE_RUNNING, + timestamp=now, + participant_address=address, + episode_id=episode_id, + sequence_number=0, + ), + ] + return self._apply(snapshot, address, state, events, replace_history=True) + + def reset( + self, + request: ParticipantEpisodeResetRequest, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + return self._new_episode( + request, + snapshot, + control_action=ParticipantEpisodeControlAction.RESET, + reset_event=ParticipantEpisodeHistoryEventType.EPISODE_RESET, + require_terminated=False, + no_episode_message="cannot reset participant {address!r}: no live episode", + wrong_state_message="cannot reset terminated participant {address!r}; use restart", + ) + + def restart( + self, + request: ParticipantEpisodeRestartRequest, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + return self._new_episode( + request, + snapshot, + control_action=ParticipantEpisodeControlAction.RESTART, + reset_event=ParticipantEpisodeHistoryEventType.EPISODE_RESTARTED, + require_terminated=True, + no_episode_message="cannot restart participant {address!r}: no live episode", + wrong_state_message="cannot restart non-terminated participant {address!r}; use reset", + ) + + def _new_episode( + self, + request: ParticipantEpisodeResetRequest | ParticipantEpisodeRestartRequest, + snapshot: RuntimeSnapshot, + *, + control_action: ParticipantEpisodeControlAction, + reset_event: ParticipantEpisodeHistoryEventType, + require_terminated: bool, + no_episode_message: str, + wrong_state_message: str, + ) -> ApplyResult: + address = request.participant_address + current_state = self._live_predecessor(snapshot, address, no_episode_message) + if isinstance(current_state, ApplyResult): + return current_state + if (current_state.status == ParticipantEpisodeStatus.TERMINATED) != require_terminated: + return self._reject(snapshot, wrong_state_message.format(address=address), address) + now = _now_iso() + new_episode_id = request.episode_id or self._allocate_episode_id(address) + new_sequence = current_state.sequence_number + 1 + new_state = ParticipantEpisodeExecutionState( + participant_address=address, + episode_id=new_episode_id, + sequence_number=new_sequence, + status=ParticipantEpisodeStatus.RUNNING, + initialized_at=now, + updated_at=now, + last_control_action=control_action, + previous_episode_id=current_state.episode_id, + ) + events = [ + ParticipantEpisodeHistoryEvent( + event_type=reset_event, + timestamp=now, + participant_address=address, + episode_id=new_episode_id, + sequence_number=new_sequence, + control_action=control_action, + details={ + "previous_episode_id": current_state.episode_id, + "reason": request.reason, + }, + ), + ParticipantEpisodeHistoryEvent( + event_type=ParticipantEpisodeHistoryEventType.EPISODE_RUNNING, + timestamp=now, + participant_address=address, + episode_id=new_episode_id, + sequence_number=new_sequence, + ), + ] + return self._apply(snapshot, address, new_state, events, replace_history=False) + + def terminate( + self, + request: ParticipantEpisodeTerminateRequest, + snapshot: RuntimeSnapshot, + ) -> ApplyResult: + address = request.participant_address + current_state = self._live_predecessor( + snapshot, address, "cannot terminate participant {address!r}: no live episode" + ) + if isinstance(current_state, ApplyResult): + return current_state + if current_state.status == ParticipantEpisodeStatus.TERMINATED: + return self._reject(snapshot, f"participant {address!r} is already terminated", address) + now = _now_iso() + terminal_reason = request.terminal_reason + new_state = ParticipantEpisodeExecutionState( + participant_address=address, + episode_id=current_state.episode_id, + sequence_number=current_state.sequence_number, + status=ParticipantEpisodeStatus.TERMINATED, + terminal_reason=terminal_reason, + initialized_at=current_state.initialized_at, + updated_at=now, + terminated_at=now, + last_control_action=current_state.last_control_action, + previous_episode_id=current_state.previous_episode_id, + ) + events = [ + ParticipantEpisodeHistoryEvent( + event_type=_TERMINAL_EVENT_FOR_REASON[terminal_reason], + timestamp=now, + participant_address=address, + episode_id=current_state.episode_id, + sequence_number=current_state.sequence_number, + terminal_reason=terminal_reason, + details={"detail": request.detail}, + ), + ] + return self._apply(snapshot, address, new_state, events, replace_history=False) + + def status(self) -> dict[str, object]: + return { + "participants": len(self._results), + "running": sum(1 for result in self._results.values() if result.get("status") == "running"), + } + + def results(self) -> dict[str, dict[str, object]]: + return {address: dict(result) for address, result in self._results.items()} + + def history(self) -> dict[str, list[dict[str, object]]]: + return {address: list(events) for address, events in self._history.items()} + + def _live_predecessor( + self, + snapshot: RuntimeSnapshot, + address: str, + no_episode_message: str, + ) -> ParticipantEpisodeExecutionState | ApplyResult: + """Resolve the participant's current episode state, or an error result. + + Collapses the empty-address, no-live-episode, and invalid-payload guards + into a single resolver so the reset/restart/terminate callers each keep + one error-return path plus their own state-specific check. + """ + + current = snapshot.participant_episode_results.get(address) if address else None + if current is None: + message = _EMPTY_ADDRESS_MSG if not address else no_episode_message.format(address=address) + return self._reject(snapshot, message, address) + try: + return ParticipantEpisodeExecutionState.from_payload(current) + except (TypeError, ValueError) as exc: + return self._reject(snapshot, f"current state is invalid: {exc}", address) + + def _apply( + self, + snapshot: RuntimeSnapshot, + address: str, + state: ParticipantEpisodeExecutionState, + new_events: list[ParticipantEpisodeHistoryEvent], + *, + replace_history: bool, + ) -> ApplyResult: + results = {addr: dict(result) for addr, result in snapshot.participant_episode_results.items()} + history = {addr: list(events) for addr, events in snapshot.participant_episode_history.items()} + results[address] = state.to_payload() + if replace_history: + history[address] = [event.to_payload() for event in new_events] + else: + history.setdefault(address, []) + history[address].extend(event.to_payload() for event in new_events) + self._results = results + self._history = history + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + participant_episode_results=results, + participant_episode_history=history, + ), + changed_addresses=[address], + ) + + @staticmethod + def _reject(snapshot: RuntimeSnapshot, message: str, address: str) -> ApplyResult: + diagnostic = Diagnostic( + code="runtime.participant-runtime.rejected", + domain="runtime", + address=address or "runtime.participant-runtime", + message=message, + ) + return ApplyResult(success=False, snapshot=snapshot, diagnostics=[diagnostic]) + + def _allocate_episode_id(self, address: str) -> str: + next_index = self._episode_counter.get(address, 0) + 1 + self._episode_counter[address] = next_index + return f"{address}-episode-{next_index}" diff --git a/implementations/python/packages/aces_reference_backend/provisioner.py b/implementations/python/packages/aces_reference_backend/provisioner.py new file mode 100644 index 000000000..b4b218831 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/provisioner.py @@ -0,0 +1,103 @@ +"""Reference provisioner: portable snapshot reconciliation + driver side effects. + +The provisioner preserves planned payloads honestly into snapshot entries +(the portable surface SEM-218 provenance is computed against), sets entry +status, and drives the injected :class:`DeploymentDriver` to realize or +destroy the corresponding emulated infrastructure. Real container +realization is a DRIVER side effect; it never mutates the portable +snapshot. Driver diagnostics are surfaced through ``ApplyResult`` -- never +as a backend-specific exception. +""" + +from __future__ import annotations + +from aces_contracts.diagnostics import Diagnostic +from aces_contracts.planning import ChangeAction, ProvisioningPlan, RuntimeDomain +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot, SnapshotEntry + +from .driver import DeploymentDriver +from .realization import ( + NETWORK_RESOURCE_TYPE, + NODE_RESOURCE_TYPE, + Realization, + interpret_provisioning_plan, +) + + +class ReferenceProvisioner: + """Container-backed provisioner over an injected deployment driver.""" + + def __init__(self, driver: DeploymentDriver) -> None: + self._driver = driver + + @staticmethod + def validate(plan: ProvisioningPlan) -> list[Diagnostic]: + realization = interpret_provisioning_plan(plan) + return list(realization.diagnostics) + + def apply(self, plan: ProvisioningPlan, snapshot: RuntimeSnapshot) -> ApplyResult: + realization = interpret_provisioning_plan(plan) + diagnostics: list[Diagnostic] = list(realization.diagnostics) + if any(diag.is_error for diag in diagnostics): + return ApplyResult(success=False, snapshot=snapshot, diagnostics=diagnostics) + + entries = dict(snapshot.entries) + changed_addresses: list[str] = [] + delete_networks: list[str] = [] + delete_containers: list[str] = [] + + for op in plan.operations: + if op.action == ChangeAction.DELETE: + entries.pop(op.address, None) + changed_addresses.append(op.address) + if op.resource_type == NETWORK_RESOURCE_TYPE: + delete_networks.append(op.address) + elif op.resource_type == NODE_RESOURCE_TYPE: + delete_containers.append(op.address) + continue + status = "unchanged" if op.action == ChangeAction.UNCHANGED else "applied" + entries[op.address] = SnapshotEntry( + address=op.address, + domain=RuntimeDomain.PROVISIONING, + resource_type=op.resource_type, + payload=op.payload, + ordering_dependencies=op.ordering_dependencies, + refresh_dependencies=op.refresh_dependencies, + status=status, + ) + if op.action != ChangeAction.UNCHANGED: + changed_addresses.append(op.address) + + driver_diagnostics = self._drive(plan, realization, delete_networks, delete_containers) + diagnostics.extend(driver_diagnostics) + if any(diag.is_error for diag in diagnostics): + return ApplyResult(success=False, snapshot=snapshot, diagnostics=diagnostics) + + return ApplyResult( + success=True, + snapshot=snapshot.with_entries(entries), + diagnostics=diagnostics, + changed_addresses=changed_addresses, + ) + + def _drive( + self, + plan: ProvisioningPlan, + realization: Realization, + delete_networks: list[str], + delete_containers: list[str], + ) -> list[Diagnostic]: + diagnostics: list[Diagnostic] = [] + active = {op.address for op in plan.operations if op.action in {ChangeAction.CREATE, ChangeAction.UPDATE}} + networks = tuple(spec for spec in realization.networks if spec.address in active) + containers = tuple(spec for spec in realization.containers if spec.address in active) + if networks or containers: + result = self._driver.realize(networks=networks, containers=containers) + diagnostics.extend(result.diagnostics) + if delete_networks or delete_containers: + result = self._driver.destroy( + networks=tuple(delete_networks), + containers=tuple(delete_containers), + ) + diagnostics.extend(result.diagnostics) + return diagnostics diff --git a/implementations/python/packages/aces_reference_backend/realization.py b/implementations/python/packages/aces_reference_backend/realization.py new file mode 100644 index 000000000..3949e819e --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/realization.py @@ -0,0 +1,215 @@ +"""Pure interpretation of provisioning plans for the reference backend. + +``interpret_provisioning_plan`` maps an ACES ``ProvisioningPlan`` into a +driver-agnostic :class:`Realization` of portable network/container specs +plus placement records, with diagnostics for unsupported resource types or +malformed payloads. It is pure (no driver, no IO) so the provisioner can +validate a plan without realizing it, and so the driver layer can be +swapped without touching interpretation. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass + +from aces_contracts.diagnostics import Diagnostic, Severity +from aces_contracts.planning import PlannedResource, ProvisioningPlan, RuntimeDomain + +from .driver import ContainerSpec, NetworkSpec + +_DOMAIN = "runtime" + +NODE_RESOURCE_TYPE = "node" +NETWORK_RESOURCE_TYPE = "network" +PLACEMENT_RESOURCE_TYPES = frozenset({"feature-binding", "content-placement", "account-placement"}) +SUPPORTED_RESOURCE_TYPES = frozenset({NODE_RESOURCE_TYPE, NETWORK_RESOURCE_TYPE}) | PLACEMENT_RESOURCE_TYPES + + +@dataclass(frozen=True) +class PlacementRealization: + """Portable record of a placement resource bound to a target address.""" + + address: str + resource_type: str + name: str + target_address: str | None + + +@dataclass(frozen=True) +class Realization: + """Driver-agnostic interpretation of a provisioning plan.""" + + networks: tuple[NetworkSpec, ...] = () + containers: tuple[ContainerSpec, ...] = () + placements: tuple[PlacementRealization, ...] = () + diagnostics: tuple[Diagnostic, ...] = () + + +def interpret_provisioning_plan(plan: ProvisioningPlan) -> Realization: + """Interpret an ACES provisioning plan as a portable realization. + + Networks are interpreted before nodes so a container's network references + (authored by name or address) resolve to the network resource *address* — + the single portable key the driver maps to a runtime network name. This + keeps ``ContainerSpec.networks`` address-keyed end to end, consistent with + the rest of the address-keyed runtime model. + """ + + diagnostics: list[Diagnostic] = [] + network_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + node_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + placement_resources: list[tuple[PlannedResource, Mapping[str, object]]] = [] + + for resource in plan.resources.values(): + if resource.domain != RuntimeDomain.PROVISIONING: + continue + if resource.resource_type not in SUPPORTED_RESOURCE_TYPES: + diagnostics.append(_unsupported_resource(resource)) + continue + payload = resource.payload + if not isinstance(payload, Mapping): + diagnostics.append(_invalid_payload(resource)) + continue + if resource.resource_type == NETWORK_RESOURCE_TYPE: + network_resources.append((resource, payload)) + elif resource.resource_type == NODE_RESOURCE_TYPE: + node_resources.append((resource, payload)) + else: + placement_resources.append((resource, payload)) + + networks = [_network_spec(resource, payload) for resource, payload in network_resources] + network_lookup = _network_address_lookup(networks) + containers = [_container_spec(resource, payload, network_lookup) for resource, payload in node_resources] + placements = [_placement(resource, payload) for resource, payload in placement_resources] + + return Realization( + networks=tuple(sorted(networks, key=lambda spec: spec.address)), + containers=tuple(sorted(containers, key=lambda spec: spec.address)), + placements=tuple(sorted(placements, key=lambda item: item.address)), + diagnostics=tuple(diagnostics), + ) + + +def _network_address_lookup(networks: list[NetworkSpec]) -> dict[str, str]: + """Map every handle a node might reference a network by to its address.""" + + lookup: dict[str, str] = {} + for spec in networks: + for key in (spec.address, spec.name, spec.address.rsplit(".", 1)[-1]): + if key: + lookup[key] = spec.address + return lookup + + +def _resource_name(resource: PlannedResource, payload: Mapping[str, object]) -> str: + name = payload.get("name") or payload.get("node_name") + if isinstance(name, str) and name: + return name + return resource.address.rsplit(".", 1)[-1] + + +def _infrastructure_spec(payload: Mapping[str, object]) -> Mapping[str, object]: + spec = payload.get("spec") + if not isinstance(spec, Mapping): + return {} + infrastructure = spec.get("infrastructure") + return infrastructure if isinstance(infrastructure, Mapping) else {} + + +def _network_spec(resource: PlannedResource, payload: Mapping[str, object]) -> NetworkSpec: + infrastructure = _infrastructure_spec(payload) + properties = infrastructure.get("properties") + labels: dict[str, str] = {} + if isinstance(properties, Mapping) and properties.get("internal") is True: + labels["internal"] = "true" + return NetworkSpec( + address=resource.address, + name=_resource_name(resource, payload), + labels=labels, + ) + + +def _container_spec( + resource: PlannedResource, + payload: Mapping[str, object], + network_lookup: dict[str, str], +) -> ContainerSpec: + infrastructure = _infrastructure_spec(payload) + networks = infrastructure.get("networks") + references: tuple[str, ...] = () + if isinstance(networks, (list, tuple)): + references = tuple(str(ref) for ref in networks if isinstance(ref, str) and ref) + # Resolve each authored reference (by name or address) to the network + # resource address; pass unresolved references through unchanged so the + # contract stays total even when a node names a network not in this plan. + network_addresses = tuple(network_lookup.get(ref, ref) for ref in references) + image_ref = _image_ref(payload) + return ContainerSpec( + address=resource.address, + name=_resource_name(resource, payload), + image_ref=image_ref, + networks=network_addresses, + ) + + +def _image_ref(payload: Mapping[str, object]) -> str: + source = _node_source(payload) + if source: + return source + os_family = payload.get("os_family") + if isinstance(os_family, str) and os_family: + return f"aces-reference/{os_family}" + return "aces-reference/base" + + +def _node_source(payload: Mapping[str, object]) -> str | None: + """Return the authored container image source for a node, if any.""" + + spec = payload.get("spec") + node = spec.get("node") if isinstance(spec, Mapping) else None + source = node.get("source") if isinstance(node, Mapping) else None + if isinstance(source, str) and source: + return source + if isinstance(source, Mapping): + name = source.get("name") + if isinstance(name, str) and name: + return name + return None + + +def _placement(resource: PlannedResource, payload: Mapping[str, object]) -> PlacementRealization: + target = payload.get("target") or payload.get("target_address") or payload.get("node") + target_address = target if isinstance(target, str) and target else None + return PlacementRealization( + address=resource.address, + resource_type=resource.resource_type, + name=_resource_name(resource, payload), + target_address=target_address, + ) + + +def _unsupported_resource(resource: PlannedResource) -> Diagnostic: + return Diagnostic( + code="reference-backend.realization.unsupported-resource", + domain=_DOMAIN, + address=resource.address, + message=( + f"Reference backend does not realize provisioning resource type " + f"'{resource.resource_type}' for '{resource.address}'." + ), + severity=Severity.ERROR, + ) + + +def _invalid_payload(resource: PlannedResource) -> Diagnostic: + return Diagnostic( + code="reference-backend.realization.invalid-payload", + domain=_DOMAIN, + address=resource.address, + message=( + f"Reference backend expected provisioning resource '{resource.address}' " + f"of type '{resource.resource_type}' to carry a mapping payload." + ), + severity=Severity.ERROR, + ) diff --git a/implementations/python/packages/aces_reference_backend/target.py b/implementations/python/packages/aces_reference_backend/target.py new file mode 100644 index 000000000..4f5b81921 --- /dev/null +++ b/implementations/python/packages/aces_reference_backend/target.py @@ -0,0 +1,67 @@ +"""Runtime target construction + registry registration (RUN-314). + +Constructs the reference backend's components and target through the +existing ``BackendRegistry`` descriptor seam. Config kwargs flow to both +the manifest factory and ``create_reference_backend_components``; the +default driver is the hermetic :class:`InProcessDriver`. +""" + +from __future__ import annotations + +from aces_backend_protocols.capabilities import BackendManifest +from aces_runtime.registry import BackendRegistry, RuntimeTarget, RuntimeTargetComponents + +from .driver import DeploymentDriver +from .drivers.inprocess import InProcessDriver +from .evaluator import ReferenceEvaluator +from .manifest import REFERENCE_BACKEND_NAME, create_reference_backend_manifest +from .orchestrator import ReferenceOrchestrator +from .participant_runtime import ReferenceParticipantRuntime +from .provisioner import ReferenceProvisioner + + +def create_reference_backend_components( + *, + manifest: BackendManifest, + driver: DeploymentDriver | None = None, + **config, +) -> RuntimeTargetComponents: + """Build the reference backend components for a manifest. + + The default ``driver`` is the hermetic in-process driver. Component + presence matches the manifest's declared capabilities. + """ + + del config + deployment_driver = driver if driver is not None else InProcessDriver() + return RuntimeTargetComponents( + provisioner=ReferenceProvisioner(deployment_driver), + orchestrator=ReferenceOrchestrator() if manifest.has_orchestrator else None, + evaluator=ReferenceEvaluator() if manifest.has_evaluator else None, + participant_runtime=ReferenceParticipantRuntime() if manifest.has_participant_runtime else None, + ) + + +def create_reference_backend_target(**config) -> RuntimeTarget: + """Return a fully configured reference emulation backend target.""" + + manifest = create_reference_backend_manifest(**config) + components = create_reference_backend_components(manifest=manifest, **config) + return RuntimeTarget( + name=REFERENCE_BACKEND_NAME, + manifest=manifest, + provisioner=components.provisioner, + orchestrator=components.orchestrator, + evaluator=components.evaluator, + participant_runtime=components.participant_runtime, + ) + + +def register_reference_backend(registry: BackendRegistry) -> None: + """Register the reference emulation backend descriptor on ``registry``.""" + + registry.register( + REFERENCE_BACKEND_NAME, + create_reference_backend_manifest, + create_reference_backend_components, + ) diff --git a/implementations/python/packages/aces_runtime/control_plane.py b/implementations/python/packages/aces_runtime/control_plane.py index 835849116..ec0df86ff 100644 --- a/implementations/python/packages/aces_runtime/control_plane.py +++ b/implementations/python/packages/aces_runtime/control_plane.py @@ -51,6 +51,7 @@ ) from .control_plane_timeouts import workflow_timeout_update from .control_plane_workflows import maybe_apply_compensation +from .participant_retrieval import ParticipantRetrievalMixin from .registry import RuntimeTarget _NO_PARTICIPANT_RUNTIME_MESSAGE = "Target does not provide a participant runtime." @@ -66,7 +67,7 @@ def _utc_now() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") -class RuntimeControlPlane: +class RuntimeControlPlane(ParticipantRetrievalMixin): """Reference control plane for async runtime submission and observation.""" def __init__( diff --git a/implementations/python/packages/aces_runtime/control_plane_api.py b/implementations/python/packages/aces_runtime/control_plane_api.py index f91b0325b..17c2d6f44 100644 --- a/implementations/python/packages/aces_runtime/control_plane_api.py +++ b/implementations/python/packages/aces_runtime/control_plane_api.py @@ -21,6 +21,7 @@ from fastapi.responses import JSONResponse from .control_plane import RuntimeControlPlane +from .control_plane_api_guards import request_size_guard_response from .control_plane_api_models import ( _evaluation_plan, _operation_status_model, @@ -33,13 +34,13 @@ _request_fingerprint, _snapshot_model, ) +from .control_plane_api_participant_retrieval import register_participant_retrieval_routes from .control_plane_security import ( ControlPlaneIdentity, ControlPlaneRole, ControlPlaneSecurityConfig, ) -_REQUEST_TOO_LARGE_DETAIL = "request too large" _CONFLICT_RESPONSES = {409: {"description": "Conflict"}} _NOT_FOUND_RESPONSES = {404: {"description": "Not found"}} _BAD_REQUEST_CONFLICT_RESPONSES = { @@ -162,6 +163,7 @@ def create_control_plane_app( _register_operation_routes(app, control_plane) _register_workflow_routes(app, control_plane) _register_participant_episode_routes(app, control_plane) + register_participant_retrieval_routes(app, control_plane) return app @@ -175,13 +177,13 @@ async def _limit_request_size( request: Request, call_next: Callable[[Request], Awaitable[Response]], ) -> Response: - content_length = request.headers.get("content-length") - if content_length is not None and _content_length_exceeds_limit(content_length, security.max_request_bytes): - return _request_too_large_response(control_plane, request) - body = await request.body() - if len(body) > security.max_request_bytes: - return _request_too_large_response(control_plane, request) - request.state.raw_body = body + guard_response = await request_size_guard_response( + control_plane, + request, + max_request_bytes=security.max_request_bytes, + ) + if guard_response is not None: + return guard_response return await call_next(request) @app.exception_handler(Exception) @@ -196,27 +198,6 @@ async def _redacted_errors(request: Request, exc: Exception) -> JSONResponse: return JSONResponse(status_code=500, content={"detail": "internal server error"}) -def _content_length_exceeds_limit(content_length: str, max_request_bytes: int) -> bool: - try: - return int(content_length) > max_request_bytes - except ValueError: - return False - - -def _request_too_large_response( - control_plane: RuntimeControlPlane, - request: Request, -) -> JSONResponse: - control_plane.record_audit( - action=request.method, - identity="anonymous", - allowed=False, - target=str(request.url.path), - reason=_REQUEST_TOO_LARGE_DETAIL, - ) - return JSONResponse(status_code=413, content={"detail": _REQUEST_TOO_LARGE_DETAIL}) - - def _register_operation_routes( app: FastAPI, control_plane: RuntimeControlPlane, diff --git a/implementations/python/packages/aces_runtime/control_plane_api_guards.py b/implementations/python/packages/aces_runtime/control_plane_api_guards.py new file mode 100644 index 000000000..ced8ebf33 --- /dev/null +++ b/implementations/python/packages/aces_runtime/control_plane_api_guards.py @@ -0,0 +1,91 @@ +"""HTTP request guards for the runtime control-plane API.""" + +from __future__ import annotations + +from fastapi import Request, Response +from fastapi.responses import JSONResponse + +from .control_plane import RuntimeControlPlane + +_REQUEST_TOO_LARGE_DETAIL = "request too large" +_INVALID_CONTENT_LENGTH_DETAIL = "invalid content-length" + + +async def request_size_guard_response( + control_plane: RuntimeControlPlane, + request: Request, + *, + max_request_bytes: int, +) -> Response | None: + guard_response = _content_length_guard_response( + control_plane, + request, + max_request_bytes=max_request_bytes, + ) + if guard_response is not None: + return guard_response + return await _body_size_guard_response( + control_plane, + request, + max_request_bytes=max_request_bytes, + ) + + +def _content_length_guard_response( + control_plane: RuntimeControlPlane, + request: Request, + *, + max_request_bytes: int, +) -> JSONResponse | None: + response: JSONResponse | None = None + content_length = request.headers.get("content-length") + if content_length is not None: + try: + content_length_value = int(content_length) + except ValueError: + response = _invalid_content_length_response(control_plane, request) + else: + if content_length_value > max_request_bytes: + response = _request_too_large_response(control_plane, request) + return response + + +async def _body_size_guard_response( + control_plane: RuntimeControlPlane, + request: Request, + *, + max_request_bytes: int, +) -> JSONResponse | None: + body = await request.body() + if len(body) > max_request_bytes: + return _request_too_large_response(control_plane, request) + request.state.raw_body = body + return None + + +def _request_too_large_response( + control_plane: RuntimeControlPlane, + request: Request, +) -> JSONResponse: + control_plane.record_audit( + action=request.method, + identity="anonymous", + allowed=False, + target=str(request.url.path), + reason=_REQUEST_TOO_LARGE_DETAIL, + ) + return JSONResponse(status_code=413, content={"detail": _REQUEST_TOO_LARGE_DETAIL}) + + +def _invalid_content_length_response( + control_plane: RuntimeControlPlane, + request: Request, +) -> JSONResponse: + control_plane.record_audit( + action=request.method, + identity="anonymous", + allowed=False, + target=str(request.url.path), + reason=_INVALID_CONTENT_LENGTH_DETAIL, + ) + return JSONResponse(status_code=400, content={"detail": _INVALID_CONTENT_LENGTH_DETAIL}) 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 1a62e8ea3..b3a69de2d 100644 --- a/implementations/python/packages/aces_runtime/control_plane_api_models.py +++ b/implementations/python/packages/aces_runtime/control_plane_api_models.py @@ -154,6 +154,10 @@ def _snapshot_model(envelope: RuntimeSnapshotEnvelope) -> RuntimeSnapshotEnvelop "participant_episode_results": dict(snapshot.participant_episode_results), "participant_episode_history": dict(snapshot.participant_episode_history), "participant_behavior_history": dict(snapshot.participant_behavior_history), + "shared_state_records": dict(snapshot.shared_state_records), + "shared_state_history": dict(snapshot.shared_state_history), + "joint_action_records": dict(snapshot.joint_action_records), + "time_management_contexts": dict(snapshot.time_management_contexts), "metadata": dict(snapshot.metadata), } ) diff --git a/implementations/python/packages/aces_runtime/control_plane_api_participant_retrieval.py b/implementations/python/packages/aces_runtime/control_plane_api_participant_retrieval.py new file mode 100644 index 000000000..5a51f6e58 --- /dev/null +++ b/implementations/python/packages/aces_runtime/control_plane_api_participant_retrieval.py @@ -0,0 +1,103 @@ +"""HTTP routes for API-408 participant retrieval views.""" + +from __future__ import annotations + +from typing import Annotated + +from aces_contracts.contracts import ( + ParticipantContextViewModel, + ParticipantHistoryViewModel, + ParticipantStatusViewModel, +) +from fastapi import Depends, FastAPI, HTTPException, Request + +from .control_plane import RuntimeControlPlane +from .control_plane_security import ControlPlaneIdentity + +_NOT_FOUND_RESPONSES = {404: {"description": "Not found"}} + + +def _read_identity_dependency(request: Request) -> ControlPlaneIdentity: + return request.app.state.control_plane_api_auth.read_identity(request) + + +_ReadIdentity = Annotated[ControlPlaneIdentity, Depends(_read_identity_dependency)] + + +def register_participant_retrieval_routes( + app: FastAPI, + control_plane: RuntimeControlPlane, +) -> None: + @app.get( + "/participants/{participant_address}/status", + responses=_NOT_FOUND_RESPONSES, + ) + async def get_participant_status_view( + participant_address: str, + request: Request, + identity: _ReadIdentity, + ) -> ParticipantStatusViewModel: + view = control_plane.get_participant_status_view(participant_address) + if view is None: + raise HTTPException(status_code=404, detail=f"Unknown participant: {participant_address}") + control_plane.record_audit( + action="get_participant_status_view", + identity=identity.identity, + allowed=True, + target=str(request.url.path), + ) + return view + + @app.get( + "/participants/{participant_address}/episodes/{episode_id}/history", + responses=_NOT_FOUND_RESPONSES, + ) + async def get_participant_history_view( + participant_address: str, + episode_id: str, + request: Request, + identity: _ReadIdentity, + ) -> ParticipantHistoryViewModel: + view = control_plane.get_participant_history_view(participant_address, episode_id) + if view is None: + raise HTTPException( + status_code=404, + detail=f"Unknown participant episode: {participant_address}/{episode_id}", + ) + control_plane.record_audit( + action="get_participant_history_view", + identity=identity.identity, + allowed=True, + target=str(request.url.path), + ) + return view + + @app.get( + "/participants/{participant_address}/context", + responses=_NOT_FOUND_RESPONSES, + ) + async def get_participant_context_view( + participant_address: str, + view_ref: str, + request: Request, + identity: _ReadIdentity, + episode_id: str | None = None, + derivation_basis_ref: str | None = None, + payload_ref: str | None = None, + ) -> ParticipantContextViewModel: + view = control_plane.get_participant_context_view( + participant_address, + view_ref=view_ref, + episode_id=episode_id, + derivation_basis_ref=derivation_basis_ref, + payload_ref=payload_ref, + ) + if view is None: + raise HTTPException(status_code=404, detail=f"Unknown participant: {participant_address}") + control_plane.record_audit( + action="get_participant_context_view", + identity=identity.identity, + allowed=True, + target=str(request.url.path), + ) + return view diff --git a/implementations/python/packages/aces_runtime/control_plane_execution.py b/implementations/python/packages/aces_runtime/control_plane_execution.py index 3b3026272..9b35fd008 100644 --- a/implementations/python/packages/aces_runtime/control_plane_execution.py +++ b/implementations/python/packages/aces_runtime/control_plane_execution.py @@ -36,12 +36,14 @@ def execute_participant_action( return existing operation_id = str(uuid4()) submitted_at = _utc_now() + target_address = getattr(request, "participant_address", "") status = OperationStatus( operation_id=operation_id, domain=RuntimeDomain.PARTICIPANT, state=OperationState.RUNNING, submitted_at=submitted_at, updated_at=submitted_at, + changed_addresses=[target_address] if isinstance(target_address, str) and target_address else [], ) receipt = OperationReceipt( operation_id=operation_id, diff --git a/implementations/python/packages/aces_runtime/control_plane_store.py b/implementations/python/packages/aces_runtime/control_plane_store.py index ef77293d4..bb650f513 100644 --- a/implementations/python/packages/aces_runtime/control_plane_store.py +++ b/implementations/python/packages/aces_runtime/control_plane_store.py @@ -3,6 +3,9 @@ from __future__ import annotations import json +import os +import tempfile +from contextlib import suppress from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Protocol @@ -95,6 +98,12 @@ def _snapshot_payload(snapshot: RuntimeSnapshot) -> dict[str, Any]: participant_address: list(events) for participant_address, events in snapshot.participant_behavior_history.items() }, + "shared_state_records": dict(snapshot.shared_state_records), + "shared_state_history": { + state_address: list(records) for state_address, records in snapshot.shared_state_history.items() + }, + "joint_action_records": dict(snapshot.joint_action_records), + "time_management_contexts": dict(snapshot.time_management_contexts), "realization_provenance": [ { "address": entry.address, @@ -142,6 +151,12 @@ def _snapshot_from_payload(payload: dict[str, Any]) -> RuntimeSnapshot: participant_address: list(events) for participant_address, events in payload.get("participant_behavior_history", {}).items() }, + shared_state_records=dict(payload.get("shared_state_records", {})), + shared_state_history={ + state_address: list(records) for state_address, records in payload.get("shared_state_history", {}).items() + }, + joint_action_records=dict(payload.get("joint_action_records", {})), + time_management_contexts=dict(payload.get("time_management_contexts", {})), realization_provenance=tuple( RealizationProvenanceEntry( address=str(item.get("address", "")), @@ -289,6 +304,19 @@ def __init__(self, base_dir: Path) -> None: self._operations_path = self._base_dir / "operations.json" self._audit_path = self._base_dir / "audit.jsonl" + @staticmethod + def _atomic_write(path: Path, content: str) -> None: + """Write content atomically via a temporary file and os.replace.""" + fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(content) + os.replace(tmp, path) + except BaseException: + with suppress(OSError): + os.unlink(tmp) + raise + def load_snapshot(self) -> RuntimeSnapshot: if not self._snapshot_path.exists(): return RuntimeSnapshot() @@ -296,10 +324,8 @@ def load_snapshot(self) -> RuntimeSnapshot: return _snapshot_from_payload(payload) def save_snapshot(self, snapshot: RuntimeSnapshot) -> None: - self._snapshot_path.write_text( - json.dumps(_snapshot_payload(snapshot), indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) + content = json.dumps(_snapshot_payload(snapshot), indent=2, sort_keys=True) + "\n" + self._atomic_write(self._snapshot_path, content) def load_records(self) -> dict[str, ControlPlaneOperationRecord]: if not self._operations_path.exists(): @@ -317,10 +343,8 @@ def save_record(self, record: ControlPlaneOperationRecord) -> None: payload = { operation_id: _record_payload(operation_record) for operation_id, operation_record in records.items() } - self._operations_path.write_text( - json.dumps(payload, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) + content = json.dumps(payload, indent=2, sort_keys=True) + "\n" + self._atomic_write(self._operations_path, content) def find_by_idempotency( self, diff --git a/implementations/python/packages/aces_runtime/participant_result_contracts.py b/implementations/python/packages/aces_runtime/participant_result_contracts.py index 350b6098e..e065eceac 100644 --- a/implementations/python/packages/aces_runtime/participant_result_contracts.py +++ b/implementations/python/packages/aces_runtime/participant_result_contracts.py @@ -7,7 +7,15 @@ iter_participant_behavior_snapshot_violations, iter_participant_runtime_history_transition_violations, ) +from aces_contracts.participant_concurrency import ( + iter_participant_concurrency_snapshot_violations, + iter_participant_concurrency_transition_violations, +) from aces_contracts.participant_episode import iter_participant_episode_snapshot_violations +from aces_contracts.participant_shared_state import ( + iter_participant_shared_state_history_transition_violations, + iter_participant_shared_state_snapshot_violations, +) from aces_contracts.runtime_state import RuntimeSnapshot from .diagnostics import _failure_diagnostic @@ -55,6 +63,19 @@ def participant_runtime_state_contract_diagnostics( participant_episode_history=snapshot.participant_episode_history, metadata=snapshot.metadata, ), + *iter_participant_shared_state_snapshot_violations( + snapshot.shared_state_records, + snapshot.shared_state_history, + participant_behavior_history=snapshot.participant_behavior_history, + metadata=snapshot.metadata, + ), + *iter_participant_concurrency_snapshot_violations( + snapshot.joint_action_records, + snapshot.time_management_contexts, + participant_behavior_history=snapshot.participant_behavior_history, + shared_state_records=snapshot.shared_state_records, + shared_state_history=snapshot.shared_state_history, + ), ] return [ _failure_diagnostic("runtime.backend-contract-invalid", address, message) for address, message in violations @@ -67,12 +88,30 @@ def participant_runtime_history_transition_diagnostics( ) -> list[Diagnostic]: """Validate append-only participant history preservation across an apply.""" - return [ - _failure_diagnostic("runtime.backend-contract-invalid", address, message) - for address, message in iter_participant_runtime_history_transition_violations( - previous_snapshot.participant_episode_history, - next_snapshot.participant_episode_history, - previous_snapshot.participant_behavior_history, - next_snapshot.participant_behavior_history, - ) - ] + return ( + [ + _failure_diagnostic("runtime.backend-contract-invalid", address, message) + for address, message in iter_participant_runtime_history_transition_violations( + previous_snapshot.participant_episode_history, + next_snapshot.participant_episode_history, + previous_snapshot.participant_behavior_history, + next_snapshot.participant_behavior_history, + ) + ] + + [ + _failure_diagnostic("runtime.backend-contract-invalid", address, message) + for address, message in iter_participant_shared_state_history_transition_violations( + previous_snapshot.shared_state_history, + next_snapshot.shared_state_history, + ) + ] + + [ + _failure_diagnostic("runtime.backend-contract-invalid", address, message) + for address, message in iter_participant_concurrency_transition_violations( + previous_snapshot.joint_action_records, + next_snapshot.joint_action_records, + previous_snapshot.time_management_contexts, + next_snapshot.time_management_contexts, + ) + ] + ) diff --git a/implementations/python/packages/aces_runtime/participant_retrieval.py b/implementations/python/packages/aces_runtime/participant_retrieval.py new file mode 100644 index 000000000..719d3d9b0 --- /dev/null +++ b/implementations/python/packages/aces_runtime/participant_retrieval.py @@ -0,0 +1,205 @@ +"""Participant retrieval projections for the runtime control plane.""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import UTC, datetime + +from aces_contracts.contracts import ( + ParticipantContextViewModel, + ParticipantHistoryViewModel, + ParticipantStatusViewModel, +) +from aces_contracts.planning import RuntimeDomain +from aces_contracts.runtime_state import OperationState, RuntimeSnapshot + +from .control_plane_store import ControlPlaneOperationRecord + +_CURRENT_SNAPSHOT_REF = "runtime.snapshot.current" + + +class ParticipantRetrievalMixin: + """API-408 participant retrieval projections over recorded runtime state.""" + + _snapshot: RuntimeSnapshot + _operations: dict[str, ControlPlaneOperationRecord] + + def get_participant_status_view(self, participant_address: str) -> ParticipantStatusViewModel | None: + if not _participant_exists(self._snapshot, participant_address): + return None + episode_state = self._snapshot.participant_episode_results.get(participant_address) + episode_id = _string_value(episode_state, "episode_id") if episode_state is not None else None + return ParticipantStatusViewModel.model_validate( + { + "view_id": _view_id("status", participant_address, episode_id), + "participant_address": participant_address, + "episode_id": episode_id, + "generated_at": _utc_now(), + "source_snapshot_ref": _CURRENT_SNAPSHOT_REF, + "episode_state": _project_scope(episode_state) if episode_state is not None else None, + "open_operation_refs": _open_participant_operation_refs(self._operations, participant_address), + "visibility_projection_ref": _visibility_projection_ref(participant_address, "status"), + "marking_definition_refs": [], + "redaction_policy_ref": None, + } + ) + + def get_participant_history_view( + self, + participant_address: str, + episode_id: str, + ) -> ParticipantHistoryViewModel | None: + if not _participant_episode_exists(self._snapshot, participant_address, episode_id): + return None + episode_history = [ + _project_scope(event) + for event in self._snapshot.participant_episode_history.get(participant_address, []) + if event.get("episode_id") == episode_id + ] + behavior_history = [ + _project_scope(event) + for event in self._snapshot.participant_behavior_history.get(participant_address, []) + if event.get("episode_id") == episode_id + ] + return ParticipantHistoryViewModel.model_validate( + { + "view_id": _view_id("history", participant_address, episode_id), + "participant_address": participant_address, + "episode_id": episode_id, + "generated_at": _utc_now(), + "source_snapshot_ref": _CURRENT_SNAPSHOT_REF, + "episode_history": episode_history, + "behavior_history": behavior_history, + "visibility_projection_ref": _visibility_projection_ref(participant_address, "history"), + "redaction_policy_ref": None, + "completeness": "complete", + "completeness_basis": None, + "marking_definition_refs": [], + } + ) + + def get_participant_context_view( + self, + participant_address: str, + *, + view_ref: str, + episode_id: str | None = None, + derivation_basis_ref: str | None = None, + payload_ref: str | None = None, + derived_from_refs: tuple[str, ...] = (), + ) -> ParticipantContextViewModel | None: + if episode_id is not None: + if not _participant_episode_exists(self._snapshot, participant_address, episode_id): + return None + elif not _participant_exists(self._snapshot, participant_address): + return None + source_refs = tuple(derived_from_refs or (_CURRENT_SNAPSHOT_REF,)) + source_ref = source_refs[0] + source_id = "source-snapshot" + resolved_observation_point = episode_id or _CURRENT_SNAPSHOT_REF + resolved_derivation_basis_ref = derivation_basis_ref or view_ref + return ParticipantContextViewModel.model_validate( + { + "view_id": _view_id("context", participant_address, episode_id or view_ref), + "participant_address": participant_address, + "episode_id": episode_id, + "generated_at": _utc_now(), + "source_snapshot_ref": _CURRENT_SNAPSHOT_REF, + "view_ref": view_ref, + "meaning_ref": view_ref, + "participant_scope": "participant_local", + "audience_scope": "participant_visible", + "observation_point": resolved_observation_point, + "derived_from_refs": list(source_refs), + "source_layers": [ + { + "source_id": source_id, + "source_layer": "source_snapshot", + "ref": source_ref, + "temporal_relation": "same_observation_point", + "observation_point": resolved_observation_point, + "evidence_refs": list(source_refs), + "provenance_refs": list(source_refs), + } + ], + "transformation": { + "transformation_rule_ref": resolved_derivation_basis_ref, + "description": "API-408 derived context view relation declared by the governed view reference", + "input_source_ids": [source_id], + "output_semantics_ref": view_ref, + }, + "comparability": { + "comparability_class": "portable_equivalent", + "comparison_basis_ref": f"comparability.{view_ref}", + "backend_disclosure_refs": [], + "limitations": [ + "Comparable only under the declared view, transformation, visibility, and evidence basis" + ], + }, + "evidence_refs": list(source_refs), + "provenance_refs": list(source_refs), + "semantic_limitations": [ + "Context-view payload remains a referenced derived view, not backend-private state" + ], + "derivation_basis_ref": derivation_basis_ref, + "payload_ref": payload_ref, + "visibility_projection_ref": _visibility_projection_ref(participant_address, "context"), + "marking_definition_refs": [], + "redaction_policy_ref": None, + } + ) + + +def _string_value(payload: dict[str, object] | None, key: str) -> str | None: + if payload is None: + return None + value = payload.get(key) + return value if isinstance(value, str) and value else None + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +def _participant_exists(snapshot: RuntimeSnapshot, participant_address: str) -> bool: + return ( + participant_address in snapshot.participant_episode_results + or participant_address in snapshot.participant_episode_history + or participant_address in snapshot.participant_behavior_history + ) + + +def _participant_episode_exists(snapshot: RuntimeSnapshot, participant_address: str, episode_id: str) -> bool: + episode_state = snapshot.participant_episode_results.get(participant_address) + if _string_value(episode_state, "episode_id") == episode_id: + return True + histories = ( + snapshot.participant_episode_history.get(participant_address, []), + snapshot.participant_behavior_history.get(participant_address, []), + ) + return any(event.get("episode_id") == episode_id for history in histories for event in history) + + +def _project_scope(payload: dict[str, object]) -> dict[str, object]: + return {key: value for key, value in payload.items() if key not in {"participant_address", "episode_id"}} + + +def _open_participant_operation_refs( + operations: Mapping[str, ControlPlaneOperationRecord], + participant_address: str, +) -> list[str]: + return [ + operation_id + for operation_id, record in sorted(operations.items()) + if record.status.domain == RuntimeDomain.PARTICIPANT + and record.status.state in {OperationState.ACCEPTED, OperationState.RUNNING} + and participant_address in record.status.changed_addresses + ] + + +def _view_id(kind: str, participant_address: str, suffix: str | None) -> str: + return f"runtime.participant-view.{kind}.{participant_address}.{suffix or 'current'}" + + +def _visibility_projection_ref(participant_address: str, kind: str) -> str: + return f"runtime.visibility-projection.{kind}.{participant_address}.v1" diff --git a/implementations/python/packages/aces_sdl/module_registry.py b/implementations/python/packages/aces_sdl/module_registry.py index a77530f42..9a404dd57 100644 --- a/implementations/python/packages/aces_sdl/module_registry.py +++ b/implementations/python/packages/aces_sdl/module_registry.py @@ -241,10 +241,13 @@ def _registry_base_url(registry: str, *, allow_insecure_http: bool) -> str: return f"https://{registry}".rstrip("/") +_HTTP_TIMEOUT_SECONDS = 30 + + def _json_request(url: str, *, headers: dict[str, str] | None = None) -> Any: request = Request(url, headers=headers or {}) try: - with urlopen(request) as response: # noqa: S310 - explicit OCI fetch + with urlopen(request, timeout=_HTTP_TIMEOUT_SECONDS) as response: return json.loads(response.read().decode("utf-8")) except (HTTPError, URLError, json.JSONDecodeError) as exc: raise SDLParseError(f"Failed to fetch OCI metadata from {url}: {exc}") from exc @@ -253,7 +256,7 @@ def _json_request(url: str, *, headers: dict[str, str] | None = None) -> Any: def _bytes_request(url: str, *, headers: dict[str, str] | None = None) -> bytes: request = Request(url, headers=headers or {}) try: - with urlopen(request) as response: # noqa: S310 - explicit OCI fetch + with urlopen(request, timeout=_HTTP_TIMEOUT_SECONDS) as response: return response.read() except (HTTPError, URLError) as exc: raise SDLParseError(f"Failed to fetch OCI blob from {url}: {exc}") from exc @@ -290,6 +293,37 @@ def _oci_cache_dir(base_dir: Path) -> Path: return base_dir / ".aces" / "module-cache" +def _safe_tar_members( + tar: tarfile.TarFile, + dest: Path, +) -> list[tarfile.TarInfo]: + """Validate every tar member before extraction (fail closed). + + The OCI bundle bytes are attacker-controlled even after registry allowlisting, + digest pinning, and signature verification, so this validation is the + filesystem-write boundary for module import resolution. It must hold on every + supported runtime, not just on Python 3.12+ where ``extractall(filter="data")`` + is available, because the PEP 706 ``filter`` keyword was backported only in + Python 3.11.4 while the project supports ``>=3.11``. Validation therefore + matches the ``data`` filter's guarantees: reject path traversal, symlinks, + hard links, and special files, and strip setuid/setgid/sticky bits. + """ + safe: list[tarfile.TarInfo] = [] + resolved_dest = dest.resolve() + for member in tar.getmembers(): + member_path = (dest / member.name).resolve() + if not member_path.is_relative_to(resolved_dest): + raise SDLParseError(f"Path traversal detected in OCI bundle tar member: {member.name!r}") + if member.issym() or member.islnk(): + raise SDLParseError(f"Links are not allowed in OCI bundle tar: {member.name!r}") + if not (member.isfile() or member.isdir()): + raise SDLParseError(f"Unsupported tar member type in OCI bundle: {member.name!r}") + # Drop setuid/setgid/sticky bits. + member.mode &= 0o777 + safe.append(member) + return safe + + def _extract_bundle_to_cache( *, bundle_bytes: bytes, @@ -298,16 +332,31 @@ def _extract_bundle_to_cache( base_dir: Path, ) -> Path: cache_dir = _oci_cache_dir(base_dir) / manifest_digest + if ".." in Path(root_file).parts or Path(root_file).is_absolute(): + raise SDLParseError(f"Invalid OCI root_file path: {root_file!r}") + resolved_cache = cache_dir.resolve() root_path = cache_dir / root_file - if root_path.exists(): - return root_path - cache_dir.mkdir(parents=True, exist_ok=True) - with tarfile.open(fileobj=io.BytesIO(bundle_bytes), mode="r:gz") as tar: - try: - tar.extractall(cache_dir, filter="data") - except TypeError: # pragma: no cover - Python < 3.12 fallback - tar.extractall(cache_dir) if not root_path.exists(): + cache_dir.mkdir(parents=True, exist_ok=True) + with tarfile.open(fileobj=io.BytesIO(bundle_bytes), mode="r:gz") as tar: + # Validate every member up front so the security property is identical on + # all supported runtimes and never depends on the runtime's tarfile filter + # support. ``filter="data"`` is applied as defense in depth where available + # (Python 3.11.4+/3.12+); on 3.11.0–3.11.3 the keyword is absent and the + # already-validated members are the guarantee. No path falls back to an + # unfiltered ``tar.extractall(cache_dir)``. + safe_members = _safe_tar_members(tar, cache_dir) + try: + tar.extractall(cache_dir, members=safe_members, filter="data") + # Python 3.11.0–3.11.3 lack the PEP 706 filter keyword. + except TypeError: + tar.extractall(cache_dir, members=safe_members) + # Enforce the root-file containment contract on EVERY return path, including + # the cache-hit fast path: a stale cache (e.g. one populated by an earlier + # unsafe extractor) could hold a symlink or a non-regular file at root_file + # that resolves outside the digest cache. Validating here fails closed + # regardless of whether extraction ran this call. + if not root_path.is_file() or not root_path.resolve().is_relative_to(resolved_cache): raise SDLParseError(f"Resolved OCI module bundle is missing declared root file '{root_file}'") return root_path @@ -399,6 +448,8 @@ def resolve_import( if source.startswith("local:"): relative = source.removeprefix("local:") import_path = (base_dir / relative).resolve() + if not import_path.is_relative_to(base_dir.resolve()): + raise SDLParseError(f"Local import path escapes base directory: {relative!r}") if not import_path.exists(): raise SDLParseError(f"Imported SDL file not found: {relative}") from .parser import _load_normalized_data @@ -585,6 +636,8 @@ def _collect_local_bundle_files( "publish a self-contained local module graph" ) child_path = (resolved.parent / source.removeprefix("local:")).resolve() + if not child_path.is_relative_to(resolved.parent): + raise SDLParseError(f"Local import path escapes base directory: {source!r}") files.update(_collect_local_bundle_files(child_path, seen=seen)) return files diff --git a/implementations/python/packages/aces_sdl/observability_plane_semantics.py b/implementations/python/packages/aces_sdl/observability_plane_semantics.py new file mode 100644 index 000000000..306fce4c2 --- /dev/null +++ b/implementations/python/packages/aces_sdl/observability_plane_semantics.py @@ -0,0 +1,194 @@ +"""Carrier-oriented observability/evidence plane classifier (SEM-224). + +ADR-066 and ``specs/formal/observability-evidence-plane.md`` define five named +planes for observability and evidence concerns. This module is the unifying, +*data-only* classifier #334 owns: it assigns exactly one primary plane to a +claim-bearing carrier by its contract role or runtime-family identity, never by +a free-form string such as ``log``, ``trace``, ``telemetry``, ``observation``, +or ``evidence`` (OE-11). + +It deliberately holds no runtime internals, inspects no backend-native DTOs, and +infers nothing from arbitrary text. The plane separation each carrier enforces +lives in the existing experiment-core, participant-runtime, and apparatus +contracts; this module is the single source of plane ownership and the source of +the portable ``x-aces-plane`` annotation published on the claim-bearing +contracts. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from enum import Enum + +from ._runtime_service_families import RUNTIME_SERVICE_FAMILIES, collect_qualified_runtime_family_refs + + +class ObservabilityEvidencePlane(str, Enum): + """The five named observability/evidence planes (ADR-066).""" + + SCENARIO_NATIVE_OBSERVABILITY = "scenario_native_observability" + AUTHORED_EVIDENCE_REQUIREMENT = "authored_evidence_requirement" + PROCESSOR_BACKEND_OPERATIONAL = "processor_backend_operational_observability" + CAPTURED_EVIDENCE = "captured_evidence" + DERIVED_ANALYSIS = "derived_analysis" + + +# Claim-bearing carriers whose contract role decides a single primary plane. +# Authored evidence requirement, captured evidence, and derived analysis are the +# three experiment-core carriers; processor/backend operational observability is +# carried by apparatus manifests, processor manifests, and apparatus context. +PLANE_BY_CONTRACT_ID: dict[str, ObservabilityEvidencePlane] = { + "experiment-capture-spec-v1": ObservabilityEvidencePlane.AUTHORED_EVIDENCE_REQUIREMENT, + "experiment-evidence-record-v1": ObservabilityEvidencePlane.CAPTURED_EVIDENCE, + "experiment-derived-measure-v1": ObservabilityEvidencePlane.DERIVED_ANALYSIS, + "backend-manifest-v2": ObservabilityEvidencePlane.PROCESSOR_BACKEND_OPERATIONAL, + "processor-manifest-v2": ObservabilityEvidencePlane.PROCESSOR_BACKEND_OPERATIONAL, + "experiment-apparatus-context-v1": ObservabilityEvidencePlane.PROCESSOR_BACKEND_OPERATIONAL, +} + +# Contracts whose ``x-aces-plane`` annotation is published as portable +# traceability (the three experiment-core carriers that map 1:1 to a plane). +PLANE_ANNOTATED_CONTRACT_IDS: tuple[str, ...] = ( + "experiment-capture-spec-v1", + "experiment-evidence-record-v1", + "experiment-derived-measure-v1", +) + +# Scenario-native observability systems are in-world SDL runtime families +# (``specs/sdl/observability-and-evidence.md``). Names are validated against the +# canonical runtime-family registry so a rename fails closed rather than letting +# the classifier silently drift. +_SCENARIO_NATIVE_FAMILY_NAMES: tuple[str, ...] = ( + "network_sensors", + "network_detection_engines", + "security_monitoring_managers", + "forwarding_agents", + "service_listeners", + "platform_applications", + "datastore_services", +) + +_REGISTERED_FAMILY_NAMES = frozenset(family.collection_name for family in RUNTIME_SERVICE_FAMILIES) + + +def _validate_scenario_native_families(names: tuple[str, ...], registered: frozenset[str]) -> frozenset[str]: + """Fail closed if a scenario-native family name is not a registered runtime family. + + Keeps the classifier from silently drifting when a runtime family is renamed. + """ + + unregistered = frozenset(names) - registered + if unregistered: + raise RuntimeError( + "SEM-224 scenario-native observability families are not registered in " + f"RUNTIME_SERVICE_FAMILIES: {sorted(unregistered)}" + ) + return frozenset(names) + + +SCENARIO_NATIVE_OBSERVABILITY_FAMILIES: frozenset[str] = _validate_scenario_native_families( + _SCENARIO_NATIVE_FAMILY_NAMES, _REGISTERED_FAMILY_NAMES +) + +# Strings the ADR calls out as ambiguous: they appear across multiple planes and +# therefore never decide ownership on their own (OE-11). +AMBIGUOUS_PLANE_TOKENS: frozenset[str] = frozenset( + { + "log", + "logs", + "trace", + "traces", + "telemetry", + "observation", + "observations", + "evidence", + "monitor", + "monitoring", + "metric", + "metrics", + } +) + +# OE-11 made structural: the set of strings authorized to decide a plane is empty +# by construction. Plane ownership is a carrier-role decision, never a token one. +_PLANE_DECIDING_TOKENS: frozenset[str] = frozenset() + + +def classify_contract_plane(contract_id: str) -> ObservabilityEvidencePlane: + """Return the single primary plane for a registered claim-bearing carrier. + + Raises ``ValueError`` for an unregistered carrier: plane ownership is decided + by carrier role, never inferred. + """ + + try: + return PLANE_BY_CONTRACT_ID[contract_id] + except KeyError: + raise ValueError( + f"no observability/evidence plane is registered for carrier '{contract_id}'; " + "plane ownership is decided by carrier role, not inferred" + ) from None + + +def classify_runtime_family(collection_name: str) -> ObservabilityEvidencePlane: + """Classify an SDL runtime family as scenario-native observability. + + Raises ``ValueError`` for a runtime family that is not a scenario-native + observability surface. + """ + + if collection_name in SCENARIO_NATIVE_OBSERVABILITY_FAMILIES: + return ObservabilityEvidencePlane.SCENARIO_NATIVE_OBSERVABILITY + raise ValueError(f"runtime family '{collection_name}' is not a scenario-native observability surface") + + +def collect_scenario_native_observability_refs(scenario: object) -> set[str]: + """Return targetable refs for scenario-native observability runtime families. + + This is a filtered view over the canonical runtime-family ref collector. It + gives DSL-123 callers an explicit observability surface without creating a + second resolver or registry. + """ + + return collect_qualified_runtime_family_refs( + scenario, + family_keys=SCENARIO_NATIVE_OBSERVABILITY_FAMILIES, + ) + + +def assert_single_primary_plane( + planes: Iterable[ObservabilityEvidencePlane], +) -> ObservabilityEvidencePlane: + """Enforce OE-01: a claim-bearing artifact has exactly one primary plane.""" + + distinct = set(planes) + if len(distinct) != 1: + observed = sorted(plane.value for plane in distinct) + raise ValueError( + f"a claim-bearing observability/evidence artifact must have exactly one primary plane, got {observed}" + ) + return next(iter(distinct)) + + +def token_decides_plane(token: str) -> bool: + """OE-11: a bare string never decides plane ownership; the carrier does. + + The set of plane-deciding tokens is empty by construction, so the answer is + always ``False`` -- plane ownership comes from the carrier role, not a token. + """ + + return token in _PLANE_DECIDING_TOKENS + + +__all__ = [ + "AMBIGUOUS_PLANE_TOKENS", + "PLANE_ANNOTATED_CONTRACT_IDS", + "PLANE_BY_CONTRACT_ID", + "SCENARIO_NATIVE_OBSERVABILITY_FAMILIES", + "ObservabilityEvidencePlane", + "assert_single_primary_plane", + "classify_contract_plane", + "classify_runtime_family", + "collect_scenario_native_observability_refs", + "token_decides_plane", +] diff --git a/implementations/python/pyproject.toml b/implementations/python/pyproject.toml index a41f49be8..b14d34e36 100644 --- a/implementations/python/pyproject.toml +++ b/implementations/python/pyproject.toml @@ -47,6 +47,7 @@ packages = [ "packages/aces_contracts", "packages/aces_backend_protocols", "packages/aces_backend_stubs", + "packages/aces_reference_backend", "packages/aces_cli", "packages/aces_conformance", "packages/aces_mcp", @@ -78,8 +79,9 @@ pythonpath = ["src", "packages", "tests"] markers = [ "fuzz: property-based fuzz tests (run with: pytest -m fuzz)", "integration: integration-style tests that read the real repo on disk (run with: pytest -m integration)", + "docker: opt-in tests that require a real container runtime (run with: pytest -m docker / nox -s integration_docker)", ] -addopts = "-m 'not fuzz and not integration'" +addopts = "-m 'not fuzz and not integration and not docker'" [tool.coverage.run] source = [ @@ -87,6 +89,7 @@ source = [ "aces_contracts", "aces_backend_protocols", "aces_backend_stubs", + "aces_reference_backend", "aces_cli", "aces_conformance", "aces_mcp", @@ -97,6 +100,12 @@ source = [ [tool.coverage.report] show_missing = true +exclude_also = [ + # The reference backend's OCI subprocess leaf (RUN-314): real container + # IO, exercised only by the opt-in docker integration tests, never in the + # hermetic suite. + "def _default_runner", +] [tool.coverage.xml] output = "coverage.xml" diff --git a/implementations/python/tests/test_backend_manifest.py b/implementations/python/tests/test_backend_manifest.py index c776a01d6..af7db3480 100644 --- a/implementations/python/tests/test_backend_manifest.py +++ b/implementations/python/tests/test_backend_manifest.py @@ -3,18 +3,25 @@ from __future__ import annotations import json +from dataclasses import replace from pathlib import Path import pytest from aces_backend_protocols.capabilities import ( + OBSERVATION_CAPABILITY_CAPTURE_KIND_SCOPE, + OBSERVATION_CAPABILITY_CHANNEL_KIND_SCOPE, + OBSERVATION_CAPABILITY_SEALING_MODE_SCOPE, PARTICIPANT_RUNTIME_BEHAVIOR_FEATURE_SCOPE, PARTICIPANT_RUNTIME_CAPABILITY_REQUIRED_CONTRACTS, PARTICIPANT_RUNTIME_INTERACTION_FEATURE_SCOPE, PARTICIPANT_RUNTIME_ROLE_SCOPE, BackendManifest, + ObservationCapabilities, OrchestratorCapabilities, + ParticipantFeatureSupport, ParticipantRuntimeCapabilities, ProvisionerCapabilities, + observation_capability_contract_gaps, participant_runtime_capability_contract_gaps, ) from aces_backend_protocols.manifest import backend_manifest_payload @@ -125,6 +132,7 @@ def test_backend_manifest_v2_declares_participant_capability_dimensions(): "interference", "shared_state_change", ] + assert participant_runtime["feature_support"] == [] model = BackendManifestV2Model.model_validate(payload) assert model.capabilities.participant_runtime is not None @@ -136,6 +144,42 @@ def test_backend_manifest_v2_declares_participant_capability_dimensions(): ] +def test_backend_manifest_v2_declares_observation_capability_dimensions(): + """EXP-715: observation and evidence-collection support is a separate + backend capability block, not an execution or evaluator side effect.""" + + payload = backend_manifest_payload(create_stub_manifest()) + observation = payload["capabilities"]["observation"] + + assert observation["supported_capture_kinds"] == ["artifact", "log", "observation", "telemetry", "trace"] + assert observation["supported_channel_kinds"] == [ + "backend-log", + "evaluation-history", + "file-artifact", + "participant-observation", + "runtime-snapshot", + "workflow-history", + ] + assert observation["supported_evidence_contracts"] == [ + "experiment-capture-spec-v1", + "experiment-derived-measure-v1", + "experiment-evidence-record-v1", + ] + assert observation["supported_sealing_modes"] == ["digest", "immutable-store"] + assert observation["supports_redaction"] is True + assert observation["supports_loss_disclosure"] is True + + model = BackendManifestV2Model.model_validate(payload) + assert model.capabilities.observation is not None + assert model.capabilities.observation.supported_capture_kinds == [ + "artifact", + "log", + "observation", + "telemetry", + "trace", + ] + + def test_backend_manifest_without_participant_runtime_declares_no_participant_runtime_surface(): payload = backend_manifest_payload(create_stub_manifest(with_participant_runtime=False)) @@ -144,6 +188,58 @@ def test_backend_manifest_without_participant_runtime_declares_no_participant_ru assert model.capabilities.participant_runtime is None +def test_backend_manifest_without_observation_declares_no_observation_surface(): + payload = backend_manifest_payload(create_stub_manifest(with_observation=False)) + + assert payload["capabilities"]["observation"] is None + model = BackendManifestV2Model.model_validate(payload) + assert model.capabilities.observation is None + + +def test_observation_capabilities_validate_exp_715_vocabularies(): + capability = ObservationCapabilities( + name="observation", + supported_capture_kinds=frozenset({"observation", "x-acme:custom-capture"}), + supported_channel_kinds=frozenset({"participant-observation", "x-acme:custom-channel"}), + supported_evidence_contracts=frozenset({"experiment-evidence-record-v1"}), + supported_media_types=frozenset({"application/json"}), + supported_sealing_modes=frozenset({"digest", "x-acme:attested-store"}), + supports_redaction=True, + supports_loss_disclosure=True, + ) + + assert "x-acme:custom-capture" in capability.supported_capture_kinds + assert "x-acme:custom-channel" in capability.supported_channel_kinds + assert "x-acme:attested-store" in capability.supported_sealing_modes + + with pytest.raises(ValueError, match="observation-capture-kinds"): + ObservationCapabilities( + name="observation", + supported_capture_kinds=frozenset({"custom_capture"}), + supported_channel_kinds=frozenset({"participant-observation"}), + supported_evidence_contracts=frozenset({"experiment-evidence-record-v1"}), + supported_media_types=frozenset({"application/json"}), + supported_sealing_modes=frozenset({"digest"}), + ) + + +def test_observation_capability_claims_require_published_contract_evidence(): + manifest = create_stub_manifest() + weak_manifest = BackendManifest( + identity=manifest.identity, + supported_contract_versions=manifest.supported_contract_versions - frozenset({"experiment-evidence-record-v1"}), + compatibility=manifest.compatibility, + realization_support=manifest.realization_support, + concept_bindings=manifest.concept_bindings, + constraints=manifest.constraints, + capabilities=manifest.capabilities, + ) + + assert observation_capability_contract_gaps(manifest) == () + gaps = observation_capability_contract_gaps(weak_manifest) + assert any("experiment-evidence-record-v1" in gap for gap in gaps) + + @pytest.mark.parametrize( "field_name", [ @@ -189,6 +285,89 @@ def test_participant_runtime_capabilities_validate_api_405_vocabularies(): ) +def test_participant_feature_support_validates_api_407_declarations(): + declaration = ParticipantFeatureSupport( + feature="coordination", + support_level=ParticipantFeatureSupportLevel.EXACT, + ) + + assert declaration.support_level == ParticipantFeatureSupportLevel.EXACT + + with pytest.raises(ValueError, match="governed participant behavior or interaction feature"): + ParticipantFeatureSupport( + feature="custom_feature", + support_level=ParticipantFeatureSupportLevel.EXACT, + ) + + with pytest.raises(ValueError, match="disclosure_refs"): + ParticipantFeatureSupport( + feature="coordination", + support_level=ParticipantFeatureSupportLevel.BOUNDED, + ) + + unsupported_declaration = ParticipantFeatureSupport( + feature="coordination", + support_level=ParticipantFeatureSupportLevel.UNSUPPORTED, + disclosure_refs=("disclosures.coordination.unsupported.v1",), + ) + with pytest.raises(ValueError, match="supported feature unsupported"): + ParticipantRuntimeCapabilities( + name="participant-runtime", + supported_participant_roles=frozenset({"blue"}), + supported_behavior_features=frozenset({"action_contracts"}), + supported_interaction_features=frozenset({"coordination"}), + feature_support=(unsupported_declaration,), + ) + + +def test_backend_manifest_payload_renders_api_407_feature_support_entries(): + manifest = create_stub_manifest() + assert manifest.participant_runtime is not None + participant_runtime = replace( + manifest.participant_runtime, + feature_support=( + ParticipantFeatureSupport( + feature="behavior_history", + support_level=ParticipantFeatureSupportLevel.BOUNDED, + constraint_refs=("constraints.behavior-history.retention-window",), + disclosure_refs=("disclosures.behavior-history.bounded.v1",), + ), + ParticipantFeatureSupport( + feature="coordination", + support_level=ParticipantFeatureSupportLevel.EXACT, + ), + ), + ) + capabilities = replace(manifest.capabilities, participant_runtime=participant_runtime) + manifest = BackendManifest( + identity=manifest.identity, + supported_contract_versions=manifest.supported_contract_versions, + compatibility=manifest.compatibility, + realization_support=manifest.realization_support, + concept_bindings=manifest.concept_bindings, + constraints=manifest.constraints, + capabilities=capabilities, + ) + + payload = backend_manifest_payload(manifest) + + assert payload["capabilities"]["participant_runtime"]["feature_support"] == [ + { + "feature": "behavior_history", + "support_level": "bounded", + "constraint_refs": ["constraints.behavior-history.retention-window"], + "disclosure_refs": ["disclosures.behavior-history.bounded.v1"], + }, + { + "feature": "coordination", + "support_level": "exact", + "constraint_refs": [], + "disclosure_refs": [], + }, + ] + BackendManifestV2Model.model_validate(payload) + + def test_participant_runtime_capability_evidence_covers_standard_vocabularies(): catalog_path = FIXTURES_ROOT / "concept-authority" / "controlled-vocabularies-v1" / "valid" / "reference.json" catalog = json.loads(catalog_path.read_text(encoding="utf-8")) @@ -213,6 +392,18 @@ def test_participant_runtime_capability_evidence_covers_standard_vocabularies(): ) +def test_observation_capability_evidence_covers_standard_vocabularies(): + catalog_path = FIXTURES_ROOT / "concept-authority" / "controlled-vocabularies-v1" / "valid" / "reference.json" + catalog = json.loads(catalog_path.read_text(encoding="utf-8")) + scopes = { + scope for definition in catalog["vocabularies"].values() for scope in definition.get("governed_scopes", ()) + } + + assert OBSERVATION_CAPABILITY_CAPTURE_KIND_SCOPE in scopes + assert OBSERVATION_CAPABILITY_CHANNEL_KIND_SCOPE in scopes + assert OBSERVATION_CAPABILITY_SEALING_MODE_SCOPE in scopes + + def test_participant_runtime_capability_claims_require_published_contract_evidence(): manifest = create_stub_manifest() weak_manifest = BackendManifest( @@ -621,6 +812,6 @@ def test_backend_manifest_v2_rejects_duplicate_binding_scopes(): def test_backend_manifest_v2_concept_bindings_roundtrip(): payload = json.loads((V2_VALID_DIR / "stub.json").read_text(encoding="utf-8")) model = BackendManifestV2Model.model_validate(payload) - assert len(model.concept_bindings) == 9 + assert len(model.concept_bindings) == 12 assert model.concept_bindings[0].scope == "capabilities.provisioner.supported_node_types" assert model.concept_bindings[0].family == "assets" diff --git a/implementations/python/tests/test_dsl_123_scenario_native_observability.py b/implementations/python/tests/test_dsl_123_scenario_native_observability.py new file mode 100644 index 000000000..223d056c1 --- /dev/null +++ b/implementations/python/tests/test_dsl_123_scenario_native_observability.py @@ -0,0 +1,143 @@ +"""DSL-123 scenario-native observability SDL semantics.""" + +from __future__ import annotations + +import pytest +from aces_sdl._errors import SDLValidationError +from aces_sdl._runtime_service_families import RUNTIME_SERVICE_FAMILIES, collect_qualified_runtime_family_refs +from aces_sdl.observability_plane_semantics import ( + SCENARIO_NATIVE_OBSERVABILITY_FAMILIES, + ObservabilityEvidencePlane, + classify_runtime_family, + collect_scenario_native_observability_refs, +) +from aces_sdl.scenario import Scenario +from aces_sdl.validator import SemanticValidator + +OBSERVABILITY_REF = "nodes.siem.runtime.service_listeners.siem-http" +NON_OBSERVABILITY_REF = "nodes.siem.runtime.applications.admin-ui" + + +def _scenario(*, objective_target: str = OBSERVABILITY_REF, interaction_target: str = OBSERVABILITY_REF) -> Scenario: + return Scenario( + name="dsl-123", + nodes={ + "siem": { + "type": "vm", + "resources": {"ram": "1 gib", "cpu": 1}, + "services": [{"port": 80, "protocol": "tcp", "name": "http"}], + "runtime": { + "service_listeners": [ + { + "service_listener_id": "siem-http", + "service": "http", + "address": "0.0.0.0", # noqa: S104 - wildcard bind is test data. + "port": 80, + "protocol": "tcp", + "address_family": "ipv4", + "scope": "wildcard", + } + ], + "applications": [{"application_id": "admin-ui", "service": "http"}], + }, + } + }, + entities={"blue": {"role": "blue"}}, + conditions={"observability-ready": {"command": "/bin/true", "interval": 15}}, + relationships={ + "dashboard-depends-on-listener": { + "type": "depends_on", + "source": "nodes.siem", + "target": OBSERVABILITY_REF, + } + }, + action_contracts={ + "inspect-dashboard": { + "semantic_version": "1.0.0", + "behavioral_granularity": "atomic", + "procedure_basis": "participant inspects an in-world monitoring endpoint", + "realization_profile": "backend-declared", + "fidelity_claim": "records the participant interaction target only", + "preconditions": [ + { + "precondition_id": "authority-in-scope", + "precondition_class": "authority", + "description": "the participant is allowed to inspect the monitoring endpoint", + "support_refs": ["agents.blue-agent", interaction_target], + } + ], + "effects": [ + { + "effect_id": "observability-endpoint-inspected", + "effect_class": "intended_effect", + "description": "the participant inspects the in-world observability endpoint", + "target_refs": [interaction_target], + } + ], + "failure_classes": ["precondition_unsatisfied", "target_unavailable"], + "interactions": [ + { + "interaction_class": "shared_state_change", + "target": interaction_target, + "rationale": "the participant interacts with the in-world observability service", + "shared_state_refs": [interaction_target], + } + ], + } + }, + agents={"blue-agent": {"entity": "blue", "actions": ["inspect-dashboard"]}}, + objectives={ + "inspect-observability": { + "entity": "blue", + "actions": ["inspect-dashboard"], + "targets": [objective_target], + "success": {"conditions": ["observability-ready"]}, + } + }, + ) + + +def _validate(scenario: Scenario) -> list[str]: + validator = SemanticValidator(scenario) + try: + validator.validate() + return [] + except SDLValidationError as exc: + return exc.errors + + +def test_dsl_123_exposes_scenario_native_observability_refs_without_second_resolver() -> None: + registered = {family.collection_name for family in RUNTIME_SERVICE_FAMILIES} + assert registered >= SCENARIO_NATIVE_OBSERVABILITY_FAMILIES + assert classify_runtime_family("service_listeners") is ObservabilityEvidencePlane.SCENARIO_NATIVE_OBSERVABILITY + + scenario = _scenario() + all_runtime_refs = collect_qualified_runtime_family_refs(scenario) + observability_refs = collect_scenario_native_observability_refs(scenario) + + assert OBSERVABILITY_REF in observability_refs + assert NON_OBSERVABILITY_REF in all_runtime_refs + assert NON_OBSERVABILITY_REF not in observability_refs + assert observability_refs <= all_runtime_refs + + +def test_dsl_123_observability_refs_are_targetable_relationship_objective_and_action_refs() -> None: + assert _validate(_scenario()) == [] + + +@pytest.mark.parametrize( + ("field", "expected"), + [ + ("objective", "Objective 'inspect-observability' target 'siem-http' does not reference any defined"), + ("interaction", "Action contract 'inspect-dashboard' interaction[0] target 'siem-http' does not reference"), + ], +) +def test_dsl_123_observability_refs_do_not_resolve_by_bare_runtime_id(field: str, expected: str) -> None: + if field == "objective": + scenario = _scenario(objective_target="siem-http") + else: + scenario = _scenario(interaction_target="siem-http") + + errors = _validate(scenario) + + assert any(expected in error for error in errors) diff --git a/implementations/python/tests/test_participant_backend_contracts.py b/implementations/python/tests/test_participant_backend_contracts.py index 41f83797f..a0bb5e6b5 100644 --- a/implementations/python/tests/test_participant_backend_contracts.py +++ b/implementations/python/tests/test_participant_backend_contracts.py @@ -15,12 +15,14 @@ ParticipantHistoryViewBehaviorEventModel, ParticipantHistoryViewEpisodeEventModel, ParticipantHistoryViewModel, + ParticipantJointActionRecordModel, ParticipantLifecycleEventModel, ParticipantObservationEnvelopeModel, ParticipantOutcomeReportModel, ParticipantSharedStateRecordModel, ParticipantStatusViewEpisodeStateModel, ParticipantStatusViewModel, + ParticipantTimeManagementContextModel, schema_bundle, ) from jsonschema import Draft202012Validator @@ -33,6 +35,8 @@ "participant-lifecycle-event-v1": ParticipantLifecycleEventModel, "participant-observation-envelope-v1": ParticipantObservationEnvelopeModel, "participant-shared-state-record-v1": ParticipantSharedStateRecordModel, + "participant-joint-action-record-v1": ParticipantJointActionRecordModel, + "participant-time-management-context-v1": ParticipantTimeManagementContextModel, "participant-outcome-report-v1": ParticipantOutcomeReportModel, } CONTROL_PLANE_VIEW_FIXTURE_MODELS = { @@ -136,6 +140,15 @@ def test_participant_outcome_report_publishes_no_score_or_reward_surface(): source_schema = schema["$defs"]["ParticipantOutcomeReportSourceModel"] assert source_schema["properties"]["source_kind"]["enum"] == ["action_result", "episode_status", "evidence"] assert schema["properties"]["outcome_sources"]["minItems"] == 1 + assert schema["properties"]["state_relationships"]["minItems"] == 1 + + +def test_participant_outcome_report_requires_state_relationships(): + payload = _valid_fixture("participant-outcome-report-v1") + payload["state_relationships"] = [] + + with pytest.raises(ValidationError, match="state_relationships"): + ParticipantOutcomeReportModel.model_validate(payload) def test_participant_history_view_schema_requires_completeness_basis_when_not_complete(): @@ -166,6 +179,22 @@ def test_participant_views_reuse_published_episode_shapes(): ) context_schema = generated["participant-context-view-v1"] assert context_schema["properties"]["derived_from_refs"]["minItems"] == 1 + assert context_schema["properties"]["source_layers"]["minItems"] == 1 + assert context_schema["properties"]["evidence_refs"]["minItems"] == 1 + assert context_schema["properties"]["provenance_refs"]["minItems"] == 1 + assert context_schema["properties"]["semantic_limitations"]["minItems"] == 1 + assert { + "meaning_ref", + "participant_scope", + "audience_scope", + "observation_point", + "source_layers", + "transformation", + "comparability", + "evidence_refs", + "provenance_refs", + "semantic_limitations", + } <= set(context_schema["required"]) def test_participant_backend_contract_valid_fixtures_pass_schema_and_model_validation(): @@ -229,9 +258,11 @@ def _projected_field_parity(source_cls, projected_cls): def test_view_projected_models_track_recorded_contract_shapes(): - _projected_field_parity(ParticipantEpisodeStateModel, ParticipantStatusViewEpisodeStateModel) - _projected_field_parity(ParticipantEpisodeHistoryEventModel, ParticipantHistoryViewEpisodeEventModel) - _projected_field_parity(ParticipantBehaviorHistoryEventModel, ParticipantHistoryViewBehaviorEventModel) + assert _projected_field_parity(ParticipantEpisodeStateModel, ParticipantStatusViewEpisodeStateModel) is None + assert _projected_field_parity(ParticipantEpisodeHistoryEventModel, ParticipantHistoryViewEpisodeEventModel) is None + assert ( + _projected_field_parity(ParticipantBehaviorHistoryEventModel, ParticipantHistoryViewBehaviorEventModel) is None + ) def test_participant_status_view_rejects_episode_state_restating_scope(): @@ -296,7 +327,8 @@ def _history_payload_with_action_result(participant_address: str, episode_id: st def test_participant_history_view_accepts_in_scope_nested_records(): payload = _history_payload_with_action_result("participants.blue.rl", "ep-blue-002") - ParticipantHistoryViewModel.model_validate(payload) + view = ParticipantHistoryViewModel.model_validate(payload) + assert view.behavior_history[0].action_result is not None def test_participant_history_view_rejects_nested_action_result_for_another_participant(): @@ -403,6 +435,40 @@ def test_participant_context_view_requires_source_snapshot_ref(): ParticipantContextViewModel.model_validate(payload) +def test_participant_context_view_rejects_hidden_or_global_source_layers(): + payload = _valid_fixture("participant-context-view-v1") + payload["source_layers"][0]["source_layer"] = "global_runtime_state" + + with pytest.raises(ValidationError, match="source_layer"): + ParticipantContextViewModel.model_validate(payload) + + +def test_participant_context_view_rejects_future_state_sources(): + payload = _valid_fixture("participant-context-view-v1") + payload["source_layers"][0]["temporal_relation"] = "future_state" + + with pytest.raises(ValidationError, match="temporal_relation"): + ParticipantContextViewModel.model_validate(payload) + + +def test_participant_context_view_bounded_staleness_requires_basis(): + payload = _valid_fixture("participant-context-view-v1") + payload["source_layers"][0]["temporal_relation"] = "bounded_staleness" + payload["source_layers"][0].pop("freshness_basis_ref", None) + + with pytest.raises(ValidationError, match="freshness_basis_ref"): + ParticipantContextViewModel.model_validate(payload) + + +def test_participant_context_view_weak_comparability_requires_backend_disclosure(): + payload = _valid_fixture("participant-context-view-v1") + payload["comparability"]["comparability_class"] = "portable_with_disclosed_weakening" + payload["comparability"]["backend_disclosure_refs"] = [] + + with pytest.raises(ValidationError, match="backend_disclosure_refs"): + ParticipantContextViewModel.model_validate(payload) + + def test_participant_lifecycle_event_rejects_unknown_mapping_loss(): payload = _valid_fixture("participant-lifecycle-event-v1") payload["mapping_loss"] = "collapsed" @@ -425,3 +491,22 @@ def test_participant_shared_state_record_rejects_unknown_conflict_policy(): with pytest.raises(ValidationError, match="conflict_policy"): ParticipantSharedStateRecordModel.model_validate(payload) + + +def test_participant_joint_action_record_rejects_implicit_last_writer_wins(): + payload = _valid_fixture("participant-joint-action-record-v1") + payload["conflict_class"] = "none" + payload["conflict_policy"] = "none" + payload["realized_order"] = [] + + with pytest.raises(ValidationError, match="conflict_class"): + ParticipantJointActionRecordModel.model_validate(payload) + + +def test_participant_time_management_context_rejects_timestamp_only_exact_claim(): + payload = _valid_fixture("participant-time-management-context-v1") + payload["basis"] = "wall_clock_only" + payload["claim_strength"] = "exact" + + with pytest.raises(ValidationError, match="wall_clock_only"): + ParticipantTimeManagementContextModel.model_validate(payload) diff --git a/implementations/python/tests/test_reference_backend_components.py b/implementations/python/tests/test_reference_backend_components.py new file mode 100644 index 000000000..3c88cef8c --- /dev/null +++ b/implementations/python/tests/test_reference_backend_components.py @@ -0,0 +1,110 @@ +"""RUN-314: orchestrator/evaluator/participant lifecycle via control plane.""" + +from __future__ import annotations + +import textwrap + +from aces_contracts.participant_episode import ( + ParticipantEpisodeTerminalReason, + iter_participant_episode_snapshot_violations, +) +from aces_reference_backend import create_reference_backend_target + +from aces.core.runtime.control_plane import RuntimeControlPlane +from aces.core.runtime.manager import RuntimeManager +from aces.core.sdl import parse_sdl + +_SCENARIO = """ +name: ref-components +nodes: + vm: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} + conditions: {health: ops} + roles: {ops: operator} +conditions: + health: {command: /bin/true, interval: 15} +entities: + blue: {role: blue} +objectives: + validate: + entity: blue + success: {conditions: [health]} +workflows: + response: + start: run + steps: + run: + type: objective + objective: validate + on-success: finish + finish: {type: end} +""" + + +def _control_plane(): + target = create_reference_backend_target() + manager = RuntimeManager(target) + execution_plan = manager.plan(parse_sdl(textwrap.dedent(_SCENARIO))) + control_plane = RuntimeControlPlane(target) + return target, control_plane, execution_plan + + +def test_orchestrator_start_records_workflow_result_and_history(): + target, control_plane, execution_plan = _control_plane() + control_plane.submit_provisioning(execution_plan.provisioning) + + receipt = control_plane.submit_orchestration(execution_plan.orchestration) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None and status.state.value == "succeeded" + assert control_plane.snapshot.orchestration_results + assert control_plane.snapshot.orchestration_history + + +def test_evaluator_start_records_results_and_history(): + target, control_plane, execution_plan = _control_plane() + control_plane.submit_provisioning(execution_plan.provisioning) + + receipt = control_plane.submit_evaluation(execution_plan.evaluation) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None and status.state.value == "succeeded" + assert control_plane.snapshot.evaluation_results + + +def test_participant_lifecycle_satisfies_run311_invariants(): + target, control_plane, execution_plan = _control_plane() + address = "participant.alice" + + control_plane.initialize_participant_episode(address) + control_plane.reset_participant_episode(address) + control_plane.terminate_participant_episode( + address, + terminal_reason=ParticipantEpisodeTerminalReason.COMPLETED, + ) + control_plane.restart_participant_episode(address) + + snapshot = control_plane.snapshot + assert snapshot.participant_episode_results + assert snapshot.participant_episode_history + violations = list( + iter_participant_episode_snapshot_violations( + snapshot.participant_episode_results, + snapshot.participant_episode_history, + ) + ) + assert violations == [] + + +def test_participant_initialize_then_reinitialize_is_rejected(): + target, control_plane, execution_plan = _control_plane() + address = "participant.bob" + + control_plane.initialize_participant_episode(address) + receipt = control_plane.initialize_participant_episode(address) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None + assert status.state.value != "succeeded" diff --git a/implementations/python/tests/test_reference_backend_conformance.py b/implementations/python/tests/test_reference_backend_conformance.py new file mode 100644 index 000000000..c6b6af856 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_conformance.py @@ -0,0 +1,50 @@ +"""RUN-314: acceptance bar -- full-profile conformance for the reference target.""" + +from __future__ import annotations + +from aces_reference_backend import create_reference_backend_target + +from aces.core.runtime.conformance import ( + BackendCapabilityProfile, + run_target_conformance, +) + + +def test_reference_target_passes_full_remote_control_plane_conformance(): + report = run_target_conformance(create_reference_backend_target()) + + assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE + assert report.passed is True, [diag.message for diag in report.diagnostics] + assert not report.unsupported_contract_gaps + assert not report.unsupported_capability_gaps + + +def test_reference_target_drives_full_participant_probe_case_set(): + report = run_target_conformance(create_reference_backend_target()) + + case_names = {case.name for case in report.cases} + expected = { + "participant-initialize", + "participant-reset", + "participant-terminate", + "participant-restart", + "participant-snapshot-consistent", + } + assert expected.issubset(case_names) + for case in report.cases: + if case.name in expected: + assert case.passed, ( + f"participant probe case {case.name!r} must pass for the reference backend; " + f"diagnostics: {[diag.message for diag in case.diagnostics]}" + ) + + +def test_reference_target_conformance_matches_stub_acceptance(): + from aces.backends.stubs import create_stub_target + + reference_report = run_target_conformance(create_reference_backend_target()) + stub_report = run_target_conformance(create_stub_target()) + + assert reference_report.profile == stub_report.profile + assert reference_report.passed == stub_report.passed is True + assert {case.name for case in reference_report.cases} == {case.name for case in stub_report.cases} diff --git a/implementations/python/tests/test_reference_backend_docker_integration.py b/implementations/python/tests/test_reference_backend_docker_integration.py new file mode 100644 index 000000000..bfeef177b --- /dev/null +++ b/implementations/python/tests/test_reference_backend_docker_integration.py @@ -0,0 +1,121 @@ +"""RUN-314: opt-in real-container integration test. + +Marked ``@pytest.mark.docker`` so it is excluded from the default hermetic +suite (``addopts = -m 'not fuzz and not integration and not docker'``). +Run it explicitly with ``pytest -m docker`` / ``nox -s integration_docker``. +It also self-skips cleanly when no container runtime is available, so an +accidental ``-m docker`` run on a runtime-less host does not fail. +""" + +from __future__ import annotations + +import shutil +import subprocess +import textwrap + +import pytest +from aces_reference_backend import create_reference_backend_target +from aces_reference_backend.drivers.oci import ImageTrustPolicy, OciDeploymentDriver + +from aces.core.runtime.conformance import ( + BackendCapabilityProfile, + run_target_conformance, +) +from aces.core.runtime.control_plane import RuntimeControlPlane +from aces.core.runtime.manager import RuntimeManager +from aces.core.sdl import parse_sdl + +pytestmark = pytest.mark.docker + +_IMAGE = "docker.io/library/alpine:3.20" +_SCENARIO = f""" +name: ref-docker +nodes: + web: + type: vm + os: linux + source: {_IMAGE} + resources: {{ram: 1 gib, cpu: 1}} +""" + + +def _available_runtime() -> str | None: + for runtime in ("docker", "podman"): + if shutil.which(runtime) is None: + continue + try: + completed = subprocess.run( + [runtime, "info"], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + except (OSError, subprocess.SubprocessError): + continue + if completed.returncode == 0: + return runtime + return None + + +@pytest.fixture(scope="module") +def container_runtime() -> str: + runtime = _available_runtime() + if runtime is None: + pytest.skip("no container runtime (docker/podman) available") + # Pre-pull the integration image; skip (not fail) if the host is offline + # or the registry is unreachable, so the test only runs when it can. + try: + completed = subprocess.run( + [runtime, "pull", _IMAGE], + capture_output=True, + text=True, + timeout=300, + check=False, + ) + except (OSError, subprocess.SubprocessError): + pytest.skip("container runtime present but image pull failed") + if completed.returncode != 0: + pytest.skip("integration image is not available (offline registry?)") + return runtime + + +def test_real_container_provision_inventory_and_teardown(container_runtime: str): + workspace = "aces-ref-it" + # The scenario pins an explicit image source, so the operator allowlists it + # through the image-trust policy (plan-pinned tags are rejected by default). + driver = OciDeploymentDriver( + runtime=container_runtime, + workspace=workspace, + image_policy=ImageTrustPolicy(allowed_images=(_IMAGE,)), + ) + target = create_reference_backend_target(driver=driver) + + manager = RuntimeManager(target) + execution_plan = manager.plan(parse_sdl(textwrap.dedent(_SCENARIO))) + + control_plane = RuntimeControlPlane(target) + try: + receipt = control_plane.submit_provisioning(execution_plan.provisioning) + status = control_plane.get_operation(receipt.operation_id) + assert status is not None and status.state.value == "succeeded" + # The driver records realization against the real runtime; the portable + # snapshot shows the realized node. + assert "provision.node.web" in control_plane.snapshot.entries + assert "provision.node.web" in driver.realized_addresses() + finally: + driver.destroy(networks=(), containers=("provision.node.web",)) + + +def test_real_driver_conformance_passes(container_runtime: str): + driver = OciDeploymentDriver( + runtime=container_runtime, + workspace="aces-ref-it-conf", + image_policy=ImageTrustPolicy(default_image=_IMAGE), + ) + target = create_reference_backend_target(driver=driver) + + report = run_target_conformance(target) + + assert report.profile == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE + assert report.passed is True, [diag.message for diag in report.diagnostics] diff --git a/implementations/python/tests/test_reference_backend_manifest.py b/implementations/python/tests/test_reference_backend_manifest.py new file mode 100644 index 000000000..d69f3a3a3 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_manifest.py @@ -0,0 +1,55 @@ +"""RUN-314: reference emulation backend manifest tests.""" + +from __future__ import annotations + +from aces_backend_protocols.manifest import backend_manifest_payload +from aces_contracts.contracts import BackendManifestV2Model +from aces_reference_backend import create_reference_backend_manifest + +from aces.core.runtime.conformance import ( + BackendCapabilityProfile, + profile_for_manifest, +) + + +def test_manifest_renders_as_valid_backend_manifest_v2(): + manifest = create_reference_backend_manifest() + + payload = backend_manifest_payload(manifest) + model = BackendManifestV2Model.model_validate(payload) + + assert model.identity.name == "reference-emulation" + + +def test_manifest_infers_full_remote_control_plane_profile(): + manifest = create_reference_backend_manifest() + + assert profile_for_manifest(manifest) == BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE + + +def test_manifest_declares_orchestrator_evaluator_participant_runtime_and_observation(): + manifest = create_reference_backend_manifest() + + assert manifest.has_orchestrator + assert manifest.has_evaluator + assert manifest.has_participant_runtime + assert manifest.has_observation + + +def test_manifest_accepts_and_ignores_extra_config_kwargs(): + # Config kwargs flow to both factories; the manifest factory must accept + # and ignore extras such as ``driver``. + manifest = create_reference_backend_manifest(driver=object(), workspace="/tmp/x") + + assert manifest.name == "reference-emulation" + + +def test_manifest_declares_only_evidence_backed_contract_ids(): + # The reference backend mirrors the stub's evidence-backed contract set; + # it must not over-claim contracts it does not actually emit/validate. + from aces_backend_stubs.stubs import create_stub_manifest + + reference = create_reference_backend_manifest() + stub = create_stub_manifest() + + assert reference.supported_contract_versions == stub.supported_contract_versions diff --git a/implementations/python/tests/test_reference_backend_oci_driver.py b/implementations/python/tests/test_reference_backend_oci_driver.py new file mode 100644 index 000000000..3e79dd40e --- /dev/null +++ b/implementations/python/tests/test_reference_backend_oci_driver.py @@ -0,0 +1,269 @@ +"""RUN-314: OCI driver security tests (subprocess mocked).""" + +from __future__ import annotations + +import subprocess + +import pytest +from aces_reference_backend.driver import ContainerSpec, NetworkSpec +from aces_reference_backend.drivers.oci import ImageTrustPolicy, OciDeploymentDriver + + +class _Recorder: + """Records subprocess.run invocations and returns a canned result.""" + + def __init__(self, *, stdout: str = "", stderr: str = "", returncode: int = 0) -> None: + self.calls: list[dict[str, object]] = [] + self._stdout = stdout + self._stderr = stderr + self._returncode = returncode + + def __call__(self, argv, **kwargs): + self.calls.append({"argv": argv, "kwargs": kwargs}) + return subprocess.CompletedProcess( + args=argv, + returncode=self._returncode, + stdout=self._stdout, + stderr=self._stderr, + ) + + +def _driver(recorder: _Recorder) -> OciDeploymentDriver: + # Allowlist the images the run-path tests use so they exercise realization; + # the image-trust policy is covered by its own dedicated tests below. + return OciDeploymentDriver( + runtime="docker", + workspace="aces-ref-test", + runner=recorder, + image_policy=ImageTrustPolicy(allowed_images=("img", "aces-reference/linux", "pinned-img")), + ) + + +def test_oci_realize_uses_fixed_argv_list_never_shell(): + recorder = _Recorder(stdout="container-native-id-abc123\n") + driver = _driver(recorder) + + driver.realize( + networks=(NetworkSpec(address="provision.network.lan", name="lan"),), + containers=( + ContainerSpec( + address="provision.node.web", + name="web", + image_ref="aces-reference/linux", + networks=("lan",), + ), + ), + ) + + assert recorder.calls + for call in recorder.calls: + assert isinstance(call["argv"], list) + assert all(isinstance(token, str) for token in call["argv"]) + assert call["kwargs"].get("shell") is not True + assert "shell" not in call["kwargs"] or call["kwargs"]["shell"] is False + + +def test_oci_realize_sets_bounded_timeout(): + recorder = _Recorder(stdout="id\n") + driver = _driver(recorder) + + driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + for call in recorder.calls: + timeout = call["kwargs"].get("timeout") + assert isinstance(timeout, (int, float)) + assert 0 < timeout <= 600 + + +def test_oci_handles_never_carry_native_ids(): + recorder = _Recorder(stdout="DEADBEEF-native-container-id\n", stderr="secret-daemon-detail") + driver = _driver(recorder) + + result = driver.realize( + networks=(NetworkSpec(address="provision.network.lan", name="lan"),), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + for handle in result.containers: + assert handle.address == "provision.node.web" + assert "DEADBEEF" not in repr(handle) + for handle in result.networks: + assert handle.address == "provision.network.lan" + + +def test_oci_failure_diagnostic_does_not_leak_native_output(): + sentinel = "TOKEN-LEAK-SENTINEL-XYZ" + recorder = _Recorder(stdout="", stderr=sentinel, returncode=1) + driver = _driver(recorder) + + result = driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + assert result.diagnostics + for diag in result.diagnostics: + assert sentinel not in diag.message + + +def test_oci_no_tokens_in_argv(): + recorder = _Recorder(stdout="id\n") + driver = OciDeploymentDriver( + runtime="docker", + workspace="aces-ref-test", + runner=recorder, + image_policy=ImageTrustPolicy(allowed_images=("img",)), + ) + + driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + assert recorder.calls + for call in recorder.calls: + flat = " ".join(call["argv"]) + assert "token" not in flat.lower() + assert "password" not in flat.lower() + + +def test_oci_timeout_becomes_diagnostic_not_raise(): + def _timeout_runner(argv, **kwargs): + raise subprocess.TimeoutExpired(cmd=argv, timeout=kwargs.get("timeout", 1)) + + driver = OciDeploymentDriver( + runtime="docker", + workspace="aces-ref-test", + runner=_timeout_runner, + image_policy=ImageTrustPolicy(allowed_images=("img",)), + ) + + result = driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + assert result.diagnostics + codes = {diag.code for diag in result.diagnostics} + assert "reference-backend.driver.timeout" in codes + + +def test_oci_rejects_unknown_runtime(): + with pytest.raises(ValueError): + OciDeploymentDriver(runtime="rm -rf /", workspace="ws") + + +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.""" + + recorder = _Recorder(stdout="id\n") + driver = _driver(recorder) + + driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="pinned-web-name", image_ref="img"),), + ) + recorder.calls.clear() + 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"]] + + +def test_oci_attaches_container_to_requested_networks(): + """A planned container/network relationship must be honored: the run argv + attaches the container to every requested network.""" + + recorder = _Recorder(stdout="id\n") + driver = _driver(recorder) + + driver.realize( + networks=(NetworkSpec(address="provision.network.lan", name="lan"),), + containers=( + ContainerSpec( + address="provision.node.web", + name="web", + image_ref="img", + # The portable spec carries the network *address*; the driver + # resolves it to the runtime name ("lan") it created. + networks=("provision.network.lan",), + ), + ), + ) + + 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" + + +def test_oci_rejects_plan_pinned_image_without_allowlist(): + """Image-trust boundary: a plan-pinned tag that is not allowlisted, not the + operator default, and not digest-pinned must NOT be run.""" + + recorder = _Recorder(stdout="id\n") + driver = OciDeploymentDriver(runtime="docker", workspace="ws", runner=recorder) + + result = driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="evil.example/x:latest"),), + ) + + assert not any("run" in call["argv"] for call in recorder.calls) + codes = {diag.code for diag in result.diagnostics} + assert "reference-backend.driver.image-not-allowed" in codes + for diag in result.diagnostics: + assert "evil.example" not in diag.message + + +def test_oci_allows_digest_pinned_image(): + """A digest-pinned ref is a trust anchor and is realized by default.""" + + recorder = _Recorder(stdout="id\n") + driver = OciDeploymentDriver(runtime="docker", workspace="ws", runner=recorder) + + digest = "docker.io/library/alpine@sha256:" + "a" * 64 + result = driver.realize( + networks=(), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref=digest),), + ) + + assert not result.diagnostics + run_argv = next(call["argv"] for call in recorder.calls if "run" in call["argv"]) + assert digest in run_argv + + +def test_oci_rolls_back_realized_resources_on_partial_failure(): + """Transactional boundary: when the container fails after the network was + created, the successful network is destroyed so no orphan is left behind.""" + + class _FailContainer: + def __init__(self) -> None: + self.calls: list[list[str]] = [] + + def __call__(self, argv, **kwargs): + self.calls.append(argv) + # network create + network rm succeed; the container run fails. + rc = 1 if "run" in argv else 0 + return subprocess.CompletedProcess(args=argv, returncode=rc, stdout="", stderr="") + + runner = _FailContainer() + driver = OciDeploymentDriver( + runtime="docker", workspace="ws", runner=runner, image_policy=ImageTrustPolicy(allowed_images=("img",)) + ) + + result = driver.realize( + networks=(NetworkSpec(address="provision.network.lan", name="lan"),), + containers=(ContainerSpec(address="provision.node.web", name="web", image_ref="img"),), + ) + + assert result.diagnostics # the failure is surfaced + 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 driver.realized_addresses() == frozenset() diff --git a/implementations/python/tests/test_reference_backend_provenance.py b/implementations/python/tests/test_reference_backend_provenance.py new file mode 100644 index 000000000..cb272e083 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_provenance.py @@ -0,0 +1,47 @@ +"""RUN-314: SEM-218 realization provenance via RuntimeManager.apply.""" + +from __future__ import annotations + +import textwrap + +from aces_reference_backend import create_reference_backend_target +from aces_sdl.explicitness import ExplicitnessClass, ExplicitnessProvenance + +from aces.core.runtime.manager import RuntimeManager +from aces.core.sdl import parse_sdl + +_EXACT_SCENARIO = """ +name: ref-sem-218 +nodes: + web: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} +""" + + +def test_apply_records_realization_provenance(): + manager = RuntimeManager(create_reference_backend_target()) + plan = manager.plan(parse_sdl(textwrap.dedent(_EXACT_SCENARIO))) + + result = manager.apply(plan) + + assert result.success, [diag.message for diag in result.diagnostics] + by_field = {entry.field_path: entry for entry in result.snapshot.realization_provenance} + assert by_field["nodes.web.os"].provenance is ExplicitnessProvenance.AUTHOR_DECLARED + assert by_field["nodes.web.os"].explicitness is ExplicitnessClass.EXACT + assert by_field["nodes.web.os"].requirement_kind == "os-family" + assert by_field["nodes.web.type"].explicitness is ExplicitnessClass.EXACT + + +def test_apply_snapshot_preserves_planned_payload_no_emulator_state(): + manager = RuntimeManager(create_reference_backend_target()) + plan = manager.plan(parse_sdl(textwrap.dedent(_EXACT_SCENARIO))) + + result = manager.apply(plan) + + entry = result.snapshot.entries["provision.node.web"] + assert entry.payload.get("os_family") == "linux" + rendered = repr(result.snapshot.entries) + for forbidden in ("container_id", "docker", "podman", "/var/run", "InProcessDriver"): + assert forbidden not in rendered diff --git a/implementations/python/tests/test_reference_backend_provisioner.py b/implementations/python/tests/test_reference_backend_provisioner.py new file mode 100644 index 000000000..54986ee28 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_provisioner.py @@ -0,0 +1,162 @@ +"""RUN-314: provisioner apply via the control plane.""" + +from __future__ import annotations + +import textwrap + +from aces_contracts.planning import ( + ChangeAction, + PlannedResource, + ProvisioningPlan, + ProvisionOp, + RuntimeDomain, +) +from aces_reference_backend import ( + create_reference_backend_components, + create_reference_backend_manifest, +) +from aces_reference_backend.drivers.inprocess import InProcessDriver + +from aces.core.runtime.control_plane import RuntimeControlPlane +from aces.core.runtime.manager import RuntimeManager +from aces.core.runtime.registry import RuntimeTarget +from aces.core.sdl import parse_sdl + +_SCENARIO = """ +name: ref-provisioner +nodes: + web: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} +""" + + +def _target_with_driver(driver: InProcessDriver) -> RuntimeTarget: + manifest = create_reference_backend_manifest() + components = create_reference_backend_components(manifest=manifest, driver=driver) + return RuntimeTarget( + name="reference-emulation", + manifest=manifest, + provisioner=components.provisioner, + orchestrator=components.orchestrator, + evaluator=components.evaluator, + participant_runtime=components.participant_runtime, + ) + + +def _provisioning_plan(target: RuntimeTarget) -> ProvisioningPlan: + manager = RuntimeManager(target) + execution_plan = manager.plan(parse_sdl(textwrap.dedent(_SCENARIO))) + return execution_plan.provisioning + + +def test_apply_via_control_plane_records_entries_and_drives_driver(): + driver = InProcessDriver() + target = _target_with_driver(driver) + plan = _provisioning_plan(target) + + control_plane = RuntimeControlPlane(target) + receipt = control_plane.submit_provisioning(plan) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None + assert status.state.value == "succeeded" + snapshot = control_plane.snapshot + assert "provision.node.web" in snapshot.entries + assert snapshot.entries["provision.node.web"].status == "applied" + # The driver was actually invoked to realize the container. + realized = [op for op in driver.recorded_ops if op.verb == "realize" and op.kind == "container"] + assert any(op.address == "provision.node.web" for op in realized) + + +def test_apply_handles_delete_and_unchanged(): + driver = InProcessDriver() + target = _target_with_driver(driver) + plan = _provisioning_plan(target) + control_plane = RuntimeControlPlane(target) + control_plane.submit_provisioning(plan) + + # Now submit a DELETE for the realized node. + delete_plan = ProvisioningPlan( + operations=[ + ProvisionOp( + action=ChangeAction.DELETE, + address="provision.node.web", + resource_type="node", + payload={}, + ) + ] + ) + receipt = control_plane.submit_provisioning(delete_plan) + status = control_plane.get_operation(receipt.operation_id) + + assert status is not None and status.state.value == "succeeded" + assert "provision.node.web" not in control_plane.snapshot.entries + destroyed = [op for op in driver.recorded_ops if op.verb == "destroy" and op.kind == "container"] + assert any(op.address == "provision.node.web" for op in destroyed) + + +def test_unchanged_op_keeps_entry_without_driver_realize(): + driver = InProcessDriver() + target = _target_with_driver(driver) + unchanged_plan = ProvisioningPlan( + resources={ + "provision.node.web": PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={"name": "web", "node_type": "vm", "os_family": "linux"}, + ) + }, + operations=[ + ProvisionOp( + action=ChangeAction.UNCHANGED, + address="provision.node.web", + resource_type="node", + payload={"name": "web", "node_type": "vm", "os_family": "linux"}, + ) + ], + ) + control_plane = RuntimeControlPlane(target) + control_plane.submit_provisioning(unchanged_plan) + + entry = control_plane.snapshot.entries["provision.node.web"] + assert entry.status == "unchanged" + assert not [op for op in driver.recorded_ops if op.verb == "realize"] + + +def test_snapshot_payload_carries_only_portable_facts(): + driver = InProcessDriver() + target = _target_with_driver(driver) + plan = _provisioning_plan(target) + control_plane = RuntimeControlPlane(target) + control_plane.submit_provisioning(plan) + + snapshot = control_plane.snapshot + # The realized snapshot entry preserves the planned payload (portable) and + # never embeds a backend-native container id, host path, or daemon repr. + entry = snapshot.entries["provision.node.web"] + rendered = repr(entry.payload) + for forbidden in ("docker", "podman", "container_id", "/var/run", "sha256:", "InProcessDriver"): + assert forbidden not in rendered + + +def test_validate_surfaces_realization_diagnostics_without_driver(): + 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) + 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 new file mode 100644 index 000000000..a94d47402 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_realization.py @@ -0,0 +1,165 @@ +"""RUN-314: pure plan interpretation tests for the reference backend.""" + +from __future__ import annotations + +from aces_contracts.diagnostics import Severity +from aces_contracts.planning import ( + ChangeAction, + PlannedResource, + ProvisioningPlan, + ProvisionOp, + RuntimeDomain, +) +from aces_reference_backend import interpret_provisioning_plan +from aces_reference_backend.realization import Realization + + +def _node_resource(address: str, name: str, os_family: str = "linux") -> PlannedResource: + return PlannedResource( + address=address, + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload={ + "name": name, + "node_name": name, + "node_type": "vm", + "os_family": os_family, + "spec": {"node": {}, "infrastructure": {"networks": ["lan"]}}, + }, + ) + + +def _network_resource(address: str, name: str) -> PlannedResource: + return PlannedResource( + address=address, + domain=RuntimeDomain.PROVISIONING, + resource_type="network", + payload={"name": name, "spec": {"infrastructure": {"properties": {"internal": True}}}}, + ) + + +def _plan(*resources: PlannedResource) -> ProvisioningPlan: + return ProvisioningPlan( + resources={resource.address: resource for resource in resources}, + operations=[ + ProvisionOp( + action=ChangeAction.CREATE, + address=resource.address, + resource_type=resource.resource_type, + payload=resource.payload, + ) + for resource in resources + ], + ) + + +def test_interpret_maps_nodes_to_container_specs(): + plan = _plan(_node_resource("provision.node.web", "web")) + + realization = interpret_provisioning_plan(plan) + + assert isinstance(realization, Realization) + assert [spec.address for spec in realization.containers] == ["provision.node.web"] + assert realization.containers[0].name == "web" + assert not realization.diagnostics + + +def test_interpret_maps_networks_to_network_specs(): + plan = _plan(_network_resource("provision.network.lan", "lan")) + + realization = interpret_provisioning_plan(plan) + + assert [spec.address for spec in realization.networks] == ["provision.network.lan"] + assert realization.networks[0].name == "lan" + assert realization.networks[0].labels.get("internal") == "true" + + +def test_interpret_resolves_container_network_references_to_addresses(): + """A node that references a network by name must yield the network's + resource address in ContainerSpec.networks, so the driver has the single + portable key it maps to a runtime network name.""" + + plan = _plan( + _node_resource("provision.node.web", "web"), # infrastructure.networks == ["lan"] + _network_resource("provision.network.lan", "lan"), + ) + + realization = interpret_provisioning_plan(plan) + + assert realization.containers[0].networks == ("provision.network.lan",) + + +def test_interpret_passes_through_unresolved_network_reference(): + """An authored reference to a network not in this plan is passed through + unchanged so the contract stays total.""" + + plan = _plan(_node_resource("provision.node.web", "web")) # references "lan", no network resource + + realization = interpret_provisioning_plan(plan) + + assert realization.containers[0].networks == ("lan",) + + +def test_interpret_records_placement_resources(): + placement = PlannedResource( + address="provision.content.payload", + domain=RuntimeDomain.PROVISIONING, + resource_type="content-placement", + payload={"name": "payload", "target": "provision.node.web"}, + ) + plan = _plan(_node_resource("provision.node.web", "web"), placement) + + realization = interpret_provisioning_plan(plan) + + assert [p.address for p in realization.placements] == ["provision.content.payload"] + assert realization.placements[0].resource_type == "content-placement" + + +def test_interpret_diagnoses_unsupported_resource_type(): + 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) + + +def test_interpret_diagnoses_invalid_node_payload(): + bad = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload="not-a-mapping", # type: ignore[arg-type] + ) + plan = ProvisioningPlan(resources={bad.address: bad}) + + realization = interpret_provisioning_plan(plan) + + codes = {diag.code for diag in realization.diagnostics} + assert "reference-backend.realization.invalid-payload" in codes + + +def test_interpret_diagnostics_never_leak_payload_internals(): + # An invalid payload diagnostic must reference the address/type, not echo + # the raw payload value. + secret = "s3cr3t-token-value" + bad = PlannedResource( + address="provision.node.web", + domain=RuntimeDomain.PROVISIONING, + resource_type="node", + payload=secret, # type: ignore[arg-type] + ) + plan = ProvisioningPlan(resources={bad.address: bad}) + + realization = interpret_provisioning_plan(plan) + + for diag in realization.diagnostics: + assert secret not in diag.message diff --git a/implementations/python/tests/test_reference_backend_registry.py b/implementations/python/tests/test_reference_backend_registry.py new file mode 100644 index 000000000..859bd0f52 --- /dev/null +++ b/implementations/python/tests/test_reference_backend_registry.py @@ -0,0 +1,65 @@ +"""RUN-314: registry/target shape + descriptor tests.""" + +from __future__ import annotations + +from aces_reference_backend import ( + REFERENCE_BACKEND_NAME, + create_reference_backend_components, + create_reference_backend_manifest, + create_reference_backend_target, + register_reference_backend, +) +from aces_reference_backend.drivers.inprocess import InProcessDriver +from aces_reference_backend.provisioner import ReferenceProvisioner + +from aces.core.runtime.registry import BackendRegistry, RuntimeTarget + + +def test_create_target_passes_shape_validation(): + target = create_reference_backend_target() + + assert isinstance(target, RuntimeTarget) + assert target.name == REFERENCE_BACKEND_NAME + assert target.orchestrator is not None + assert target.evaluator is not None + assert target.participant_runtime is not None + + +def test_register_and_create_via_registry(): + registry = BackendRegistry() + register_reference_backend(registry) + + assert registry.is_registered(REFERENCE_BACKEND_NAME) + target = registry.create(REFERENCE_BACKEND_NAME) + + assert target.name == REFERENCE_BACKEND_NAME + assert target.manifest.name == REFERENCE_BACKEND_NAME + + +def test_driver_config_flows_through_to_components(): + driver = InProcessDriver() + manifest = create_reference_backend_manifest() + components = create_reference_backend_components(manifest=manifest, driver=driver) + + assert isinstance(components.provisioner, ReferenceProvisioner) + assert components.provisioner._driver is driver + + +def test_registry_create_threads_driver_config(): + registry = BackendRegistry() + register_reference_backend(registry) + driver = InProcessDriver() + + target = registry.create(REFERENCE_BACKEND_NAME, driver=driver) + + assert target.provisioner._driver is driver + + +def test_manifest_factory_is_single_source_of_truth(): + registry = BackendRegistry() + register_reference_backend(registry) + + manifest = registry.manifest(REFERENCE_BACKEND_NAME) + + assert manifest.name == REFERENCE_BACKEND_NAME + assert manifest.has_participant_runtime diff --git a/implementations/python/tests/test_reference_processor.py b/implementations/python/tests/test_reference_processor.py new file mode 100644 index 000000000..c5c9e3177 --- /dev/null +++ b/implementations/python/tests/test_reference_processor.py @@ -0,0 +1,242 @@ +"""RUN-313: repository-owned reference processor. + +The reference processor (``aces_processor.reference``) realizes the normative +processing model: it carries SDL authoring input through instantiation, +compilation, and planning to a portable :class:`ExecutionPlan`, and exposes the +published processor manifest. Per ADR-008 the processor's responsibility ends at +the execution plan; backend realization (apply) is the runtime's job. These +tests cover the processor in isolation and then drive its plan through the +reference runtime to prove every contract version the processor manifest +declares is exercised end to end. +""" + +from __future__ import annotations + +import json +from textwrap import dedent + +import pytest +from aces_backend_stubs.stubs import create_stub_manifest, create_stub_target +from aces_contracts.contracts import ( + ProcessorManifestV2Model, + WorkflowCancellationRequestModel, +) +from aces_processor.manifest import ( + REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2, + reference_processor_manifest_payload, +) +from aces_processor.models import ExecutionPlan, RuntimeModel +from aces_processor.reference import ( + ReferenceProcessor, + ReferenceProcessorResult, + run_reference_processor, +) +from aces_runtime import RuntimeControlPlane +from aces_runtime.control_plane_api import _receipt_response +from aces_runtime.control_plane_api_models import _operation_status_model, _snapshot_model +from aces_sdl import parse_sdl + +WORKFLOW_ADDRESS = "orchestration.workflow.response" + +_SCENARIO = dedent( + """ + name: reference-processor + nodes: + vm1: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} + conditions: {health: ops} + roles: {ops: operator} + conditions: + health: {command: /bin/true, interval: 15} + metrics: + uptime: {type: conditional, max-score: 100, condition: health} + entities: + blue: {role: blue} + objectives: + validate: + entity: blue + success: {conditions: [health]} + workflows: + response: + start: run + steps: + run: {type: objective, objective: validate, on-success: finish} + finish: {type: end} + """ +) + +_PARAM_SCENARIO = dedent( + """ + name: parametrized-reference + variables: + cpu_count: {type: integer, default: 1} + nodes: + vm1: + type: vm + os: linux + resources: + ram: 1 gib + cpu: ${cpu_count} + conditions: {health: ops} + roles: {ops: operator} + conditions: + health: {command: /bin/true, interval: 15} + metrics: + uptime: {type: conditional, max-score: 100, condition: health} + entities: + blue: {role: blue} + objectives: + validate: + entity: blue + success: {conditions: [health]} + workflows: + response: + start: run + steps: + run: {type: objective, objective: validate, on-success: finish} + finish: {type: end} + """ +) + + +def _stub_manifest(): + return create_stub_manifest() + + +def _plan_fingerprint(execution_plan: ExecutionPlan) -> str: + """Stable, order-independent fingerprint of every planned operation.""" + + operations = [] + for sub_plan in ( + execution_plan.provisioning, + execution_plan.orchestration, + execution_plan.evaluation, + ): + for op in sub_plan.operations: + operations.append( + { + "address": op.address, + "action": op.action.value, + "resource_type": op.resource_type, + "payload": op.payload, + } + ) + return json.dumps(operations, sort_keys=True, default=str) + + +class TestReferenceProcessorRealization: + def test_realizes_valid_execution_plan_across_all_domains(self): + result = run_reference_processor(_SCENARIO, _stub_manifest()) + + assert isinstance(result, ReferenceProcessorResult) + assert result.is_valid, result.diagnostics + assert result.diagnostics == () + assert result.scenario_name == "reference-processor" + assert isinstance(result.runtime_model, RuntimeModel) + assert isinstance(result.execution_plan, ExecutionPlan) + assert result.execution_plan.provisioning.operations + assert result.execution_plan.orchestration.operations + assert result.execution_plan.evaluation.operations + + def test_accepts_text_path_and_parsed_scenario_inputs(self, tmp_path): + from_text = run_reference_processor(_SCENARIO, _stub_manifest()) + + sdl_file = tmp_path / "scenario.yaml" + sdl_file.write_text(_SCENARIO) + from_path = run_reference_processor(sdl_file, _stub_manifest()) + + from_parsed = run_reference_processor(parse_sdl(_SCENARIO), _stub_manifest()) + + assert ( + _plan_fingerprint(from_text.execution_plan) + == _plan_fingerprint(from_path.execution_plan) + == _plan_fingerprint(from_parsed.execution_plan) + ) + + def test_invalid_input_type_raises_type_error(self): + with pytest.raises(TypeError): + run_reference_processor(123, _stub_manifest()) # type: ignore[arg-type] + + def test_parameters_change_realized_plan(self): + one = run_reference_processor(_PARAM_SCENARIO, _stub_manifest(), parameters={"cpu_count": 1}) + two = run_reference_processor(_PARAM_SCENARIO, _stub_manifest(), parameters={"cpu_count": 2}) + + assert one.is_valid, one.diagnostics + assert two.is_valid, two.diagnostics + assert _plan_fingerprint(one.execution_plan) != _plan_fingerprint(two.execution_plan) + + def test_realization_is_deterministic(self): + first = run_reference_processor(_SCENARIO, _stub_manifest()) + second = run_reference_processor(_SCENARIO, _stub_manifest()) + + assert _plan_fingerprint(first.execution_plan) == _plan_fingerprint(second.execution_plan) + + +class TestReferenceProcessorManifest: + def test_manifest_payload_delegates_to_canonical_renderer(self): + assert ReferenceProcessor.manifest_payload() == reference_processor_manifest_payload() + + def test_manifest_payload_validates_against_contract_model(self): + model = ProcessorManifestV2Model.model_validate(ReferenceProcessor.manifest_payload()) + + assert model.identity.name == "aces-reference-processor" + assert set(model.supported_contract_versions) == set(REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2) + + +class TestReferenceProcessorEndToEndEvidence: + def test_every_claimed_contract_is_exercised_end_to_end(self): + target = create_stub_target() + result = run_reference_processor(_SCENARIO, target.manifest) + assert result.is_valid, result.diagnostics + + exercised: dict[str, object] = {} + + # processor-manifest-v2: the published declaration this processor renders. + exercised["processor-manifest-v2"] = ProcessorManifestV2Model.model_validate( + ReferenceProcessor.manifest_payload() + ) + + # The three plan contracts are produced directly by the processor. + assert result.execution_plan.provisioning.operations + exercised["provisioning-plan-v1"] = result.execution_plan.provisioning + assert result.execution_plan.orchestration.operations + exercised["orchestration-plan-v1"] = result.execution_plan.orchestration + assert result.execution_plan.evaluation.operations + exercised["evaluation-plan-v1"] = result.execution_plan.evaluation + + # Drive the plan through the reference runtime; the runtime emits the + # operation receipt/status and snapshot contracts. + 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), + ): + receipt = submit(sub_plan) + assert receipt.accepted, receipt.diagnostics + exercised["operation-receipt-v1"] = _receipt_response(receipt) + status = control_plane.get_operation(receipt.operation_id) + assert status is not None + exercised["operation-status-v1"] = _operation_status_model(status) + + exercised["runtime-snapshot-v1"] = _snapshot_model(control_plane.get_snapshot()) + + # Workflow cancellation closes the loop on the cancellation request + # contract: the runtime consumes a schema-valid request and returns a + # contract-valid receipt. + cancellation_request = WorkflowCancellationRequestModel(reason="reference cancel") + cancel_receipt = control_plane.cancel_workflow( + WORKFLOW_ADDRESS, + reason=cancellation_request.reason, + ) + assert cancel_receipt.accepted, cancel_receipt.diagnostics + exercised["operation-receipt-v1"] = _receipt_response(cancel_receipt) + exercised["workflow-cancellation-request-v1"] = cancellation_request + + assert set(exercised) == set(REFERENCE_SUPPORTED_CONTRACT_VERSIONS_V2), ( + "The reference processor manifest must claim exactly the contracts " + "the end-to-end reference path exercises (no unbacked claims, no " + "exercised-but-undeclared contracts)." + ) diff --git a/implementations/python/tests/test_repo_policy_tools.py b/implementations/python/tests/test_repo_policy_tools.py index 3258bef43..1ec607e18 100644 --- a/implementations/python/tests/test_repo_policy_tools.py +++ b/implementations/python/tests/test_repo_policy_tools.py @@ -105,6 +105,7 @@ def setup_policy_repo(tmp_path: Path) -> Path: "aces_runtime", "aces_backend_protocols", "aces_backend_stubs", + "aces_reference_backend", "aces_conformance", "aces_cli", "aces_mcp", diff --git a/implementations/python/tests/test_run_307_shared_operational_state.py b/implementations/python/tests/test_run_307_shared_operational_state.py new file mode 100644 index 000000000..0bd934bfa --- /dev/null +++ b/implementations/python/tests/test_run_307_shared_operational_state.py @@ -0,0 +1,324 @@ +"""RUN-307 shared operational state model tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_conformance.conformance import _semantic_diagnostics +from aces_contracts.contracts import ParticipantSharedStateRecordModel, schema_bundle +from aces_contracts.participant_shared_state import ( + iter_participant_shared_state_history_transition_violations, + iter_participant_shared_state_snapshot_violations, +) +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from aces_runtime.backend_calls import _call_backend_apply +from jsonschema import Draft202012Validator +from pydantic import ValidationError +from starlette.testclient import TestClient + +from aces.backends.stubs import create_stub_target +from aces.core.runtime.control_plane import RuntimeControlPlane +from aces.core.runtime.control_plane_api import create_control_plane_app +from aces.core.runtime.control_plane_security import ( + ControlPlaneIdentity, + ControlPlaneRole, + ControlPlaneSecurityConfig, +) + +PARTICIPANT = "participants.red.llm" +EPISODE = "ep-red-004" +ACTION_INSTANCE = "scan-0001" +STATE_ADDRESS = "hosts.web01.service.http" +T0 = "2026-05-26T10:50:00Z" + + +def _fixture_record(**overrides: object) -> dict[str, object]: + repo_root = Path(__file__).resolve().parents[3] + fixture_path = ( + repo_root + / "contracts" + / "fixtures" + / "participant-runtime" + / "participant-shared-state-record-v1" + / "valid" + / "serialized-service-state-commit.json" + ) + payload = json.loads(fixture_path.read_text(encoding="utf-8")) + payload.update(overrides) + return payload + + +def _record_for(state_address: str, **overrides: object) -> dict[str, object]: + record = _fixture_record(state_address=state_address) + for access in record["accesses"]: + access["state_address"] = state_address + record.update(overrides) + return record + + +def _state_update_event(**overrides: object) -> dict[str, object]: + payload: dict[str, object] = { + "event_type": "state_transition_recorded", + "timestamp": T0, + "participant_address": PARTICIPANT, + "episode_id": EPISODE, + "action_instance_id": ACTION_INSTANCE, + "state_transition_kind": "shared_state_updated", + "post_state_digest": "sha256:known", + "lifecycle_phase": "state_update_commit", + "phase_realization": "runtime_mediated", + "shared_state_refs": [STATE_ADDRESS], + "details": {}, + } + payload.update(overrides) + return payload + + +def _snapshot_payload( + *, + shared_state_records: dict[str, dict[str, object]] | None = None, + shared_state_history: dict[str, list[dict[str, object]]] | None = None, + participant_behavior_history: dict[str, list[dict[str, object]]] | None = None, + metadata: dict[str, object] | None = None, +) -> dict[str, object]: + return { + "schema_version": "runtime-snapshot/v1", + "entries": {}, + "orchestration_results": {}, + "orchestration_history": {}, + "evaluation_results": {}, + "evaluation_history": {}, + "participant_episode_results": {}, + "participant_episode_history": {}, + "participant_behavior_history": participant_behavior_history or {}, + "shared_state_records": shared_state_records or {}, + "shared_state_history": shared_state_history or {}, + "metadata": metadata or {}, + } + + +def _security(target_name: str) -> ControlPlaneSecurityConfig: + return ControlPlaneSecurityConfig( + max_request_bytes=1_000_000, + trust_proxy_identity_headers=True, + trusted_identities={ + "backend-service": ControlPlaneIdentity( + identity="backend-service", + roles=frozenset({ControlPlaneRole.BACKEND}), + target_name=target_name, + ), + }, + ) + + +def _headers() -> dict[str, str]: + return { + "x-aces-client-verified": "true", + "x-aces-client-identity": "backend-service", + } + + +def test_runtime_snapshot_exposes_shared_state_records_and_history() -> None: + record = _fixture_record() + snapshot = RuntimeSnapshot( + shared_state_records={STATE_ADDRESS: record}, + shared_state_history={STATE_ADDRESS: [record]}, + ) + target = create_stub_target() + control_plane = RuntimeControlPlane(target, initial_snapshot=snapshot) + app = create_control_plane_app(control_plane, security=_security(target.name)) + + with TestClient(app) as client: + response = client.get("/snapshot", headers=_headers()) + + assert response.status_code == 200 + body = response.json() + assert body["shared_state_records"][STATE_ADDRESS]["revision"] == "rev8" + assert body["shared_state_history"][STATE_ADDRESS][0]["state_address"] == STATE_ADDRESS + + validator = Draft202012Validator(schema_bundle()["runtime-snapshot-v1"]) + assert list(validator.iter_errors(body)) == [] + + +def test_shared_state_record_requires_revision_or_digest() -> None: + invalid = _fixture_record(revision=None, digest=None) + + with pytest.raises(ValidationError, match="revision or digest"): + ParticipantSharedStateRecordModel.model_validate(invalid) + + validator = Draft202012Validator(schema_bundle()["participant-shared-state-record-v1"]) + assert list(validator.iter_errors(invalid)) + + +def test_runtime_snapshot_semantics_reject_unresolved_shared_state_ref() -> None: + diagnostics = _semantic_diagnostics( + "runtime-snapshot-v1", + _snapshot_payload( + participant_behavior_history={PARTICIPANT: [_state_update_event()]}, + ), + ) + + assert any("shared_state_refs" in item.message and STATE_ADDRESS in item.message for item in diagnostics) + + +def test_shared_state_semantics_reject_invalid_container_shapes() -> None: + assert list(iter_participant_shared_state_snapshot_violations({}, {})) == [] + + violations = list(iter_participant_shared_state_snapshot_violations([], [], metadata=[])) + messages = [message for _, message in violations] + + assert "RuntimeSnapshot.metadata must be a mapping" in messages + assert "shared_state_records must be a mapping" in messages + assert "shared_state_history must be a mapping" in messages + + +def test_shared_state_semantics_reject_invalid_record_shapes_and_accesses() -> None: + records: dict[object, object] = { + "": _record_for(STATE_ADDRESS), + "not.mapping": "invalid", + "missing.scope": _record_for("missing.scope", state_scope=None), + "outer.key": _record_for("inner.key"), + "no.version": _record_for("no.version", revision=None, digest=None), + "bad.accesses": _record_for("bad.accesses", accesses="invalid"), + "access.not.mapping": _record_for("access.not.mapping", accesses=["invalid"]), + "access.no.address": _record_for( + "access.no.address", + accesses=[{"access_kind": "read", "read_revision": "rev1"}], + ), + "access.mismatch": _record_for( + "access.mismatch", + accesses=[{"state_address": "other.address", "access_kind": "read", "read_revision": "rev1"}], + ), + "access.bad.kind": _record_for( + "access.bad.kind", + accesses=[{"state_address": "access.bad.kind", "access_kind": "observe"}], + ), + "access.no.read": _record_for( + "access.no.read", + accesses=[{"state_address": "access.no.read", "access_kind": "read"}], + ), + "access.no.write": _record_for( + "access.no.write", + accesses=[{"state_address": "access.no.write", "access_kind": "write"}], + ), + } + + messages = [ + message + for _, message in iter_participant_shared_state_snapshot_violations( + records, + {}, + ) + ] + + expected_messages = [ + "shared_state_records keys must be non-empty strings", + "shared state record must be a mapping", + "shared state record is missing required fields: state_scope", + "shared state record outer key 'outer.key' does not match state_address 'inner.key'", + "shared state record requires revision or digest", + "shared state record accesses must be a list", + "shared state access must be a mapping", + "shared state access state_address must be a non-empty string", + "shared state access state_address 'other.address' does not match record state_address", + "shared state access_kind 'observe' is not supported", + "shared state read access requires read_revision or read_digest", + "shared state write access requires write_revision or write_digest", + ] + for expected in expected_messages: + assert expected in messages + + +def test_shared_state_semantics_reject_invalid_history_shapes() -> None: + history: dict[object, object] = { + "": [_record_for(STATE_ADDRESS)], + "not.list": "invalid", + "bad.record": ["invalid"], + } + + messages = [ + message + for _, message in iter_participant_shared_state_snapshot_violations( + {}, + history, + ) + ] + + assert "shared_state_history keys must be non-empty strings" in messages + assert "shared_state_history entries must be lists" in messages + assert "shared state history record must be a mapping" in messages + + +def test_shared_state_history_transition_rejects_removed_or_shrunk_history() -> None: + original = _fixture_record() + second = _fixture_record(revision="rev9") + + removed = list(iter_participant_shared_state_history_transition_violations({STATE_ADDRESS: [original]}, {})) + shrunk = list( + iter_participant_shared_state_history_transition_violations( + {STATE_ADDRESS: [original, second]}, + {STATE_ADDRESS: [original]}, + ) + ) + + assert any("history was removed" in message for _, message in removed) + assert any("history shrank from 2 to 1 records" in message for _, message in shrunk) + + +def test_backend_apply_rejects_shared_state_in_metadata() -> None: + base_snapshot = RuntimeSnapshot() + record = _fixture_record() + + def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + metadata={"shared_state_records": {STATE_ADDRESS: record}}, + ), + changed_addresses=[STATE_ADDRESS], + ) + + result = _call_backend_apply( + _backend_apply, + object(), + base_snapshot, + address="runtime.control-plane.shared-state", + snapshot=base_snapshot, + ) + + assert result.success is False + assert any("shared_state_records" in diagnostic.message for diagnostic in result.diagnostics) + + +def test_backend_apply_rejects_shared_state_history_rewrite() -> None: + original = _fixture_record() + rewritten = _fixture_record(revision="rev9") + base_snapshot = RuntimeSnapshot( + shared_state_records={STATE_ADDRESS: original}, + shared_state_history={STATE_ADDRESS: [original]}, + ) + + def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + shared_state_records={STATE_ADDRESS: rewritten}, + shared_state_history={STATE_ADDRESS: [rewritten]}, + ), + changed_addresses=[STATE_ADDRESS], + ) + + result = _call_backend_apply( + _backend_apply, + object(), + base_snapshot, + address="runtime.control-plane.shared-state", + snapshot=base_snapshot, + ) + + assert result.success is False + assert any("shared_state_history must be append-only" in item.message for item in result.diagnostics) diff --git a/implementations/python/tests/test_run_308_concurrent_participant_execution.py b/implementations/python/tests/test_run_308_concurrent_participant_execution.py new file mode 100644 index 000000000..beddc98b7 --- /dev/null +++ b/implementations/python/tests/test_run_308_concurrent_participant_execution.py @@ -0,0 +1,832 @@ +"""RUN-308 concurrent participant execution integration tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_contracts.contracts import ( + ParticipantJointActionRecordModel, + ParticipantTimeManagementContextModel, + RuntimeSnapshotEnvelopeModel, + schema_bundle, +) +from aces_contracts.participant_concurrency import ( + iter_participant_concurrency_snapshot_violations, + iter_participant_concurrency_transition_violations, +) +from aces_contracts.runtime_state import ApplyResult, RuntimeSnapshot +from aces_runtime.backend_calls import _call_backend_apply +from aces_runtime.participant_result_contracts import participant_runtime_state_contract_diagnostics +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +STATE_ADDRESS = "hosts.web01.service.http" +PARTICIPANT_RED = "participants.red.llm" +PARTICIPANT_BLUE = "participants.blue.llm" +EPISODE = "episode-main" +T0 = "2026-06-20T08:00:00Z" + + +def _shared_state_record() -> dict[str, object]: + fixture_path = ( + REPO_ROOT + / "contracts" + / "fixtures" + / "participant-runtime" + / "participant-shared-state-record-v1" + / "valid" + / "serialized-service-state-commit.json" + ) + return json.loads(fixture_path.read_text(encoding="utf-8")) + + +def _behavior_event(participant_address: str, action_instance_id: str, realized_order: int) -> dict[str, object]: + return { + "event_type": "action_attempted", + "timestamp": T0, + "participant_address": participant_address, + "episode_id": EPISODE, + "action_instance_id": action_instance_id, + "action_contract_address": "participant.action-contract.scan", + "actor_provenance": f"participant:{participant_address.rsplit('.', 1)[-1]}", + "joint_action_set_id": "joint-red-blue-0001", + "realized_order": realized_order, + "interaction_class": "shared_state_change", + "shared_state_refs": [STATE_ADDRESS], + "details": {}, + } + + +def _base_envelope(*, event_id: str, schema_name: str, event_type: str) -> dict[str, object]: + return { + "event_id": event_id, + "schema_name": schema_name, + "schema_version": f"{schema_name}/v1", + "event_type": event_type, + "extension_policy": "forbid_unknown_fields", + "participant_address": None, + "episode_id": None, + "sequence_number": None, + "occurred_at": T0, + "recorded_at": T0, + "ingested_at": T0, + "clock_authority": "clock.logical.runtime", + "ordering_basis": "serialized_backend_order", + "logical_order_ref": "order.joint-red-blue-0001", + "actor_ref": "runtime.coordinator", + "producer_ref": "backend.stub", + "authorization_scope": "operators", + } + + +def _time_context(**overrides: object) -> dict[str, object]: + payload = { + **_base_envelope( + event_id="tm-red-blue-0001", + schema_name="participant-time-management-context", + event_type="time_management_context_recorded", + ), + "context_id": "tm-red-blue-0001", + "mode": "backend_serialized", + "claim_strength": "bounded", + "basis": "serialized_backend_order", + "clock_ref": "clock.logical.runtime", + "backend_serialized": True, + } + payload.update(overrides) + return payload + + +def _joint_action(**overrides: object) -> dict[str, object]: + payload = { + **_base_envelope( + event_id="joint-red-blue-0001", + schema_name="participant-joint-action-record", + event_type="joint_action_recorded", + ), + "joint_action_set_id": "joint-red-blue-0001", + "member_event_refs": ["scan-red-0001", "scan-blue-0001"], + "access_sets": [ + { + "member_event_ref": "scan-red-0001", + "shared_state_write_refs": [STATE_ADDRESS], + }, + { + "member_event_ref": "scan-blue-0001", + "shared_state_read_refs": [STATE_ADDRESS], + }, + ], + "conflict_class": "read_write", + "conflict_policy": "serialize", + "isolation_guarantee": "serializable", + "atomicity_scope": "single_object", + "realized_order": ["scan-red-0001", "scan-blue-0001"], + "time_management_context_ref": "tm-red-blue-0001", + } + payload.update(overrides) + return payload + + +def _snapshot_payload(**overrides: object) -> dict[str, object]: + shared_state = _shared_state_record() + payload = { + "schema_version": "runtime-snapshot/v1", + "entries": {}, + "orchestration_results": {}, + "orchestration_history": {}, + "evaluation_results": {}, + "evaluation_history": {}, + "participant_episode_results": {}, + "participant_episode_history": {}, + "participant_behavior_history": { + PARTICIPANT_RED: [_behavior_event(PARTICIPANT_RED, "scan-red-0001", 0)], + PARTICIPANT_BLUE: [_behavior_event(PARTICIPANT_BLUE, "scan-blue-0001", 1)], + }, + "shared_state_records": {STATE_ADDRESS: shared_state}, + "shared_state_history": {STATE_ADDRESS: [shared_state]}, + "joint_action_records": {"joint-red-blue-0001": _joint_action()}, + "time_management_contexts": {"tm-red-blue-0001": _time_context()}, + "metadata": {}, + } + payload.update(overrides) + return payload + + +def _concurrency_violation_messages(payload: dict[str, object]) -> list[str]: + return [ + message + for _address, message in iter_participant_concurrency_snapshot_violations( + payload.get("joint_action_records"), + payload.get("time_management_contexts"), + participant_behavior_history=payload.get("participant_behavior_history"), + shared_state_records=payload.get("shared_state_records"), + shared_state_history=payload.get("shared_state_history"), + ) + ] + + +def test_runtime_snapshot_publishes_joint_action_and_time_context_records() -> None: + payload = _snapshot_payload() + + model = RuntimeSnapshotEnvelopeModel.model_validate(payload) + assert model.joint_action_records["joint-red-blue-0001"].conflict_policy == "serialize" + assert model.time_management_contexts["tm-red-blue-0001"].mode == "backend_serialized" + + validator = Draft202012Validator(schema_bundle()["runtime-snapshot-v1"]) + assert list(validator.iter_errors(payload)) == [] + + +def test_joint_action_record_contract_rejects_unordered_conflicting_writes() -> None: + payload = _joint_action( + access_sets=[ + {"member_event_ref": "scan-red-0001", "shared_state_write_refs": [STATE_ADDRESS]}, + {"member_event_ref": "scan-blue-0001", "shared_state_write_refs": [STATE_ADDRESS]}, + ], + conflict_class="none", + conflict_policy="none", + isolation_guarantee="none", + realized_order=[], + exact_concurrency_claim=True, + ) + + with pytest.raises(ValidationError, match="conflict_class"): + ParticipantJointActionRecordModel.model_validate(payload) + + +def test_joint_action_record_contract_rejects_exact_claim_without_time_context() -> None: + payload = _joint_action( + exact_concurrency_claim=True, + time_management_context_ref=None, + ) + + with pytest.raises(ValidationError, match="time_management_context_ref"): + ParticipantJointActionRecordModel.model_validate(payload) + + +@pytest.mark.parametrize( + ("overrides", "message"), + [ + ({"member_event_refs": ["scan-red-0001", "scan-red-0001"]}, "member_event_refs must be unique"), + ( + {"access_sets": [{"member_event_ref": "scan-red-0001"}]}, + "access_sets must cover member_event_refs", + ), + ({"realized_order": ["scan-red-0001", "scan-missing-0001"]}, "realized_order"), + ( + {"unsupported_disclosure": True, "exact_concurrency_claim": True}, + "unsupported concurrency disclosure", + ), + ( + {"conflict_policy": "unsupported", "unsupported_disclosure": False}, + "unsupported conflict_policy", + ), + ( + { + "conflict_policy": "retry", + "isolation_guarantee": "serializable", + "realized_order": [], + "retry_limit": 1, + "rollback_event_refs": ["scan-red-0001"], + }, + "serializable joint action isolation", + ), + ( + {"conflict_policy": "serialize", "isolation_guarantee": "none", "realized_order": []}, + "serialize conflict_policy", + ), + ( + { + "conflict_policy": "retry", + "isolation_guarantee": "none", + "realized_order": [], + "retry_limit": None, + "rollback_event_refs": [], + }, + "retry conflict_policy", + ), + ( + {"conflict_policy": "none", "isolation_guarantee": "none", "realized_order": []}, + "none conflict_policy", + ), + ( + { + "conflict_policy": "reject", + "isolation_guarantee": "none", + "atomicity_scope": "multi_object", + "realized_order": [], + }, + "multi_object conflicting joint actions", + ), + ( + { + "conflict_class": "none", + "conflict_policy": "reject", + "isolation_guarantee": "none", + "unsupported_disclosure": True, + }, + "conflict_class cannot be none", + ), + ], +) +def test_joint_action_record_contract_rejects_concurrency_guardrails( + overrides: dict[str, object], + message: str, +) -> None: + payload = _joint_action(**overrides) + + with pytest.raises(ValidationError, match=message): + ParticipantJointActionRecordModel.model_validate(payload) + + +def test_joint_action_record_contract_accepts_unsupported_policy_disclosure() -> None: + payload = _joint_action( + conflict_policy="unsupported", + unsupported_disclosure=True, + exact_concurrency_claim=False, + ) + + model = ParticipantJointActionRecordModel.model_validate(payload) + + assert model.conflict_policy == "unsupported" + + +def test_time_management_context_contract_rejects_wall_clock_exact_claim() -> None: + payload = _time_context( + mode="display", + claim_strength="exact", + basis="wall_clock_only", + clock_ref="clock.wall", + backend_serialized=False, + ) + + with pytest.raises(ValidationError, match="wall_clock_only"): + ParticipantTimeManagementContextModel.model_validate(payload) + + +@pytest.mark.parametrize( + ("overrides", "message"), + [ + ( + {"mode": "display", "claim_strength": "bounded", "basis": "logical_clock", "clock_ref": None}, + "clock_ref", + ), + ({"backend_serialized": False}, "backend_serialized mode"), + ( + { + "mode": "lookahead", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "backend_serialized": False, + }, + "lookahead mode", + ), + ( + { + "mode": "pacing", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "backend_serialized": False, + }, + "pacing mode", + ), + ( + { + "mode": "rollback", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "rollback_event_refs": [], + "backend_serialized": False, + }, + "rollback mode", + ), + ( + { + "mode": "devs", + "claim_strength": "display", + "basis": "wall_clock_only", + "clock_ref": None, + "backend_serialized": False, + }, + "devs and fmi modes", + ), + ( + { + "mode": "unsupported", + "claim_strength": "display", + "basis": "wall_clock_only", + "clock_ref": None, + "backend_serialized": False, + }, + "unsupported time-management mode", + ), + ( + { + "mode": "display", + "claim_strength": "exact", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "unsupported_disclosure": True, + "backend_serialized": False, + }, + "unsupported time-management disclosure", + ), + ], +) +def test_time_management_context_contract_rejects_mode_guardrails( + overrides: dict[str, object], + message: str, +) -> None: + payload = _time_context(**overrides) + + with pytest.raises(ValidationError, match=message): + ParticipantTimeManagementContextModel.model_validate(payload) + + +def test_participant_runtime_state_diagnostics_reject_unresolved_concurrency_refs() -> None: + payload = _snapshot_payload( + joint_action_records={ + "joint-red-blue-0001": _joint_action( + member_event_refs=["scan-red-0001", "scan-missing-0001"], + access_sets=[ + {"member_event_ref": "scan-red-0001", "shared_state_write_refs": [STATE_ADDRESS]}, + {"member_event_ref": "scan-missing-0001", "shared_state_read_refs": [STATE_ADDRESS]}, + ], + realized_order=["scan-red-0001", "scan-missing-0001"], + ) + } + ) + snapshot = RuntimeSnapshot( + participant_behavior_history=dict(payload["participant_behavior_history"]), + shared_state_records=dict(payload["shared_state_records"]), + shared_state_history=dict(payload["shared_state_history"]), + joint_action_records=dict(payload["joint_action_records"]), + time_management_contexts=dict(payload["time_management_contexts"]), + ) + + diagnostics = participant_runtime_state_contract_diagnostics(snapshot) + + assert any("scan-missing-0001" in diagnostic.message for diagnostic in diagnostics) + + +def test_participant_runtime_state_diagnostics_reject_shared_state_refs_when_index_empty() -> None: + payload = _snapshot_payload( + shared_state_records={}, + shared_state_history={}, + ) + snapshot = RuntimeSnapshot( + participant_behavior_history=dict(payload["participant_behavior_history"]), + shared_state_records={}, + shared_state_history={}, + joint_action_records=dict(payload["joint_action_records"]), + time_management_contexts=dict(payload["time_management_contexts"]), + ) + + diagnostics = participant_runtime_state_contract_diagnostics(snapshot) + + assert any(f"{STATE_ADDRESS!r} does not resolve" in diagnostic.message for diagnostic in diagnostics) + + +def test_participant_runtime_state_diagnostics_reject_exact_claim_with_bounded_time_context() -> None: + payload = _snapshot_payload( + joint_action_records={ + "joint-red-blue-0001": _joint_action( + exact_concurrency_claim=True, + ) + } + ) + snapshot = RuntimeSnapshot( + participant_behavior_history=dict(payload["participant_behavior_history"]), + shared_state_records=dict(payload["shared_state_records"]), + shared_state_history=dict(payload["shared_state_history"]), + joint_action_records=dict(payload["joint_action_records"]), + time_management_contexts=dict(payload["time_management_contexts"]), + ) + + diagnostics = participant_runtime_state_contract_diagnostics(snapshot) + + assert any("exact time-management context" in diagnostic.message for diagnostic in diagnostics) + + +def test_participant_runtime_state_diagnostics_reject_rollback_refs_when_history_empty() -> None: + payload = _snapshot_payload( + participant_behavior_history={}, + time_management_contexts={ + "tm-red-blue-0001": _time_context( + mode="rollback", + claim_strength="bounded", + rollback_event_refs=["scan-red-0001"], + backend_serialized=False, + ) + }, + ) + snapshot = RuntimeSnapshot( + participant_behavior_history={}, + shared_state_records=dict(payload["shared_state_records"]), + shared_state_history=dict(payload["shared_state_history"]), + joint_action_records={}, + time_management_contexts=dict(payload["time_management_contexts"]), + ) + + diagnostics = participant_runtime_state_contract_diagnostics(snapshot) + + assert any( + "rollback_event_ref 'scan-red-0001' does not resolve" in diagnostic.message for diagnostic in diagnostics + ) + + +def test_participant_concurrency_snapshot_validator_rejects_malformed_maps() -> None: + violations = list( + iter_participant_concurrency_snapshot_violations( + joint_action_records=[], + time_management_contexts=[], + participant_behavior_history=None, + shared_state_records=None, + shared_state_history=None, + ) + ) + messages = [message for _address, message in violations] + + assert "joint_action_records must be a mapping" in messages + assert "time_management_contexts must be a mapping" in messages + + +def test_participant_concurrency_snapshot_validator_rejects_malformed_records() -> None: + payload = _snapshot_payload( + joint_action_records={"": {}, "joint-red-blue-0001": "not-a-record"}, + time_management_contexts={"": {}, "tm-red-blue-0001": "not-a-context"}, + ) + + messages = _concurrency_violation_messages(payload) + + assert "joint_action_records keys must be non-empty strings" in messages + assert "joint action record must be a mapping" in messages + assert "time_management_contexts keys must be non-empty strings" in messages + assert "time management context must be a mapping" in messages + + +def test_participant_concurrency_snapshot_validator_rejects_malformed_joint_action_fields() -> None: + payload = _snapshot_payload( + joint_action_records={ + "joint-red-blue-0001": _joint_action( + joint_action_set_id="joint-other-0001", + member_event_refs=["scan-red-0001", ""], + access_sets=[ + {}, + { + "member_event_ref": "scan-red-0001", + "shared_state_read_refs": "not-a-list", + "shared_state_write_refs": ["", "state.missing"], + }, + ], + realized_order="not-a-list", + conflict_class="none", + conflict_policy="none", + isolation_guarantee="none", + time_management_context_ref="tm-missing-0001", + ), + "joint-missing-id": _joint_action( + joint_action_set_id="", + member_event_refs=["scan-red-0001", "scan-blue-0001"], + access_sets=[], + realized_order=["scan-red-0001", "scan-missing-0001"], + conflict_class="none", + conflict_policy="none", + isolation_guarantee="none", + time_management_context_ref=None, + exact_concurrency_claim=True, + ), + "joint-bad-access": _joint_action( + joint_action_set_id="joint-bad-access", + member_event_refs=["scan-red-0001"], + access_sets=["not-a-map"], + realized_order=[], + conflict_class="none", + conflict_policy="none", + isolation_guarantee="none", + time_management_context_ref=None, + ), + } + ) + + messages = _concurrency_violation_messages(payload) + + assert any("does not match joint_action_set_id" in message for message in messages) + assert "joint action record requires joint_action_set_id" in messages + assert "joint action member_event_refs entries must be non-empty strings" in messages + assert "joint action access_sets must be a non-empty list" in messages + assert "joint action access set must be a mapping" in messages + assert "joint action access set requires member_event_ref" in messages + assert "shared_state_read_refs must be a list" in messages + assert "shared_state_write_refs entries must be non-empty strings" in messages + assert "shared_state_write_refs entry 'state.missing' does not resolve" in messages + assert "joint action realized_order must be a list" in messages + assert "joint action realized_order must be an exact permutation of member_event_refs" in messages + assert "time_management_context_ref 'tm-missing-0001' does not resolve" in messages + assert "exact concurrency claims require time_management_context_ref" in messages + + +@pytest.mark.parametrize( + ("overrides", "message"), + [ + ( + { + "unsupported_disclosure": True, + "exact_concurrency_claim": True, + }, + "unsupported concurrency disclosure", + ), + ( + { + "conflict_policy": "unsupported", + "unsupported_disclosure": False, + "realized_order": [], + "isolation_guarantee": "none", + }, + "unsupported conflict_policy", + ), + ( + { + "conflict_policy": "serialize", + "realized_order": [], + "isolation_guarantee": "none", + }, + "serialize conflict_policy", + ), + ( + { + "conflict_policy": "retry", + "isolation_guarantee": "serializable", + "realized_order": [], + "retry_limit": 1, + "rollback_event_refs": ["scan-red-0001"], + }, + "serializable joint action isolation", + ), + ( + { + "conflict_policy": "retry", + "realized_order": [], + "isolation_guarantee": "none", + "retry_limit": None, + "rollback_event_refs": [], + }, + "retry conflict_policy", + ), + ( + { + "conflict_policy": "none", + "realized_order": [], + "isolation_guarantee": "none", + }, + "none conflict_policy", + ), + ( + { + "conflict_policy": "reject", + "realized_order": [], + "isolation_guarantee": "none", + "atomicity_scope": "multi_object", + }, + "multi_object conflicting joint actions", + ), + ( + { + "access_sets": [ + {"member_event_ref": "scan-red-0001", "shared_state_write_refs": [STATE_ADDRESS]}, + {"member_event_ref": "scan-blue-0001", "shared_state_write_refs": [STATE_ADDRESS]}, + ], + "conflict_class": "read_write", + "conflict_policy": "reject", + "isolation_guarantee": "none", + }, + "conflict_class must match", + ), + ( + { + "conflict_class": "none", + "conflict_policy": "reject", + "isolation_guarantee": "none", + "unsupported_disclosure": True, + }, + "conflict_class cannot be none", + ), + ], +) +def test_participant_concurrency_snapshot_validator_rejects_conflict_guardrails( + overrides: dict[str, object], + message: str, +) -> None: + payload = _snapshot_payload(joint_action_records={"joint-red-blue-0001": _joint_action(**overrides)}) + + messages = _concurrency_violation_messages(payload) + + assert any(message in violation for violation in messages) + + +@pytest.mark.parametrize( + ("overrides", "message"), + [ + ({"context_id": ""}, "requires context_id"), + ({"context_id": "tm-other-0001"}, "does not match context_id"), + ( + {"mode": "display", "claim_strength": "bounded", "basis": "logical_clock", "clock_ref": None}, + "clock_ref", + ), + ( + { + "mode": "display", + "claim_strength": "exact", + "basis": "wall_clock_only", + "clock_ref": "clock.wall", + "backend_serialized": False, + }, + "wall_clock_only time basis", + ), + ({"backend_serialized": False}, "backend_serialized mode"), + ( + { + "mode": "lookahead", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "backend_serialized": False, + }, + "lookahead mode", + ), + ( + { + "mode": "pacing", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "backend_serialized": False, + }, + "pacing mode", + ), + ( + { + "mode": "rollback", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "rollback_event_refs": [], + "backend_serialized": False, + }, + "rollback mode", + ), + ( + { + "mode": "devs", + "claim_strength": "display", + "basis": "wall_clock_only", + "clock_ref": None, + "backend_serialized": False, + }, + "devs and fmi modes", + ), + ( + { + "mode": "unsupported", + "claim_strength": "display", + "basis": "wall_clock_only", + "clock_ref": None, + "backend_serialized": False, + }, + "unsupported time-management mode", + ), + ( + { + "mode": "display", + "claim_strength": "exact", + "basis": "logical_clock", + "clock_ref": "clock.logical.runtime", + "unsupported_disclosure": True, + "backend_serialized": False, + }, + "unsupported time-management disclosure", + ), + ], +) +def test_participant_concurrency_snapshot_validator_rejects_time_context_guardrails( + overrides: dict[str, object], + message: str, +) -> None: + payload = _snapshot_payload(time_management_contexts={"tm-red-blue-0001": _time_context(**overrides)}) + + messages = _concurrency_violation_messages(payload) + + assert any(message in violation for violation in messages) + + +def test_backend_apply_rejects_rewriting_joint_action_records() -> None: + payload = _snapshot_payload() + base_snapshot = RuntimeSnapshot( + participant_behavior_history=dict(payload["participant_behavior_history"]), + shared_state_records=dict(payload["shared_state_records"]), + shared_state_history=dict(payload["shared_state_history"]), + joint_action_records=dict(payload["joint_action_records"]), + time_management_contexts=dict(payload["time_management_contexts"]), + ) + + def _backend_apply(_request: object, snapshot: RuntimeSnapshot) -> ApplyResult: + return ApplyResult( + success=True, + snapshot=snapshot.with_entries( + dict(snapshot.entries), + joint_action_records={}, + time_management_contexts=dict(snapshot.time_management_contexts), + ), + ) + + result = _call_backend_apply( + _backend_apply, + object(), + base_snapshot, + address="runtime.control-plane.concurrent-participants", + snapshot=base_snapshot, + ) + + assert result.success is False + assert any("joint_action_records must be append-only" in item.message for item in result.diagnostics) + + +def test_participant_concurrency_transition_validator_rejects_rewriting_records() -> None: + assert ( + list( + iter_participant_concurrency_transition_violations( + {"": {}}, + {}, + [], + {}, + ) + ) + == [] + ) + + violations = list( + iter_participant_concurrency_transition_violations( + {"joint-red-blue-0001": _joint_action()}, + {"joint-red-blue-0001": _joint_action(conflict_policy="reject")}, + {"tm-red-blue-0001": _time_context()}, + { + "tm-red-blue-0001": _time_context( + mode="display", + claim_strength="display", + basis="wall_clock_only", + backend_serialized=False, + ) + }, + ) + ) + messages = [message for _address, message in violations] + + assert "joint_action_records must be append-only; record 'joint-red-blue-0001' changed" in messages + assert "time_management_contexts must be append-only; record 'tm-red-blue-0001' changed" in messages + + +def test_joint_action_and_time_context_model_round_trip() -> None: + joint_action = ParticipantJointActionRecordModel.model_validate(_joint_action()) + time_context = ParticipantTimeManagementContextModel.model_validate(_time_context()) + + assert joint_action.model_dump(mode="json")["conflict_class"] == "read_write" + assert time_context.model_dump(mode="json")["claim_strength"] == "bounded" diff --git a/implementations/python/tests/test_runtime_conformance.py b/implementations/python/tests/test_runtime_conformance.py index 0eeb37fdb..6d4f689f2 100644 --- a/implementations/python/tests/test_runtime_conformance.py +++ b/implementations/python/tests/test_runtime_conformance.py @@ -19,6 +19,12 @@ ) from aces.core.runtime.registry import RuntimeTarget +API_406_CARRIER_CONTRACTS = { + "participant-lifecycle-event-v1", + "participant-observation-envelope-v1", + "participant-shared-state-record-v1", +} + def test_fixture_suite_passes_for_orchestration_evaluation_profile(): report = run_fixture_suite(profile=BackendCapabilityProfile.ORCHESTRATION_EVALUATION) @@ -61,6 +67,46 @@ def test_target_conformance_passes_for_stub_target(): ) +def test_full_remote_control_plane_profile_requires_api_406_carriers(): + contracts = required_contracts(BackendCapabilityProfile.FULL_REMOTE_CONTROL_PLANE) + + assert contracts >= API_406_CARRIER_CONTRACTS + assert { + "runtime-snapshot-v1", + "participant-episode-state-envelope-v1", + "participant-episode-history-event-stream-v1", + "participant-behavior-history-event-stream-v1", + } <= contracts + + +def test_fixture_suite_validates_api_406_carrier_fixtures(tmp_path: Path): + backend_dir = tmp_path / "backend" + backend_dir.mkdir() + (backend_dir / "provisioning-only.json").write_text( + json.dumps( + { + "schema_version": "backend-profile/v1", + "profile": "provisioning-only", + "required_contracts": sorted(API_406_CARRIER_CONTRACTS), + } + ) + + "\n", + encoding="utf-8", + ) + + report = run_fixture_suite( + profile=BackendCapabilityProfile.PROVISIONING_ONLY, + profiles_root=backend_dir, + ) + + assert report.passed is True + assert {case.contract_name for case in report.cases} == API_406_CARRIER_CONTRACTS + assert any(case.valid is False for case in report.cases) + assert all( + diagnostic.code != "conformance.contract-unknown" for case in report.cases for diagnostic in case.diagnostics + ) + + def test_profile_is_inferred_as_full_when_manifest_declares_participant_runtime(): """RUN-311 — finding 3: a manifest that declares orchestrator, evaluator, and participant_runtime must infer the @@ -337,6 +383,84 @@ def test_runtime_snapshot_behavior_history_refs_must_match_snapshot_entries(): assert not any("unknown action_contract_address" in message for message in messages) +def test_runtime_snapshot_behavior_history_requires_participant_behavior_binding(): + action_address = "participant.action-contract.scan" + boundary_address = "participant.observation-boundary.red-view" + participant_address = "participant.behavior.red-agent" + + def _snapshot_entry(address: str, resource_type: str) -> dict[str, object]: + return { + "address": address, + "domain": "participant", + "resource_type": resource_type, + "payload": {}, + "ordering_dependencies": [], + "refresh_dependencies": [], + "status": "ready", + } + + snapshot_payload = { + "schema_version": "runtime-snapshot/v1", + "entries": { + action_address: _snapshot_entry(action_address, "participant-action-contract"), + boundary_address: _snapshot_entry(boundary_address, "participant-observation-boundary"), + }, + "orchestration_results": {}, + "orchestration_history": {}, + "evaluation_results": {}, + "evaluation_history": {}, + "participant_episode_results": {}, + "participant_episode_history": {}, + "participant_behavior_history": { + participant_address: [ + { + "event_type": "action_attempted", + "timestamp": "2026-05-18T18:30:00Z", + "participant_address": participant_address, + "episode_id": "episode-1", + "action_instance_id": "scan-1", + "action_contract_address": action_address, + "actor_provenance": participant_address, + "details": {}, + }, + { + "event_type": "state_transition_recorded", + "timestamp": "2026-05-18T18:30:01Z", + "participant_address": participant_address, + "episode_id": "episode-1", + "action_instance_id": "scan-1", + "action_contract_address": action_address, + "state_transition_kind": "knowledge-expanded", + "post_state_digest": "sha256:scan-1", + "details": {}, + }, + { + "event_type": "observation_emitted", + "timestamp": "2026-05-18T18:30:02Z", + "participant_address": participant_address, + "episode_id": "episode-1", + "action_instance_id": "scan-1", + "action_contract_address": action_address, + "observation_boundary_address": boundary_address, + "observation_status": "terminal", + "post_state_digest": "sha256:scan-1", + "details": {}, + }, + ] + }, + "metadata": {}, + } + + diagnostics = _semantic_diagnostics("runtime-snapshot-v1", snapshot_payload) + + assert any( + diagnostic.code == "conformance.semantic-invalid" + and diagnostic.address == f"runtime.snapshot.participant-behavior-history.{participant_address}" + and "requires a participant.behavior snapshot entry" in diagnostic.message + for diagnostic in diagnostics + ) + + def test_runtime_snapshot_behavior_history_validates_joint_action_order_across_participants(): action_address = "participant.action-contract.scan" boundary_address = "participant.observation-boundary.red-view" @@ -701,6 +825,9 @@ def test_target_conformance_fails_when_declared_contracts_do_not_cover_profile_r "participant-behavior-history-event-stream-v1", "participant-episode-history-event-stream-v1", "participant-episode-state-envelope-v1", + "participant-lifecycle-event-v1", + "participant-observation-envelope-v1", + "participant-shared-state-record-v1", "provisioning-plan-v1", "runtime-snapshot-v1", "workflow-history-event-stream-v1", diff --git a/implementations/python/tests/test_runtime_contracts.py b/implementations/python/tests/test_runtime_contracts.py index 8640c67a7..cdd39b3b2 100644 --- a/implementations/python/tests/test_runtime_contracts.py +++ b/implementations/python/tests/test_runtime_contracts.py @@ -12,7 +12,11 @@ AcesSemanticInvariantProfileReferenceModel, BackendManifestV2Model, ExperimentApparatusContextModel, + ExperimentCaptureSpecModel, + ExperimentDerivedMeasureModel, + ExperimentEvidenceRecordModel, ExperimentRunModel, + ExperimentRunTraceabilityModel, ExperimentStudyModel, ExperimentTaskModel, ParticipantImplementationManifestModel, @@ -38,6 +42,9 @@ EXPERIMENT_CORE_FIXTURE_MODELS = { "experiment-apparatus-context-v1": ExperimentApparatusContextModel, + "experiment-capture-spec-v1": ExperimentCaptureSpecModel, + "experiment-derived-measure-v1": ExperimentDerivedMeasureModel, + "experiment-evidence-record-v1": ExperimentEvidenceRecordModel, "experiment-run-v1": ExperimentRunModel, "experiment-study-v1": ExperimentStudyModel, "experiment-task-v1": ExperimentTaskModel, @@ -427,6 +434,139 @@ def test_experiment_core_schemas_publish_closed_world_contracts(): ) +def test_experiment_evidence_measure_schemas_publish_separate_surfaces(): + generated = schema_bundle() + + capture_schema = generated["experiment-capture-spec-v1"] + evidence_schema = generated["experiment-evidence-record-v1"] + measure_schema = generated["experiment-derived-measure-v1"] + + assert capture_schema["additionalProperties"] is False + assert evidence_schema["additionalProperties"] is False + assert measure_schema["additionalProperties"] is False + assert capture_schema["properties"]["capture_requirements"]["type"] == "object" + assert ( + capture_schema["properties"]["capture_requirements"]["additionalProperties"]["$ref"] + == "#/$defs/ExperimentCaptureRequirementModel" + ) + assert "capture-requirement-key-matches-requirement-id" in _invariant_ids(capture_schema) + assert "capture-window-interval-valid" in _invariant_ids(capture_schema["$defs"]["ExperimentCaptureWindowModel"]) + assert set(capture_schema["required"]) >= { + "schema_version", + "capture_spec_id", + "spec_version", + "scope_refs", + "capture_windows", + "capture_requirements", + } + + assert evidence_schema["properties"]["capture_spec_ref"]["$ref"] == "#/$defs/ExperimentCaptureSpecReferenceModel" + assert evidence_schema["properties"]["raw_content"]["$ref"] == "#/$defs/ExperimentRawEvidenceContentModel" + assert "metric_id" not in evidence_schema["properties"] + assert "value" not in evidence_schema["properties"] + assert "evidence-record-raw-content-present" in _invariant_ids(evidence_schema) + assert "evidence-record-captured-at-valid" in _invariant_ids(evidence_schema) + assert set(evidence_schema["required"]) >= { + "schema_version", + "evidence_record_id", + "record_version", + "capture_spec_ref", + "run_ref", + "evidence_kind", + "captured_at", + "raw_content", + "sensitivity", + "redaction_state", + } + + assert measure_schema["properties"]["source_evidence_refs"]["items"]["$ref"] == ( + "#/$defs/ExperimentEvidenceRecordReferenceModel" + ) + assert "derived-measure-reported-value-present" in _invariant_ids(measure_schema) + assert "derived-measure-generated-at-valid" in _invariant_ids(measure_schema) + assert set(measure_schema["required"]) >= { + "schema_version", + "derived_measure_id", + "measure_version", + "measure_kind", + "metric_ref", + "method", + "source_evidence_refs", + "generated_at", + "value_status", + } + + +def test_experiment_run_schema_publishes_canonical_provenance_surface(): + generated = schema_bundle() + run_schema = generated["experiment-run-v1"] + + assert run_schema["properties"]["traceability"]["$ref"] == "#/$defs/ExperimentRunTraceabilityModel" + assert run_schema["properties"]["realized_form_disclosures"]["items"]["$ref"] == ( + "#/$defs/ExperimentRealizedFormDisclosureModel" + ) + assert "traceability" in run_schema["required"] + + traceability_schema = run_schema["$defs"]["ExperimentRunTraceabilityModel"] + assert traceability_schema["additionalProperties"] is False + assert set(traceability_schema["required"]) >= {"capture_spec_refs", "evidence_record_refs"} + assert "run-traceability-refs-unique" in _invariant_ids(traceability_schema) + + disclosure_schema = run_schema["$defs"]["ExperimentRealizedFormDisclosureModel"] + assert disclosure_schema["additionalProperties"] is False + assert "realized-form-disclosure-substantive" in _invariant_ids(disclosure_schema) + assert any( + rule.get("if", {}).get("properties", {}).get("basis", {}).get("const") == "processor-realized" + for rule in disclosure_schema["allOf"] + ) + + +def test_experiment_run_provenance_contracts_reject_boundary_blurring(): + payload = _experiment_fixture("experiment-run-v1") + + missing_traceability = deepcopy(payload) + del missing_traceability["traceability"] + _assert_schema_and_model_reject("experiment-run-v1", missing_traceability) + + duplicate_evidence_record = deepcopy(payload) + duplicate_evidence_record["traceability"]["evidence_record_refs"].append( + deepcopy(duplicate_evidence_record["traceability"]["evidence_record_refs"][0]) + ) + with pytest.raises(ValidationError, match="traceability evidence_record_refs must not contain duplicates"): + ExperimentRunTraceabilityModel.model_validate(duplicate_evidence_record["traceability"]) + + unsupported_realized_form = deepcopy(payload) + unsupported_realized_form["realized_form_disclosures"][0]["realized_ref"] = None + unsupported_realized_form["realized_form_disclosures"][0]["realized_value_summary"] = None + _assert_schema_and_model_reject("experiment-run-v1", unsupported_realized_form) + + processor_disclosure_by_backend = deepcopy(payload) + processor_disclosure_by_backend["realized_form_disclosures"][0]["realized_by_ref"]["ref_kind"] = "backend" + _assert_schema_and_model_reject("experiment-run-v1", processor_disclosure_by_backend) + + disclosure_without_traced_evidence = deepcopy(payload) + disclosure_without_traced_evidence["realized_form_disclosures"][0]["evidence_refs"][0]["ref_id"] = ( + "untraced-evidence-record" + ) + with pytest.raises(ValidationError, match="realized_form_disclosures evidence_refs must be listed"): + ExperimentRunModel.model_validate(disclosure_without_traced_evidence) + + +def test_experiment_evidence_measure_contracts_reject_boundary_blurring(): + capture_payload = _experiment_fixture("experiment-capture-spec-v1") + capture_payload["capture_requirements"]["network-trace"]["requirement_id"] = "different-id" + with pytest.raises(ValidationError, match="capture_requirements keys"): + ExperimentCaptureSpecModel.model_validate(capture_payload) + + raw_evidence_payload = _experiment_fixture("experiment-evidence-record-v1") + raw_evidence_payload["metric_id"] = "latency-ms" + _assert_schema_and_model_reject("experiment-evidence-record-v1", raw_evidence_payload) + + derived_payload = _experiment_fixture("experiment-derived-measure-v1") + derived_payload["source_evidence_refs"] = [] + _assert_schema_and_model_reject("experiment-derived-measure-v1", derived_payload) + + def test_aces_semantic_invariant_annotations_have_published_shape(): generated = schema_bundle() run_schema = generated["experiment-run-v1"] @@ -647,6 +787,141 @@ def test_experiment_core_rejects_under_specified_apparatus_contexts(): ExperimentApparatusContextModel.model_validate(extra_digest_manifest_with_supported_processor_subject) +def test_experiment_core_rejects_under_specified_capture_specs(): + payload = _experiment_fixture("experiment-capture-spec-v1") + + mismatched_requirement_key = deepcopy(payload) + mismatched_requirement_key["capture_requirements"]["network-trace"]["requirement_id"] = "different-id" + with pytest.raises(ValidationError, match="capture_requirements keys"): + ExperimentCaptureSpecModel.model_validate(mismatched_requirement_key) + + unresolved_window_ref = deepcopy(payload) + unresolved_window_ref["capture_requirements"]["network-trace"]["window_refs"] = ["missing-window"] + with pytest.raises(ValidationError, match="window_refs must resolve"): + ExperimentCaptureSpecModel.model_validate(unresolved_window_ref) + + reversed_capture_window = deepcopy(payload) + reversed_capture_window["capture_windows"][0]["starts_at"] = "2026-05-26T00:40:00Z" + reversed_capture_window["capture_windows"][0]["ends_at"] = "2026-05-26T00:10:00Z" + with pytest.raises(ValidationError, match="ends_at must be greater"): + ExperimentCaptureSpecModel.model_validate(reversed_capture_window) + + under_specified_window = deepcopy(payload) + under_specified_window["capture_windows"][0].pop("starts_at", None) + under_specified_window["capture_windows"][0].pop("ends_at", None) + under_specified_window["capture_windows"][0].pop("trigger_ref", None) + _assert_schema_and_model_reject("experiment-capture-spec-v1", under_specified_window) + + +def test_experiment_core_rejects_under_specified_evidence_records(): + payload = _experiment_fixture("experiment-evidence-record-v1") + + content_uri_without_checksum = deepcopy(payload) + content_uri_without_checksum["raw_content"].pop("content_checksum", None) + with pytest.raises(ValidationError, match="content_checksum"): + ExperimentEvidenceRecordModel.model_validate(content_uri_without_checksum) + + empty_source_refs = deepcopy(payload) + empty_source_refs["source_refs"] = [] + _assert_schema_and_model_reject("experiment-evidence-record-v1", empty_source_refs) + + invalid_captured_at = deepcopy(payload) + invalid_captured_at["captured_at"] = "not-a-timestamp" + _assert_schema_and_model_reject("experiment-evidence-record-v1", invalid_captured_at) + + redacted_without_loss_disclosure = deepcopy(payload) + redacted_without_loss_disclosure["redaction_state"] = "redacted" + redacted_without_loss_disclosure["raw_content"].pop("loss_disclosure", None) + with pytest.raises(ValidationError, match="loss_disclosure"): + ExperimentEvidenceRecordModel.model_validate(redacted_without_loss_disclosure) + + +def test_experiment_core_rejects_under_specified_derived_measures(): + payload = _experiment_fixture("experiment-derived-measure-v1") + + reported_without_value = deepcopy(payload) + reported_without_value["value_status"] = "reported" + reported_without_value.pop("value", None) + with pytest.raises(ValidationError, match="reported derived measures must include value"): + ExperimentDerivedMeasureModel.model_validate(reported_without_value) + + non_reported_with_value = deepcopy(payload) + non_reported_with_value["value_status"] = "withheld" + non_reported_with_value["value"] = True + with pytest.raises(ValidationError, match="non-reported derived measures must not include value"): + ExperimentDerivedMeasureModel.model_validate(non_reported_with_value) + + invalid_generated_at = deepcopy(payload) + invalid_generated_at["generated_at"] = "not-a-timestamp" + _assert_schema_and_model_reject("experiment-derived-measure-v1", invalid_generated_at) + + empty_source_evidence_refs = deepcopy(payload) + empty_source_evidence_refs["source_evidence_refs"] = [] + _assert_schema_and_model_reject("experiment-derived-measure-v1", empty_source_evidence_refs) + + +def test_experiment_core_rejects_under_specified_run_provenance(): + payload = _experiment_fixture("experiment-run-v1") + + claim_without_derived_measure = deepcopy(payload) + claim_without_derived_measure["traceability"]["claim_refs"] = [{"ref_kind": "result", "ref_id": "claim-ungrounded"}] + claim_without_derived_measure["traceability"]["derived_measure_refs"] = [] + with pytest.raises(ValidationError, match="claim_refs require at least one derived_measure_refs entry"): + ExperimentRunModel.model_validate(claim_without_derived_measure) + + duplicate_capture_spec_ref = deepcopy(payload) + duplicate_capture_spec_ref["traceability"]["capture_spec_refs"].append( + deepcopy(duplicate_capture_spec_ref["traceability"]["capture_spec_refs"][0]) + ) + with pytest.raises(ValidationError, match="traceability capture_spec_refs must not contain duplicates"): + ExperimentRunTraceabilityModel.model_validate(duplicate_capture_spec_ref["traceability"]) + + realized_form_missing_target = deepcopy(payload) + realized_form_missing_target["realized_form_disclosures"][0]["realized_ref"] = None + realized_form_missing_target["realized_form_disclosures"][0]["realized_value_summary"] = None + _assert_schema_and_model_reject("experiment-run-v1", realized_form_missing_target) + + backend_realized_by_processor = deepcopy(payload) + backend_realized_by_processor["realized_form_disclosures"][0]["basis"] = "backend-realized" + _assert_schema_and_model_reject("experiment-run-v1", backend_realized_by_processor) + + empty_traceability_capture_specs = deepcopy(payload) + empty_traceability_capture_specs["traceability"]["capture_spec_refs"] = [] + _assert_schema_and_model_reject("experiment-run-v1", empty_traceability_capture_specs) + + +def test_experiment_core_rejects_under_specified_realized_form_disclosures(): + # EXP-722: realized forms chosen for underspecified concerns must be preserved + # substantively, attributed to the right processor/backend authority, and grounded + # in run-traced evidence so they stay distinct from the authored scenario and from + # derived results. The model and published schema shipped under #89; this is the + # conformance test of record and must not change them. + payload = _experiment_fixture("experiment-run-v1") + + missing_realized_target = deepcopy(payload) + missing_realized_target["realized_form_disclosures"][0]["realized_ref"] = None + missing_realized_target["realized_form_disclosures"][0]["realized_value_summary"] = None + _assert_schema_and_model_reject("experiment-run-v1", missing_realized_target) + + processor_realized_by_backend = deepcopy(payload) + processor_realized_by_backend["realized_form_disclosures"][0]["realized_by_ref"]["ref_kind"] = "backend" + _assert_schema_and_model_reject("experiment-run-v1", processor_realized_by_backend) + + disclosure_evidence_not_traced = deepcopy(payload) + disclosure_evidence_not_traced["realized_form_disclosures"][0]["evidence_refs"][0]["ref_id"] = ( + "evidence-realized-form-untraced" + ) + with pytest.raises(ValidationError, match="realized_form_disclosures evidence_refs must be listed"): + ExperimentRunModel.model_validate(disclosure_evidence_not_traced) + + duplicate_disclosure_evidence = deepcopy(payload) + duplicate_disclosure_evidence["realized_form_disclosures"][0]["evidence_refs"].append( + deepcopy(duplicate_disclosure_evidence["realized_form_disclosures"][0]["evidence_refs"][0]) + ) + with pytest.raises(ValidationError, match="realized form disclosure evidence_refs must not contain duplicates"): + ExperimentRunModel.model_validate(duplicate_disclosure_evidence) + + def test_experiment_core_rejects_under_specified_task_disclosure_surfaces(): payload = _experiment_fixture("experiment-task-v1") diff --git a/implementations/python/tests/test_runtime_control_plane.py b/implementations/python/tests/test_runtime_control_plane.py index d4b8b1a44..988c8583a 100644 --- a/implementations/python/tests/test_runtime_control_plane.py +++ b/implementations/python/tests/test_runtime_control_plane.py @@ -5,13 +5,22 @@ import textwrap from aces_backend_stubs.stubs import create_stub_components, create_stub_manifest +from aces_contracts.contracts import ( + ParticipantContextViewModel, + ParticipantHistoryViewModel, + ParticipantStatusViewModel, +) +from aces_contracts.runtime_state import RuntimeSnapshot from aces_processor.models import iter_participant_episode_snapshot_violations from aces.backends.stubs import create_stub_target from aces.core.runtime.compiler import compile_runtime_model from aces.core.runtime.control_plane import RuntimeControlPlane +from aces.core.runtime.control_plane_store import ControlPlaneOperationRecord from aces.core.runtime.models import ( + OperationReceipt, OperationState, + OperationStatus, ParticipantEpisodeTerminalReason, RuntimeDomain, ) @@ -24,6 +33,66 @@ def _scenario(yaml_str: str): return parse_sdl(textwrap.dedent(yaml_str)) +def _episode_state(participant_address: str, episode_id: str) -> dict[str, object]: + return { + "state_schema_version": "participant-episode-state/v1", + "participant_address": participant_address, + "episode_id": episode_id, + "sequence_number": 0, + "status": "running", + "terminal_reason": None, + "initialized_at": "2026-06-05T10:00:00Z", + "updated_at": "2026-06-05T10:00:00Z", + "terminated_at": None, + "last_control_action": "initialize", + "previous_episode_id": None, + } + + +def _episode_history_event(participant_address: str, episode_id: str) -> dict[str, object]: + return { + "event_type": "episode_running", + "timestamp": "2026-06-05T10:00:00Z", + "participant_address": participant_address, + "episode_id": episode_id, + "sequence_number": 0, + "terminal_reason": None, + "control_action": None, + "details": {}, + } + + +def _behavior_history_event(participant_address: str, episode_id: str) -> dict[str, object]: + return { + "event_type": "action_attempted", + "timestamp": "2026-06-05T10:00:01Z", + "participant_address": participant_address, + "episode_id": episode_id, + "action_instance_id": f"{participant_address}.action-1", + "details": {}, + } + + +def _participant_operation_record(operation_id: str, participant_address: str) -> ControlPlaneOperationRecord: + submitted_at = "2026-06-05T10:00:00Z" + return ControlPlaneOperationRecord( + receipt=OperationReceipt( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + submitted_at=submitted_at, + accepted=True, + ), + status=OperationStatus( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + state=OperationState.RUNNING, + submitted_at=submitted_at, + updated_at=submitted_at, + changed_addresses=[participant_address], + ), + ) + + def test_control_plane_submits_provisioning_and_updates_snapshot(): scenario = _scenario(""" name: provision @@ -299,3 +368,115 @@ def test_initialize_is_idempotent_via_idempotency_key(self): "episode_initialized", "episode_running", ] + + def test_status_view_projects_current_participant_episode_state(self): + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode("participant.alice") + + view = control_plane.get_participant_status_view("participant.alice") + + assert isinstance(view, ParticipantStatusViewModel) + assert view.participant_address == "participant.alice" + assert view.episode_id == "participant.alice-episode-1" + assert view.source_snapshot_ref == "runtime.snapshot.current" + assert view.episode_state is not None + assert view.episode_state.status == "running" + episode_state = view.episode_state.model_dump(mode="json") + assert "participant_address" not in episode_state + assert "episode_id" not in episode_state + + def test_status_view_scopes_open_operations_to_participant(self): + snapshot = RuntimeSnapshot( + participant_episode_results={ + "participant.alice": _episode_state("participant.alice", "episode-1"), + "participant.bob": _episode_state("participant.bob", "episode-1"), + } + ) + control_plane = RuntimeControlPlane(create_stub_target(), initial_snapshot=snapshot) + control_plane._operations = { + "op-alice": _participant_operation_record("op-alice", "participant.alice"), + "op-bob": _participant_operation_record("op-bob", "participant.bob"), + } + + view = control_plane.get_participant_status_view("participant.alice") + + assert view is not None + assert view.open_operation_refs == ["op-alice"] + + def test_history_view_filters_to_one_participant_episode_and_projects_scope(self): + snapshot = RuntimeSnapshot( + participant_episode_results={ + "participant.alice": _episode_state("participant.alice", "episode-1"), + "participant.bob": _episode_state("participant.bob", "episode-1"), + }, + participant_episode_history={ + "participant.alice": [ + _episode_history_event("participant.alice", "episode-1"), + _episode_history_event("participant.alice", "episode-2"), + ], + "participant.bob": [_episode_history_event("participant.bob", "episode-1")], + }, + participant_behavior_history={ + "participant.alice": [ + _behavior_history_event("participant.alice", "episode-1"), + _behavior_history_event("participant.alice", "episode-2"), + ], + "participant.bob": [_behavior_history_event("participant.bob", "episode-1")], + }, + ) + control_plane = RuntimeControlPlane(create_stub_target(), initial_snapshot=snapshot) + + view = control_plane.get_participant_history_view("participant.alice", "episode-1") + + assert isinstance(view, ParticipantHistoryViewModel) + assert view.participant_address == "participant.alice" + assert view.episode_id == "episode-1" + assert view.completeness == "complete" + assert len(view.episode_history) == 1 + assert len(view.behavior_history) == 1 + for event in [*view.episode_history, *view.behavior_history]: + payload = event.model_dump(mode="json") + assert "participant_address" not in payload + assert "episode_id" not in payload + + def test_context_view_declares_sem214_reference_semantics(self): + control_plane = RuntimeControlPlane(create_stub_target()) + control_plane.initialize_participant_episode("participant.alice") + + view = control_plane.get_participant_context_view( + "participant.alice", + view_ref="views.context.network-posture.v1", + episode_id="participant.alice-episode-1", + derivation_basis_ref="rules.context.network-posture.v1", + payload_ref="evidence.context.alice.network-posture", + ) + + assert isinstance(view, ParticipantContextViewModel) + assert view.participant_address == "participant.alice" + assert view.episode_id == "participant.alice-episode-1" + assert view.view_ref == "views.context.network-posture.v1" + assert view.derived_from_refs == ["runtime.snapshot.current"] + assert view.meaning_ref == "views.context.network-posture.v1" + assert view.participant_scope == "participant_local" + assert view.audience_scope == "participant_visible" + assert view.observation_point == "participant.alice-episode-1" + assert view.source_layers[0].source_layer == "source_snapshot" + assert view.source_layers[0].temporal_relation == "same_observation_point" + assert view.transformation.transformation_rule_ref == "rules.context.network-posture.v1" + assert view.transformation.input_source_ids == ["source-snapshot"] + assert view.comparability.comparability_class == "portable_equivalent" + assert view.comparability.comparison_basis_ref == "comparability.views.context.network-posture.v1" + assert view.payload_ref == "evidence.context.alice.network-posture" + + def test_participant_retrieval_views_return_none_for_unknown_participant(self): + control_plane = RuntimeControlPlane(create_stub_target()) + + assert control_plane.get_participant_status_view("participant.unknown") is None + assert control_plane.get_participant_history_view("participant.unknown", "episode-1") is None + assert ( + control_plane.get_participant_context_view( + "participant.unknown", + view_ref="views.context.network-posture.v1", + ) + is None + ) diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index f9300eabe..720acf143 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -5,6 +5,14 @@ import textwrap from pathlib import Path +import aces_runtime.control_plane_store as control_plane_store_module +import pytest +from aces_contracts.contracts import ( + ParticipantContextViewModel, + ParticipantHistoryViewModel, + ParticipantStatusViewModel, +) +from aces_contracts.runtime_state import RuntimeSnapshot from starlette.testclient import TestClient from aces.backends.stubs import create_stub_target @@ -16,7 +24,8 @@ ControlPlaneRole, ControlPlaneSecurityConfig, ) -from aces.core.runtime.control_plane_store import LocalControlPlaneStore +from aces.core.runtime.control_plane_store import ControlPlaneOperationRecord, LocalControlPlaneStore +from aces.core.runtime.models import OperationReceipt, OperationState, OperationStatus, RuntimeDomain from aces.core.runtime.planner import plan from aces.core.sdl import parse_sdl @@ -25,6 +34,26 @@ def _scenario(yaml_str: str): return parse_sdl(textwrap.dedent(yaml_str)) +def _participant_operation_record(operation_id: str, participant_address: str) -> ControlPlaneOperationRecord: + submitted_at = "2026-06-05T10:00:00Z" + return ControlPlaneOperationRecord( + receipt=OperationReceipt( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + submitted_at=submitted_at, + accepted=True, + ), + status=OperationStatus( + operation_id=operation_id, + domain=RuntimeDomain.PARTICIPANT, + state=OperationState.RUNNING, + submitted_at=submitted_at, + updated_at=submitted_at, + changed_addresses=[participant_address], + ), + ) + + def _test_security(target_name: str, *, max_request_bytes: int = 1_000_000) -> ControlPlaneSecurityConfig: return ControlPlaneSecurityConfig( max_request_bytes=max_request_bytes, @@ -101,6 +130,10 @@ def test_control_plane_api_openapi_documents_explicit_error_responses(): ] assert "400" in terminate_responses assert "409" in terminate_responses + assert "404" in operation_responses["/participants/{participant_address}/status"]["get"]["responses"] + history_path = "/participants/{participant_address}/episodes/{episode_id}/history" + assert "404" in operation_responses[history_path]["get"]["responses"] + assert "404" in operation_responses["/participants/{participant_address}/context"]["get"]["responses"] def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): @@ -306,6 +339,73 @@ def test_control_plane_api_enforces_request_size_limit(): assert response.status_code == 413 +def test_control_plane_api_rejects_invalid_content_length_header(): + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app( + control_plane, + security=_test_security(target.name), + ) + headers = { + "x-aces-client-verified": "true", + "x-aces-client-identity": "backend-service", + "content-type": "application/json", + "content-length": "not-a-number", + } + + with TestClient(app) as client: + response = client.post( + "/operations/provisioning", + content=b'{"operations":[],"diagnostics":[]}', + headers=headers, + ) + + assert response.status_code == 400 + assert response.json() == {"detail": "invalid content-length"} + assert control_plane.audit_log()[-1].reason == "invalid content-length" + + +def test_local_control_plane_store_saves_snapshot_with_atomic_replace( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + store = LocalControlPlaneStore(tmp_path / "cp-store") + replace_calls: list[tuple[Path, Path]] = [] + real_replace = control_plane_store_module.os.replace + + def tracked_replace(source: str, destination: str) -> None: + replace_calls.append((Path(source), Path(destination))) + real_replace(source, destination) + + monkeypatch.setattr(control_plane_store_module.os, "replace", tracked_replace) + + store.save_snapshot(RuntimeSnapshot()) + + assert replace_calls + assert replace_calls[0][1] == tmp_path / "cp-store" / "snapshot.json" + assert not replace_calls[0][0].exists() + assert not list((tmp_path / "cp-store").glob("*.tmp")) + + +def test_local_control_plane_store_cleans_temp_file_after_atomic_replace_failure( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + store = LocalControlPlaneStore(tmp_path / "cp-store") + + def fail_replace(source: str, destination: str) -> None: + del source, destination + raise OSError("replace failed") + + monkeypatch.setattr(control_plane_store_module.os, "replace", fail_replace) + + with pytest.raises(OSError, match="replace failed"): + store.save_snapshot(RuntimeSnapshot()) + + assert not (tmp_path / "cp-store" / "snapshot.json").exists() + assert not list((tmp_path / "cp-store").glob("*.tmp")) + + def test_control_plane_api_cancels_workflow_runs(): scenario = _scenario(""" name: workflow @@ -440,12 +540,22 @@ def test_control_plane_api_reconciles_workflow_timeouts(): }, headers=headers, ) + workflow_address = "orchestration.workflow.response" + seeded = dict(control_plane._snapshot.orchestration_results[workflow_address]) + seeded["started_at"] = "2000-01-01T00:00:00Z" + seeded["updated_at"] = "2000-01-01T00:00:01Z" + control_plane._snapshot = control_plane._snapshot.with_entries( + dict(control_plane._snapshot.entries), + orchestration_results={ + **control_plane._snapshot.orchestration_results, + workflow_address: seeded, + }, + ) reconcile = client.post( "/workflows/reconcile-timeouts", headers=headers, ) assert reconcile.status_code == 200 - control_plane.reconcile_workflow_timeouts(now="2099-01-01T00:00:00Z") snapshot = client.get("/snapshot", headers=headers).json() result = snapshot["orchestration_results"]["orchestration.workflow.response"] @@ -823,6 +933,22 @@ def test_routes_require_authenticated_identity(self): ) assert response.status_code == 401 + def test_retrieval_routes_require_authenticated_identity(self): + client = self._build_client() + + status = client.get("/participants/participant.alice/status") + history = client.get( + "/participants/participant.alice/episodes/participant.alice-episode-1/history", + ) + context = client.get( + "/participants/participant.alice/context", + params={"view_ref": "views.context.network-posture.v1"}, + ) + + assert status.status_code == 401 + assert history.status_code == 401 + assert context.status_code == 401 + def test_routes_reject_unknown_body_fields(self): """Closed-world request bodies — unknown fields must be rejected.""" client = self._build_client() @@ -833,3 +959,128 @@ def test_routes_reject_unknown_body_fields(self): json={"episode_id": "alice-1", "unknown": "value"}, ) assert response.status_code == 422 + + def test_status_route_returns_api_408_status_view(self): + client = self._build_client() + client.post( + "/participants/participant.alice/episodes/initialize", + headers=self._headers, + json={}, + ) + + response = client.get( + "/participants/participant.alice/status", + headers=self._headers, + ) + + assert response.status_code == 200 + view = ParticipantStatusViewModel.model_validate(response.json()) + assert view.participant_address == "participant.alice" + assert view.episode_id == "participant.alice-episode-1" + assert view.episode_state is not None + assert view.episode_state.status == "running" + + def test_status_route_scopes_open_operations_to_participant(self): + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + control_plane.initialize_participant_episode("participant.alice") + control_plane.initialize_participant_episode("participant.bob") + control_plane._operations = { + "op-alice": _participant_operation_record("op-alice", "participant.alice"), + "op-bob": _participant_operation_record("op-bob", "participant.bob"), + } + client = TestClient( + create_control_plane_app( + control_plane, + security=_test_security(target.name), + ) + ) + + response = client.get( + "/participants/participant.alice/status", + headers=self._headers, + ) + + assert response.status_code == 200 + view = ParticipantStatusViewModel.model_validate(response.json()) + assert view.open_operation_refs == ["op-alice"] + + def test_history_route_returns_api_408_history_view(self): + client = self._build_client() + client.post( + "/participants/participant.alice/episodes/initialize", + headers=self._headers, + json={}, + ) + + response = client.get( + "/participants/participant.alice/episodes/participant.alice-episode-1/history", + headers=self._headers, + ) + + assert response.status_code == 200 + view = ParticipantHistoryViewModel.model_validate(response.json()) + assert view.participant_address == "participant.alice" + assert view.episode_id == "participant.alice-episode-1" + assert [event.event_type for event in view.episode_history] == [ + "episode_initialized", + "episode_running", + ] + assert view.completeness == "complete" + + def test_context_route_returns_api_408_sem214_view(self): + client = self._build_client() + client.post( + "/participants/participant.alice/episodes/initialize", + headers=self._headers, + json={}, + ) + + response = client.get( + "/participants/participant.alice/context", + params={ + "view_ref": "views.context.network-posture.v1", + "episode_id": "participant.alice-episode-1", + "payload_ref": "evidence.context.alice.network-posture", + "meaning_ref": "attacker.override", + "audience_scope": "audience_neutral", + "observation_point": "future-state", + "comparability_class": "backend_specific_non_comparable", + "backend_disclosure_ref": "attacker.disclosure", + }, + headers=self._headers, + ) + + assert response.status_code == 200 + view = ParticipantContextViewModel.model_validate(response.json()) + assert view.participant_address == "participant.alice" + assert view.view_ref == "views.context.network-posture.v1" + assert view.derived_from_refs == ["runtime.snapshot.current"] + assert view.meaning_ref == "views.context.network-posture.v1" + assert view.participant_scope == "participant_local" + assert view.audience_scope == "participant_visible" + assert view.observation_point == "participant.alice-episode-1" + assert view.source_layers[0].source_layer == "source_snapshot" + assert view.source_layers[0].evidence_refs == ["runtime.snapshot.current"] + assert view.transformation.transformation_rule_ref == "views.context.network-posture.v1" + assert view.comparability.comparability_class == "portable_equivalent" + assert view.comparability.backend_disclosure_refs == [] + assert view.payload_ref == "evidence.context.alice.network-posture" + + def test_retrieval_routes_return_404_for_unknown_participants(self): + client = self._build_client() + + status = client.get("/participants/participant.unknown/status", headers=self._headers) + history = client.get( + "/participants/participant.unknown/episodes/episode-1/history", + headers=self._headers, + ) + context = client.get( + "/participants/participant.unknown/context", + params={"view_ref": "views.context.network-posture.v1"}, + headers=self._headers, + ) + + assert status.status_code == 404 + assert history.status_code == 404 + assert context.status_code == 404 diff --git a/implementations/python/tests/test_sdl_module_registry.py b/implementations/python/tests/test_sdl_module_registry.py index 05de2f5a1..4946d068b 100644 --- a/implementations/python/tests/test_sdl_module_registry.py +++ b/implementations/python/tests/test_sdl_module_registry.py @@ -3,13 +3,16 @@ from __future__ import annotations import base64 +import io import json import shutil +import tarfile import textwrap import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +import aces_sdl.module_registry as module_registry import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -364,6 +367,293 @@ def test_local_import_lockfile_is_checkout_independent(tmp_path: Path): assert "stale" in stale.output.lower() +def test_local_imports_cannot_escape_base_dir(tmp_path: Path): + _local_module(tmp_path / "shared.yaml") + root = _root_import( + tmp_path / "scenario" / "root.yaml", + "source: local:../shared.yaml\n namespace: shared", + ) + + with pytest.raises(SDLParseError, match="escapes base directory"): + parse_sdl_file(root) + + +def test_publishing_local_bundle_rejects_import_escape(tmp_path: Path): + _local_module(tmp_path / "shared.yaml") + root = _write( + tmp_path / "scenario" / "root.yaml", + """ + name: root + version: 1.0.0 + module: + id: acme/root + version: 1.0.0 + exports: + nodes: [vm] + infrastructure: [vm] + imports: + - source: local:../shared.yaml + namespace: shared + nodes: + vm: + type: vm + os: linux + resources: {ram: 1 gib, cpu: 1} + infrastructure: + vm: 1 + """, + ) + + with pytest.raises(SDLParseError, match="escapes base directory"): + publish_module_to_oci_layout(root, output_dir=tmp_path / "dist") + + +def test_oci_registry_requests_use_bounded_timeouts(monkeypatch: pytest.MonkeyPatch): + responses = [b'{"ok": true}', b"bundle-bytes"] + timeouts: list[float | None] = [] + + class _Response: + def __init__(self, payload: bytes) -> None: + self._payload = payload + + def __enter__(self) -> _Response: + return self + + def __exit__(self, exc_type, exc, tb) -> None: + del exc_type, exc, tb + + def read(self) -> bytes: + return self._payload + + def fake_urlopen(request, *, timeout=None): + del request + timeouts.append(timeout) + return _Response(responses.pop(0)) + + monkeypatch.setattr(module_registry, "urlopen", fake_urlopen) + + assert module_registry._json_request("https://registry.example/v2/acme/tags/list") == {"ok": True} + assert module_registry._bytes_request("https://registry.example/v2/acme/blobs/sha256:abc") == b"bundle-bytes" + assert timeouts == [module_registry._HTTP_TIMEOUT_SECONDS, module_registry._HTTP_TIMEOUT_SECONDS] + + +def test_oci_bundle_rejects_root_file_escape(tmp_path: Path): + with pytest.raises(SDLParseError, match="Invalid OCI root_file path"): + module_registry._extract_bundle_to_cache( + bundle_bytes=b"", + manifest_digest="abc123", + root_file="../module.yaml", + base_dir=tmp_path, + ) + + +def test_oci_bundle_rejects_unsafe_tar_members(tmp_path: Path): + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + payload = b"name: unsafe\n" + member = tarfile.TarInfo(name="../escape.yaml") + member.size = len(payload) + tar.addfile(member, io.BytesIO(payload)) + bundle_buffer.seek(0) + + with ( + tarfile.open(fileobj=bundle_buffer, mode="r:gz") as tar, + pytest.raises(SDLParseError, match="Path traversal detected"), + ): + module_registry._safe_tar_members(tar, tmp_path / "cache") + + +def test_oci_bundle_rejects_special_member_types(tmp_path: Path): + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + fifo = tarfile.TarInfo(name="pipe") + fifo.type = tarfile.FIFOTYPE + tar.addfile(fifo) + bundle_buffer.seek(0) + + with ( + tarfile.open(fileobj=bundle_buffer, mode="r:gz") as tar, + pytest.raises(SDLParseError, match="Unsupported tar member"), + ): + module_registry._safe_tar_members(tar, tmp_path / "cache") + + +def test_oci_bundle_rejects_symlink_members(tmp_path: Path): + # A symlink whose own name passes the traversal check (e.g. name='module.yaml') + # but whose linkname escapes the cache is a distinct attack vector and must be + # rejected before extraction. + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + member = tarfile.TarInfo(name="module.yaml") + member.type = tarfile.SYMTYPE + member.linkname = "../outside.yaml" + tar.addfile(member) + bundle_buffer.seek(0) + + with ( + tarfile.open(fileobj=bundle_buffer, mode="r:gz") as tar, + pytest.raises(SDLParseError, match="Links are not allowed"), + ): + module_registry._safe_tar_members(tar, tmp_path / "cache") + + +def test_oci_bundle_rejects_hardlink_members(tmp_path: Path): + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + target = tarfile.TarInfo(name="module.yaml") + target.size = 0 + tar.addfile(target, io.BytesIO(b"")) + link = tarfile.TarInfo(name="link.yaml") + link.type = tarfile.LNKTYPE + link.linkname = "module.yaml" + tar.addfile(link) + bundle_buffer.seek(0) + + with ( + tarfile.open(fileobj=bundle_buffer, mode="r:gz") as tar, + pytest.raises(SDLParseError, match="Links are not allowed"), + ): + module_registry._safe_tar_members(tar, tmp_path / "cache") + + +def test_oci_bundle_strips_dangerous_mode_bits(tmp_path: Path): + payload = b"name: m\n" + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + member = tarfile.TarInfo(name="module.yaml") + member.size = len(payload) + member.mode = 0o4755 # setuid bit set by an attacker-controlled bundle + tar.addfile(member, io.BytesIO(payload)) + bundle_buffer.seek(0) + + with tarfile.open(fileobj=bundle_buffer, mode="r:gz") as tar: + safe = module_registry._safe_tar_members(tar, tmp_path / "cache") + + assert safe[0].mode & 0o7000 == 0 + assert safe[0].mode == 0o755 + + +def test_oci_bundle_extracts_safe_members(tmp_path: Path): + payload = b"name: ok\n" + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + member = tarfile.TarInfo(name="module.yaml") + member.size = len(payload) + tar.addfile(member, io.BytesIO(payload)) + bundle_buffer.seek(0) + + root_path = module_registry._extract_bundle_to_cache( + bundle_bytes=bundle_buffer.getvalue(), + manifest_digest="deadbeef", + root_file="module.yaml", + base_dir=tmp_path, + ) + + assert root_path.is_file() + assert root_path.read_bytes() == payload + cache = module_registry._oci_cache_dir(tmp_path) / "deadbeef" + assert root_path.resolve().is_relative_to(cache.resolve()) + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_oci_bundle_fallback_extraction_validates_members(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + # Simulate Python 3.11.0–3.11.3, where TarFile.extractall lacks the PEP 706 + # `filter` keyword (backported in 3.11.4). The fallback path must still extract + # only validated members rather than performing an unfiltered extraction. + real_extractall = tarfile.TarFile.extractall + + def no_filter_extractall(self, path=None, members=None, **kwargs): + if "filter" in kwargs: + raise TypeError("extractall() got an unexpected keyword argument 'filter'") + return real_extractall(self, path, members=members) + + monkeypatch.setattr(tarfile.TarFile, "extractall", no_filter_extractall) + + payload = b"name: ok\n" + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + member = tarfile.TarInfo(name="module.yaml") + member.size = len(payload) + tar.addfile(member, io.BytesIO(payload)) + bundle_buffer.seek(0) + + root_path = module_registry._extract_bundle_to_cache( + bundle_bytes=bundle_buffer.getvalue(), + manifest_digest="cafef00d", + root_file="module.yaml", + base_dir=tmp_path, + ) + + assert root_path.is_file() + assert root_path.read_bytes() == payload + + +def test_oci_bundle_fallback_rejects_traversal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + # The fallback path used on Python 3.11.0–3.11.3 must still reject traversal. + real_extractall = tarfile.TarFile.extractall + + def no_filter_extractall(self, path=None, members=None, **kwargs): + if "filter" in kwargs: + raise TypeError("extractall() got an unexpected keyword argument 'filter'") + return real_extractall(self, path, members=members) + + monkeypatch.setattr(tarfile.TarFile, "extractall", no_filter_extractall) + + payload = b"owned\n" + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + member = tarfile.TarInfo(name="../escape.yaml") + member.size = len(payload) + tar.addfile(member, io.BytesIO(payload)) + bundle_buffer.seek(0) + + with pytest.raises(SDLParseError, match="Path traversal detected"): + module_registry._extract_bundle_to_cache( + bundle_bytes=bundle_buffer.getvalue(), + manifest_digest="badbad", + root_file="module.yaml", + base_dir=tmp_path, + ) + assert not (tmp_path / "escape.yaml").exists() + + +def test_oci_bundle_rejects_root_file_directory(tmp_path: Path): + bundle_buffer = io.BytesIO() + with tarfile.open(fileobj=bundle_buffer, mode="w:gz") as tar: + directory = tarfile.TarInfo(name="module.yaml") + directory.type = tarfile.DIRTYPE + directory.mode = 0o755 + tar.addfile(directory) + bundle_buffer.seek(0) + + with pytest.raises(SDLParseError, match="root file"): + module_registry._extract_bundle_to_cache( + bundle_bytes=bundle_buffer.getvalue(), + manifest_digest="d1rd1r", + root_file="module.yaml", + base_dir=tmp_path, + ) + + +def test_oci_bundle_cache_hit_enforces_root_file_containment(tmp_path: Path): + # Simulate a cache populated by an earlier unsafe extractor: a symlink at the + # root_file location resolving outside the digest cache. The cache-hit fast path + # must still fail closed rather than returning the escaping path. + cache_root = module_registry._oci_cache_dir(tmp_path) / "stale" + cache_root.mkdir(parents=True) + outside = tmp_path / "outside.yaml" + outside.write_text("name: evil\n", encoding="utf-8") + (cache_root / "module.yaml").symlink_to(outside) + + with pytest.raises(SDLParseError, match="root file"): + module_registry._extract_bundle_to_cache( + bundle_bytes=b"", + manifest_digest="stale", + root_file="module.yaml", + base_dir=tmp_path, + ) + + def test_signed_oci_import_resolution_and_publish_cli(tmp_path: Path): module_path = _local_module(tmp_path / "shared.yaml") runner = CliRunner() diff --git a/implementations/python/tests/test_sem_216_boundary_semantics.py b/implementations/python/tests/test_sem_216_boundary_semantics.py new file mode 100644 index 000000000..d462bdd50 --- /dev/null +++ b/implementations/python/tests/test_sem_216_boundary_semantics.py @@ -0,0 +1,154 @@ +"""SEM-216 boundary semantics: explicit distinction between runtime-observable +state, captured evidence, derived evaluations, analysis outputs, and +audience-specific views. + +Each test names one of the five cross-stratum boundary obligations (B1-B5) and +proves the violation is rejected by BOTH the published JSON Schema and the +closed-world Pydantic model, with a positive case proving the legitimate +mediated view is admitted. SEM-216 is enforced over the existing contract +families (no super-schema); see +``docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md`` and the +``## SEM-216`` section of +``specs/formal/participant-semantics/README.md``. +""" + +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest +from aces_contracts.contracts import ( + ExperimentEvidenceRecordModel, + ParticipantContextViewModel, + schema_bundle, +) +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURES_ROOT = REPO_ROOT / "contracts" / "fixtures" +CONTEXT_VIEW_DIR = FIXTURES_ROOT / "control-plane" / "participant-context-view-v1" +EVIDENCE_RECORD_DIR = FIXTURES_ROOT / "experiment-core" / "experiment-evidence-record-v1" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _assert_schema_and_model_reject(contract_id: str, model_cls, payload: dict) -> None: + validator = Draft202012Validator(schema_bundle()[contract_id]) + assert list(validator.iter_errors(payload)), f"{contract_id} schema unexpectedly accepted {payload}" + with pytest.raises(ValidationError): + model_cls.model_validate(payload) + + +# --- B1: archived evidence cannot become participant-visible without a view rule --- + + +def test_b1_archival_evidence_participant_visible_without_view_rule_is_rejected(): + payload = _load(CONTEXT_VIEW_DIR / "invalid" / "sem216-archival-evidence-participant-visible.json") + assert payload["audience_scope"] == "participant_visible" + assert any(layer["source_layer"] == "evidence_record" for layer in payload["source_layers"]) + assert "derivation_basis_ref" not in payload + _assert_schema_and_model_reject("participant-context-view-v1", ParticipantContextViewModel, payload) + + +# --- B2: hidden adjudication / evaluation output cannot reach a participant view +# without a redaction policy governing the disclosure --- + + +def test_b2_hidden_adjudication_in_evaluation_output_without_redaction_policy_is_rejected(): + payload = _load(CONTEXT_VIEW_DIR / "invalid" / "sem216-hidden-adjudication-in-evaluation-output.json") + assert payload["audience_scope"] == "participant_visible" + assert any(layer["source_layer"] == "derived_measure" for layer in payload["source_layers"]) + assert "redaction_policy_ref" not in payload + _assert_schema_and_model_reject("participant-context-view-v1", ParticipantContextViewModel, payload) + + +# --- B3: derived analysis is never captured evidence --- + + +def test_b3_derived_analysis_as_captured_evidence_is_rejected(): + payload = _load(EVIDENCE_RECORD_DIR / "invalid" / "sem216-analysis-output-as-evidence.json") + # Carries derived-measure shape (measure_kind / value) that an evidence record must not hold. + assert payload.get("measure_kind") == "score" + _assert_schema_and_model_reject("experiment-evidence-record-v1", ExperimentEvidenceRecordModel, payload) + + +# --- B4: evidence claims must disclose redaction/loss --- + + +def test_b4_withheld_evidence_without_loss_disclosure_is_rejected(): + payload = _load(EVIDENCE_RECORD_DIR / "invalid" / "sem216-withheld-without-loss-disclosure.json") + assert payload["redaction_state"] == "withheld" + assert "loss_disclosure" not in payload["raw_content"] + _assert_schema_and_model_reject("experiment-evidence-record-v1", ExperimentEvidenceRecordModel, payload) + + +# --- B5: backend observability is not a portable semantic observation --- + + +def test_b5_backend_observability_as_portable_observation_is_rejected(): + payload = _load(CONTEXT_VIEW_DIR / "invalid" / "sem216-backend-observability-as-observation.json") + assert any(layer["source_layer"] == "backend_observability_stream" for layer in payload["source_layers"]) + _assert_schema_and_model_reject("participant-context-view-v1", ParticipantContextViewModel, payload) + + +# --- Positive: a participant-visible view that correctly mediates an archived +# evidence record through a governed view rule + redaction policy is admitted --- + + +def test_mediated_participant_visible_evidence_view_is_accepted(): + payload = _load(CONTEXT_VIEW_DIR / "valid" / "sem216-mediated-evidence-view.json") + Draft202012Validator(schema_bundle()["participant-context-view-v1"]).validate(payload) + model = ParticipantContextViewModel.model_validate(payload) + assert model.audience_scope == "participant_visible" + assert model.derivation_basis_ref is not None + assert model.redaction_policy_ref is not None + archival = [layer for layer in model.source_layers if layer.source_layer in {"evidence_record", "derived_measure"}] + assert archival, "fixture must exercise an archival source layer" + for layer in archival: + assert layer.source_id in model.transformation.input_source_ids + + +# --- Relational mediation rule (model-side): an archival source layer that is +# present in a participant-visible view but NOT consumed by the transformation +# view rule is rejected even when derivation_basis_ref and redaction_policy_ref +# are both declared. --- + + +def test_unmediated_archival_source_layer_is_rejected_model_side(): + payload = _load(CONTEXT_VIEW_DIR / "valid" / "sem216-mediated-evidence-view.json") + unmediated = copy.deepcopy(payload) + unmediated["transformation"]["input_source_ids"] = [ + source_id + for source_id in unmediated["transformation"]["input_source_ids"] + if source_id != "evidence-archive-0001" + ] + with pytest.raises(ValidationError, match="mediated"): + ParticipantContextViewModel.model_validate(unmediated) + + +# --- B1/B2 payload boundary (model-side): a participant-visible view must not alias +# payload_ref to a raw archival evidence/measure ref even when the source is +# mediated and the governance refs are present, or the consumer can resolve raw +# archived evidence instead of the redacted view output. --- + + +def test_payload_ref_aliasing_raw_archival_source_is_rejected_model_side(): + payload = _load(CONTEXT_VIEW_DIR / "valid" / "sem216-mediated-evidence-view.json") + aliased = copy.deepcopy(payload) + aliased["payload_ref"] = "evidence.archive.blue.0001" # the raw evidence_record source ref + with pytest.raises(ValidationError, match="payload_ref"): + ParticipantContextViewModel.model_validate(aliased) + + +def test_view_schema_publishes_sem216_relational_invariants(): + # The relational obligations that JSON Schema cannot express are still part of the published + # portable contract via x-aces-invariants, so the documented boundary is not model-only. + schema = schema_bundle()["participant-context-view-v1"] + invariant_ids = {entry["id"] for entry in schema.get("x-aces-invariants", [])} + assert "context-view-sem216-archival-source-mediated" in invariant_ids + assert "context-view-sem216-payload-not-raw-archival" in invariant_ids diff --git a/implementations/python/tests/test_sem_217_knowledge_bindings.py b/implementations/python/tests/test_sem_217_knowledge_bindings.py new file mode 100644 index 000000000..f1ff31252 --- /dev/null +++ b/implementations/python/tests/test_sem_217_knowledge_bindings.py @@ -0,0 +1,117 @@ +"""SEM-217 external knowledge binding effect tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_contracts.contracts import ( + ConceptFamilyCatalogModel, + SemanticProfileModel, + UcoAlignmentCatalogModel, +) +from aces_contracts.semantic_binding_effects import ( + ExternalKnowledgeBindingEffect, + semantic_profile_required_binding_effects, + uco_alignment_binding_effects, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +CONCEPT_AUTHORITY_ROOT = REPO_ROOT / "contracts" / "concept-authority" +PROFILE_PATH = REPO_ROOT / "contracts" / "profiles" / "semantic" / "reference-stack-v1.json" + + +def _concept_catalog() -> ConceptFamilyCatalogModel: + payload = json.loads((CONCEPT_AUTHORITY_ROOT / "concept-families-v1.json").read_text(encoding="utf-8")) + return ConceptFamilyCatalogModel.model_validate(payload) + + +def _uco_alignment_catalog() -> UcoAlignmentCatalogModel: + payload = json.loads((CONCEPT_AUTHORITY_ROOT / "uco-alignment-v1.json").read_text(encoding="utf-8")) + return UcoAlignmentCatalogModel.model_validate(payload) + + +def _semantic_profile() -> SemanticProfileModel: + payload = json.loads(PROFILE_PATH.read_text(encoding="utf-8")) + return SemanticProfileModel.model_validate(payload) + + +def test_adopted_uco_binding_annotates_and_aligns_native_meaning(): + records = uco_alignment_binding_effects(_concept_catalog(), _uco_alignment_catalog()) + assets = records["assets"] + + assert assets.family == "assets" + assert assets.provenance == "adopted" + assert assets.effects == frozenset( + { + ExternalKnowledgeBindingEffect.ANNOTATES, + ExternalKnowledgeBindingEffect.ALIGNS, + } + ) + assert ExternalKnowledgeBindingEffect.CONSTRAINS not in assets.effects + assert assets.divergences == () + + +def test_adapted_uco_binding_annotates_and_refines_instead_of_aligning(): + records = uco_alignment_binding_effects(_concept_catalog(), _uco_alignment_catalog()) + relationships = records["relationships"] + + assert relationships.family == "relationships" + assert relationships.provenance == "adapted" + assert relationships.effects == frozenset( + { + ExternalKnowledgeBindingEffect.ANNOTATES, + ExternalKnowledgeBindingEffect.REFINES, + } + ) + assert ExternalKnowledgeBindingEffect.ALIGNS not in relationships.effects + assert relationships.divergences + + +def test_required_semantic_profile_bindings_constrain_governed_surfaces(): + records = semantic_profile_required_binding_effects(_semantic_profile(), "execution") + + provisioner_node_types = next( + record for record in records if record.scope == "capabilities.provisioner.supported_node_types" + ) + assert provisioner_node_types.family == "assets" + assert provisioner_node_types.effects == frozenset({ExternalKnowledgeBindingEffect.CONSTRAINS}) + + +def test_phases_without_governed_required_bindings_have_no_constraint_effects(): + records = semantic_profile_required_binding_effects(_semantic_profile(), "authoring") + + assert records == () + + +def test_invalid_semantic_profile_phase_is_rejected_explicitly(): + with pytest.raises(ValueError, match="semantic profile phase"): + semantic_profile_required_binding_effects(_semantic_profile(), "deployment") # type: ignore[arg-type] + + +def test_uco_alignment_effects_reject_catalog_without_aligned_family(): + payload = json.loads((CONCEPT_AUTHORITY_ROOT / "concept-families-v1.json").read_text(encoding="utf-8")) + payload["families"].pop("assets") + catalog = ConceptFamilyCatalogModel.model_validate(payload) + + with pytest.raises(ValueError, match="unknown concept family"): + uco_alignment_binding_effects(catalog, _uco_alignment_catalog()) + + +def test_uco_alignment_effects_reject_provenance_mismatch(): + payload = json.loads((CONCEPT_AUTHORITY_ROOT / "concept-families-v1.json").read_text(encoding="utf-8")) + payload["families"]["relationships"]["provenance"] = "adopted" + catalog = ConceptFamilyCatalogModel.model_validate(payload) + + with pytest.raises(ValueError, match="provenance"): + uco_alignment_binding_effects(catalog, _uco_alignment_catalog()) + + +def test_sem217_effect_vocabulary_is_closed_over_required_effects(): + assert {effect.value for effect in ExternalKnowledgeBindingEffect} == { + "annotates", + "constrains", + "refines", + "aligns", + } diff --git a/implementations/python/tests/test_sem_224_observability_plane_semantics.py b/implementations/python/tests/test_sem_224_observability_plane_semantics.py new file mode 100644 index 000000000..b576ff2c7 --- /dev/null +++ b/implementations/python/tests/test_sem_224_observability_plane_semantics.py @@ -0,0 +1,208 @@ +"""SEM-224 observability plane separation semantics. + +ADR-066 and ``specs/formal/observability-evidence-plane.md`` define five named +observability/evidence planes and require a carrier-oriented plane classifier +plus portable traceability over them (issue #334). The plane separation itself +is realised by the existing experiment-core and participant carriers; this +module proves the *unifying* obligations #334 owns: + +- OE-01: every claim-bearing observability/evidence carrier has exactly one + primary plane; +- OE-11: a bare string (``log``, ``trace``, ``telemetry``, ``observation``, + ``evidence``) never decides plane ownership -- the carrier role does; +- the three claim-bearing experiment-core contracts publish their plane as a + portable ``x-aces-plane`` annotation; and +- the five distinctions hold end-to-end (reusing the EXP-707/708/709 structural + rules and the SEM-216 boundary fixtures where they already cover a probe). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from aces_contracts.contracts import ( + ExperimentDerivedMeasureModel, + ExperimentEvidenceRecordModel, + schema_bundle, +) +from aces_sdl._runtime_service_families import RUNTIME_SERVICE_FAMILIES +from aces_sdl.observability_plane_semantics import ( + AMBIGUOUS_PLANE_TOKENS, + PLANE_BY_CONTRACT_ID, + SCENARIO_NATIVE_OBSERVABILITY_FAMILIES, + ObservabilityEvidencePlane, + _validate_scenario_native_families, + assert_single_primary_plane, + classify_contract_plane, + classify_runtime_family, + token_decides_plane, +) +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURES_ROOT = REPO_ROOT / "contracts" / "fixtures" +EVIDENCE_RECORD_DIR = FIXTURES_ROOT / "experiment-core" / "experiment-evidence-record-v1" +DERIVED_MEASURE_DIR = FIXTURES_ROOT / "experiment-core" / "experiment-derived-measure-v1" +CONTEXT_VIEW_DIR = FIXTURES_ROOT / "control-plane" / "participant-context-view-v1" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _assert_schema_and_model_reject(contract_id: str, model_cls, payload: dict) -> None: + validator = Draft202012Validator(schema_bundle()[contract_id]) + assert list(validator.iter_errors(payload)), f"{contract_id} schema unexpectedly accepted {payload}" + with pytest.raises(ValidationError): + model_cls.model_validate(payload) + + +# --- OE-01: one primary plane per claim-bearing carrier --------------------- + + +def test_each_claim_bearing_contract_maps_to_exactly_one_plane(): + # The three experiment-core carriers each own exactly one of the five planes. + assert classify_contract_plane("experiment-capture-spec-v1") is ( + ObservabilityEvidencePlane.AUTHORED_EVIDENCE_REQUIREMENT + ) + assert classify_contract_plane("experiment-evidence-record-v1") is (ObservabilityEvidencePlane.CAPTURED_EVIDENCE) + assert classify_contract_plane("experiment-derived-measure-v1") is (ObservabilityEvidencePlane.DERIVED_ANALYSIS) + # No carrier is registered under two planes. + assert len(set(PLANE_BY_CONTRACT_ID.values())) >= 4 + for contract_id, plane in PLANE_BY_CONTRACT_ID.items(): + assert isinstance(plane, ObservabilityEvidencePlane), contract_id + + +def test_assert_single_primary_plane_rejects_zero_or_multiple(): + plane = assert_single_primary_plane( + [ObservabilityEvidencePlane.CAPTURED_EVIDENCE, ObservabilityEvidencePlane.CAPTURED_EVIDENCE] + ) + assert plane is ObservabilityEvidencePlane.CAPTURED_EVIDENCE + with pytest.raises(ValueError): + assert_single_primary_plane([]) + with pytest.raises(ValueError): + assert_single_primary_plane( + [ObservabilityEvidencePlane.CAPTURED_EVIDENCE, ObservabilityEvidencePlane.DERIVED_ANALYSIS] + ) + + +def test_classify_contract_plane_fails_closed_on_unknown_carrier(): + # Plane ownership comes from a registered carrier role, never a guess. + with pytest.raises(ValueError): + classify_contract_plane("some-unregistered-contract-v9") + + +# --- OE-11: a bare string never decides plane ownership --------------------- + + +def test_token_never_decides_plane(): + for token in AMBIGUOUS_PLANE_TOKENS: + assert token_decides_plane(token) is False + # OE-11 is universal: no string decides a plane, including a registered + # contract id or an arbitrary unknown string -- only the carrier role does. + assert token_decides_plane("experiment-capture-spec-v1") is False + assert token_decides_plane("some-unknown-string") is False + # The vocabulary the ADR calls out as ambiguous is covered. + assert {"log", "trace", "telemetry", "observation", "evidence"} <= AMBIGUOUS_PLANE_TOKENS + + +# --- Distinction 1: scenario-native observability is an SDL runtime family --- + + +def test_scenario_native_observability_families_are_registered_and_distinct(): + registered = {family.collection_name for family in RUNTIME_SERVICE_FAMILIES} + assert SCENARIO_NATIVE_OBSERVABILITY_FAMILIES, "scenario-native family set must be non-empty" + assert SCENARIO_NATIVE_OBSERVABILITY_FAMILIES.issubset(registered) + for collection_name in SCENARIO_NATIVE_OBSERVABILITY_FAMILIES: + assert classify_runtime_family(collection_name) is (ObservabilityEvidencePlane.SCENARIO_NATIVE_OBSERVABILITY) + + +def test_scenario_native_family_validation_fails_closed_on_unregistered_name(): + # A scenario-native family that is not in the runtime-family registry must + # raise at construction time rather than silently misclassify. + with pytest.raises(RuntimeError, match="not registered"): + _validate_scenario_native_families(("not_a_real_family",), frozenset({"service_listeners"})) + # The happy path returns the validated set unchanged. + assert _validate_scenario_native_families( + ("service_listeners",), frozenset({"service_listeners", "applications"}) + ) == frozenset({"service_listeners"}) + + +def test_backend_observability_is_not_a_participant_observation(): + # SEM-216 B5 fixture: a backend observability stream presented as a portable + # participant observation is rejected -- the scenario-native plane is not the + # processor/backend operational plane. + from aces_contracts.contracts import ParticipantContextViewModel + + payload = _load(CONTEXT_VIEW_DIR / "invalid" / "sem216-backend-observability-as-observation.json") + _assert_schema_and_model_reject("participant-context-view-v1", ParticipantContextViewModel, payload) + + +# --- Distinction 2: authored evidence requirement is not proof of capture --- + + +def test_capture_spec_and_evidence_record_are_different_planes(): + assert classify_contract_plane("experiment-capture-spec-v1") is not ( + classify_contract_plane("experiment-evidence-record-v1") + ) + + +def test_capture_record_without_requirement_ref_is_rejected(): + # OE-04: a raw capture record cannot claim authored-requirement satisfaction + # with no requirement reference. + payload = _load(EVIDENCE_RECORD_DIR / "invalid" / "sem224-capture-record-without-requirement-ref.json") + assert "capture_requirement_ref" not in payload + _assert_schema_and_model_reject("experiment-evidence-record-v1", ExperimentEvidenceRecordModel, payload) + + +# --- Distinction 3: processor/backend operational observability ------------- + + +def test_operational_carriers_map_to_processor_backend_plane(): + for contract_id in ("backend-manifest-v2", "processor-manifest-v2", "experiment-apparatus-context-v1"): + assert classify_contract_plane(contract_id) is (ObservabilityEvidencePlane.PROCESSOR_BACKEND_OPERATIONAL) + + +# --- Distinction 4: captured evidence is not derived analysis --------------- + + +def test_derived_analysis_is_not_captured_evidence(): + # SEM-216 B3 fixture: a derived-measure shape (measure_kind/value) presented + # as a raw evidence record is rejected. + payload = _load(EVIDENCE_RECORD_DIR / "invalid" / "sem216-analysis-output-as-evidence.json") + _assert_schema_and_model_reject("experiment-evidence-record-v1", ExperimentEvidenceRecordModel, payload) + assert classify_contract_plane("experiment-evidence-record-v1") is not ( + classify_contract_plane("experiment-derived-measure-v1") + ) + + +# --- Distinction 5: derived analysis must cite source evidence (OE-06) ------ + + +def test_derived_measure_without_source_evidence_is_rejected(): + payload = _load(DERIVED_MEASURE_DIR / "invalid" / "missing-source-evidence.json") + _assert_schema_and_model_reject("experiment-derived-measure-v1", ExperimentDerivedMeasureModel, payload) + + +def test_reference_derived_measure_cites_source_evidence(): + payload = _load(DERIVED_MEASURE_DIR / "valid" / "reference.json") + Draft202012Validator(schema_bundle()["experiment-derived-measure-v1"]).validate(payload) + model = ExperimentDerivedMeasureModel.model_validate(payload) + assert model.source_evidence_refs, "a derived measure must cite at least one source evidence record" + + +# --- Portable plane traceability published on the three carriers ------------ + + +def test_claim_bearing_contracts_publish_their_plane_annotation(): + bundle = schema_bundle() + expected = { + "experiment-capture-spec-v1": ObservabilityEvidencePlane.AUTHORED_EVIDENCE_REQUIREMENT.value, + "experiment-evidence-record-v1": ObservabilityEvidencePlane.CAPTURED_EVIDENCE.value, + "experiment-derived-measure-v1": ObservabilityEvidencePlane.DERIVED_ANALYSIS.value, + } + for contract_id, plane_value in expected.items(): + assert bundle[contract_id].get("x-aces-plane") == plane_value, contract_id diff --git a/implementations/python/tests/test_sem_225_augmentation_semantics.py b/implementations/python/tests/test_sem_225_augmentation_semantics.py new file mode 100644 index 000000000..708db30ca --- /dev/null +++ b/implementations/python/tests/test_sem_225_augmentation_semantics.py @@ -0,0 +1,176 @@ +"""SEM-225 realization augmentation and visibility semantics.""" + +from __future__ import annotations + +import json +from copy import deepcopy +from pathlib import Path + +import pytest +from aces_contracts.contracts import ( + ExperimentAugmentationDisclosureModel, + ExperimentRunModel, + schema_bundle, +) +from jsonschema import Draft202012Validator +from pydantic import ValidationError + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _experiment_fixture(contract_id: str, fixture_name: str = "reference.json") -> dict: + fixture_path = REPO_ROOT / "contracts" / "fixtures" / "experiment-core" / contract_id / "valid" / fixture_name + return json.loads(fixture_path.read_text(encoding="utf-8")) + + +def _base_augmentation_disclosure() -> dict: + return { + "augmentation_id": "packet-capture-sidecar", + "purpose": "evidence", + "realization_layer": "backend", + "classifications": ["apparatus_only", "comparability_relevant"], + "augmented_by_ref": { + "ref_kind": "backend", + "ref_id": "stub-backend", + "ref_version": "0.1.0", + }, + "carrier_refs": [ + { + "ref_kind": "measurement-channel", + "ref_id": "evaluation-history-channel", + "ref_version": "1.0.0", + }, + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0", + }, + ], + "affected_refs": [ + { + "ref_kind": "capture-spec", + "ref_id": "capture-techvault-evidence-v1", + "ref_version": "1.0.0", + } + ], + "evidence_refs": [ + { + "ref_kind": "evidence-record", + "ref_id": "evidence-techvault-network-trace-001", + "ref_version": "1.0.0", + } + ], + "disclosure_policy": "Internal run-provenance disclosure; no raw packet content is embedded.", + "markings": ["internal"], + "observer_effect": "The sidecar observes run traffic without modifying scenario services.", + "comparability_effect": "Compare only with runs that declare equivalent capture support.", + } + + +def _assert_schema_and_model_reject(payload: dict) -> None: + validator = Draft202012Validator(schema_bundle()["experiment-run-v1"]) + assert list(validator.iter_errors(payload)) + with pytest.raises(ValidationError): + ExperimentRunModel.model_validate(payload) + + +def _conditional_then_for(disclosure_schema: dict, classification: str) -> dict: + for branch in disclosure_schema["allOf"]: + contains = branch.get("if", {}).get("properties", {}).get("classifications", {}).get("contains", {}) + if contains.get("const") == classification: + return branch["then"] + raise AssertionError(f"missing conditional schema branch for {classification!r}") + + +def test_sem_225_accepts_run_augmentation_disclosure(): + payload = _experiment_fixture("experiment-run-v1") + payload["augmentation_disclosures"] = [_base_augmentation_disclosure()] + + schema = schema_bundle()["experiment-run-v1"] + assert not list(Draft202012Validator(schema).iter_errors(payload)) + run = ExperimentRunModel.model_validate(payload) + + disclosure = run.augmentation_disclosures[0] + assert disclosure.augmentation_id == "packet-capture-sidecar" + assert "comparability_relevant" in disclosure.classifications + + +def test_experiment_run_schema_publishes_sem_225_augmentation_surface(): + run_schema = schema_bundle()["experiment-run-v1"] + + assert run_schema["properties"]["augmentation_disclosures"]["items"]["$ref"] == ( + "#/$defs/ExperimentAugmentationDisclosureModel" + ) + disclosure_schema = run_schema["$defs"]["ExperimentAugmentationDisclosureModel"] + assert disclosure_schema["additionalProperties"] is False + invariant_ids = {invariant["id"] for invariant in disclosure_schema.get("x-aces-invariants", [])} + assert "augmentation-disclosure-semantics-valid" in invariant_ids + run_invariant_ids = {invariant["id"] for invariant in run_schema.get("x-aces-invariants", [])} + assert "augmentation-disclosure-evidence-refs-traced" in run_invariant_ids + environment_then = _conditional_then_for(disclosure_schema, "environment_visible") + assert {"carrier_refs", "environment_effect", "evidence_refs"} <= set(environment_then["required"]) + participant_then = _conditional_then_for(disclosure_schema, "participant_visible") + assert {"participant_visibility", "markings", "evidence_refs"} <= set(participant_then["required"]) + + +def test_sem_225_rejects_environment_visible_backend_log_only_disclosure(): + payload = _experiment_fixture("experiment-run-v1") + disclosure = _base_augmentation_disclosure() + disclosure["classifications"] = ["environment_visible"] + disclosure["environment_effect"] = "Instrumentation adds an in-world sensor service." + disclosure["carrier_refs"] = [{"ref_kind": "other", "ref_id": "backend-log"}] + payload["augmentation_disclosures"] = [disclosure] + + _assert_schema_and_model_reject(payload) + + +def test_sem_225_rejects_participant_visible_augmentation_without_markings(): + payload = _experiment_fixture("experiment-run-v1") + disclosure = _base_augmentation_disclosure() + disclosure["classifications"] = ["participant_visible"] + disclosure["participant_visibility"] = "Participant sees the monitoring dashboard." + disclosure["markings"] = [] + payload["augmentation_disclosures"] = [disclosure] + + _assert_schema_and_model_reject(payload) + + +def test_sem_225_rejects_comparability_relevant_augmentation_without_observer_effect(): + payload = _experiment_fixture("experiment-run-v1") + disclosure = _base_augmentation_disclosure() + disclosure["observer_effect"] = None + payload["augmentation_disclosures"] = [disclosure] + + _assert_schema_and_model_reject(payload) + + +def test_sem_225_rejects_non_processor_backend_augmentation_authority(): + payload = _experiment_fixture("experiment-run-v1") + disclosure = _base_augmentation_disclosure() + disclosure["augmented_by_ref"] = { + "ref_kind": "participant-implementation", + "ref_id": "reference-red-agent", + "ref_version": "1.0.0", + } + payload["augmentation_disclosures"] = [disclosure] + + _assert_schema_and_model_reject(payload) + + +def test_sem_225_augmentation_evidence_refs_must_be_run_traced(): + payload = _experiment_fixture("experiment-run-v1") + disclosure = _base_augmentation_disclosure() + disclosure["evidence_refs"] = [{"ref_kind": "evidence-record", "ref_id": "untraced-evidence"}] + payload["augmentation_disclosures"] = [disclosure] + + assert not list(Draft202012Validator(schema_bundle()["experiment-run-v1"]).iter_errors(payload)) + with pytest.raises(ValidationError, match="augmentation_disclosures evidence_refs must be listed"): + ExperimentRunModel.model_validate(payload) + + +def test_sem_225_rejects_duplicate_disclosure_references(): + duplicate = deepcopy(_base_augmentation_disclosure()) + duplicate["carrier_refs"].append(deepcopy(duplicate["carrier_refs"][0])) + + with pytest.raises(ValidationError, match="augmentation disclosure carrier_refs must not contain duplicates"): + ExperimentAugmentationDisclosureModel.model_validate(duplicate) diff --git a/noxfile.py b/noxfile.py index 0bf843b3b..01e355ada 100644 --- a/noxfile.py +++ b/noxfile.py @@ -700,6 +700,13 @@ def _run_integration_tests(session: nox.Session, reporter: SessionReporter) -> N ) +def _run_docker_integration_tests(session: nox.Session, reporter: SessionReporter) -> None: + reporter.run( + "tests / pytest docker integration", + lambda: _run_pytest(session, "-m", "docker", "-v"), + ) + + def _run_docs(session: nox.Session, reporter: SessionReporter) -> None: _sync_project(session) docs_dir = REPO_ROOT / "docs" @@ -793,6 +800,22 @@ def integration(session: nox.Session) -> None: reporter.summary() +@nox.session(name="integration_docker") +def integration_docker(session: nox.Session) -> None: + """Run the opt-in container-runtime integration tests (`docker` marker). + + Requires a real container runtime (docker/podman). The tests self-skip + cleanly when no runtime is available. This session is intentionally NOT + wired into `verify`, so the canonical verification graph stays hermetic. + """ + reporter = SessionReporter(session, "integration_docker") + try: + _sync_project(session) + _run_docker_integration_tests(session, reporter) + finally: + reporter.summary() + + @nox.session def docs(session: nox.Session) -> None: reporter = SessionReporter(session, "docs") diff --git a/sonar-project.properties b/sonar-project.properties index 8e2bdfed8..f78246cc5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -24,6 +24,24 @@ sonar.exclusions=\ **/dist/**,\ **/*.egg-info/** +# Copy/paste (CPD) exclusions (RUN-314). +# +# The reference emulation backend's orchestrator/evaluator/participant-runtime +# components intentionally reproduce the SAME portable runtime envelopes +# (workflow/evaluation/participant result + history) as the non-normative +# in-memory stub in aces_backend_stubs. That structural parallel is by design: +# the reference backend is a standalone, independently-readable implementation +# of the published backend contracts, and its parity with the stub is asserted +# directly by tests (test_reference_backend_conformance.py). Coupling the +# canonical reference backend to shared helpers with the throwaway stub would be +# worse design, so this duplication is accepted here rather than factored out. +# The driver-backed provisioner (the part that makes this backend "real") is NOT +# excluded and is held to the normal duplication bar. +sonar.cpd.exclusions=\ + implementations/python/packages/aces_reference_backend/orchestrator.py,\ + implementations/python/packages/aces_reference_backend/evaluator.py,\ + implementations/python/packages/aces_reference_backend/participant_runtime.py + # Python sonar.python.version=3.12 diff --git a/specs/formal/README.md b/specs/formal/README.md index e717d3aae..781f4c1f3 100644 --- a/specs/formal/README.md +++ b/specs/formal/README.md @@ -11,6 +11,7 @@ Examples: - `specs/formal/planner/` - `specs/formal/runtime-contracts/` - `specs/formal/participant-semantics/` +- `specs/formal/participant-behavior-model/` - `specs/formal/participant-runtime/` - `specs/formal/experiment-core/` diff --git a/specs/formal/assurance-fulfillment.yaml b/specs/formal/assurance-fulfillment.yaml index 1afd394f1..533d76518 100644 --- a/specs/formal/assurance-fulfillment.yaml +++ b/specs/formal/assurance-fulfillment.yaml @@ -55,6 +55,9 @@ subsystems: - id: participant-semantics path: specs/formal/participant-semantics fm_level: FM3 + - id: participant-behavior-model + path: specs/formal/participant-behavior-model + fm_level: FM2 - id: realization path: specs/formal/realization fm_level: FM2 @@ -231,6 +234,52 @@ entries: No executable abstract model exists; #487 tracks the executable invariant oracle (I1-I18) for the abstract model. + - subsystem: participant-behavior-model + # Design coverage for ACT-602/603/606/607/608 plus executable ACT-602 + # conformance coverage. Remaining executable artifacts are owned by the + # other spawned implementation issues. + delivered_artifacts: + - kind: invariant_list + path: specs/formal/participant-behavior-model/README.md + - kind: unit_tests + path: implementations/python/tests/test_runtime_conformance.py + - kind: typed_ir_or_contract_coverage + path: implementations/python/packages/aces_processor/models.py + waived_artifacts: + - kind: unit_tests + date: 2026-06-23 + tracking: + - "#205" + - "#206" + - "#207" + - "#208" + rationale: >- + ACT-602 conformance tests now cover the executable behavior-model + binding gate; the remaining behavior-model test coverage is owned by + the other per-UID implementation issues. + - kind: typed_ir_or_contract_coverage + date: 2026-06-23 + tracking: + - "#205" + - "#206" + - "#207" + - "#208" + rationale: >- + ACT-602 reuses the existing compiled participant behavior binding; + additional SDL fields, contract schemas, and runtime carriers are + owned by the other per-UID implementation issues. + - kind: property_based_or_differential_tests + date: 2026-06-23 + tracking: + - "#205" + - "#206" + - "#207" + - "#208" + rationale: >- + No property-based or differential behavior-model coverage ships with + the joint design or ACT-602 conformance gate; that evidence is owned + by the remaining per-UID implementation issues. + - subsystem: realization # SEM-218 realization enforcement is in flight across #489/#490/#491; the # explicitness/realization semantic boundary is documented, but the diff --git a/specs/formal/experiment-core/README.md b/specs/formal/experiment-core/README.md index 331874bd1..367380e25 100644 --- a/specs/formal/experiment-core/README.md +++ b/specs/formal/experiment-core/README.md @@ -1,12 +1,19 @@ # Experiment Core Formal Specification This domain specifies the EXP-701 through EXP-705 experiment-core contract -boundary: +boundary plus the EXP-707, EXP-708, EXP-709, and EXP-715 evidence/measure +extension and the EXP-710, EXP-720, and EXP-722 run provenance extension: - `experiment-task-v1` - `experiment-apparatus-context-v1` - `experiment-run-v1` - `experiment-study-v1` +- `experiment-capture-spec-v1` +- `experiment-evidence-record-v1` +- `experiment-derived-measure-v1` +- optional `backend-manifest-v2` `capabilities.observation` +- canonical run traceability and realized-form disclosures inside + `experiment-run-v1` The contracts describe cyber range experiment artifacts. They do not implement execution, storage, scheduling, APIs, or analysis engines. @@ -30,7 +37,11 @@ Rationale: ## Authoritative Artifacts - Normative prose: this directory. -- Architecture decision: `docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md`. +- Architecture decisions: + `docs/decisions/adrs/adr-055-experiment-core-contract-boundary.md` and + `docs/decisions/adrs/adr-064-experiment-evidence-and-measure-contract-boundary.md`, + and + `docs/decisions/adrs/adr-065-experiment-run-provenance-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`. @@ -44,8 +55,10 @@ metadata. Each invariant records a stable id, severity, validator, and input contract/path set; the annotation shape is published as `aces-semantic-invariants-v1` and is validated during schema generation. Examples include metric key equality, task/run protocol binding, run time -ordering, result-evidence reference resolution, study metric grounding, and -manifest-selection and manifest-payload consistency. +ordering, result-evidence reference resolution, capture-requirement key +resolution, raw evidence content disclosure, derived-measure source evidence +requirements, run provenance traceability, realized-form disclosure, study +metric grounding, and manifest-selection and manifest-payload consistency. ## Definitions @@ -134,6 +147,100 @@ artifact id or by an artifact `satisfies_refs` entry. If a task or metric evidence requirement carries digest or path metadata, the matching run artifact MUST satisfy those fields with its concrete checksum and URI/path. +### Run Traceability + +Run traceability is the EXP-710 path from the run to the evidence and claims +that interpret it. The `experiment-run-v1` `traceability` block binds: + +- capture specification refs; +- raw evidence-record refs; +- derived-measure refs; +- claim, result, report, or analysis refs; +- optional notes for human review. + +Traceability belongs in the run because the run is the record that knows the +task, scenario snapshot, apparatus, evidence, result summaries, and generated +artifacts together. It is not a separate graph service and not an alternative +run schema. + +### Realized Form Disclosure + +Realized-form disclosure is the EXP-722 record of concrete forms chosen for +concerns left open by the authored scenario, task, or apparatus declaration. +Each disclosure binds: + +- a stable concern id and concern kind; +- the realization basis, such as processor-realized or backend-realized; +- the processor, backend, operator, or observation reference that made or + recorded the realization; +- the authored reference when one exists; +- either a realized reference or a realized value summary; +- disclosure prose and optional evidence-record refs. + +Realized-form disclosures are part of run provenance. They are not authored +scenario meaning, are not raw evidence records, and are not derived measures or +results. + +### Capture Specification + +An experiment capture specification is the declarative EXP-707 statement of +what evidence should be captured for an experiment scope. It binds: + +- task, run, apparatus, or adjacent scope references; +- capture windows; +- capture requirements keyed by requirement id; +- measurement channel references; +- expected media types and artifact roles; +- sensitivity, redaction, integrity, retention, and loss-disclosure + expectations; +- validity notes and supporting artifacts. + +A capture specification is not proof that capture occurred. It is an intent and +review surface that raw evidence records can cite. + +### Evidence Record + +An experiment evidence record is the raw EXP-708 evidence surface. It binds: + +- a capture specification reference; +- a capture requirement reference; +- a run reference, plus optional task and apparatus context references; +- source references and evidence kind; +- capture timestamp and capture window reference; +- raw content as an artifact reference, content URI with checksum, or bounded + payload summary; +- sensitivity, redaction state, loss disclosure when needed, and provenance. + +Evidence records MUST NOT carry metric ids, computed values, scores, or +evaluation decisions. Those belong in derived measures or run summaries. + +### Derived Measure + +An experiment derived measure is the EXP-709 interpreted output surface. It +binds: + +- a metric or evaluation reference; +- derivation method id, version, parameters, and description; +- one or more source evidence-record references; +- generation timestamp; +- value status and value when reported; +- uncertainty, limitations, and provenance. + +Derived measures MUST NOT stand in for raw observations. Their reviewability +depends on following `source_evidence_refs` back to evidence records. + +### Backend Observation Capability + +The optional EXP-715 `backend-manifest-v2` `capabilities.observation` block +declares whether a backend can support observation/evidence collection +surfaces. It binds supported capture kinds, source channel kinds, evidence +contracts, media types, sealing modes, redaction support, loss-disclosure +support, chain-of-custody support, and constraints. + +Observation capability is not orchestrator, evaluator, or participant-runtime +capability. It is a backend apparatus claim that must be backed by published +experiment evidence contracts and governed concept bindings. + ### Study Or Collection A study groups tasks, runs, results, evidence, reports, and analysis artifacts @@ -206,6 +313,27 @@ Studies carry accountable analysis context: 12. Required task apparatus capabilities MUST resolve to capability references in the run apparatus compatibility declarations or component compatibility references. +13. Capture specifications, evidence records, and derived measures MUST use + distinct reference kinds: `capture-spec`, `evidence-record`, and + `derived-measure`. +14. Capture specification `capture_requirements` keys MUST match embedded + `requirement_id` values, and requirement `window_refs` MUST resolve to + declared capture windows. +15. Evidence records MUST cite a capture specification and requirement, carry + raw content, and MUST NOT include metric ids or derived values. +16. Derived measures MUST cite at least one evidence record and MUST NOT be + treated as raw evidence. +17. Backends that declare `capabilities.observation` MUST declare the published + experiment evidence contracts that make the observation claim inspectable. +18. `experiment-run-v1` is the canonical run provenance record. ACES MUST NOT + publish a parallel run-provenance root schema for the same archival run + facts unless a later ADR supersedes this boundary. +19. Run traceability MUST link at least one capture specification and at least + one evidence record. Claim refs MUST be grounded by derived-measure refs. +20. Realized-form disclosures MUST carry a realized reference or a realized + value summary. Processor-realized disclosures MUST be attributed to a + processor reference, and backend-realized disclosures MUST be attributed to + a backend reference. ### Provenance @@ -276,6 +404,19 @@ Studies carry accountable analysis context: not-evaluated evaluation runs. 15. Run-allocation `blocking_factors` MUST reference declared blocking, stratification, apparatus, or control study factors with declared levels. +16. Capture windows MUST declare a start, end, or trigger, and an interval with + both start and end MUST NOT end before it starts. +17. Evidence records MUST use valid RFC 3339 `captured_at` timestamps. + Redacted or withheld evidence records MUST disclose the loss in + `raw_content.loss_disclosure`. +18. Derived measures MUST use valid RFC 3339 `generated_at` timestamps. + Reported measures MUST include a value; missing, withheld, and + not-applicable measures MUST NOT include a value. +19. Observation capability terms MUST be validated through the governed + concept-authority scopes for capture kinds, channel kinds, and sealing + modes. +20. Realized-form disclosure evidence refs MUST be present in the containing + run's traceability evidence-record refs. ### Closed-World Contracts @@ -331,3 +472,7 @@ base. The most load-bearing criteria are: - HTTP APIs. - New SDL authoring syntax. - PROV, RO-Crate, OpenML, or MLflow as the internal ACES schema. +- Runtime evidence capture, artifact storage, retention jobs, or capture + schedulers. +- Backend-native packet/log/trace parsers. +- Processor logic that computes derived measures from evidence records. diff --git a/specs/formal/observability-evidence-plane.md b/specs/formal/observability-evidence-plane.md new file mode 100644 index 000000000..f59d0db6c --- /dev/null +++ b/specs/formal/observability-evidence-plane.md @@ -0,0 +1,212 @@ +# Observability and Evidence Plane Formal Design + +This cross-domain formal design artifact supports ADR-066 and issue #127 for: + +- `SEM-224` - Observability Plane Separation Semantics +- `SEM-225` - Realization Augmentation And Environment-Visibility Semantics +- `DSL-123` - Scenario-Native Observability And Telemetry Systems +- `DSL-124` - Authored Data And Evidence Requirements + +It is design coverage. It defines the invariant set and source-to-contract-to- +test matrix that the spawned implementation issues must realize in SDL models, +semantic helpers, contract fields, fixtures, validators, and tests. + +## FM Classification + +Classification: FM2, Semantic Graph / Constraint. + +Rationale: + +- The design defines cross-artifact relationships between SDL authoring, + participant-runtime visibility, experiment-core capture/evidence contracts, + processor/backend apparatus telemetry, and derived analysis outputs. +- The key obligations are type separation, reference ownership, visibility + projection, provenance links, loss/redaction disclosure, and comparability + claims across artifacts. +- This artifact does not define a live state machine or runtime protocol. + +## Plane Definitions + +### Scenario-Native Observability + +In-world observability systems authored as part of the scenario. They are +targetable only when represented by an SDL section, runtime family, or typed +relationship endpoint. Examples include network sensors, detection engines, +security monitoring managers, forwarding agents, telemetry collectors, tracing +services, dashboards, or metrics stores when the scenario makes them part of +the environment. + +### Authored Evidence Requirement + +An authoring obligation that names the data, evidence, output, source, scope, +window, channel, or boundary that must be captured. It may compile to or bind +with `experiment-capture-spec-v1`, but it is not captured evidence and is not a +participant objective. + +### Processor/Backend Operational Observability + +Apparatus data used to operate, audit, or diagnose processors and backends: +logs, traces, diagnostics, audit records, health checks, setup evidence, +measurement-channel facts, and capability declarations. These facts are not +scenario meaning unless explicitly projected into an SDL or runtime contract. + +### Captured Evidence + +Concrete evidence artifacts or `experiment-evidence-record-v1` records. They +cite the capture specification or authored requirement they satisfy and carry +source, capture time/window, raw content reference or bounded summary, +sensitivity, redaction state, provenance, and integrity metadata. + +### Derived Analysis + +Interpreted outputs over evidence: derived measures, result summaries, outcome +interpretations, studies, reports, exports, and claims. They must cite source +evidence and must not stand in for raw evidence. + +## Augmentation Classification + +An augmentation is processor/backend-added apparatus behavior or +instrumentation used to satisfy evidence, evaluation, operational, or +comparability needs. The classification set is additive: + +| Classification | Meaning | Required carrier | +| --- | --- | --- | +| `apparatus_only` | Visible only to processor/backend/operator/control apparatus. | Diagnostic, manifest, apparatus context, audit, setup evidence, or other apparatus carrier. | +| `environment_visible` | Changes or adds realized environment behavior. | Runtime/evidence/provenance carrier plus realized-form or equivalent disclosure. | +| `participant_visible` | Can affect participant-visible information. | Participant visibility projection, observation envelope, marking, and redaction carrier. | +| `comparability_relevant` | Can affect run/backend/participant/condition comparison. | Run provenance, validity note, realized-form disclosure, or derived-analysis support record. | + +## Invariants + +| ID | Invariant | Enforcement target | +| --- | --- | --- | +| OE-01 | Every claim-bearing observability/evidence artifact has exactly one primary plane, even when it references artifacts in other planes. | Plane classifier over SDL, runtime, experiment, and apparatus carriers. | +| OE-02 | Scenario-native observability is targetable only through SDL authoring surfaces, runtime-family refs, or typed relationship endpoints. | SDL semantic validation and reference-resolution catalog. | +| OE-03 | Authored evidence requirements name source, scope, window or trigger, channel or boundary, sensitivity, integrity, and loss/redaction expectations before they can claim capture intent. | SDL evidence-requirement model and validator. | +| OE-04 | A capture requirement is not proof of capture. Satisfaction requires a captured evidence record or artifact with an explicit satisfaction link. | Experiment-core reference validation and traceability checks. | +| OE-05 | Captured evidence must not contain metric values, scores, evaluation decisions, or derived conclusions as raw evidence meaning. | Contract model validators and invalid fixtures. | +| OE-06 | Derived analysis must cite source evidence and must not reveal hidden adjudication assets without governed marking, redaction, and authorization. | Derived-measure/run/study validators and redaction checks. | +| OE-07 | Backend diagnostics, logs, traces, and audit records are apparatus operational observability until a governed projection maps them into another plane. | Runtime diagnostics and control-plane API gates. | +| OE-08 | Participant-visible observation claims must pass the ADR-022/ADR-054 visibility projection, marking, redaction, and information-guarantee rules. | Participant observation envelope and behavior-history validators. | +| OE-09 | Augmentation that is environment-visible, participant-visible, or comparability-relevant must have a first-class disclosure carrier. | Augmentation disclosure validator and provenance links. | +| OE-10 | Loss, redaction, latency, observer effects, weaker capability guarantees, and unsupported capture concerns are explicit when a claim depends on them. | SDL, experiment-core, and participant-runtime validators. | +| OE-11 | The same string value, such as `log`, `telemetry`, `observation`, or `evidence`, does not decide plane ownership by itself. | Concept/vocabulary binding plus carrier-type classifier. | +| OE-12 | No observability/evidence plane may use `RuntimeSnapshot.metadata`, evaluator detail fields, audit blobs, raw backend DTOs, or free-form tags as its only portable carrier. | Policy, semantic validation, and review gates. | + +## Source-To-Contract-To-Test Matrix + +| Requirement | Clause | Current design artifact | Future contract/helper | Lifecycle enforcement point | Positive fixture | Negative fixture | Owner | +| --- | --- | --- | --- | --- | --- | --- | --- | +| SEM-224 | Distinguish scenario-native observability systems. | ADR-066; this spec; SDL catalog. | Plane classifier plus SDL runtime-family target helper. | SDL semantic validation and compiler address emission. | In-world detection engine referenced by participant action and capture requirement source. | Backend-only log treated as a participant-visible observation. | #334 | +| SEM-224 | Distinguish authored evidence requirements. | ADR-066; this spec; SDL catalog. | Authored evidence-requirement model and capture-spec binding helper. | SDL validation, instantiation revalidation, experiment capture binding. | Requirement names source, scope, window, channel, sensitivity, integrity, and loss disclosure. | Raw capture record treated as authored requirement satisfaction with no requirement ref. | #334, #337 | +| SEM-224 | Distinguish processor/backend operational observability. | ADR-066; this spec. | Apparatus observability classifier over diagnostics, manifests, audit, setup evidence, and measurement channels. | Processor/backend manifest and control-plane diagnostic gates. | Backend health trace cited as setup evidence with sensitivity metadata. | Backend trace projected into scenario meaning by free-form metadata. | #334 | +| SEM-224 | Distinguish captured evidence. | ADR-066; this spec; ADR-064. | Evidence satisfaction validator over capture spec, evidence record, artifact refs, and provenance. | Experiment-core contract validation and run traceability. | Evidence record cites capture spec, requirement, source, window, raw content, redaction state, and checksum. | Evidence record carries a derived score as raw evidence meaning. | #334 | +| SEM-224 | Distinguish derived analysis outputs. | ADR-066; this spec; ADR-065. | Derived-analysis source-evidence validator. | Derived-measure, run, study, and report validation. | Derived measure cites source evidence and method. | Hidden adjudication asset leaks through analysis output without marking/redaction. | #334 | +| SEM-225 | Define augmentation used to satisfy evidence, evaluation, or operational requirements. | ADR-066; this spec. | Augmentation disclosure model with concern, carrier, classification, evidence refs, and markings. | Compiler/runtime provenance and experiment run validation. | Apparatus-only packet capture sidecar disclosed as measurement-channel augmentation. | Instrumentation modifies environment but appears only in backend logs. | #335 | +| SEM-225 | Include environment-visible augmentation. | ADR-066; this spec. | Environment-visible augmentation validator. | Runtime/provenance validation. | Added sensor service has realized-form disclosure and scenario/runtime refs. | Environment behavior changes without disclosure. | #335 | +| SEM-225 | Include participant-visible augmentation. | ADR-066; this spec; ADR-054. | Participant-visible augmentation validator. | Participant observation envelope and visibility projection. | Participant sees monitoring dashboard through explicit projection. | Hidden adjudication asset reaches visible observation history. | #335 | +| SEM-225 | Include comparability-relevant augmentation. | ADR-066; this spec; ADR-065. | Comparability disclosure support record. | Run/study validity and derived-analysis validation. | Augmentation names comparison impact and supporting evidence refs. | Observer effect omitted from a benchmark comparison claim. | #335 | +| DSL-123 | Support scenario-native observability, telemetry, logging, tracing, and monitoring as first-class scenario elements. | ADR-066; SDL catalog. | Runtime-family or section model for product-neutral in-world service identity. | SDL parser, schema, semantic validator, and reference resolver. | Telemetry collector has stable id and typed refs. | Generic top-level `observability` bag accepts unrelated vendor payloads. | #336 | +| DSL-123 | Allow those elements to be depended on, interacted with, or targeted. | ADR-066; SDL catalog. | Typed relationship/reference edges and target helper. | SDL references, typed relationship subtypes, and compiler addresses. | Objective/action targets an in-world observability service. | Bare ambiguous ref resolves by first match. | #336 | +| DSL-124 | Support authored requirements for data/evidence/output capture. | ADR-066; SDL catalog. | Evidence-requirement model and capture-spec binding. | SDL parser, schema, semantic validator, instantiation, compiler. | Requirement names source, scope, window, channel, role, sensitivity, and loss disclosure. | Capture requirement has no source or window. | #337 | +| DSL-124 | Requirements come from declared sources, scopes, windows, or comparable boundaries. | ADR-066; SDL catalog. | Qualified source/scope/window resolver. | SDL semantic validation and experiment binding. | Requirement references runtime sensor and run window. | Requirement references an unknown hidden backend object. | #337 | +| DSL-124 | Requirements are independent of participant objectives. | ADR-066; SDL catalog. | Cross-plane validation between objectives and evidence requirements. | SDL semantic validation and compiler. | Evidence requirement exists without an objective. | Objective success criterion is treated as capture requirement by implication. | #337 | +| DSL-124 | Requirements are distinct from scenario-native observability systems. | ADR-066; SDL catalog. | Plane classifier and source binding helper. | SDL semantic validation. | Requirement cites an observability system as source but remains a separate obligation. | Observability system declaration is treated as proof of capture. | #337 | + +## Negative Probe Set + +The minimum adversarial fixture set for implementation is: + +- backend logs treated as participant observations; +- raw capture treated as evidence-requirement satisfaction; +- hidden adjudication assets leaking through analysis; +- augmentation changing environment-visible behavior without disclosure; +- participant-visible augmentation without a visibility projection; +- comparability-relevant observer effects omitted from evidence claims; +- loss, redaction, or latency omitted from evidence claims; +- a generic observability bag accepting vendor payloads without typed refs; +- ambiguous evidence source refs accepted by first match; and +- derived measures accepted with no source evidence. + +## Non-Goals + +- This design does not add SDL syntax, schemas, fixtures, runtime services, + APIs, storage, capture scheduling, telemetry collection, packet parsing, + analysis engines, or backend adapters. +- This design does not replace ADR-022, ADR-054, ADR-064, ADR-065, SEM-218, or + existing control-plane security and diagnostics. +- The design criteria above do not by themselves transition SEM-224, SEM-225, + DSL-123, or DSL-124 to implementation coverage. The implementation coverage + sections below record realized subsets as spawned issues land. + +## Implementation Coverage (#334 / SEM-224) + +SEM-224 is realized as a carrier-oriented plane classifier plus portable plane +traceability over the existing carriers. The classifier +`aces_sdl.observability_plane_semantics` is the single source of plane +ownership; it assigns exactly one primary plane by contract role or runtime +family identity and never by a free string (OE-01, OE-11). The three +claim-bearing experiment-core carriers publish their plane as an `x-aces-plane` +annotation sourced from that classifier. The plane *separation* each carrier +enforces was already realized by the EXP-707/708/709 contracts and SEM-216; this +issue adds the unifying classifier, the portable annotation, and the SEM-224 +probe set. + +| Invariant / matrix row | Realizing artifact | Test | New in #334? | +| --- | --- | --- | --- | +| OE-01 single primary plane | `observability_plane_semantics.classify_contract_plane`, `assert_single_primary_plane` | `test_each_claim_bearing_contract_maps_to_exactly_one_plane`, `test_assert_single_primary_plane_rejects_zero_or_multiple` | yes | +| OE-11 carrier, not string, decides plane | `classify_contract_plane` (fail-closed), `token_decides_plane`, `AMBIGUOUS_PLANE_TOKENS` | `test_classify_contract_plane_fails_closed_on_unknown_carrier`, `test_token_never_decides_plane` | yes | +| Distinguish scenario-native observability | `SCENARIO_NATIVE_OBSERVABILITY_FAMILIES` over `RUNTIME_SERVICE_FAMILIES`; SEM-216 B5 boundary | `test_scenario_native_observability_families_are_registered_and_distinct`, `test_backend_observability_is_not_a_participant_observation` | classifier new; B5 reused | +| OE-04 capture requirement is not proof of capture | required `capture_requirement_ref` on `ExperimentEvidenceRecordModel` | `test_capture_record_without_requirement_ref_is_rejected` (fixture `sem224-capture-record-without-requirement-ref.json`) | probe new | +| Distinguish processor/backend operational | `PLANE_BY_CONTRACT_ID` (manifests, apparatus context) | `test_operational_carriers_map_to_processor_backend_plane` | yes | +| OE-05 captured evidence is not derived analysis | `ExperimentEvidenceRecordModel` shape; SEM-216 B3 | `test_derived_analysis_is_not_captured_evidence` | classifier new; B3 reused | +| OE-06 derived analysis must cite source evidence | `ExperimentDerivedMeasureModel.source_evidence_refs` (`min_length=1`) | `test_derived_measure_without_source_evidence_is_rejected`, `test_reference_derived_measure_cites_source_evidence` | pre-existing rule, SEM-224 probe | +| Plane traceability published portably | `x-aces-plane` on the three experiment-core schemas | `test_claim_bearing_contracts_publish_their_plane_annotation` | yes | + +## Implementation Coverage (#335 / SEM-225) + +SEM-225 is realized as run-level augmentation disclosure on +`experiment-run-v1`. `ExperimentAugmentationDisclosureModel` records the +augmentation purpose, realization layer, additive classifications, processor or +backend authority, first-class carrier refs, disclosure policy, markings, +observer/comparability effects, and evidence-record refs. The run validator +keeps augmentation evidence refs tied to `traceability.evidence_record_refs` so +augmentation claims do not float free of captured evidence. + +The model keeps the three SEM-225 axes separate: + +- `environment_visible` requires an explicit environment effect and a portable + carrier ref rather than a backend-log-only reference; +- `participant_visible` requires participant visibility text plus markings; and +- `comparability_relevant` requires both comparability impact and observer + effect disclosure. + +| Invariant / matrix row | Realizing artifact | Test | New in #335? | +| --- | --- | --- | --- | +| OE-09 first-class augmentation disclosure | `ExperimentAugmentationDisclosureModel`, `ExperimentRunModel.augmentation_disclosures` | `test_sem_225_accepts_run_augmentation_disclosure`, `test_experiment_run_schema_publishes_sem_225_augmentation_surface` | yes | +| Environment-visible augmentation is not backend-log-only | portable carrier validation in `ExperimentAugmentationDisclosureModel` | `test_sem_225_rejects_environment_visible_backend_log_only_disclosure` | yes | +| Participant-visible augmentation carries visibility/marking context | participant visibility and marking validation | `test_sem_225_rejects_participant_visible_augmentation_without_markings` | yes | +| Comparability-relevant augmentation names observer effect | comparability and observer-effect validation | `test_sem_225_rejects_comparability_relevant_augmentation_without_observer_effect` | yes | +| Processor/backend authority boundary | `augmented_by_ref` constrained to processor/backend refs | `test_sem_225_rejects_non_processor_backend_augmentation_authority` | yes | +| Evidence provenance remains traced | run-level augmentation evidence refs checked against traceability | `test_sem_225_augmentation_evidence_refs_must_be_run_traced` | yes | + +## Implementation Coverage (#336 / DSL-123) + +DSL-123 is realized as SDL scenario-native observability over existing runtime +families and targetable reference edges. It does not add a generic top-level +`observability`, `telemetry`, `logs`, or `traces` bag. The implementation keeps +the carrier-oriented plane classifier from SEM-224, exposes an explicit +scenario-native observability reference collector, and proves that qualified +runtime-family refs can be relationship endpoints, objective targets, and +participant action interaction targets. + +| Invariant / matrix row | Realizing artifact | Test | New in #336? | +| --- | --- | --- | --- | +| Scenario-native observability systems are first-class SDL elements | `SCENARIO_NATIVE_OBSERVABILITY_FAMILIES` validated against `RUNTIME_SERVICE_FAMILIES`; `classify_runtime_family()` | `test_dsl_123_exposes_scenario_native_observability_refs_without_second_resolver` | helper/test coverage new; classifier reused | +| Observability target refs are explicit runtime-family refs | `collect_scenario_native_observability_refs()` as a filtered view over `collect_qualified_runtime_family_refs()` | `test_dsl_123_exposes_scenario_native_observability_refs_without_second_resolver` | yes | +| In-world observability systems can be depended on or targeted | `SemanticValidator._named_ref_index(targetable=True)` and relationship endpoint validation | `test_dsl_123_observability_refs_are_targetable_relationship_objective_and_action_refs` | test coverage new; resolver reused | +| Participant actions can interact with in-world observability systems | `ParticipantInteractionDeclaration.target` and `shared_state_refs` validation through the targetable index | `test_dsl_123_observability_refs_are_targetable_relationship_objective_and_action_refs` | test coverage new | +| Bare runtime ids do not resolve by first match | Fail-closed targetable reference resolution requires the qualified runtime-family path | `test_dsl_123_observability_refs_do_not_resolve_by_bare_runtime_id` | test coverage new | + +DSL-124 authored evidence-requirement surfaces (#337) extend the +carrier-to-plane registry rather than re-implementing it. diff --git a/specs/formal/participant-behavior-model/README.md b/specs/formal/participant-behavior-model/README.md new file mode 100644 index 000000000..a476d4d3a --- /dev/null +++ b/specs/formal/participant-behavior-model/README.md @@ -0,0 +1,370 @@ +# Participant Behavior Model Formal Design + +This document is the issue #77 formal design artifact for: + +- `ACT-602` - Executable Participant Behavior Model +- `ACT-603` - Abstract Participant Interaction Model +- `ACT-606` - First-Class Participant Behavior Specifications +- `ACT-607` - Participant Authority And Scope Boundaries +- `ACT-608` - Participant Behavior Modes + +It is governed by ADR-067. It composes the participant semantics from ADR-022 +with SDL participant framing, participant runtime records, backend-facing +contracts, controlled vocabularies, and participant implementation provenance. +It is a design artifact, not an implementation artifact. + +## Current Coverage And Gap + +Existing coverage: + +- ADR-020 pins authored participant framing on `agents.*`. +- ADR-022 and `specs/formal/participant-semantics/` define action, + observation, visibility, interaction, attribution, temporal, and outcome + semantics. +- ADR-041 defines participant implementation manifest and provenance records. +- ADR-054 and `specs/formal/participant-runtime/` define observable runtime + lifecycle, behavior history, shared state, observation envelopes, and + concurrency. +- ADR-060 and `specs/formal/runtime-contracts/participant-backend-contracts.md` + define backend-facing carrier, retrieval, support, and outcome surfaces. +- `controlled-vocabularies-v1` already defines + `participant-decision-surface-modes`. + +Remaining issue #77 gap: + +- no single behavior-model composition binding these surfaces to ACT-602, + ACT-603, ACT-606, ACT-607, and ACT-608; +- no first-class behavior specification aggregate; +- no clause matrix tying authority/scope and mode selection to the behavior + model; and +- no child-issue boundary that prevents each UID from publishing local behavior + semantics. + +## Model Summary + +The participant behavior model is: + +```text +ParticipantBehaviorModel = + ParticipantFraming + + ActionContractSet + + ObservationBoundarySet + + OutcomeInterpretationRuleSet + + AuthorityScopeBoundarySet + + BehaviorSpecificationSet + + BehaviorModeSelection + + RealizationAndEvidenceBindings + + RuntimeBehaviorEvidence +``` + +The model has three layers: + +1. **Authored layer** - SDL `agents.*`, action contracts, observation + boundaries, outcome interpretation rules, authority/scope refs, and + behavior specification aggregates. +2. **Realization layer** - participant implementation manifests, selected + decision-surface mode, backend capability and feature-support claims, + realization profile, fidelity claims, and disclosure refs. +3. **Evidence layer** - participant behavior history, lifecycle events, + observation envelopes, shared-state records, attribution edges, outcome + reports, conformance diagnostics, and evidence refs. + +These layers are linked by stable references. They are not interchangeable. + +## ACT-603 - Abstract Participant Interaction Model + +An abstract participant interaction is the tuple: + +```text +Interaction = + participant_address + action_contract_ref + attempt_ref + observation_boundary_ref* + precondition_result* + effect_claim* + failure_class? + authority_scope_ref* + state_ref* + temporal_context + ordering_relation + joint_action_ref? + attribution_ref* + outcome_interpretation_ref* + evidence_ref* +``` + +Rules: + +- `participant_address` identifies the runtime participant and episode scope; + authored identity and role remain SDL framing refs. +- `action_contract_ref` is required for portable action meaning. A raw action + name, command, tool label, technique label, or benchmark milestone is not + enough. +- `observation_boundary_ref` defines what the participant may see or infer. + Hidden truth and archival evidence are outside the participant-visible view + unless an explicit disclosure rule projects them. +- Preconditions and effects are evaluated against declared state, authority, + scope, visibility, and temporal context. Unknown or unresolved references fail + closed. +- Failure classes use the existing participant-semantics taxonomy. Backend + errors must map to governed failure classes before becoming portable + semantics. +- Interaction among participants is represented as joint action, + coordination, contention, interference, or shared-state change, never as + backend scheduler order alone. +- Outcome interpretation is participant-local until a named rule relates it to + scenario, workflow, objective, evaluation, evidence, or reward surfaces. + +### ACT-603 Implementation Preflight Guardrails + +Executable ACT-603 work must be a binding over existing ACES surfaces, not a +new participant stack. The canonical incumbents are: + +- SDL authored semantics: `agents.*`, `action_contracts`, + `observation_boundaries`, and `outcome_interpretation_rules` in + `implementations/python/packages/aces_sdl/`. +- SDL shape and reference validation: `SDLModel`, `parse_sdl`, + `SemanticValidator`, `analyze_participant_behavior`, and + `analyze_participant_outcome_interpretations`. +- Compiled runtime addresses: `participant.action-contract.*`, + `participant.observation-boundary.*`, + `participant.outcome-interpretation-rule.*`, and + `participant.behavior.*` from `aces_processor.compiler`. +- Runtime interaction evidence: `ParticipantActionResult`, + precondition/effect/result records, outcome interpretation records, and + `iter_participant_behavior_history_violations` in + `aces_processor.models`. +- Published contract authority: `ContractModel`, `schema_bundle()`, + `contracts/schemas/`, `contracts/schema-publication-manifest.json`, and the + `contracts/fixtures/` positive/negative fixture pattern. +- Governed terms: `controlled-vocabularies-v1`, especially participant + decision-surface modes, runtime behavior features, runtime interaction + features, and participant runtime feature support levels. +- Runtime/control-plane boundaries: `RuntimeSnapshot`, participant result + contract diagnostics, participant retrieval views, `OperationReceipt`, + `OperationStatus`, control-plane audit records, and conformance semantic + diagnostics. + +Security and boundary gates that any ACT-603 implementation touches must remain +in force: + +- SDL authoring is closed by `SDLModel(extra="forbid")`; instantiated scenarios + reject unresolved `${name}` tokens; `SemanticValidator` resolves participant + refs and fails closed on unknown action contracts, observation boundaries, + targetable refs, authority anchors, operating scope, and outcome-rule refs. +- Published exchange payloads are closed `ContractModel` payloads and must + validate through JSON Schema plus semantic validators; a schema change must + update the publication manifest ledger and fixtures, not only Python models. +- Runtime behavior history must pass participant episode/state/history, + shared-state, concurrency, visibility, temporal, precondition/effect, + failure-class, attribution, and outcome-grounding checks through the existing + participant runtime validators. +- Control-plane exposure must use the existing FastAPI security model: strict + auth defaults, read vs mutating role dependencies, request-size guard, + idempotency fingerprinting, redacted internal-error envelopes, and audit + records. Participant authority is not control-plane authorization. +- Credentials, bearer tokens, hidden prompts, answer keys, raw secret-bearing + argv/env/config values, backend-private objects, and raw logs are not + portable interaction data. They require refs, digests, markings, redaction + policy, disclosure basis, or evidence records through the existing runtime + value and evidence surfaces. + +The extensibility seam is declaration plus disclosure, not a backend-specific +DTO. New or weaker realizations should be expressed through governed feature +support, support level, disclosure refs, mapping-loss labels, limitations, and +`x-:` governed extensions where the vocabulary allows them. New +portable concepts must extend the existing typed precondition, effect, failure, +observation, attribution, temporal, outcome, interaction, or controlled +vocabulary surface before they appear in runtime evidence. + +Anti-patterns for ACT-603 implementations: + +- introducing a second action/precondition/effect/failure taxonomy; +- treating action names, tool labels, ATT&CK/CVE labels, backend commands, + scheduler order, timestamps, rewards, or raw logs as portable semantics; +- adding participant-specific persistence, exceptions, audit, schema + publication, or validation paths when the runtime snapshot, diagnostics, + control-plane store, schemas, and validators already cover the boundary; +- weakening hidden-truth, evidence-only, disclosure, or redaction boundaries in + order to make observations easier to emit; or +- making backend capability declarations prove that a specific participant + implementation ran. Use provenance and runtime evidence for that claim. + +## ACT-602 - Executable Participant Behavior Model + +Executable behavior means the model is machine-checkable through ACES gates. +The executable chain is: + +```text +SDL authoring + -> parser normalization and closed models + -> semantic validation + -> compiled participant addresses and runtime refs + -> runtime carrier emission + -> contract/schema validation + -> semantic conformance diagnostics + -> traceable evidence refs +``` + +Required executable properties: + +- authored symbol keys are stable and cannot be created by variables; +- action, observation, outcome, authority, scope, and behavior-spec refs + resolve before compilation; +- compiled runtime addresses are canonical and stable enough for traceability; +- runtime records preserve participant address, episode, order, source, + marking, evidence, and redaction context; +- backend support claims resolve through governed vocabularies and contract + evidence; +- weaker guarantees are explicit through support level, mapping loss, + disclosure refs, and diagnostics; and +- conformance cannot rely on schema acceptance alone. + +Implementation issue #204 owns executable contract bindings, validators, +fixtures, and conformance evidence for this requirement. + +## ACT-606 - First-Class Participant Behavior Specifications + +A behavior specification is a named, versioned aggregate: + +```text +BehaviorSpecification = + spec_id + semantic_version + lifecycle_state + participant_ref* + participant_role_ref* + action_contract_ref* + observation_boundary_ref* + outcome_interpretation_rule_ref* + authority_scope_ref* + behavior_mode? + realization_profile_ref? + backend_feature_support_ref* + evidence_contract_ref* + extension_policy +``` + +Rules: + +- A behavior specification is first-class because it can be named, versioned, + traced, reviewed, and validated as an artifact. +- It aggregates existing behavior surfaces. It does not replace action + contracts, observation boundaries, outcome rules, manifests, backend + capabilities, or runtime evidence. +- `behavior_mode` binds to the controlled vocabulary described in ACT-608. +- `realization_profile_ref` records how the behavior can be realized without + exposing private implementation configuration. +- `evidence_contract_ref` names the contracts needed to prove the behavior + claim in a run or conformance report. +- Extension fields follow the governed `x-:` discipline. + +Implementation issue #206 owns the executable authoring and validation surface +for this aggregate. + +## ACT-607 - Authority And Scope Boundaries + +Authority and scope are authored semantics. The model distinguishes: + +| Facet | Portable meaning | Not equivalent to | +| --- | --- | --- | +| `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 | +| `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 | +| observation boundaries | Participant-visible information rules | backend logs or world truth | +| backend capability | Realization support claim | scenario permission | +| control-plane auth | API caller authorization | participant authority | + +Rules: + +- Authority and scope refs resolve through existing named-reference and + targetable-element validation patterns. +- Action authority belongs in typed preconditions, evidence refs, and governed + failure classes such as `authority_denied`. +- Observation access belongs in observation boundaries and visibility + transitions. +- Credentials, tokens, prompts, answer keys, hidden truth, and backend private + config are never inline authority evidence. +- Runtime denial, backend sandboxing, or control-plane authorization may enforce + a boundary, but they do not define the authored boundary by themselves. + +Implementation issue #207 owns executable authority/scope extensions beyond +the ACT-601 fields already shipped. + +## ACT-608 - Participant Behavior Modes + +Behavior mode declares how decisions are selected or controlled at the +participant decision-surface boundary. + +| Requirement wording | Controlled vocabulary term | +| --- | --- | +| autonomous | `autonomous` | +| scripted | `scripted` | +| policy-directed | `policy-directed` | +| replayed | `replayed` | +| supervised | `human-supervised` | +| mixed-control | `mixed-control` | + +Rules: + +- Mode values resolve through `participant-decision-surface-modes`. +- The current `supervised` requirement wording maps to `human-supervised`. + A broader supervised concept requires a governed vocabulary update. +- Behavior mode is distinct from participant role, implementation kind, + backend feature support, control-plane authorization, and interaction class. +- `replayed` mode identifies decision-source replay. It does not by itself + define trajectory corpus, evidence retention, dataset, or benchmark split + semantics. +- `policy-directed` mode identifies policy-mediated decisions. It does not + grant scenario authority or control-plane permission. +- `mixed-control` mode identifies combined control over one participant + decision surface. It is not multi-participant interaction semantics. + +Implementation issue #208 owns executable declaration, selection, validation, +and conformance for behavior modes. + +## Cross-Clause Invariants + +| ID | Invariant | Primary clauses | +| --- | --- | --- | +| PBM-01 | Action names are not portable behavior semantics without action contracts. | ACT-602, ACT-603 | +| PBM-02 | Observation is a participant-visible projection, not hidden truth or archival evidence. | ACT-603, ACT-606 | +| PBM-03 | Authority is authored scenario meaning, not credential possession or control-plane auth. | ACT-607 | +| PBM-04 | Behavior mode resolves through controlled vocabularies, not artifact-local strings. | ACT-608 | +| PBM-05 | Runtime behavior history is evidence of realized behavior, not the authored behavior specification. | ACT-602, ACT-606 | +| PBM-06 | Backend support claims require governed feature terms, support levels, disclosure refs, and evidence contracts. | ACT-602, ACT-608 | +| PBM-07 | Hidden prompts, credentials, answer keys, raw command output, backend-private objects, and adjudication assets stay out of portable behavior artifacts. | ACT-606, ACT-607 | +| PBM-08 | Unknown, opaque, unsupported, not applicable, bounded, lossy, and exact are distinct claims. | ACT-602, ACT-603, ACT-608 | + +## Child-Issue Mapping + +| Issue | UID | Executable ownership | +| --- | --- | --- | +| #204 | ACT-602 | Machine-checkable behavior model gates, fixtures, and conformance evidence. | +| #205 | ACT-603 | Abstract interaction implementation coverage over actions, observations, state, preconditions, effects, failure classes, and joint interactions. | +| #206 | ACT-606 | First-class behavior specification authoring, validation, versioning, and traceability. | +| #207 | ACT-607 | Authority/scope boundary authoring, validation, evidence, and failure mapping. | +| #208 | ACT-608 | Behavior-mode declaration, selection, controlled-vocabulary validation, and conformance. | + +## Verification Expectations + +Any executable issue that claims this model must provide: + +- parser/model negative tests for unknown fields and variable-created keys; +- semantic validation tests for unresolved action, observation, outcome, + authority, scope, and behavior-spec refs; +- generated schema and publication-manifest checks when a contract is added or + changed; +- valid and invalid fixtures for every new portable contract; +- runtime or conformance tests that prove behavior-history, observation, + authority, mode, evidence, and redaction invariants; and +- Ground Control IMPLEMENTS/TESTS or DOCUMENTS traceability links appropriate + to the artifact. + +Issue #77 satisfies the design requirement by publishing ADR-067 and this +formal spec. It does not claim runtime emission, SDL syntax, schema, fixture, +or conformance implementation for #204 through #208. diff --git a/specs/formal/participant-semantics/README.md b/specs/formal/participant-semantics/README.md index b53c025a7..db60de0b0 100644 --- a/specs/formal/participant-semantics/README.md +++ b/specs/formal/participant-semantics/README.md @@ -1024,6 +1024,44 @@ This slice implements participant-local temporal contracts and conformance checks. It does not claim the broader ACES clock/time-model work owned by `SEM-227`, `SEM-228`, and `SEM-229`. +## SEM-214 - Portable Semantics For Derived Context Views + +`SEM-214` requires explicit meaning and comparability semantics for derived +operational context views so their interpretation remains portable across +runtimes and backends. + +Design commitments: + +- a context view is participant-local and must name its audience scope; +- every view names the observation point at which the derived context applies; +- source layers are explicit and limited to governed snapshot, participant + observation, participant history/status, evidence, derived-measure, and + control-plane operation records; +- hidden/global runtime state is not a valid context-view source layer; +- future-state sources are not valid, and bounded stale sources require a + freshness-basis reference; +- the transformation rule and input source ids are explicit; +- evidence and provenance references are required; +- comparability is an explicit claim with a comparison-basis reference, + limitations, and backend disclosures whenever the claim is weakened or + backend-specific. + +Implementation artifacts: + +- `participant-context-view-v1` carries the SEM-214 envelope in the existing + API-408 control-plane carrier; +- `implementations/python/packages/aces_contracts/contracts.py` defines the + closed-world Pydantic model and JSON Schema reference output; +- `contracts/fixtures/control-plane/participant-context-view-v1/` contains + positive and negative fixtures for source-layer, temporal, audience-scope, + evidence/provenance, and comparability constraints; +- `implementations/python/packages/aces_runtime/participant_retrieval.py` + constructs the SEM-214 envelope for the existing context retrieval path; +- `implementations/python/tests/test_participant_backend_contracts.py`, + `test_runtime_control_plane.py`, and `test_runtime_control_plane_api.py` + verify schema/model rejection, runtime construction, and HTTP response + binding. + ## SEM-215 - Participant Outcome Interpretation Semantics `SEM-215` requires semantics for interpreting participant-local outcomes and @@ -1060,6 +1098,172 @@ Implementation artifacts: - runtime conformance checks that reject ungrounded action outcome, evidence claim, and episode-status interpretation sources. +## SEM-216 - Boundary Semantics For State, Evidence, Evaluation, Analysis, And Views + +`SEM-216` requires explicit semantics distinguishing runtime-observable state, +captured evidence, derived evaluations, analysis outputs, and audience-specific +views so one stratum is never silently substituted for another. + +`SEM-216` is a **boundary-semantics requirement over the existing contract +families**, not a new universal taxonomy. Per the architecture preflight +(`docs/decisions/issue-248-sem-216-boundary-semantics-preflight.md`) there is no +"state/evidence/result/view" super-schema: each stratum keeps its own governed +carrier, and cross-boundary movement is by typed references, source layers, +traceability blocks, checksums, and provenance refs only. + +The five strata and their governing carriers: + +- **runtime-observable state** is live, mutable control-plane/runtime material: + `RuntimeSnapshot`, snapshot entries, workflow/evaluation results and history, + participant episode/behavior/shared-state/joint-action records, operation + status, and audit metadata. It is not archival run provenance by itself. +- **captured evidence** is the EXP-708 `experiment-evidence-record-v1` surface: + typed source refs, content URI plus checksum or bounded summary, sensitivity, + redaction state, loss disclosure, and provenance. A capture spec declares + intent; it is not proof that evidence exists. +- **derived evaluations** are compiled evaluation result/history contracts and + the EXP-709 `experiment-derived-measure-v1` archival measure; a raw evidence + record is never a metric value, score, or measure. +- **analysis outputs** are study/report artifacts or derived measures with + `measure_kind: analysis-output`, kept grounded through run traceability and at + least one derived-measure reference; they must not float from raw runtime + state or evaluator detail. +- **audience-specific views** are projections over recorded carriers + (`participant-status-view-v1`, `participant-history-view-v1`, + `participant-context-view-v1`), never sources of truth. + +Design commitments: + +- the five strata are distinct objects carried by named existing contracts; no + contract may carry another stratum's shape (closed-world models reject it); +- archival evidence and derived evaluation/adjudication outputs reach a + participant-visible view only through a governed view rule and a redaction + policy, only when the archival source is mediated by the view transformation + rather than passed through raw, and only when the disclosed `payload_ref` is + the transformed view output rather than an alias of the raw archival ref; +- evidence claims disclose redaction and loss at the evidence boundary; +- backend-native observability is not an admissible portable view source and is + not a portable semantic observation; +- analysis outputs remain grounded in run traceability and derived measures. + +Boundary obligations (each is exercised by an adversarial negative fixture): + +- **B1** - archived evidence must not become participant-visible without a view + rule. A `participant_visible` `participant-context-view-v1` that draws on an + `evidence_record` source layer must declare a `derivation_basis_ref` view rule + and a `redaction_policy_ref` (published `allOf`), the evidence source must + appear in `transformation.input_source_ids`, and `payload_ref` must not alias + the raw evidence ref (both relational rules published as `x-aces-invariants`). +- **B2** - hidden adjudication / derived-evaluation outputs must not reach a + participant view without redaction governance. A `participant_visible` view + that draws on a `derived_measure` source layer must additionally declare a + `redaction_policy_ref`, mediate the source through the transformation, and + expose a transformed `payload_ref` rather than the raw measure ref. + +The required-ref clauses of B1/B2 (and B4) are enforced both by the closed-world +model and by the published JSON Schema `allOf`, so schema-only consumers reject +them. The relational clauses that standard JSON Schema cannot express - archival +source mediation and `payload_ref` non-aliasing - are enforced by the model and +published as `x-aces-invariants` on `participant-context-view-v1` (the ACES +semantic-invariant profile, per ADR-009 §7 and the experiment-core convention), +so the portable contract advertises every obligation and names its validator. +- **B3** - derived analysis is never captured evidence. An + `experiment-evidence-record-v1` record is closed-world and carries no + `measure_kind`/`value`/metric shape, and its `evidence_kind` has no + measure/analysis member. +- **B4** - evidence claims must disclose redaction/loss. A `redacted` or + `withheld` evidence record must carry `raw_content.loss_disclosure`, enforced + both by the model and by the published schema. +- **B5** - backend observability is not a portable semantic observation. Only + the governed `source_layer` vocabulary is an admissible portable view source; + raw backend-native observability streams are rejected by the closed enum. + +Stratum boundary traceability: + +| Stratum | Carrier (contract / model) | Enforcement point | Invariant | +| ------- | -------------------------- | ----------------- | --------- | +| runtime-observable state | `RuntimeSnapshotEnvelopeModel` / `RuntimeSnapshot` | runtime snapshot diagnostics in `aces_conformance/conformance.py` | I2, I13 | +| captured evidence | `experiment-evidence-record-v1` / `ExperimentEvidenceRecordModel` | `_validate_evidence_record`, `_validate_raw_content`, closed-world `evidence_kind` (B3, B4) | I3, I13 | +| derived evaluations | `experiment-derived-measure-v1` / `ExperimentDerivedMeasureModel` | `_validate_derived_measure`, typed `source_evidence_refs` | I10 | +| analysis outputs | derived measure `measure_kind: analysis-output` + `ExperimentRunTraceabilityModel` | `_validate_run_traceability` claim grounding | I10, I15 | +| audience-specific views | `participant-context-view-v1` / `ParticipantContextViewModel` | `_validate_sem214_source_binding` + `_validate_sem216_audience_boundary` (B1, B2, B5) | I2, I3, I13 | + +The obligations are projections of the existing abstract invariants - **I2** +(hidden-truth boundary), **I3**/**I13** (observation projection / apparatus +disclosure), **I10** (outcome-layer separation), and **I15** (run/study +provenance) - so `SEM-216` introduces no new `### I*` invariant heading and the +invariant oracle is unchanged. + +Current implementation artifacts for the `SEM-216` slice: + +- `implementations/python/packages/aces_contracts/contracts.py` adds + `ParticipantContextViewModel._validate_sem216_audience_boundary` with its + published `allOf` (required view rule + redaction policy) and + `x-aces-invariants` (archival source mediation, `payload_ref` non-aliasing) + for the B1/B2/B5 view boundary, and publishes the evidence + redaction/loss-disclosure rule as a portable schema constraint on + `ExperimentEvidenceRecordModel` (B4); +- `contracts/schemas/control-plane/participant-context-view-v1.json` and + `contracts/schemas/experiment-core/experiment-evidence-record-v1.json` carry + the regenerated boundary constraints, recorded in + `contracts/schema-publication-manifest.json`; +- `contracts/fixtures/control-plane/participant-context-view-v1/` and + `contracts/fixtures/experiment-core/experiment-evidence-record-v1/` add the + positive mediated-view fixture and the five adversarial negative fixtures + (B1-B5); +- `implementations/python/tests/test_sem_216_boundary_semantics.py` proves each + boundary obligation is rejected by both schema and model, with the mediated + view admitted; the existing fixture-walk tests in + `test_participant_backend_contracts.py` and `test_runtime_contracts.py` carry + the same fixtures. + +## SEM-217 - External Knowledge Binding Semantics + +`SEM-217` requires explicit semantics for external knowledge bindings so a +reference to UCO, another ontology, a vocabulary, or an interoperability +profile cannot silently rewrite ACES-native meaning. + +An external knowledge binding has exactly the effect declared by the governed +ACES surface that carries it: + +- **annotates** - an external reference, reviewed class, evidence source, or + citation adds context for a native ACES concept without changing validation, + planning, runtime, or conformance semantics by itself. +- **aligns** - a reviewed external authority has equivalent meaning for the + native ACES family. In the current concept-authority slice, adopted UCO + concept families align with UCO meaning and carry no divergence list. +- **refines** - a reviewed external authority is used with ACES-specific + narrowing, loss, or divergence. In the current slice, adapted UCO concept + families refine rather than align and must enumerate the divergence. +- **constrains** - a governed surface must bind a vocabulary, capability, or + phase assumption to a declared concept family; missing, unknown, duplicate, + or out-of-scope bindings are validation failures, not advisory metadata. + +Design commitments: + +- native ACES contracts, concept families, reference models, semantic profiles, + and validators remain the authority for ACES behavior; +- external authority references are versioned, review-scoped evidence rather + than live network dependencies; +- annotation never implies constraint, refinement never weakens existing ACES + invariants, and alignment never means schema inheritance; +- artifact-local labels do not define portable semantics unless they bind to a + governed concept family or controlled vocabulary surface. + +Current implementation artifacts for the `SEM-217` slice: + +- `implementations/python/packages/aces_contracts/semantic_binding_effects.py` + resolves the four SEM-217 effects over the existing UCO alignment and shared + semantic-profile records; +- `implementations/python/tests/test_sem_217_knowledge_bindings.py` proves that + adopted UCO bindings annotate and align, adapted UCO bindings annotate and + refine, profile required bindings constrain governed surfaces, phases without + governed bindings do not create constraint effects, and the effect vocabulary + is closed over the four SEM-217 terms; +- `docs/explain/reference/shared-concept-model.md` records the + implementation-facing guardrails and anti-patterns for external knowledge + bindings. + ## Required Future Verification The complete participant surface is `FM3`. diff --git a/specs/formal/runtime-contracts/participant-backend-contracts.md b/specs/formal/runtime-contracts/participant-backend-contracts.md index ebebdf118..33234d19a 100644 --- a/specs/formal/runtime-contracts/participant-backend-contracts.md +++ b/specs/formal/runtime-contracts/participant-backend-contracts.md @@ -131,7 +131,7 @@ never a second source of truth. They live in the `control-plane` family: | --- | --- | --- | | `participant-status-view-v1` | Episode state and lifecycle facts for one participant (+ open operation refs) | embeds the scope-projected episode-state shape | | `participant-history-view-v1` | Episode and behavior history retrieval | carries `completeness` (`complete`/`truncated`/`filtered`) with a required basis when not complete | -| `participant-context-view-v1` | Derived operational context views | reference-and-provenance only; see below | +| `participant-context-view-v1` | Derived operational context views | reference carrier plus SEM-214 meaning/comparability envelope; see below | Views are one-participant projections, and the contract makes that structural: `participant_address` and `episode_id` are carried exactly once, at the view @@ -163,9 +163,14 @@ Rules: - views carry no retrieval-only state: every field is derivable from recorded contracts; - `participant-context-view-v1` carries the governed `view_ref`, the - `derived_from_refs` provenance, and an optional `payload_ref`. The - *semantics* of derived context views — meaning and comparability — belong - to `SEM-214` (wave 3) and are deliberately not claimed here; + `derived_from_refs` provenance, an optional `payload_ref`, and the SEM-214 + meaning/comparability envelope. A context view must declare its + `meaning_ref`, participant-local scope, audience scope, observation point, + consumed source layers, transformation rule, evidence/provenance basis, + semantic limitations, and comparability class/basis. Hidden/global source + layers, future-state sources, participant-local state presented as + audience-neutral, and weakened backend comparability claims without a + disclosure are invalid; - endpoint binding, authentication, role checks, request limits, audit recording, and error envelopes reuse the existing control-plane contract (API-403/404) and are implementation scope (#202). diff --git a/specs/sdl/README.md b/specs/sdl/README.md index 6aab64781..a8621ecf0 100644 --- a/specs/sdl/README.md +++ b/specs/sdl/README.md @@ -67,6 +67,7 @@ tests, rather than a prose rewrite. The catalogs are: | [`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. | ## Acceptance-question map @@ -83,13 +84,17 @@ An implementer can answer each structural question from the named file alone: [`variables-and-instantiation.md`](variables-and-instantiation.md). - *What is the runtime-inventory surface and which ADR owns each family?* → [`runtime-inventory.md`](runtime-inventory.md). +- *How are scenario-native observability systems and authored evidence + requirements kept distinct?* → + [`observability-and-evidence.md`](observability-and-evidence.md). - *When is a problem an error versus an advisory?* → [`diagnostics.md`](diagnostics.md). ## Scope In scope: the SDL authoring model — document structure, references, variables, -instantiation, the runtime-inventory index, and the diagnostic boundary. +instantiation, the runtime-inventory index, observability/evidence plane +rules, and the diagnostic boundary. Out of scope: delivery-level concerns (container, infrastructure-as-code, and cloud-API mechanics), processor and backend execution contracts, and the diff --git a/specs/sdl/observability-and-evidence.md b/specs/sdl/observability-and-evidence.md new file mode 100644 index 000000000..8942ccab8 --- /dev/null +++ b/specs/sdl/observability-and-evidence.md @@ -0,0 +1,150 @@ +# 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. + +## Plane Rule + +An SDL authoring construct that makes an observability or evidence claim MUST +have a primary plane: + +- scenario-native observability; +- authored evidence requirement; +- processor/backend operational observability; +- captured evidence; or +- derived analysis. + +The carrier decides the plane. A string such as `log`, `trace`, `telemetry`, +`observation`, or `evidence` does not decide meaning on its own. + +## Scenario-Native Observability + +A scenario-native observability system is an in-world resource. It may be a +runtime service, sensor, detection engine, monitoring manager, telemetry +collector, tracing backend, metrics store, dashboard, forwarding agent, or +comparable scenario element when the scenario makes that system part of the +environment. + +Scenario-native observability systems: + +- MUST have stable SDL identity when they are targetable; +- MUST use the runtime-family model under `nodes..runtime.*` when the + system is node-scoped logical service state; +- MAY use existing runtime families such as `network_sensors`, + `network_detection_engines`, `security_monitoring_managers`, + `forwarding_agents`, `service_listeners`, `platform_applications`, or + `datastore_services` when those families carry the intended meaning; +- MUST NOT be represented as a generic top-level observability bag; and +- MUST NOT satisfy an authored evidence requirement merely by existing. + +A new runtime family is appropriate only when the observability system has a +distinct product-neutral logical service identity, collection name, primary id, +child-ref tree, owning ADR, schema, validation, and tests. + +## Authored Evidence Requirements + +An authored evidence requirement says what data, evidence, or output must be +captured. It is an authoring obligation, not proof of capture. + +An authored evidence requirement MUST declare: + +- the source or source class; +- the scope; +- the capture window, trigger, or comparable boundary; +- the channel, modality, or boundary kind; +- expected artifact role or media kind when applicable; +- sensitivity and redaction expectation; +- integrity or chain-of-custody expectation when applicable; and +- loss-disclosure expectation. + +Authored evidence requirements: + +- MAY reference a scenario-native observability system as a source; +- MAY map to `experiment-capture-spec-v1` concepts when executable capture + contracts are generated; +- MUST remain independent of participant objectives, metrics, evaluations, + TLOs, and goals; +- MUST remain distinct from `experiment-evidence-record-v1` raw evidence; and +- MUST remain distinct from `experiment-derived-measure-v1` interpreted + outputs. + +## Processor/Backend Operational Observability + +Processor/backend operational observability includes apparatus logs, +diagnostics, audit records, health checks, traces, setup evidence, measurement +channels, and capability declarations. These facts are not scenario meaning +unless an SDL or runtime contract explicitly projects them. + +SDL authoring MUST NOT depend on backend-private object ids, raw trace payloads, +operator secrets, process argv, environment dumps, full stack traces, or +diagnostic text as portable evidence or participant-visible state. + +## Captured Evidence And Derived Analysis + +Captured evidence belongs to evidence records or artifact references. Derived +analysis belongs to derived measures, result summaries, outcome +interpretations, studies, reports, exports, or claims. + +SDL authoring MAY name the requirement or source that later evidence must +satisfy, but it MUST NOT treat a captured artifact, backend log, result +summary, metric value, or analysis output as the authored requirement itself. + +Derived analysis MUST cite source evidence before it supports a claim. Hidden +truth, hidden answer keys, evaluator state, private traces, prompts, secrets, +and adjudication assets MUST NOT become participant-visible or public analysis +content without an explicit marking, redaction, and authorization boundary. + +## Augmentation + +When processor or backend augmentation is represented in SDL-adjacent +contracts, the augmentation classification is additive: + +- `apparatus_only`; +- `environment_visible`; +- `participant_visible`; and +- `comparability_relevant`. + +Participant-visible augmentation MUST route through visibility projection and +participant observation rules. Environment-visible and comparability-relevant +augmentation MUST have first-class provenance or evidence disclosure. They MUST +NOT be hidden in metadata, diagnostics, audit blobs, backend-native DTOs, or +raw logs. + +Run-level processor/backend augmentation disclosures are carried by +`experiment-run-v1` `augmentation_disclosures`. That carrier records the +augmentation purpose, realization layer, additive classifications, portable +carrier refs, disclosure policy, markings, observer/comparability effects, and +run-traced evidence refs. SDL authoring that depends on augmentation output +must map to that run-provenance carrier rather than relying on backend logs, +free-form metadata, evaluator internals, or untyped diagnostic text. + +## Reference And Validation Requirements + +Future SDL fields for this catalog must satisfy the existing SDL gates: + +- parser and model closure through `SDLModel`; +- concrete identifier keys, with no `${var}` placeholders in symbol-defining + keys; +- fail-closed reference resolution through `references.md`; +- instantiation followed by full semantic revalidation; +- controlled vocabulary or concept-authority bindings for portable kinds; +- published schema parity with the reference implementation; and +- ADR-056/057 redaction for observed values and secret-bearing facts. + +## Extension Rule + +To add a scenario-native observability or authored evidence-requirement +surface, update the canonical catalogs instead of adding parallel prose: + +1. add or amend the owning ADR; +2. add a row to `sections.md` and reference edges to `references.md` when a new + SDL field exists; +3. add a runtime-family row to `runtime-inventory.md` when the surface is a + node-scoped logical service; +4. update published schemas and the schema publication manifest when the + structural contract changes; +5. update semantic validation and fixtures; and +6. add positive and negative tests from + `specs/formal/observability-evidence-plane.md`. diff --git a/specs/sdl/sections.md b/specs/sdl/sections.md index 7e90704f6..d7241b776 100644 --- a/specs/sdl/sections.md +++ b/specs/sdl/sections.md @@ -98,4 +98,7 @@ A new top-level authoring section is added by: defining its model and the published schema field, adding a row to this catalog (with its shape, requiredness, key shape, and references), adding its reference edges to [`references.md`](references.md), and updating the reference implementation and -its tests. No parallel section registry exists or should be created. +its tests. Scenario-native observability or authored evidence-requirement +sections must also satisfy +[`observability-and-evidence.md`](observability-and-evidence.md). No parallel +section registry exists or should be created. diff --git a/tools/generate_contract_schemas.py b/tools/generate_contract_schemas.py index 9192c7227..56a278252 100644 --- a/tools/generate_contract_schemas.py +++ b/tools/generate_contract_schemas.py @@ -41,6 +41,8 @@ def _schema_output_path(schemas_dir: Path, name: str) -> Path: "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", }: return schemas_dir / "participant-runtime" / f"{name}.json" diff --git a/tools/policy/adr_policy.yaml b/tools/policy/adr_policy.yaml index ff4a3d8d3..63e573152 100644 --- a/tools/policy/adr_policy.yaml +++ b/tools/policy/adr_policy.yaml @@ -114,6 +114,22 @@ module_boundaries: public_import_prefixes: aces_runtime: - aces_runtime.registry + - id: aces_reference_backend + root: implementations/python/packages/aces_reference_backend + allowed_top_level_imports: + - aces_backend_protocols + - aces_contracts + - aces_runtime + forbidden_import_prefixes: + - aces_backend_stubs + - aces_cli + - aces_conformance + - aces_mcp + - aces_processor + - aces_sdl + public_import_prefixes: + aces_runtime: + - aces_runtime.registry - id: aces_conformance root: implementations/python/packages/aces_conformance allowed_top_level_imports: @@ -128,6 +144,7 @@ module_boundaries: - aces_processor.compiler - aces_processor.models - aces_processor.planner + - aces_processor.reference aces_runtime: - aces_runtime.control_plane - aces_runtime.registry