From a6313bc542b121373a7f91e015fe7fe808e596b3 Mon Sep 17 00:00:00 2001 From: Alan Szmyt Date: Wed, 21 Oct 2026 03:35:37 -0400 Subject: [PATCH] fix(optiflow): validate saved read-only receipts Require complete contract shape and correlated native artifact evidence before accepting saved receipts. Add shared Rust/schema regressions and focused validation evidence for #81. --- .github/workflows/ci.yml | 3 + docs/architecture/foundation/ARCHITECTURE.md | 16 +- .../integrations/optiflow-v0.1.1-read-only.md | 29 + docs/validation/flow-81-local.json | 251 ++ docs/work/81/HANDOFF.md | 73 + requirements-ci.txt | 1 + src/optiflow.rs | 479 +++- tests/fixtures/optiflow-receipt-parity.json | 2442 +++++++++++++++++ tests/optiflow_receipts.rs | 295 ++ tools/test_optiflow_receipts.py | 75 + 10 files changed, 3586 insertions(+), 78 deletions(-) create mode 100644 docs/validation/flow-81-local.json create mode 100644 docs/work/81/HANDOFF.md create mode 100644 tests/fixtures/optiflow-receipt-parity.json create mode 100644 tests/optiflow_receipts.rs create mode 100644 tools/test_optiflow_receipts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76029eb..217fc73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,9 @@ jobs: - name: Validate contracts run: python3 tools/validate_contracts.py + - name: Validate native Optiflow receipt schema and regression fixtures + run: python3 tools/test_optiflow_receipts.py + - name: Check deterministic scenario sources run: python3 tools/generate_scenario_sources.py --check diff --git a/docs/architecture/foundation/ARCHITECTURE.md b/docs/architecture/foundation/ARCHITECTURE.md index dc301ae..d77c09b 100644 --- a/docs/architecture/foundation/ARCHITECTURE.md +++ b/docs/architecture/foundation/ARCHITECTURE.md @@ -3,12 +3,12 @@ schema: aether.architecture-document/v1 id: flow-architecture title: Flow Architecture kind: architecture-document -version: 0.13.3 +version: 0.13.4 status: draft owners: - egohygiene created: 2026-08-13 -updated: 2026-09-27 +updated: 2026-10-21 governed_by: - architecture-architecture depends_on: @@ -201,8 +201,16 @@ bindings, and the review-only plan safety fields. The adapter records a separate `flow.optiflow-read-only-receipt/v1` local attempt before launch and after scan, report, and plan. A reopened intermediate -attempt requires review; a completed receipt must still match its retained -artifact bytes. This receipt is local audit evidence, not an execution grant, +attempt requires review. Saving and reopening share one pure receipt-contract +gate: closed shapes and explicit nullable fields, pinned environment and +capabilities, ordered command evidence and exact artifact roles, digest forms, +and status consistency. Completion requires a present plan digest correlated +with the retained plan artifact; absent values never count as matching proof. +Reopening additionally confines and re-hashes the recorded artifact files, then +binds source/run/plan/set identities and marker members to those same observed +native bytes. It never authenticates an editable local record. +Independent schema/runtime fixtures cover this native receipt outside the +generic suite contract-set manifest. This receipt is local audit evidence, not an execution grant, checkpoint in the generic durable graph, provider signature, sandbox claim, or approval. The existing Flow JSONL runner and durable graph contracts remain unchanged. ADR-0013 owns this native-protocol exception and the staged handoff diff --git a/docs/integrations/optiflow-v0.1.1-read-only.md b/docs/integrations/optiflow-v0.1.1-read-only.md index 650b503..7d2ce4a 100644 --- a/docs/integrations/optiflow-v0.1.1-read-only.md +++ b/docs/integrations/optiflow-v0.1.1-read-only.md @@ -96,6 +96,26 @@ can start a fresh attempt after review; the adapter never automatically promotes or retries an uncertain attempt. The local receipt and state are trusted local audit data, not cryptographic authentication. +Saving and reopening use the same pure contract check. All required keys must +be present, including explicit nulls, and unknown fields in closed objects or +tagged paths are refused. The environment and seven capability states must match +the pinned profile. Commands retain the ordered scan/report/plan prefix, valid +outcome/coverage pairs, object diagnostics and the exact native artifact roles +with correlated marker digests. Complete/partial status requires a present plan +digest and matching plan artifact. Refusals may retain any verified command +prefix, including all three commands when final source verification fails. +Reopening checks the recorded run, source-set, plan and command-set identifiers +against the hashed native documents, including marker membership and the +source-root binding. Editing an outer identifier alone cannot preserve completed +status. These consistency checks still provide no signature or execution authority. + +Provider-independent receipt cases run in ordinary Rust CI. A separate Python +Draft 2020-12 check applies the published JSON Schema to the same fixture +mutations; runtime-only evidence/state refusals are recorded separately from +schema shape failures. This native receipt is not part of the generic +25-contract suite manifest. Neither check turns an editable local record into +authenticated evidence or an execution grant. + ## Reproducing the clean-room fixture From a fresh Flow checkout with Rust 1.85, verify and extract the official @@ -106,6 +126,15 @@ FLOW_OPTIFLOW_V011_EXECUTABLE="/absolute/path/to/release/optiflow" \ cargo test --locked --test optiflow_read_only ``` +Without a provider executable, run the deterministic receipt regressions and +independent schema check: + +```bash +cargo test --locked --test optiflow_receipts +python3 -m pip install --disable-pip-version-check --requirement requirements-ci.txt +python3 tools/test_optiflow_receipts.py +``` + The test creates only disposable text files with duplicates and Unicode paths, checks no source byte change, and probes refusal and interrupted evidence. It never scans personal media. Without the variable, the local diff --git a/docs/validation/flow-81-local.json b/docs/validation/flow-81-local.json new file mode 100644 index 0000000..d280998 --- /dev/null +++ b/docs/validation/flow-81-local.json @@ -0,0 +1,251 @@ +{ + "issue": "https://github.com/egohygiene/flow/issues/81", + "date": "2026-10-21", + "base_commit": "c673654fb7ad86569d3bb32f1412f5eb2fbf2bf3", + "candidate_identity": "The containing PR/commit identifies the candidate. File SHA-256 values below bind the checked implementation and harness without self-referential commit metadata.", + "platform": "x86_64-unknown-linux-gnu", + "toolchain": { + "rust": "1.85.0", + "python": "3.12", + "jsonschema": "4.26.0", + "PyYAML": "6.0.3" + }, + "scope": "Focused local correctness and compatibility checks, not full-suite or release qualification. Raw logs were retained locally; their hashes identify observations, not authenticated execution attestations.", + "source_files_sha256": { + ".github/workflows/ci.yml": "f9aad9ecdd5da8f456e91ea280585538be10c41e835b7bc116e3568e28f1edd7", + "Cargo.lock": "b7a16689cb7ec533bb4a50bf14e1035a2bf5d123e0491f5f63d6cb675891c1ac", + "Cargo.toml": "761f66920af6c8d59dddbe16dccba91a42b4da2148e7f6058e60729c775d4f68", + "docs/architecture/foundation/ARCHITECTURE.md": "979ab326ef9dd27343c1455c635893c23e041a68a84abf27756885e5b90924be", + "docs/integrations/optiflow-v0.1.1-read-only.md": "7130524836b9a54bfc92f6d8d2a0c81b7f36cbf1f267d168012991c1ed532fb3", + "docs/work/81/HANDOFF.md": "ad10bfaf4dc106ce842ea31abf5263979e554c5c39e2a0d733c5d599cf0eac08", + "requirements-ci.txt": "0b0709dc3105d98b29f4fc6a870c15c0cf42191498384ef8a2a32d3fcf5f34ed", + "schemas/optiflow-read-only-receipt-v1.schema.json": "66962c7f088389e3f6d6a53abbf0fdf07f77fc8cd88b27ccc1ba751e99fa202d", + "src/optiflow.rs": "46006f2fe9cc8a39e986fa7bf5c89d88e1fa69be96c65fb9cfe98ebb8430002f", + "tests/fixtures/optiflow-receipt-parity.json": "ffd22f47faccb50463b70c12deb53a9419db539b51a20b0e9304ebbe31fa494f", + "tests/optiflow_read_only.rs": "9ead83e8013399298891083ded69d51f97b9590fbfcdc5ea1259f15e80845096", + "tests/optiflow_receipts.rs": "f63d07dc6d46974d5a106994e8eb1cbb6352772298aa2b90d34c99c71d3ba208", + "tools/test_optiflow_receipts.py": "1b2bcf02cd13190368e9beccdd5dabb6773e09a145b38030dd193998da5f6cf2" + }, + "native_provider": { + "version": "0.1.1", + "source_revision": "b82599a2231e997d42fd9f26f4b59587f4ae14cf", + "executable_sha256": "46af9399f3785607f835d9a805a8daa42c93597772c6ee812911615ea4b76280", + "environment": "FLOW_OPTIFLOW_V011_EXECUTABLE explicitly set to the separately installed pinned binary; synthetic disposable text sources only", + "coverage": "All three existing adapter tests executed: two use the actual pinned executable; the missing/incompatible-provider test uses refusal fixtures. Source bytes remain unchanged, receipt reopening succeeds, artifact tampering and overlapping paths refuse." + }, + "shared_corpus": { + "total": 84, + "accepted_controls": 12, + "schema_invalid": 41, + "additional_runtime_invalid": 31, + "statuses": [ + "scanning", + "reporting", + "planning", + "complete", + "partial", + "refused" + ], + "refusal_prefix_lengths": [ + 0, + 1, + 2, + 3 + ], + "rust_boundary": "ReadOnlyReceipt::load with materialized synthetic native artifacts", + "schema_boundary": "Independent Draft202012Validator on the same portable mutations" + }, + "baseline_replay": { + "base_commit": "c673654fb7ad86569d3bb32f1412f5eb2fbf2bf3", + "change_to_baseline": "Only the three new test/harness/corpus files copied into a detached base worktree; no product-source changes", + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "--test", + "optiflow_receipts", + "--", + "--test-threads=1" + ], + "exit_code": 101, + "expected_failure": true, + "wrongly_accepted_count": 45, + "schema_invalid_wrongly_accepted_count": 27, + "wrongly_rejected_count": 0, + "wrongly_accepted_ids": [ + "audit-complete-without-artifacts", + "audit-wrong-environment", + "audit-extra-capability", + "audit-missing-nullable-field", + "audit-unknown-native-path-property", + "missing-required-source_set_id", + "missing-required-plan_id", + "missing-required-plan_sha256", + "missing-required-approval", + "missing-required-duplicate_logical_bytes", + "missing-required-physical_savings_bytes", + "missing-required-failure", + "missing-required-failure-provider-stdout", + "missing-plan-marker", + "schema-valid-duplicate-plan-role", + "schema-valid-missing-scan-role", + "schema-valid-duplicate-scan-role", + "schema-valid-wrong-artifact-schema", + "schema-valid-marker-digest-mismatch", + "schema-valid-outcome-coverage-disagreement", + "invalid-digest-short-stdout", + "invalid-digest-uppercase-marker", + "invalid-command-diagnostic-object", + "invalid-failure-diagnostic-object", + "invalid-command-outcome", + "invalid-command-coverage", + "empty-attempt-identity", + "empty-command-set-identity", + "empty-artifact-kind", + "empty-artifact-schema", + "empty-failure-code", + "empty-failure-detail", + "unknown-unix-path-property", + "outer-plan-identity-mismatch", + "outer-run-identity-mismatch", + "outer-source-set-identity-mismatch", + "scan-marker-set-identity-mismatch", + "report-marker-set-identity-mismatch", + "plan-marker-set-identity-mismatch", + "native-marker-source-linkage-mismatch", + "native-marker-member-digest-mismatch", + "native-plan-identity-mismatch", + "source-overlaps-state", + "source-relative", + "source-parent-traversal" + ], + "log_sha256": "3cd05057113beb5b5d2722e38998665bbfa9ab356898a04d6830f7cc62ae825b" + }, + "checks": [ + { + "name": "rust-focused", + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "--lib", + "--test", + "optiflow_receipts", + "--test", + "optiflow_read_only", + "--", + "--test-threads=1" + ], + "exit_code": 0, + "elapsed_seconds": 5.341, + "coverage": "3 library tests; 3 adapter tests with the pinned executable explicitly available; 1 harness executing 84 corpus cases", + "log_sha256": "7a2a2a02aa2df69aec19f2086cafb678d990d27b0182d095abd6944dee804799" + }, + { + "name": "rust-clippy", + "command": [ + "cargo", + "clippy", + "--locked", + "--offline", + "--lib", + "--test", + "optiflow_read_only", + "--test", + "optiflow_receipts", + "--", + "-D", + "warnings" + ], + "exit_code": 0, + "elapsed_seconds": 0.093, + "coverage": "Focused library and integration targets", + "log_sha256": "f176431a2418a6ab88058b00a81f877856e5297ee5034c6ba128d88888c03a18" + }, + { + "name": "rust-docs", + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "--doc" + ], + "exit_code": 0, + "elapsed_seconds": 0.838, + "coverage": "2 compile-fail documentation tests", + "log_sha256": "92fa01ec2d9f151282340a7cee075c9e5d1d1fb4a1970aad721aa3b1d9c8f42d" + }, + { + "name": "rust-format", + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "exit_code": 0, + "elapsed_seconds": 0.259, + "coverage": "Workspace formatting", + "log_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "name": "python-parity", + "command": [ + "python3", + "tools/test_optiflow_receipts.py" + ], + "exit_code": 0, + "elapsed_seconds": 0.233, + "coverage": "2 unittest methods; all 84 schema expectations", + "log_sha256": "d6383b13a4eb2ffd96733ab1bd87818f09f147421aa768b45d08401be5180335" + }, + { + "name": "contract-validation", + "command": [ + "python3", + "tools/validate_contracts.py" + ], + "exit_code": 0, + "elapsed_seconds": 0.062, + "coverage": "Existing 25 contracts, 35 positive examples, 22 negative fixtures", + "log_sha256": "64d8433a3bf6c88d9aabf9f748f6c175341934b0befeb0a0be75e87c151cda82" + }, + { + "name": "architecture-specs", + "command": [ + "python3", + ".agents/specs/validate-specs.py" + ], + "exit_code": 0, + "elapsed_seconds": 0.086, + "coverage": "Applicable architecture specification validation", + "log_sha256": "163b4172b608a75b50c64d86a720541a8068a1fe101a3570b48da42eddf1fc4f" + }, + { + "name": "diff-whitespace", + "command": [ + "git", + "diff", + "--check" + ], + "exit_code": 0, + "elapsed_seconds": 0.005, + "coverage": "Changed-file whitespace check", + "log_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "review": "Independent source review covered product logic, shared fixture/harness, CI ownership and documentation. Its identity-correlation finding was fixed before this final validation. Reviewer did not execute checks.", + "limits": [ + "No new hosted CI dispatch or polling; ordinary PR workflows may run automatically. No hosted green claim.", + "The earlier all-target acceptance wrapper reached its unchanged 600-second budget and did not qualify the entire suite. This focused checkpoint does not rerun it or raise its budget; follow-up remains Flow #34.", + "Previously observed moving-stable Clippy failures in unchanged source/tests remain under Flow #34; focused Clippy is Rust 1.85.0 only.", + "No macOS execution or Windows native-adapter qualification. The synthetic native receipt corpus is Unix-gated.", + "No Aniflow/Renderflow deferred product checks, private-media operation, mutation grant, paid model call, release or deployment.", + "Hashes and correlated local JSON are editable audit evidence, not signatures, authentication, or proof against coordinated forgery." + ], + "next_issue": "https://github.com/egohygiene/flow/issues/82" +} diff --git a/docs/work/81/HANDOFF.md b/docs/work/81/HANDOFF.md new file mode 100644 index 0000000..83aa9fc --- /dev/null +++ b/docs/work/81/HANDOFF.md @@ -0,0 +1,73 @@ +# Flow #81 — Optiflow receipt contract correctness + +Date: 2026-10-21. Issue: [#81](https://github.com/egohygiene/flow/issues/81). +Base: `c673654fb7ad86569d3bb32f1412f5eb2fbf2bf3`. +The containing PR identifies the candidate commit; this file does not predict it. + +## Failure and root cause + +The loader could accept a completed receipt with empty command artifact lists +and a null plan digest. An absent digest compared equal to absent artifact proof, +and the empty verification loop succeeded. Typed decoding also accepted omitted +required nullable keys, unknown native-path fields and other schema violations. +The old loader did not correlate recorded identities with retained native JSON. + +## Fix + +`src/optiflow.rs` owns one pure receipt validation gate, used before persistence +and after decoding. Required nullable keys must be present, nested paths remain +closed, and constants, capability membership, diagnostics, digests and state +transitions are checked. Complete/partial records require the expected unique +artifact roles and a present matching plan digest and identity. + +Reopening retains bounded regular-file, canonical confinement, size, SHA-256 and +BLAKE3 checks. The same observed bytes bind native schema markers, source roots, +run/source-set/plan IDs and committed artifact-set membership to the receipt. +Source existence is not required to reopen historical local evidence. Paths used +as argv tokens retain their original behavior. Refusal after any valid command +prefix, including all three completed commands, remains possible. + +No provider protocol, receipt schema version, runtime dependency or mutation +authority changes. These editable local records remain evidence, not signatures. +The architecture owner and integration guide describe the stronger boundary. +This repairs an existing contract; no new architecture decision is introduced. + +## Regression protection + +The shared corpus has **84 cases**: 12 accepted controls, 41 schema-invalid +cases, and 31 further runtime-invalid cases. It covers all six statuses, refusal +prefixes of length zero through three, the five audit counterexamples, all nine +required nullable keys, native identity/member correlation, tampering and paths. +The persistence unit test checks rejection before creating or replacing a file. + +The Rust harness materializes consistent synthetic native artifacts in disposable +directories. The independent Python Draft 2020-12 validator checks schema outcomes +against the same mutations. The repository-validator CI job owns the Python +check explicitly; this native receipt is outside the existing 25-contract set. +`jsonschema` is a pinned CI/development dependency, not a Rust runtime dependency. + +Baseline replay at the base above, with only the new harness/fixture files added, +incorrectly accepted **45 cases**, including all five audit counterexamples and +all nine omitted nullable keys; it rejected no positive control. Current focused +checks pass. The [local validation receipt](../../validation/flow-81-local.json) +records commands, source/log hashes and the expected failing baseline replay. + +## Validation and residual risk + +Local evidence is Linux x86-64, Rust 1.85.0. The three existing adapter tests ran +with the independently pinned Optiflow v0.1.1 executable explicitly installed; +two invoked that executable and one exercised missing/incompatible-provider +refusal. Inputs were disposable synthetic text, including Unicode and spaces. +This is actual native compatibility evidence, separate from synthetic parity. +The corpus is Unix-gated to the native adapter's supported platform family; +macOS execution and Windows native support are not qualified here. + +Do not infer a green full suite or release from these focused checks. The prior +600-second all-target wrapper timeout and moving-stable Clippy failures in +unchanged code remain owned by [#34](https://github.com/egohygiene/flow/issues/34). +No timeout was raised, lint suppressed, hosted CI polled or release published. +Aniflow/Renderflow validation deferrals remain in force. + +Next checkpoint: [#82](https://github.com/egohygiene/flow/issues/82), reconcile +current implementation and continuation documentation. Re-query live state +before starting; do not fold that broader reconciliation into this repair. diff --git a/requirements-ci.txt b/requirements-ci.txt index f62ce0c..1c1b1a8 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -1 +1,2 @@ PyYAML==6.0.3 +jsonschema==4.26.0 diff --git a/src/optiflow.rs b/src/optiflow.rs index 54ab40e..089deb9 100644 --- a/src/optiflow.rs +++ b/src/optiflow.rs @@ -31,6 +31,16 @@ const MAX_STDERR: usize = 64 * 1024; const MAX_ARTIFACT: u64 = 16 * 1024 * 1024; const SCAN_DEADLINE: Duration = Duration::from_secs(120); const SHORT_DEADLINE: Duration = Duration::from_secs(30); +const RECEIPT_ENVIRONMENT: &str = "cleared; no config; no media probe; no network grant"; +const RECEIPT_CAPABILITIES: [(&str, Capability); 7] = [ + ("scan", Capability::Scan), + ("report", Capability::Report), + ("plan-exact-duplicates", Capability::PlanExactDuplicates), + ("dry-run", Capability::DryRun), + ("quarantine", Capability::Quarantine), + ("restore", Capability::Restore), + ("finalize", Capability::Finalize), +]; /// The pinned release archives and their executable digests. The Linux /// executable was exercised locally; the two macOS archives were inspected and @@ -150,7 +160,7 @@ impl AdapterError { /// Lossless provider path. Non-UTF-8 paths retain their original Unix bytes. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(tag = "encoding", rename_all = "snake_case")] +#[serde(tag = "encoding", rename_all = "snake_case", deny_unknown_fields)] pub enum NativePath { Utf8 { value: String }, UnixBytes { base64: String }, @@ -298,9 +308,29 @@ pub struct FailureEvidence { pub code: String, pub detail: String, pub provider_diagnostics: Vec, + #[serde(deserialize_with = "deserialize_required_nullable")] pub provider_stdout_sha256: Option, } +impl FailureEvidence { + fn validate(&self) -> Result<(), AdapterError> { + if self.code.is_empty() + || self.detail.is_empty() + || self + .provider_diagnostics + .iter() + .any(|item| !item.is_object()) + || self + .provider_stdout_sha256 + .as_deref() + .is_some_and(|digest| !valid_receipt_digest(digest)) + { + return Err(AdapterError::InvalidEvidence("receipt failure evidence")); + } + Ok(()) + } +} + /// A local, durable attempt record. An intermediate status after reopen is /// recovery-required. No record conveys approval or source mutation authority. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -315,107 +345,169 @@ pub struct ReadOnlyReceipt { pub environment: String, pub capabilities: BTreeMap, pub commands: Vec, + #[serde(deserialize_with = "deserialize_required_nullable")] pub source_run_id: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub source_set_id: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub plan_id: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub plan_sha256: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub approval: Option, pub recovery: String, + #[serde(deserialize_with = "deserialize_required_nullable")] pub duplicate_logical_bytes: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub physical_savings_bytes: Option, + #[serde(deserialize_with = "deserialize_required_nullable")] pub failure: Option, } +// Unlike Serde's default Option handling, an omitted key is not explicit null. +fn deserialize_required_nullable<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer) +} + impl ReadOnlyReceipt { /// Reopen evidence without promoting an unfinished attempt to completion. /// # Errors - /// Rejects invalid JSON, a different receipt schema, or changed evidence - /// files. A receipt and its hashes are local audit evidence, not a signature. + /// Rejects invalid or incomplete receipt contracts, inconsistent states, or + /// changed evidence files. Hashes remain local audit evidence, not a signature. pub fn load(path: &Path) -> Result { let receipt: Self = serde_json::from_slice(&read_regular(path, MAX_ARTIFACT)?)?; - if receipt.schema_version != RECEIPT_SCHEMA { + receipt.validate()?; + let state = receipt.state_directory.path()?; + for command in &receipt.commands { + for artifact in &command.artifacts { + let path = artifact.path.path()?; + if !path.starts_with(&state) + || path + .components() + .any(|part| matches!(part, Component::ParentDir | Component::CurDir)) + || fs::canonicalize(&path)? != path + { + return Err(AdapterError::PathEscape); + } + let bytes = read_regular(&path, MAX_ARTIFACT)?; + if bytes.len() as u64 != artifact.size_bytes + || sha256(&bytes) != artifact.sha256 + || blake3::hash(&bytes).to_hex().as_str() != artifact.blake3_256 + { + return Err(AdapterError::InvalidEvidence("receipt artifact changed")); + } + validate_receipt_document(&receipt, command, artifact, &bytes)?; + } + } + Ok(receipt) + } + + /// Pure validation is shared by emitted and reopened evidence. Filesystem + /// re-observation stays in load; constructing a receipt never grants authority. + fn validate(&self) -> Result<(), AdapterError> { + if self.schema_version != RECEIPT_SCHEMA { return Err(AdapterError::InvalidEvidence("receipt schema")); } + if self.attempt_id.is_empty() || self.environment != RECEIPT_ENVIRONMENT { + return Err(AdapterError::InvalidEvidence( + "receipt identity or environment", + )); + } + validate_receipt_roots(&self.source.root, &self.state_directory)?; let profile = RELEASE_PROFILES .iter() - .find(|profile| profile.target == receipt.provider.target) + .find(|profile| profile.target == self.provider.target) .ok_or(AdapterError::InvalidEvidence("receipt target"))?; - if receipt.provider.version != PROVIDER_VERSION - || receipt.provider.source_revision != PROVIDER_SOURCE_REVISION - || receipt.provider.interface != COMMAND_RESULT_SCHEMA - || receipt.provider.archive_sha256 != profile.archive_sha256 - || receipt.provider.executable_sha256 != profile.executable_sha256 + if self.provider.version != PROVIDER_VERSION + || self.provider.source_revision != PROVIDER_SOURCE_REVISION + || self.provider.interface != COMMAND_RESULT_SCHEMA + || self.provider.archive_sha256 != profile.archive_sha256 + || self.provider.executable_sha256 != profile.executable_sha256 { return Err(AdapterError::InvalidEvidence("receipt provider identity")); } - let names = receipt + if self.capabilities.len() != RECEIPT_CAPABILITIES.len() + || RECEIPT_CAPABILITIES + .iter() + .any(|(key, capability)| self.capabilities.get(*key) != Some(&capability.state())) + { + return Err(AdapterError::InvalidEvidence("receipt capability state")); + } + if self.commands.len() > 3 + || self + .commands + .iter() + .zip(["scan", "report", "plan"]) + .any(|(command, expected)| command.command != expected) + || self.approval.is_some() + || self.physical_savings_bytes.is_some() + || !matches!( + self.recovery.as_str(), + "required-if-interrupted" + | "inspect-provider-state-before-retry" + | "no-source-mutation" + | "none" + ) + { + return Err(AdapterError::InvalidEvidence("receipt state or authority")); + } + if self + .plan_sha256 + .as_deref() + .is_some_and(|digest| !valid_receipt_digest(digest)) + { + return Err(AdapterError::InvalidEvidence("receipt plan digest")); + } + if let Some(failure) = &self.failure { + failure.validate()?; + } + for command in &self.commands { + validate_receipt_command(command)?; + } + let names = self .commands .iter() .map(|command| command.command.as_str()) .collect::>(); - let partial = receipt + let partial = self .commands .iter() .any(|command| command.coverage == "partial"); - let consistent = match receipt.status { + let consistent = match self.status { ReceiptStatus::Scanning => names.is_empty(), ReceiptStatus::Reporting => names == ["scan"], ReceiptStatus::Planning => names == ["scan", "report"], ReceiptStatus::Complete | ReceiptStatus::Partial => { names == ["scan", "report", "plan"] - && receipt.source_run_id.is_some() - && receipt.source_set_id.is_some() - && receipt.plan_id.is_some() - && receipt.plan_sha256.as_deref() - == receipt.commands[2] + && self + .source_run_id + .as_deref() + .is_some_and(|id| !id.is_empty()) + && self + .source_set_id + .as_deref() + .is_some_and(|id| !id.is_empty()) + && self.plan_id.as_deref().is_some_and(|id| !id.is_empty()) + && self.plan_sha256.as_deref().is_some_and(|digest| { + self.commands[2] .artifacts .iter() - .find(|artifact| artifact.kind == "plan") - .map(|artifact| artifact.sha256.as_str()) - && receipt.failure.is_none() - && receipt.recovery == "none" - && partial == matches!(receipt.status, ReceiptStatus::Partial) + .any(|artifact| artifact.kind == "plan" && artifact.sha256 == digest) + }) + && self.failure.is_none() + && self.recovery == "none" + && partial == matches!(self.status, ReceiptStatus::Partial) } - ReceiptStatus::Refused => receipt.failure.is_some(), + ReceiptStatus::Refused => self.failure.is_some(), }; - if !consistent || receipt.approval.is_some() || receipt.physical_savings_bytes.is_some() { + if !consistent { return Err(AdapterError::InvalidEvidence("receipt state or authority")); } - for (key, capability) in [ - ("scan", Capability::Scan), - ("report", Capability::Report), - ("plan-exact-duplicates", Capability::PlanExactDuplicates), - ("dry-run", Capability::DryRun), - ("quarantine", Capability::Quarantine), - ("restore", Capability::Restore), - ("finalize", Capability::Finalize), - ] { - if receipt.capabilities.get(key) != Some(&capability.state()) { - return Err(AdapterError::InvalidEvidence("receipt capability state")); - } - } - let state = receipt.state_directory.path()?; - for command in &receipt.commands { - for artifact in &command.artifacts { - let path = artifact.path.path()?; - if !path.starts_with(&state) - || path - .components() - .any(|part| matches!(part, Component::ParentDir | Component::CurDir)) - || fs::canonicalize(&path)? != path - { - return Err(AdapterError::PathEscape); - } - let bytes = read_regular(&path, MAX_ARTIFACT)?; - if bytes.len() as u64 != artifact.size_bytes - || sha256(&bytes) != artifact.sha256 - || blake3::hash(&bytes).to_hex().as_str() != artifact.blake3_256 - { - return Err(AdapterError::InvalidEvidence("receipt artifact changed")); - } - } - } - Ok(receipt) + Ok(()) } #[must_use] @@ -427,6 +519,205 @@ impl ReadOnlyReceipt { } } +fn valid_receipt_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn validate_receipt_roots(source: &NativePath, state: &NativePath) -> Result<(), AdapterError> { + let source = source.path()?; + let state = state.path()?; + if [&source, &state].iter().any(|path| { + !path.is_absolute() + || path + .components() + .any(|part| matches!(part, Component::ParentDir | Component::CurDir)) + }) || source.starts_with(&state) + || state.starts_with(&source) + { + return Err(AdapterError::PathEscape); + } + Ok(()) +} + +fn validate_receipt_command(command: &CommandEvidence) -> Result<(), AdapterError> { + let expected: &[(&str, &str)] = match command.command.as_str() { + "scan" | "report" => &[ + ("effective_policy", POLICY_SCHEMA), + ("report", REPORT_SCHEMA), + ("run", RUN_SCHEMA), + ("artifact_set", ARTIFACT_SET_SCHEMA), + ], + "plan" => &[("plan", PLAN_SCHEMA), ("artifact_set", ARTIFACT_SET_SCHEMA)], + _ => return Err(AdapterError::InvalidEvidence("receipt command")), + }; + if !matches!( + (command.outcome.as_str(), command.coverage.as_str()), + ("success", "complete") | ("partial_success", "partial") + ) || command.set_id.is_empty() + || !valid_receipt_digest(&command.stdout_sha256) + || !valid_receipt_digest(&command.marker_sha256) + || command.diagnostics.iter().any(|item| !item.is_object()) + || command.artifacts.len() != expected.len() + { + return Err(AdapterError::InvalidEvidence("receipt command evidence")); + } + for (kind, schema) in expected { + let mut members = command + .artifacts + .iter() + .filter(|artifact| artifact.kind == *kind); + let member = members + .next() + .ok_or(AdapterError::InvalidEvidence("receipt artifact roles"))?; + if members.next().is_some() + || member.schema != *schema + || !valid_receipt_digest(&member.sha256) + || !valid_receipt_digest(&member.blake3_256) + || (*kind == "artifact_set" && member.sha256 != command.marker_sha256) + { + return Err(AdapterError::InvalidEvidence("receipt artifact evidence")); + } + } + Ok(()) +} + +fn require_receipt_id(value: &Value, recorded: Option<&str>) -> Result<(), AdapterError> { + if recorded.is_some_and(|id| !id.is_empty() && value.as_str() == Some(id)) { + Ok(()) + } else { + Err(AdapterError::InvalidEvidence( + "receipt native identity binding", + )) + } +} + +fn validate_receipt_document( + receipt: &ReadOnlyReceipt, + command: &CommandEvidence, + artifact: &ArtifactEvidence, + bytes: &[u8], +) -> Result<(), AdapterError> { + if artifact.kind == "artifact_set" { + return validate_receipt_marker(receipt, command, artifact, bytes); + } + let document: Value = serde_json::from_slice(bytes)?; + let schema_field = if artifact.kind == "effective_policy" { + "/schema" + } else { + "/schema_version" + }; + if field(&document, schema_field)?.as_str() != Some(artifact.schema.as_str()) { + return Err(AdapterError::InvalidEvidence( + "receipt native artifact schema", + )); + } + match artifact.kind.as_str() { + "run" | "report" => { + let run = if artifact.kind == "report" { + field(&document, "/run")? + } else { + &document + }; + require_receipt_id(field(run, "/run_id")?, receipt.source_run_id.as_deref())?; + require_receipt_id( + field(run, "/artifact_set_id")?, + receipt.source_set_id.as_deref(), + )?; + let inputs = field(run, "/inputs")? + .as_array() + .ok_or(AdapterError::InvalidEvidence("receipt source roots"))?; + let root = receipt.source.root.path()?; + if inputs.len() != 1 || inputs[0].as_str().map(Path::new) != Some(root.as_path()) { + return Err(AdapterError::InvalidEvidence("receipt source root binding")); + } + } + "plan" => { + require_receipt_id(field(&document, "/plan_id")?, receipt.plan_id.as_deref())?; + require_receipt_id( + field(&document, "/source_run_id")?, + receipt.source_run_id.as_deref(), + )?; + require_receipt_id( + field(&document, "/source_artifact_set_id")?, + receipt.source_set_id.as_deref(), + )?; + if field(&document, "/safety/mutates_files")?.as_bool() != Some(false) + || field(&document, "/safety/requires_explicit_apply")?.as_bool() != Some(true) + { + return Err(AdapterError::InvalidEvidence("receipt plan safety")); + } + } + "effective_policy" => {} + _ => return Err(AdapterError::InvalidEvidence("receipt artifact role")), + } + Ok(()) +} + +fn validate_receipt_marker( + receipt: &ReadOnlyReceipt, + command: &CommandEvidence, + artifact: &ArtifactEvidence, + bytes: &[u8], +) -> Result<(), AdapterError> { + let marker: NativeMarker = serde_json::from_slice(bytes)?; + let kind = if command.command == "plan" { + "plan" + } else { + "scan" + }; + let source_set = if kind == "plan" { + receipt.source_set_id.as_deref() + } else { + None + }; + if marker.schema != ARTIFACT_SET_SCHEMA + || marker.state != "committed" + || marker.set_kind != kind + || marker.set_id != command.set_id + || Some(marker.run_id.as_str()) != receipt.source_run_id.as_deref() + || marker.source_set_id.as_deref() != source_set + || (kind == "scan" && Some(marker.set_id.as_str()) != receipt.source_set_id.as_deref()) + || marker.members.len() + 1 != command.artifacts.len() + { + return Err(AdapterError::InvalidEvidence( + "receipt artifact set binding", + )); + } + let marker_path = artifact.path.path()?; + let directory = marker_path.parent().ok_or(AdapterError::PathEscape)?; + for member in command + .artifacts + .iter() + .filter(|member| member.kind != "artifact_set") + { + let mut recorded = marker + .members + .iter() + .filter(|recorded| recorded.kind == member.kind); + let native = recorded + .next() + .ok_or(AdapterError::InvalidEvidence("receipt marker member"))?; + let relative = native.path.path()?; + if recorded.next().is_some() + || relative.components().count() != 1 + || !matches!(relative.components().next(), Some(Component::Normal(_))) + || directory.join(&relative) != member.path.path()? + || native.schema != member.schema + || native.size_bytes != member.size_bytes + || native.digest.algorithm != "blake3-256" + || native.digest.value != member.blake3_256 + { + return Err(AdapterError::InvalidEvidence( + "receipt marker member binding", + )); + } + } + Ok(()) +} + /// A pinned native CLI, with exactly three read-only capabilities. pub struct OptiflowReadOnlyAdapter { executable: PathBuf, @@ -537,18 +828,10 @@ impl OptiflowReadOnlyAdapter { .as_nanos() ); let path = receipt_dir.join(format!("{attempt_id}.json")); - let capabilities = [ - ("scan", Capability::Scan), - ("report", Capability::Report), - ("plan-exact-duplicates", Capability::PlanExactDuplicates), - ("dry-run", Capability::DryRun), - ("quarantine", Capability::Quarantine), - ("restore", Capability::Restore), - ("finalize", Capability::Finalize), - ] - .into_iter() - .map(|(key, capability)| (key.to_owned(), capability.state())) - .collect(); + let capabilities = RECEIPT_CAPABILITIES + .into_iter() + .map(|(key, capability)| (key.to_owned(), capability.state())) + .collect(); let mut receipt = ReadOnlyReceipt { schema_version: RECEIPT_SCHEMA.to_owned(), attempt_id: attempt_id.clone(), @@ -556,7 +839,7 @@ impl OptiflowReadOnlyAdapter { provider: self.identity.clone(), source: source.clone(), state_directory: NativePath::from_path(&state), - environment: "cleared; no config; no media probe; no network grant".to_owned(), + environment: RECEIPT_ENVIRONMENT.to_owned(), capabilities, commands: Vec::new(), source_run_id: None, @@ -827,6 +1110,9 @@ impl OptiflowReadOnlyAdapter { "command result schema or command", )); } + if response.diagnostics.iter().any(|item| !item.is_object()) { + return Err(AdapterError::InvalidEvidence("provider diagnostic shape")); + } let (class, coverage) = match ( response.outcome.class.as_str(), response.outcome.exit_code, @@ -1313,6 +1599,7 @@ fn read_capped(stream: impl Read, limit: usize) -> io::Result> { } fn persist(path: &Path, receipt: &ReadOnlyReceipt, first: bool) -> Result<(), AdapterError> { + receipt.validate()?; let mut bytes = serde_json::to_vec_pretty(receipt)?; bytes.push(b'\n'); let target = if first { @@ -1339,10 +1626,54 @@ fn persist(path: &Path, receipt: &ReadOnlyReceipt, first: bool) -> Result<(), Ad #[cfg(test)] mod tests { - use super::{AdapterError, NativePath, SourceIdentity, validate_plan_paths}; + use super::{ + AdapterError, NativePath, RECEIPT_ENVIRONMENT, ReadOnlyReceipt, ReceiptStatus, + SourceIdentity, persist, validate_plan_paths, + }; use serde_json::json; use std::fs; + #[test] + fn invalid_receipts_cannot_create_or_replace_durable_evidence() { + let suffix = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "flow-receipt-persistence-{}-{suffix}", + std::process::id() + )); + fs::create_dir(&root).unwrap(); + let root = fs::canonicalize(root).unwrap(); + let corpus: serde_json::Value = serde_json::from_str(include_str!( + "../tests/fixtures/optiflow-receipt-parity.json" + )) + .unwrap(); + let mut receipt: ReadOnlyReceipt = serde_json::from_value(corpus["base"].clone()).unwrap(); + receipt.status = ReceiptStatus::Scanning; + receipt.commands.clear(); + receipt.source_run_id = None; + receipt.source_set_id = None; + receipt.plan_id = None; + receipt.plan_sha256 = None; + receipt.recovery = "required-if-interrupted".to_owned(); + receipt.source.root = NativePath::from_path(&root.join("source")); + receipt.state_directory = NativePath::from_path(&root.join("state")); + let path = root.join("receipt.json"); + receipt.environment = "wrong".to_owned(); + assert!(persist(&path, &receipt, true).is_err()); + assert!(!path.exists()); + + receipt.environment = RECEIPT_ENVIRONMENT.to_owned(); + persist(&path, &receipt, true).unwrap(); + let original = fs::read(&path).unwrap(); + receipt.plan_sha256 = Some("invalid digest".to_owned()); + assert!(persist(&path, &receipt, false).is_err()); + assert_eq!(fs::read(&path).unwrap(), original); + assert!(!path.with_extension("tmp").exists()); + fs::remove_dir_all(root).unwrap(); + } + #[test] fn plan_candidate_escape_is_refused_even_with_a_safe_keep_path() { let root = std::env::temp_dir().join(format!("flow-optiflow-root-{}", std::process::id())); diff --git a/tests/fixtures/optiflow-receipt-parity.json b/tests/fixtures/optiflow-receipt-parity.json new file mode 100644 index 0000000..c03d74f --- /dev/null +++ b/tests/fixtures/optiflow-receipt-parity.json @@ -0,0 +1,2442 @@ +{ + "schema_version": "flow.optiflow-receipt-parity-tests/v1", + "description": "Portable synthetic receipt data. Runtime tests substitute canonical @STATE@/@SOURCE@ roots and real artifact identities before applying the same case operations. Schema validity describes JSON shape; runtime additionally checks semantic correlation and local evidence. These are not native provider qualification fixtures.", + "base": { + "schema_version": "flow.optiflow-read-only-receipt/v1", + "attempt_id": "synthetic-receipt-parity", + "status": "complete", + "provider": { + "version": "0.1.1", + "source_revision": "b82599a2231e997d42fd9f26f4b59587f4ae14cf", + "target": "x86_64-unknown-linux-gnu", + "archive_sha256": "5e8a0eea84f5ab55fe75fc8a8abc2e9d36e3512537b754262b1f34a9c40aae31", + "executable_sha256": "46af9399f3785607f835d9a805a8daa42c93597772c6ee812911615ea4b76280", + "interface": "optiflow.command-result.v1" + }, + "source": { + "root": { + "encoding": "utf8", + "value": "@SOURCE@" + }, + "filesystem_id": 0, + "file_id": 0 + }, + "state_directory": { + "encoding": "utf8", + "value": "@STATE@" + }, + "environment": "cleared; no config; no media probe; no network grant", + "capabilities": { + "scan": "available-read-only", + "report": "available-read-only", + "plan-exact-duplicates": "available-read-only", + "dry-run": "unsupported-by-pinned-release", + "quarantine": "unsupported-by-pinned-release", + "restore": "unsupported-by-pinned-release", + "finalize": "unsupported-by-pinned-release" + }, + "commands": [ + { + "command": "scan", + "arguments": [ + { + "encoding": "utf8", + "value": "scan" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000001", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "report", + "schema": "optiflow.report.v6", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/report.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "run", + "schema": "optiflow.run.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/run.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + }, + { + "command": "report", + "arguments": [ + { + "encoding": "utf8", + "value": "report" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000001", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "report", + "schema": "optiflow.report.v6", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/report.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "run", + "schema": "optiflow.run.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/run.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + }, + { + "command": "plan", + "arguments": [ + { + "encoding": "utf8", + "value": "plan" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000002", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "plan", + "schema": "optiflow.plan.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json.artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + ], + "source_run_id": "00000000-0000-4000-8000-000000000004", + "source_set_id": "00000000-0000-4000-8000-000000000001", + "plan_id": "00000000-0000-4000-8000-000000000003", + "plan_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "approval": null, + "recovery": "none", + "duplicate_logical_bytes": 0, + "physical_savings_bytes": null, + "failure": null + }, + "cases": [ + { + "id": "valid-complete", + "operations": [], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-partial", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "partial" + }, + { + "op": "replace", + "path": "/commands/0/outcome", + "value": "partial_success" + }, + { + "op": "replace", + "path": "/commands/0/coverage", + "value": "partial" + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-scanning", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": true + }, + { + "id": "valid-reporting", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "reporting" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": true + }, + { + "id": "valid-planning", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "planning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": true + }, + { + "id": "valid-refused-prefix-0", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "replace", + "path": "/recovery", + "value": "no-source-mutation" + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-refused-prefix-1", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/recovery", + "value": "no-source-mutation" + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-refused-prefix-2", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/recovery", + "value": "no-source-mutation" + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-refused-prefix-3", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "replace", + "path": "/recovery", + "value": "no-source-mutation" + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-refused-recovery", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/recovery", + "value": "inspect-provider-state-before-retry" + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": true + }, + { + "id": "audit-complete-without-artifacts", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts", + "value": [] + }, + { + "op": "replace", + "path": "/commands/1/artifacts", + "value": [] + }, + { + "op": "replace", + "path": "/commands/2/artifacts", + "value": [] + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "audit-wrong-environment", + "operations": [ + { + "op": "replace", + "path": "/environment", + "value": "incorrect environment claim" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "audit-extra-capability", + "operations": [ + { + "op": "replace", + "path": "/capabilities", + "value": { + "scan": "available-read-only", + "report": "available-read-only", + "plan-exact-duplicates": "available-read-only", + "dry-run": "unsupported-by-pinned-release", + "quarantine": "unsupported-by-pinned-release", + "restore": "unsupported-by-pinned-release", + "finalize": "unsupported-by-pinned-release", + "undeclared-capability": "available-read-only" + } + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "audit-missing-nullable-field", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/source_run_id" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "audit-unknown-native-path-property", + "operations": [ + { + "op": "replace", + "path": "/state_directory", + "value": { + "encoding": "utf8", + "value": "@STATE@", + "unknown": "extra" + } + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-source_set_id", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/source_set_id" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-plan_id", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/plan_id" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-plan_sha256", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/plan_sha256" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-approval", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/approval" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-duplicate_logical_bytes", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/duplicate_logical_bytes" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-physical_savings_bytes", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/physical_savings_bytes" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-failure", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "scanning" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "remove", + "path": "/failure" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-required-failure-provider-stdout", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + }, + { + "op": "remove", + "path": "/failure/provider_stdout_sha256" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-plan-artifact", + "operations": [ + { + "op": "remove", + "path": "/commands/2/artifacts/0" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "missing-plan-marker", + "operations": [ + { + "op": "remove", + "path": "/commands/2/artifacts/1" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "schema-valid-missing-plan-role", + "operations": [ + { + "op": "replace", + "path": "/commands/2/artifacts/0/kind", + "value": "report" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-duplicate-plan-role", + "operations": [ + { + "op": "replace", + "path": "/commands/2/artifacts/1/kind", + "value": "plan" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-missing-scan-role", + "operations": [ + { + "op": "remove", + "path": "/commands/0/artifacts/0" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-duplicate-scan-role", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/kind", + "value": "report" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-wrong-artifact-schema", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/schema", + "value": "unknown.schema.v1" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-marker-digest-mismatch", + "operations": [ + { + "op": "replace", + "path": "/commands/0/marker_sha256", + "value": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-plan-digest-mismatch", + "operations": [ + { + "op": "replace", + "path": "/plan_sha256", + "value": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-absent-complete-plan-digest", + "operations": [ + { + "op": "replace", + "path": "/plan_sha256", + "value": null + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-outcome-coverage-disagreement", + "operations": [ + { + "op": "replace", + "path": "/commands/0/coverage", + "value": "partial" + }, + { + "op": "replace", + "path": "/status", + "value": "partial" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-wrong-command-order", + "operations": [ + { + "op": "replace", + "path": "/commands/0/command", + "value": "report" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "schema-valid-complete-with-partial-command", + "operations": [ + { + "op": "replace", + "path": "/commands/0/outcome", + "value": "partial_success" + }, + { + "op": "replace", + "path": "/commands/0/coverage", + "value": "partial" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "invalid-digest-short-stdout", + "operations": [ + { + "op": "replace", + "path": "/commands/0/stdout_sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-digest-uppercase-marker", + "operations": [ + { + "op": "replace", + "path": "/commands/0/marker_sha256", + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-digest-nonhex-artifact", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/sha256", + "value": "gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-digest-long-blake3", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/blake3_256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-digest-bad-plan", + "operations": [ + { + "op": "replace", + "path": "/plan_sha256", + "value": "bad" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-digest-short-provider", + "operations": [ + { + "op": "replace", + "path": "/provider/executable_sha256", + "value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-command-diagnostic-object", + "operations": [ + { + "op": "replace", + "path": "/commands/0/diagnostics", + "value": [ + "not-an-object" + ] + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-failure-diagnostic-object", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + }, + { + "op": "replace", + "path": "/failure/provider_diagnostics", + "value": [ + null + ] + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-command-enum", + "operations": [ + { + "op": "replace", + "path": "/commands/0/command", + "value": "quarantine" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-command-outcome", + "operations": [ + { + "op": "replace", + "path": "/commands/0/outcome", + "value": "failed" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-command-coverage", + "operations": [ + { + "op": "replace", + "path": "/commands/0/coverage", + "value": "unknown" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-status-enum", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "succeeded" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-recovery-enum", + "operations": [ + { + "op": "replace", + "path": "/recovery", + "value": "automatic-retry" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-capability-state", + "operations": [ + { + "op": "replace", + "path": "/capabilities/scan", + "value": "unsupported-by-pinned-release" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-command-count", + "operations": [ + { + "op": "replace", + "path": "/commands", + "value": [ + { + "command": "scan", + "arguments": [ + { + "encoding": "utf8", + "value": "scan" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000001", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "report", + "schema": "optiflow.report.v6", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/report.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "run", + "schema": "optiflow.run.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/run.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + }, + { + "command": "report", + "arguments": [ + { + "encoding": "utf8", + "value": "report" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000001", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "report", + "schema": "optiflow.report.v6", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/report.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "run", + "schema": "optiflow.run.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/run.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + }, + { + "command": "plan", + "arguments": [ + { + "encoding": "utf8", + "value": "plan" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000002", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "plan", + "schema": "optiflow.plan.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json.artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + }, + { + "command": "plan", + "arguments": [ + { + "encoding": "utf8", + "value": "plan" + }, + { + "encoding": "utf8", + "value": "--state-directory" + }, + { + "encoding": "utf8", + "value": "@STATE@" + } + ], + "outcome": "success", + "coverage": "complete", + "stdout_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "diagnostics": [], + "set_id": "00000000-0000-4000-8000-000000000002", + "marker_sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "artifacts": [ + { + "kind": "plan", + "schema": "optiflow.plan.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/plans/attempt.json.artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + ] + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-artifact-count", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts", + "value": [ + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "report", + "schema": "optiflow.report.v6", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/report.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "run", + "schema": "optiflow.run.v5", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/run.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "artifact_set", + "schema": "optiflow.artifact-set.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/artifact-set.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "kind": "effective_policy", + "schema": "optiflow.effective-policy.v1", + "path": { + "encoding": "utf8", + "value": "@STATE@/runs/source-run/effective-policy.json" + }, + "size_bytes": 0, + "sha256": "0000000000000000000000000000000000000000000000000000000000000000", + "blake3_256": "0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-approval-authority", + "operations": [ + { + "op": "replace", + "path": "/approval", + "value": "approved" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-physical-savings-claim", + "operations": [ + { + "op": "replace", + "path": "/physical_savings_bytes", + "value": 1 + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "invalid-negative-byte-count", + "operations": [ + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": -1 + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-attempt-identity", + "operations": [ + { + "op": "replace", + "path": "/attempt_id", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-command-set-identity", + "operations": [ + { + "op": "replace", + "path": "/commands/0/set_id", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-artifact-kind", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/kind", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-artifact-schema", + "operations": [ + { + "op": "replace", + "path": "/commands/0/artifacts/0/schema", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-failure-code", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + }, + { + "op": "replace", + "path": "/failure/code", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "empty-failure-detail", + "operations": [ + { + "op": "replace", + "path": "/status", + "value": "refused" + }, + { + "op": "replace", + "path": "/recovery", + "value": "required-if-interrupted" + }, + { + "op": "remove", + "path": "/commands/2" + }, + { + "op": "remove", + "path": "/commands/1" + }, + { + "op": "remove", + "path": "/commands/0" + }, + { + "op": "replace", + "path": "/plan_id", + "value": null + }, + { + "op": "replace", + "path": "/plan_sha256", + "value": null + }, + { + "op": "replace", + "path": "/source_run_id", + "value": null + }, + { + "op": "replace", + "path": "/source_set_id", + "value": null + }, + { + "op": "replace", + "path": "/duplicate_logical_bytes", + "value": null + }, + { + "op": "replace", + "path": "/failure", + "value": { + "code": "synthetic_refusal", + "detail": "Deliberate public synthetic refusal", + "provider_diagnostics": [], + "provider_stdout_sha256": null + } + }, + { + "op": "replace", + "path": "/failure/detail", + "value": "" + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "unknown-unix-path-property", + "operations": [ + { + "op": "replace", + "path": "/source/root", + "value": { + "encoding": "unix_bytes", + "base64": "L3N5bnRoZXRpYw==", + "unknown": "extra" + } + } + ], + "schema_valid": false, + "runtime_valid": false + }, + { + "id": "artifact-path-parent-traversal", + "operations": [ + { + "op": "replace", + "path": "/commands/2/artifacts/0/path/value", + "value": "@STATE@/../outside.json" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "artifact-path-outside-state", + "operations": [ + { + "op": "replace", + "path": "/commands/2/artifacts/0/path/value", + "value": "@SOURCE@/outside.json" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "artifact-path-relative", + "operations": [ + { + "op": "replace", + "path": "/commands/2/artifacts/0/path/value", + "value": "relative.json" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "artifact-bytes-tampered", + "operations": [], + "schema_valid": true, + "runtime_valid": false, + "file_effect": "tamper-plan" + }, + { + "id": "artifact-is-symlink", + "operations": [], + "schema_valid": true, + "runtime_valid": false, + "file_effect": "artifact-symlink" + }, + { + "id": "outer-plan-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/plan_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "outer-run-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/source_run_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "outer-source-set-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/source_set_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "scan-marker-set-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/commands/0/set_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "report-marker-set-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/commands/1/set_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "plan-marker-set-identity-mismatch", + "operations": [ + { + "op": "replace", + "path": "/commands/2/set_id", + "value": "00000000-0000-4000-8000-000000000099" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "native-marker-source-linkage-mismatch", + "operations": [], + "schema_valid": true, + "runtime_valid": false, + "file_effect": "marker-source-set" + }, + { + "id": "native-marker-member-digest-mismatch", + "operations": [], + "schema_valid": true, + "runtime_valid": false, + "file_effect": "marker-member-digest" + }, + { + "id": "native-plan-identity-mismatch", + "operations": [], + "schema_valid": true, + "runtime_valid": false, + "file_effect": "plan-identity" + }, + { + "id": "valid-relative-unix-byte-argument", + "operations": [ + { + "op": "replace", + "path": "/commands/0/arguments/0", + "value": { + "encoding": "unix_bytes", + "base64": "c2Nhbg==" + } + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "valid-object-diagnostic", + "operations": [ + { + "op": "replace", + "path": "/commands/0/diagnostics", + "value": [ + { + "code": "synthetic_notice", + "detail": "Public synthetic metadata" + } + ] + } + ], + "schema_valid": true, + "runtime_valid": true, + "needs_recovery": false + }, + { + "id": "state-relative", + "operations": [ + { + "op": "replace", + "path": "/state_directory/value", + "value": "relative" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "state-parent-traversal", + "operations": [ + { + "op": "replace", + "path": "/state_directory/value", + "value": "@STATE@/../local state" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "state-overlaps-source", + "operations": [ + { + "op": "replace", + "path": "/state_directory/value", + "value": "@SOURCE@/state" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "source-overlaps-state", + "operations": [ + { + "op": "replace", + "path": "/source/root/value", + "value": "@STATE@/source" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "source-relative", + "operations": [ + { + "op": "replace", + "path": "/source/root/value", + "value": "relative" + } + ], + "schema_valid": true, + "runtime_valid": false + }, + { + "id": "source-parent-traversal", + "operations": [ + { + "op": "replace", + "path": "/source/root/value", + "value": "@SOURCE@/../source 🌿 with spaces" + } + ], + "schema_valid": true, + "runtime_valid": false + } + ] +} diff --git a/tests/optiflow_receipts.rs b/tests/optiflow_receipts.rs new file mode 100644 index 0000000..6f1c755 --- /dev/null +++ b/tests/optiflow_receipts.rs @@ -0,0 +1,295 @@ +//! Shared, provider-free receipt regressions. Native JSON below is deliberately +//! minimal: it binds identities and local evidence without qualifying a provider. +//! Pinned adapter release profiles support Unix; symlink confinement is exercised +//! on those platforms rather than panicking on unsupported Windows hosts. + +#![cfg(unix)] + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use flow::optiflow::ReadOnlyReceipt; +use serde::Deserialize; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; + +const FIXTURE: &str = include_str!("fixtures/optiflow-receipt-parity.json"); +static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0); + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Manifest { + schema_version: String, + description: String, + base: Value, + cases: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Case { + id: String, + operations: Vec, + schema_valid: bool, + runtime_valid: bool, + needs_recovery: Option, + file_effect: Option, +} + +#[derive(Deserialize)] +#[serde(tag = "op", rename_all = "lowercase", deny_unknown_fields)] +enum Operation { + Replace { path: String, value: Value }, + Remove { path: String }, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +enum FileEffect { + TamperPlan, + ArtifactSymlink, + MarkerSourceSet, + MarkerMemberDigest, + PlanIdentity, +} + +struct Fixture { + root: PathBuf, + source: PathBuf, + state: PathBuf, +} + +impl Fixture { + fn new() -> Self { + let suffix = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "flow-receipt-contract-{}-{suffix}", + std::process::id() + )); + fs::create_dir(&root).unwrap(); + let source = root.join("source 🌿 with spaces"); + let state = root.join("local state"); + fs::create_dir(&source).unwrap(); + fs::create_dir(&state).unwrap(); + Self { + root: fs::canonicalize(root).unwrap(), + source: fs::canonicalize(source).unwrap(), + state: fs::canonicalize(state).unwrap(), + } + } + + fn expand(&self, value: &mut Value) { + match value { + Value::String(text) => { + *text = text + .replace("@STATE@", self.state.to_str().unwrap()) + .replace("@SOURCE@", self.source.to_str().unwrap()); + } + Value::Array(items) => items.iter_mut().for_each(|item| self.expand(item)), + Value::Object(items) => items.values_mut().for_each(|item| self.expand(item)), + _ => {} + } + } + + fn materialize(&self, receipt: &mut Value) { + self.expand(receipt); + let run = json!({ + "schema_version": "optiflow.run.v5", + "run_id": receipt["source_run_id"], + "artifact_set_id": receipt["source_set_id"], + "inputs": [self.source], + }); + let plan = json!({ + "schema_version": "optiflow.plan.v5", + "plan_id": receipt["plan_id"], + "source_run_id": receipt["source_run_id"], + "source_artifact_set_id": receipt["source_set_id"], + "safety": {"mutates_files": false, "requires_explicit_apply": true}, + }); + for command in receipt["commands"].as_array_mut().unwrap() { + for artifact in command["artifacts"].as_array_mut().unwrap() { + let document = match artifact["kind"].as_str().unwrap() { + "effective_policy" => json!({"schema": "optiflow.effective-policy.v1"}), + "run" => run.clone(), + "report" => json!({"schema_version": "optiflow.report.v6", "run": run}), + "plan" => plan.clone(), + "artifact_set" => continue, + other => panic!("unexpected base artifact {other}"), + }; + write_artifact(artifact, &document); + } + } + write_markers(receipt); + receipt["plan_sha256"] = receipt["commands"][2]["artifacts"][0]["sha256"].clone(); + } + + fn effect(&self, receipt: &mut Value, effect: &FileEffect) { + let plan_path = artifact_path(&receipt["commands"][2]["artifacts"][0]); + match effect { + FileEffect::TamperPlan => { + let mut bytes = fs::read(&plan_path).unwrap(); + bytes.push(b' '); + fs::write(plan_path, bytes).unwrap(); + } + FileEffect::ArtifactSymlink => self.symlink_artifact(&plan_path), + FileEffect::MarkerSourceSet | FileEffect::MarkerMemberDigest => { + let marker = &mut receipt["commands"][2]["artifacts"][1]; + let mut document: Value = + serde_json::from_slice(&fs::read(artifact_path(marker)).unwrap()).unwrap(); + match effect { + FileEffect::MarkerSourceSet => { + document["source_set_id"] = json!("00000000-0000-4000-8000-000000000099"); + } + FileEffect::MarkerMemberDigest => { + document["members"][0]["digest"]["value"] = json!("f".repeat(64)); + } + _ => unreachable!(), + } + write_artifact(marker, &document); + receipt["commands"][2]["marker_sha256"] = marker["sha256"].clone(); + } + FileEffect::PlanIdentity => { + let artifact = &mut receipt["commands"][2]["artifacts"][0]; + let mut document: Value = + serde_json::from_slice(&fs::read(&plan_path).unwrap()).unwrap(); + document["plan_id"] = json!("00000000-0000-4000-8000-000000000099"); + write_artifact(artifact, &document); + receipt["plan_sha256"] = artifact["sha256"].clone(); + write_markers(receipt); + } + } + } + + fn symlink_artifact(&self, path: &Path) { + let outside = self.source.join("identical outside evidence.json"); + fs::copy(path, &outside).unwrap(); + fs::remove_file(path).unwrap(); + std::os::unix::fs::symlink(outside, path).unwrap(); + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fn artifact_path(artifact: &Value) -> PathBuf { + PathBuf::from(artifact["path"]["value"].as_str().unwrap()) +} + +fn write_artifact(artifact: &mut Value, document: &Value) { + let path = artifact_path(artifact); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let bytes = serde_json::to_vec(document).unwrap(); + fs::write(path, &bytes).unwrap(); + artifact["size_bytes"] = json!(bytes.len()); + artifact["sha256"] = json!(format!("{:x}", Sha256::digest(&bytes))); + artifact["blake3_256"] = json!(blake3::hash(&bytes).to_hex().as_str()); +} + +fn write_markers(receipt: &mut Value) { + let run_id = receipt["source_run_id"].clone(); + let source_set = receipt["source_set_id"].clone(); + for command in receipt["commands"].as_array_mut().unwrap() { + let is_plan = command["command"] == "plan"; + let members = command["artifacts"] + .as_array() + .unwrap() + .iter() + .filter(|artifact| artifact["kind"] != "artifact_set") + .map(|artifact| { + let path = artifact_path(artifact); + json!({ + "kind": artifact["kind"], "schema": artifact["schema"], + "path": {"encoding": "utf8", "value": path.file_name().unwrap().to_str().unwrap()}, + "size_bytes": artifact["size_bytes"], + "digest": {"algorithm": "blake3-256", "value": artifact["blake3_256"]}, + }) + }) + .collect::>(); + let marker = json!({ + "schema": "optiflow.artifact-set.v1", "set_id": command["set_id"], + "set_kind": if is_plan { "plan" } else { "scan" }, + "run_id": run_id, "state": "committed", + "source_set_id": if is_plan { source_set.clone() } else { Value::Null }, + "members": members, + }); + let artifact = command["artifacts"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|artifact| artifact["kind"] == "artifact_set") + .unwrap(); + write_artifact(artifact, &marker); + command["marker_sha256"] = artifact["sha256"].clone(); + } +} + +fn apply_operation(document: &mut Value, operation: &Operation, fixture: &Fixture) { + let (path, replacement) = match operation { + Operation::Replace { path, value } => { + let mut value = value.clone(); + fixture.expand(&mut value); + (path, Some(value)) + } + Operation::Remove { path } => (path, None), + }; + assert!(path.starts_with('/')); + let (parent_path, key) = path.rsplit_once('/').unwrap(); + let key = key.replace("~1", "/").replace("~0", "~"); + let parent = document.pointer_mut(parent_path).unwrap(); + match (parent, replacement) { + (Value::Array(items), Some(value)) => items[key.parse::().unwrap()] = value, + (Value::Array(items), None) => { + items.remove(key.parse::().unwrap()); + } + (Value::Object(items), Some(value)) => { + *items.get_mut(&key).unwrap() = value; + } + (Value::Object(items), None) => { + items.remove(&key).unwrap(); + } + _ => panic!("invalid fixture operation {path}"), + } +} + +#[test] +fn shared_receipt_contract_and_evidence_cases() { + let manifest: Manifest = serde_json::from_str(FIXTURE).unwrap(); + assert_eq!( + manifest.schema_version, + "flow.optiflow-receipt-parity-tests/v1" + ); + assert!(!manifest.description.is_empty()); + let mut identities = BTreeSet::new(); + let mut failures = Vec::new(); + for case in manifest.cases { + assert!(identities.insert(case.id.clone()), "duplicate {}", case.id); + assert!(!case.runtime_valid || case.schema_valid, "{}", case.id); + let fixture = Fixture::new(); + let mut receipt = manifest.base.clone(); + fixture.materialize(&mut receipt); + for operation in &case.operations { + apply_operation(&mut receipt, operation, &fixture); + } + if let Some(effect) = &case.file_effect { + fixture.effect(&mut receipt, effect); + } + let path = fixture.root.join("receipt.json"); + fs::write(&path, serde_json::to_vec(&receipt).unwrap()).unwrap(); + let result = ReadOnlyReceipt::load(&path); + match result { + Ok(receipt) if case.runtime_valid => { + if let Some(expected) = case.needs_recovery { + assert_eq!(receipt.needs_recovery(), expected, "{}", case.id); + } + } + Err(_) if !case.runtime_valid => {} + result => failures.push(format!("{}: unexpected {result:?}", case.id)), + } + } + assert_eq!(failures, Vec::::new(), "{}", failures.join("\n")); +} diff --git a/tools/test_optiflow_receipts.py b/tools/test_optiflow_receipts.py new file mode 100644 index 0000000..2a5a8be --- /dev/null +++ b/tools/test_optiflow_receipts.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check the shared receipt corpus with the independent Draft 2020-12 validator.""" + +import copy +import json +from pathlib import Path +import unittest + +from jsonschema import Draft202012Validator + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "tests/fixtures/optiflow-receipt-parity.json" +SCHEMA = ROOT / "schemas/optiflow-read-only-receipt-v1.schema.json" + + +def apply_operations(document, operations): + """Apply the fixture's bounded existing-key replace/remove operations.""" + for operation in operations: + assert operation["op"] in {"replace", "remove"} + expected_keys = {"op", "path", "value"} if operation["op"] == "replace" else {"op", "path"} + assert set(operation) == expected_keys + assert operation["path"].startswith("/") + parts = [part.replace("~1", "/").replace("~0", "~") for part in operation["path"][1:].split("/")] + parent = document + for part in parts[:-1]: + parent = parent[int(part)] if isinstance(parent, list) else parent[part] + key = int(parts[-1]) if isinstance(parent, list) else parts[-1] + if isinstance(parent, list): + assert 0 <= key < len(parent) + else: + assert key in parent + if operation["op"] == "replace": + parent[key] = copy.deepcopy(operation["value"]) + else: + del parent[key] + return document + + +class OptiflowReceiptSchemaParity(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.fixture = json.loads(FIXTURE.read_text(encoding="utf-8")) + schema = json.loads(SCHEMA.read_text(encoding="utf-8")) + Draft202012Validator.check_schema(schema) + cls.validator = Draft202012Validator(schema) + + def test_fixture_contract_and_positive_status_coverage(self): + self.assertEqual(self.fixture["schema_version"], "flow.optiflow-receipt-parity-tests/v1") + self.assertTrue(self.validator.is_valid(self.fixture["base"])) + identities = [case["id"] for case in self.fixture["cases"]] + self.assertEqual(len(identities), len(set(identities))) + positive_statuses = set() + refused_prefixes = set() + for case in self.fixture["cases"]: + self.assertIsInstance(case["schema_valid"], bool) + self.assertIsInstance(case["runtime_valid"], bool) + self.assertFalse(case["runtime_valid"] and not case["schema_valid"], case["id"]) + value = apply_operations(copy.deepcopy(self.fixture["base"]), case["operations"]) + if case["runtime_valid"]: + positive_statuses.add(value["status"]) + if value["status"] == "refused": + refused_prefixes.add(len(value["commands"])) + self.assertEqual(positive_statuses, {"scanning", "reporting", "planning", "complete", "partial", "refused"}) + self.assertEqual(refused_prefixes, {0, 1, 2, 3}) + + def test_shared_receipt_cases(self): + for case in self.fixture["cases"]: + with self.subTest(case=case["id"]): + value = apply_operations(copy.deepcopy(self.fixture["base"]), case["operations"]) + errors = list(self.validator.iter_errors(value)) + self.assertEqual(not errors, case["schema_valid"], "\n".join(error.message for error in errors)) + + +if __name__ == "__main__": + unittest.main()