diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d8d69ed..5da49394 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -206,6 +206,33 @@ jobs: - name: clippy (wasm32 Edict provider adapter) run: cargo +1.90.0 clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs + build-edict-provider-verifier: + name: Build and check Edict provider verifier + runs-on: ubuntu-24.04 + container: + image: docker.io/library/rust@sha256:3914072ca0c3b8aad871db9169a651ccfce30cf58303e5d6f2db16d1d8a7e58f + options: --platform linux/amd64 + steps: + - uses: actions/checkout@v4 + with: + submodules: false + persist-credentials: false + - uses: dtolnay/rust-toolchain@1.90.0 + with: + components: clippy + targets: wasm32-unknown-unknown + - name: build, audit, and check exact verifier component bytes + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: ${{ github.workspace }} + run: | + cargo xtask provider-verifier-component check \ + --target-dir target/provider-verifier-component \ + --output schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm + - name: clippy (wasm32 Edict provider verifier) + run: cargo +1.90.0 clippy -p echo-edict-provider-verifier --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs + edict-provider-host-v1: name: Edict provider host contract (Rust 1.94) runs-on: ubuntu-24.04 diff --git a/.github/workflows/det-gates.yml b/.github/workflows/det-gates.yml index 3bf04c51..22bbaf93 100644 --- a/.github/workflows/det-gates.yml +++ b/.github/workflows/det-gates.yml @@ -257,6 +257,11 @@ jobs: --output target/lowerer.echo-dpo.component.wasm sha256sum target/lowerer.echo-dpo.component.wasm > "lowerer-hash${CANDIDATE}.txt" cp target/lowerer.echo-dpo.component.wasm "build${CANDIDATE}.lowerer.component.wasm" + cargo xtask provider-verifier-component designated-build \ + --target-dir target/provider-verifier-repro \ + --output target/verifier.echo-dpo.component.wasm + sha256sum target/verifier.echo-dpo.component.wasm > "verifier-hash${CANDIDATE}.txt" + cp target/verifier.echo-dpo.component.wasm "build${CANDIDATE}.verifier.component.wasm" - name: Upload isolated candidate uses: actions/upload-artifact@v4 with: @@ -267,6 +272,8 @@ jobs: build${{ matrix.candidate }}.wasm lowerer-hash${{ matrix.candidate }}.txt build${{ matrix.candidate }}.lowerer.component.wasm + verifier-hash${{ matrix.candidate }}.txt + build${{ matrix.candidate }}.verifier.component.wasm build-repro: name: G4 independent build comparison @@ -303,6 +310,10 @@ jobs: cp candidate2/lowerer-hash2.txt lowerer-hash2.txt cp candidate1/build1.lowerer.component.wasm build1.lowerer.component.wasm cp candidate2/build2.lowerer.component.wasm build2.lowerer.component.wasm + cp candidate1/verifier-hash1.txt verifier-hash1.txt + cp candidate2/verifier-hash2.txt verifier-hash2.txt + cp candidate1/build1.verifier.component.wasm build1.verifier.component.wasm + cp candidate2/build2.verifier.component.wasm build2.verifier.component.wasm - name: Compare, promote, and check exact bytes run: | diff hash1.txt hash2.txt || (echo "Reproducibility failure: Hashes differ!" && exit 1) @@ -317,8 +328,19 @@ jobs: cmp build1.lowerer.component.wasm target/lowerer.echo-dpo.promoted.component.wasm cmp build1.lowerer.component.wasm schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm cmp build2.lowerer.component.wasm schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm + diff verifier-hash1.txt verifier-hash2.txt || (echo "Verifier reproducibility failure: Hashes differ!" && exit 1) + cmp build1.verifier.component.wasm build2.verifier.component.wasm + cargo xtask provider-verifier-component promote \ + --candidate-a build1.verifier.component.wasm \ + --candidate-b build2.verifier.component.wasm \ + --output target/verifier.echo-dpo.promoted.component.wasm \ + --write + cmp build1.verifier.component.wasm target/verifier.echo-dpo.promoted.component.wasm + cmp build1.verifier.component.wasm schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm + cmp build2.verifier.component.wasm schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm echo "Hashes match: $(cat hash1.txt)" echo "Lowerer hashes match: $(cat lowerer-hash1.txt)" + echo "Verifier hashes match: $(cat verifier-hash1.txt)" - name: Upload build artifacts if: always() uses: actions/upload-artifact@v4 @@ -334,6 +356,10 @@ jobs: lowerer-hash2.txt build1.lowerer.component.wasm build2.lowerer.component.wasm + verifier-hash1.txt + verifier-hash2.txt + build1.verifier.component.wasm + build2.verifier.component.wasm validate-evidence: name: Evidence artifact presence @@ -416,6 +442,10 @@ jobs: gathered-artifacts/build-repro-artifacts/lowerer-hash2.txt gathered-artifacts/build-repro-artifacts/build1.lowerer.component.wasm gathered-artifacts/build-repro-artifacts/build2.lowerer.component.wasm + gathered-artifacts/build-repro-artifacts/verifier-hash1.txt + gathered-artifacts/build-repro-artifacts/verifier-hash2.txt + gathered-artifacts/build-repro-artifacts/build1.verifier.component.wasm + gathered-artifacts/build-repro-artifacts/build2.verifier.component.wasm ) fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 02230015..03dae4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,28 @@ witness binding them to the checked generated corpus. Its full package gate follows publication of `echo-edict-canonical 0.1.0`. These artifacts describe and translate provider semantics; they confer no Echo runtime authority. +- Echo now provides the exact `edict:target-provider/verifier@1.0.0` + Component Model implementation for the checked provider closure. The pure + verifier independently compares explicit digest-bound Core and Target IR + artifacts under the exact target profile and ordered semantic inputs. It + emits a canonical accepted report for the reviewed relation, admits a + well-formed intrinsic disagreement as a rejected report with an error + diagnostic and host-authored output manifest, and preserves an unsupported + output-role overclaim as a typed provider refusal with neither response nor + manifest. Its bounded native preflight validates complete known expression, + predicate, input-constraint, require-failure, and Core-value shapes before + separating malformed artifacts from well-formed unsupported semantics, and + one admitted diagnostic-ABI identity now binds both the target profile and + every emitted report. The 188,736-byte checked component has SHA-256 + `e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b` + and reproduces byte-for-byte across independently provisioned designated + `linux/amd64` builders. The isolated pinned Edict host preflights the exact + request artifacts and declared output schema, invokes that checked component, + then schema-admits each returned accepted or rejected report and authors its + output manifest. It replays accepted, rejected, and refused completed outcomes + identically in independent fresh stores and separate host processes. These + witnesses prove provider verification and host replay only; they do not + install, authorize, execute, or observe an operation in Echo. - `echo-edict-canonical` now owns the shared pure implementation of Edict's canonical CBOR and domain-framed digest contracts as a publishable `0.1.0` leaf. `echo-wesley-gen` retains its existing compatibility surface through a diff --git a/Cargo.lock b/Cargo.lock index 89e06c00..87729c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,6 +666,16 @@ dependencies = [ "wit-bindgen 0.58.0", ] +[[package]] +name = "echo-edict-provider-verifier" +version = "0.1.0" +dependencies = [ + "echo-edict-canonical", + "hex", + "sha2", + "wit-bindgen 0.58.0", +] + [[package]] name = "echo-file-aperture" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 59381ddd..a763c35c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/echo-registry-api", "crates/echo-edict-canonical", "crates/echo-edict-provider-lowerer", + "crates/echo-edict-provider-verifier", "crates/echo-wesley-gen", "crates/echo-dry-tests", "crates/echo-cas", diff --git a/crates/echo-edict-provider-verifier/Cargo.toml b/crates/echo-edict-provider-verifier/Cargo.toml new file mode 100644 index 00000000..b9a88d05 --- /dev/null +++ b/crates/echo-edict-provider-verifier/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-edict-provider-verifier" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +description = "Pure Echo semantic verifier for Edict's frozen Component Model boundary." +license = "Apache-2.0" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "edict", "wasm", "verification", "compiler"] +categories = ["compilers", "wasm", "development-tools"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +echo-edict-canonical = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wit-bindgen = { version = "=0.58.0", default-features = false, features = ["macros", "realloc"] } + +[dev-dependencies] +hex = "0.4" +sha2 = "0.10" + +[lints] +workspace = true diff --git a/crates/echo-edict-provider-verifier/README.md b/crates/echo-edict-provider-verifier/README.md new file mode 100644 index 00000000..5a58ccd0 --- /dev/null +++ b/crates/echo-edict-provider-verifier/README.md @@ -0,0 +1,56 @@ + + + +# Echo Edict Provider Verifier + +This crate owns Echo's pure semantic decision for Edict's frozen verifier +boundary. It compares explicit, digest-bound Edict Core and Target IR artifacts +under the checked Echo provider closure. It performs no discovery or I/O and +grants no Echo runtime authority. + +The first native slice is intentionally independent from the provider lowerer. +A supported but semantically false Target IR produces a rejected verifier +report; malformed input in the selected native closure and unsupported source +semantics produce typed provider refusals. Complete structural admission for +every CDDL alternative remains the Edict host's owning-schema check before the +component runs; decoding this native model's result is never admission. Before +classifying a known but unsupported expression as semantic disagreement, the +native verifier checks its complete expression, predicate, input-constraint, +require-failure, and Core-value shape under the fixed recursion bound. The +target profile's diagnostic ABI and every emitted report consume one shared +admitted identity. +The `wasm32` guest adapter vendors Edict's exact frozen +`edict:target-provider/verifier@1.0.0` WIT world and performs only exhaustive +transport-to-model conversion. Its reproducibly built 188,736-byte checked +component has SHA-256 +`e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b`. +Component identity and admitted host replay remain separate propositions: the +pinned Edict host preflights the request artifacts and declared output schema, +invokes the checked component, then admits and manifests each returned accepted +or rejected report. It preserves an unsupported output-role overclaim as a typed +refusal without a response or manifest and replays all three completed outcome +classes identically in independent fresh stores and separate host processes. + +For this first one-operation closure, the exact checked target profile, exact +lowerability facts, and exact `echo.dpo@1.replace` intrinsic jointly bind the +`precommit-atomic` guard posture and `echo.dpo.footprint/v1` algebra identity. The +current Target IR has no independent footprint field and its requirements list +is empty, so this crate does not claim a general guard-order or footprint- +expression proof. The crate embeds the exact generated type, intrinsic, +footprint, cost, operation-profile, obstruction, lawpack-adapter, and verifier +resources. Before comparing Core with Target IR, it reproduces their +domain-framed identities, resolves the profile and lawpack references, and +checks the complete reviewed semantic crossing. Raw byte identities remain a +separate pinned proposition, and workspace validation must still prove these +package-local copies equal the CDDL-admitted checked corpus. + +A verifier report's proposition is deliberately narrow: the fixed verifier +accepted or rejected the exact Target IR reference named by that report. The +report alone does not identify the Core, target profile, or semantic closure. +For each successful invocation, the pinned Edict host's output manifest binds +the exact Core, target profile, Target IR, ordered semantic inputs, requested +output, and admitted report digest. That per-invocation envelope is distinct +from the digest-locked package assembled by the next campaign goalpost, which +binds the static inputs and verifier component together. Neither proposition +has an Echo causal site or witnessed hologram, and neither grants Echo runtime +installation, execution, or consequence authority. diff --git a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor new file mode 100644 index 00000000..807c2186 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor @@ -0,0 +1 @@ +fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor new file mode 100644 index 00000000..a3a7b681 Binary files /dev/null and b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor differ diff --git a/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor b/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor new file mode 100644 index 00000000..bb7a2d6e --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor @@ -0,0 +1 @@ +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor new file mode 100644 index 00000000..354071e7 Binary files /dev/null and b/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-verifier/resources/resource.lawpack-exports.cbor b/crates/echo-edict-provider-verifier/resources/resource.lawpack-exports.cbor new file mode 100644 index 00000000..a0887cb0 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.lawpack-exports.cbor @@ -0,0 +1 @@ +etypesjcoordinateha.b@1.Idjdefinitionx!StringgeffectsiinputTypeha.b@1.Idjcoordinatentarget.replacejoutputTypema.b@1.ReceiptlguardSupportncostObligationstarget.replace.costneffectFailureshrejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappableneffectKindHintgreplacenexecutionClassgruntimentypeParameterssfootprintObligationxtarget.replace.footprinticonstantslobstructionsjcoordinatetdomain.WriteRejectedmpayloadSchemaxdomain.WriteRejected.PayloadnauthorityClassndomainMappablempureFunctionsqoperationProfiles \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.lawpack-target-adapter.cbor b/crates/echo-edict-provider-verifier/resources/resource.lawpack-target-adapter.cbor new file mode 100644 index 00000000..5a57d65b --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.lawpack-target-adapter.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionx-echo.edict-provider.lawpack-target-adapter/v1mtargetProfilejecho.dpo@1ntargetIrDomainoecho.span-ir/v1ueffectImplementationsntarget.replacedkindfnativejcapabilityrecho.dpo@1.replacejwriteClassgreplace \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.lawpack-verifier.cbor b/crates/echo-edict-provider-verifier/resources/resource.lawpack-verifier.cbor new file mode 100644 index 00000000..a6d10bd7 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.lawpack-verifier.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionx'echo.edict-provider.lawpack-verifier/v1uoperationObstructionsga.b@1.tfeffectntarget.replaceofailureMappingshrejectedtdomain.WriteRejected \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-cost-algebra.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-cost-algebra.cbor new file mode 100644 index 00000000..2242fed3 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-cost-algebra.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionpecho.dpo.cost/v1lcapabilitiesrecho.dpo@1.replacefeffectntarget.replacelcostTemplatestarget.replace.costrsemanticObligationstarget.replace.cost \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-footprint-algebra.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-footprint-algebra.cbor new file mode 100644 index 00000000..3118bbd0 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-footprint-algebra.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionuecho.dpo.footprint/v1lcapabilitiesrecho.dpo@1.replacefeffectntarget.replacejwriteClassgreplaceqfootprintTemplatextarget.replace.footprintrsemanticObligationxtarget.replace.footprint \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor new file mode 100644 index 00000000..3250617a --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor @@ -0,0 +1 @@ +japiVersionx"edict.target-profile.intrinsics/v1jintrinsicsrecho.dpo@1.replacejeffectKindgreplacejreturnTypema.b@1.ReceiptjwriteClassgreplacelcostTemplatestarget.replace.costlguardSupportmargumentTypesha.b@1.IdneffectFailureshrejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappablenintrinsicClassfeffectntypeParametersqfootprintTemplatextarget.replace.footprintxcanParticipateInAtomicGuard \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-ir.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-ir.cbor new file mode 100644 index 00000000..47cd6d6a --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-ir.cbor @@ -0,0 +1 @@ +eclasskdeclarativefdomainoecho.span-ir/v1japiVersionoecho.span-ir/v1lcapabilitiesrecho.dpo@1.replacemtargetProfilejecho.dpo@1 \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-obstruction-taxonomy.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-obstruction-taxonomy.cbor new file mode 100644 index 00000000..a15d8a30 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-obstruction-taxonomy.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionxecho.dpo.obstructions/v1neffectFailureswtarget.replace.rejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappablerdomainObstructionstdomain.WriteRejectedmpayloadSchemaxdomain.WriteRejected.PayloadnauthorityClassndomainMappable \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-operation-profiles.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-operation-profiles.cbor new file mode 100644 index 00000000..f4445c6a --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-operation-profiles.cbor @@ -0,0 +1 @@ +hprofilesxcontinuum.profile.write/v1mopticTemplateiopticKindsaffectReintegrationlboundaryKindfaffectmsupportPolicyx&continuum.support.carry-or-obstruct/v1olossDispositionx#continuum.support.reject-on-loss/v1sapertureRequirementcrefxtarget.replace.footprintdkindxabstractFootprintObligationoeffectPredicatex'echo.dpo.operation-mode.replace-only/v1japiVersionx*edict.target-profile.operation-profiles/v1 \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/resource.target-verifier-contract.cbor b/crates/echo-edict-provider-verifier/resources/resource.target-verifier-contract.cbor new file mode 100644 index 00000000..6fd4f838 --- /dev/null +++ b/crates/echo-edict-provider-verifier/resources/resource.target-verifier-contract.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersiontecho.dpo.verifier/v1lcapabilitiesrecho.dpo@1.replacemtargetProfilejecho.dpo@1nopticContractsxcontinuum.profile.write/v1mreplace-pointntargetIrDomainoecho.span-ir/v1qoperationProfilesxcontinuum.profile.write/v1 \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor new file mode 100644 index 00000000..b3ca7861 Binary files /dev/null and b/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-verifier/src/component.rs b/crates/echo-edict-provider-verifier/src/component.rs new file mode 100644 index 00000000..b62024ee --- /dev/null +++ b/crates/echo-edict-provider-verifier/src/component.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! `wasm32` guest adapter for the frozen Edict target-provider verifier world. + +// The generated canonical-ABI trampoline necessarily contains unsafe exports; +// all authored conversion and verification code remains safe Rust. +#![allow(unsafe_code)] + +wit_bindgen::generate!({ + path: "wit", + world: "verifier", +}); + +use edict::target_provider::protocol as wit; + +struct Component; + +impl Guest for Component { + fn verify(request: wit::VerificationRequestV1) -> wit::VerificationResultV1 { + from_model_result(super::verify(into_model_request(request))) + } +} + +fn into_model_request(request: wit::VerificationRequestV1) -> super::VerificationRequestV1 { + super::VerificationRequestV1 { + protocol_version: super::ProtocolVersionV1 { + major: request.protocol_version.major, + minor: request.protocol_version.minor, + patch: request.protocol_version.patch, + }, + core: into_model_bound_artifact(request.core), + target_profile: into_model_bound_artifact(request.target_profile), + target_ir: into_model_bound_artifact(request.target_ir), + semantic_inputs: request + .semantic_inputs + .into_iter() + .map(into_model_semantic_input) + .collect(), + requested_outputs: request + .requested_outputs + .into_iter() + .map(into_model_output_request) + .collect(), + limits: super::ResponseLimitsV1 { + max_output_count: request.limits.max_output_count, + max_diagnostic_count: request.limits.max_diagnostic_count, + max_total_response_bytes: request.limits.max_total_response_bytes, + }, + } +} + +fn into_model_bound_artifact(artifact: wit::BoundArtifact) -> super::BoundArtifact { + super::BoundArtifact { + reference: super::ResourceRef { + coordinate: artifact.reference.coordinate, + digest: super::Digest { + algorithm: match artifact.reference.digest.algorithm { + wit::DigestAlgorithm::Sha256 => super::DigestAlgorithm::Sha256, + }, + bytes: artifact.reference.digest.bytes, + }, + }, + artifact: super::Artifact { + domain: artifact.artifact.domain, + bytes: artifact.artifact.bytes, + }, + } +} + +fn into_model_semantic_input(input: wit::SemanticInput) -> super::SemanticInput { + super::SemanticInput { + role: input.role, + kind: match input.kind { + wit::SemanticInputKind::Lawpack => super::SemanticInputKind::Lawpack, + wit::SemanticInputKind::AuthorityFacts => super::SemanticInputKind::AuthorityFacts, + wit::SemanticInputKind::LowerabilityFacts => { + super::SemanticInputKind::LowerabilityFacts + } + wit::SemanticInputKind::Auxiliary(label) => super::SemanticInputKind::Auxiliary(label), + }, + artifact: into_model_bound_artifact(input.artifact), + } +} + +fn into_model_output_request( + request: wit::VerificationOutputRequest, +) -> super::VerificationOutputRequest { + super::VerificationOutputRequest { + role: request.role, + kind: into_model_output_kind(request.kind), + domain: request.domain, + } +} + +const fn into_model_output_kind( + kind: wit::VerificationOutputKind, +) -> super::VerificationOutputKind { + match kind { + wit::VerificationOutputKind::VerifierReport => { + super::VerificationOutputKind::VerifierReport + } + } +} + +fn from_model_result(result: super::VerificationResultV1) -> wit::VerificationResultV1 { + result.map(from_model_success).map_err(from_model_refusal) +} + +fn from_model_success(success: super::VerificationSuccessV1) -> wit::VerificationSuccessV1 { + wit::VerificationSuccessV1 { + outputs: success.outputs.into_iter().map(from_model_output).collect(), + diagnostics: success + .diagnostics + .into_iter() + .map(from_model_diagnostic) + .collect(), + } +} + +fn from_model_output(output: super::VerificationOutputArtifact) -> wit::VerificationOutputArtifact { + wit::VerificationOutputArtifact { + role: output.role, + kind: from_model_output_kind(output.kind), + artifact: wit::Artifact { + domain: output.artifact.domain, + bytes: output.artifact.bytes, + }, + logical_path: output.logical_path, + } +} + +const fn from_model_output_kind( + kind: super::VerificationOutputKind, +) -> wit::VerificationOutputKind { + match kind { + super::VerificationOutputKind::VerifierReport => { + wit::VerificationOutputKind::VerifierReport + } + } +} + +fn from_model_refusal(refusal: super::ProviderRefusalV1) -> wit::ProviderRefusalV1 { + wit::ProviderRefusalV1 { + kind: match refusal.kind { + super::ProviderRefusalKind::UnsupportedCoreAbi => { + wit::ProviderRefusalKind::UnsupportedCoreAbi + } + super::ProviderRefusalKind::UnsupportedTargetProfile => { + wit::ProviderRefusalKind::UnsupportedTargetProfile + } + super::ProviderRefusalKind::UnsupportedSemantics => { + wit::ProviderRefusalKind::UnsupportedSemantics + } + super::ProviderRefusalKind::UnsupportedOutputRole => { + wit::ProviderRefusalKind::UnsupportedOutputRole + } + super::ProviderRefusalKind::InvalidSemanticArtifact => { + wit::ProviderRefusalKind::InvalidSemanticArtifact + } + }, + subject: refusal.subject, + diagnostics: refusal + .diagnostics + .into_iter() + .map(from_model_diagnostic) + .collect(), + } +} + +fn from_model_diagnostic(diagnostic: super::Diagnostic) -> wit::Diagnostic { + wit::Diagnostic { + code: diagnostic.code, + severity: match diagnostic.severity { + super::DiagnosticSeverity::Error => wit::DiagnosticSeverity::Error, + super::DiagnosticSeverity::Warning => wit::DiagnosticSeverity::Warning, + super::DiagnosticSeverity::Info => wit::DiagnosticSeverity::Info, + }, + message: diagnostic.message, + repair: diagnostic.repair, + } +} + +export!(Component); diff --git a/crates/echo-edict-provider-verifier/src/lib.rs b/crates/echo-edict-provider-verifier/src/lib.rs new file mode 100644 index 00000000..754ba3ce --- /dev/null +++ b/crates/echo-edict-provider-verifier/src/lib.rs @@ -0,0 +1,1862 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure Echo semantic verification for Edict's frozen provider boundary. +//! +//! This crate compares only explicit, digest-bound canonical artifacts. It +//! performs no discovery or I/O and grants no Echo runtime authority. +//! Reports bind only the exact Target IR reference they name. The Edict host +//! manifest binds each invocation's Core, profile, semantic closure, and report; +//! the provider package separately binds the static closure and component. + +#![deny(unsafe_code)] + +#[cfg(target_arch = "wasm32")] +mod component; +mod semantic_resources; + +use std::fmt::Write as _; + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, +}; + +const PROVIDER_ABI: ProtocolVersionV1 = ProtocolVersionV1 { + major: 1, + minor: 0, + patch: 0, +}; +const CORE_DOMAIN: &str = "edict.core.module/v1"; +const CORE_ABI: &str = "edict.core/v1"; +const CORE_COORDINATE: &str = "a.b@1"; +const TARGET_PROFILE_DOMAIN: &str = "edict.target-profile/v1"; +const TARGET_PROFILE_COORDINATE: &str = "echo.dpo@1"; +const LAWPACK_DOMAIN: &str = "edict.lawpack/v1"; +const AUTHORITY_DOMAIN: &str = "edict.authority-facts/v1"; +const LOWERABILITY_DOMAIN: &str = "edict.lowering-requirements/v1"; +const LOWERABILITY_COORDINATE: &str = "echo.dpo-lowerability@1"; +const TARGET_IR_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const INNER_TARGET_IR_DOMAIN: &str = "echo.span-ir/v1"; +const REPORT_DOMAIN: &str = "echo.verifier-report/v1"; +const REPORT_ROLE: &str = "verifier-report.echo-dpo"; +const OPERATION_COORDINATE: &str = "a.b@1.t"; +const OPERATION_INPUT_TYPE: &str = "a.b@1.Input"; +const OPERATION_OUTPUT_TYPE: &str = "a.b@1.Output"; +const OPERATION_RECEIPT_TYPE: &str = "a.b@1.Receipt"; +const INPUT_ID_TYPE: &str = "a.b@1.Input.id"; +const OUTPUT_ID_TYPE: &str = "a.b@1.Output.id"; +const RECEIPT_ID_TYPE: &str = "a.b@1.Receipt.id"; +const OPERATION_PROFILE: &str = "continuum.profile.write/v1"; +const SEMANTIC_EFFECT: &str = "target.replace"; +const TARGET_INTRINSIC: &str = "echo.dpo@1.replace"; +const FAILURE_COORDINATE: &str = "rejected"; +const FAILURE_PAYLOAD_TYPE: &str = "target.replace.rejected"; +const DOMAIN_OBSTRUCTION: &str = "domain.WriteRejected"; +const MAX_EXPRESSION_DEPTH: usize = 64; + +#[derive(Clone, Copy)] +struct PinnedResourceIdentity { + coordinate: &'static str, + framed_sha256: [u8; 32], +} + +const DIAGNOSTIC_ABI: PinnedResourceIdentity = PinnedResourceIdentity { + coordinate: "edict.diagnostics/v1", + framed_sha256: [ + 0x28, 0xfd, 0x72, 0xa9, 0x82, 0x23, 0x15, 0x39, 0x82, 0xca, 0x08, 0x4c, 0x29, 0xdb, 0xb1, + 0xb2, 0xd4, 0x30, 0x62, 0x39, 0x67, 0xab, 0x3b, 0x6d, 0xb9, 0xd7, 0xfe, 0xe6, 0x68, 0xe6, + 0x14, 0xb9, + ], +}; + +const TARGET_PROFILE_BYTES: &[u8] = include_bytes!("../resources/target-profile.echo-dpo.cbor"); +const LAWPACK_BYTES: &[u8] = include_bytes!("../resources/lawpack.echo-dpo.cbor"); +const TARGET_AUTHORITY_BYTES: &[u8] = include_bytes!("../resources/authority-facts.echo-dpo.cbor"); +const LAWPACK_AUTHORITY_BYTES: &[u8] = + include_bytes!("../resources/authority-facts.echo-lawpack.cbor"); + +/// Semantic version carried by every invocation of the frozen provider ABI. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProtocolVersionV1 { + /// Major protocol version. + pub major: u32, + /// Minor protocol version. + pub minor: u32, + /// Patch protocol version. + pub patch: u32, +} + +/// Digest algorithms admitted by the provider transport. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DigestAlgorithm { + /// SHA-256. + Sha256, +} + +/// Typed digest bytes carried by a resource reference. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Digest { + /// Digest algorithm. + pub algorithm: DigestAlgorithm, + /// Raw digest bytes. + pub bytes: Vec, +} + +/// Digest-bound semantic resource coordinate. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResourceRef { + /// Stable semantic coordinate. + pub coordinate: String, + /// Host-verified digest. + pub digest: Digest, +} + +/// Opaque canonical artifact transported with an explicit owning domain. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Artifact { + /// Owning artifact domain. + pub domain: String, + /// Exact canonical bytes. + pub bytes: Vec, +} + +/// Artifact bound to its semantic coordinate and digest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BoundArtifact { + /// Digest-bound resource reference. + pub reference: ResourceRef, + /// Exact artifact domain and bytes. + pub artifact: Artifact, +} + +/// Structural role of one semantic-closure input. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SemanticInputKind { + /// Lawpack semantics. + Lawpack, + /// Source-partitioned authority facts. + AuthorityFacts, + /// Explicit lowerability requirements and facts. + LowerabilityFacts, + /// A separately constrained auxiliary semantic input. + Auxiliary(String), +} + +/// One role-constrained semantic input. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SemanticInput { + /// Unique invocation role. + pub role: String, + /// Structural semantic kind. + pub kind: SemanticInputKind, + /// Digest-bound artifact. + pub artifact: BoundArtifact, +} + +/// Output authority available to the verifier. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VerificationOutputKind { + /// Target-owned verifier report. + VerifierReport, +} + +/// One requested verifier output role. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerificationOutputRequest { + /// Unique output role. + pub role: String, + /// Structurally permitted output kind. + pub kind: VerificationOutputKind, + /// Required owning domain. + pub domain: String, +} + +/// One provider-authored verifier output without a provider-authored digest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerificationOutputArtifact { + /// Requested output role. + pub role: String, + /// Requested output kind. + pub kind: VerificationOutputKind, + /// Canonical output artifact. + pub artifact: Artifact, + /// Optional logical package-relative path. + pub logical_path: Option, +} + +/// Host-enforced response bounds carried through the WIT request. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ResponseLimitsV1 { + /// Maximum number of successful outputs. + pub max_output_count: u32, + /// Maximum number of diagnostics. + pub max_diagnostic_count: u32, + /// Maximum provider-authored response bytes. + pub max_total_response_bytes: u64, +} + +/// Diagnostic severity declared by the provider ABI. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DiagnosticSeverity { + /// Error diagnostic. + Error, + /// Warning diagnostic. + Warning, + /// Informational diagnostic. + Info, +} + +/// Stable bounded diagnostic attached to success or refusal. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Diagnostic { + /// Stable machine-readable code. + pub code: String, + /// Severity. + pub severity: DiagnosticSeverity, + /// Deterministic human-readable explanation. + pub message: String, + /// Optional deterministic repair guidance. + pub repair: Option, +} + +/// Target-owned refusal categories frozen by the provider ABI. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderRefusalKind { + /// The request or Core ABI is unsupported. + UnsupportedCoreAbi, + /// The selected target profile is unsupported. + UnsupportedTargetProfile, + /// The supplied semantics cannot be evaluated faithfully. + UnsupportedSemantics, + /// The requested output role, kind, or domain is unsupported. + UnsupportedOutputRole, + /// A semantic artifact is malformed, noncanonical, or incorrectly bound. + InvalidSemanticArtifact, +} + +/// Typed target-owned refusal. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderRefusalV1 { + /// Stable refusal kind. + pub kind: ProviderRefusalKind, + /// Optional stable subject of the refusal. + pub subject: Option, + /// Deterministically ordered diagnostics. + pub diagnostics: Vec, +} + +/// Explicit pure verification request mirroring the frozen WIT record. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerificationRequestV1 { + /// Requested provider protocol version. + pub protocol_version: ProtocolVersionV1, + /// Exact canonical Edict Core artifact. + pub core: BoundArtifact, + /// Exact canonical target-profile artifact. + pub target_profile: BoundArtifact, + /// Exact canonical Target IR artifact under judgment. + pub target_ir: BoundArtifact, + /// Complete explicit semantic closure. + pub semantic_inputs: Vec, + /// Exact requested verifier-report roles. + pub requested_outputs: Vec, + /// Host-owned response limits, which cannot alter canonical provider output. + pub limits: ResponseLimitsV1, +} + +/// Successful semantic adjudication response. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerificationSuccessV1 { + /// Exactly the requested and supported outputs. + pub outputs: Vec, + /// Deterministically ordered semantic diagnostics. + pub diagnostics: Vec, +} + +/// Pure verifier result using the frozen provider refusal vocabulary. +pub type VerificationResultV1 = Result; + +/// Verify the first supported Echo Core-to-Target-IR semantic relation. +/// +/// A supported but false Target IR returns a successful response containing a +/// rejected report. A refusal means the component could not adjudicate the +/// relation under its declared closure. +pub fn verify(request: VerificationRequestV1) -> VerificationResultV1 { + if request.protocol_version != PROVIDER_ABI { + return Err(refusal( + ProviderRefusalKind::UnsupportedCoreAbi, + "protocol-version", + "echo.verifier.unsupported-protocol", + "the verifier accepts only target-provider protocol 1.0.0", + )); + } + let _semantic_closure = + semantic_resources::admit_packaged_semantic_resources().map_err(|error| { + match error.kind() { + semantic_resources::SemanticResourceErrorKind::InvalidArtifact + | semantic_resources::SemanticResourceErrorKind::ReferenceMismatch => { + invalid_artifact( + error.subject(), + "the packaged semantic resource closure is malformed or incorrectly bound", + ) + } + semantic_resources::SemanticResourceErrorKind::SemanticMismatch => refusal( + ProviderRefusalKind::UnsupportedSemantics, + error.subject(), + "echo.verifier.semantic-resource-mismatch", + "the packaged semantic resource closure contains a contradictory crossing", + ), + } + })?; + validate_target_profile(&request.target_profile)?; + validate_semantic_closure(&request.semantic_inputs)?; + validate_requested_outputs(&request.requested_outputs)?; + + let core = validate_core(&request.core)?; + let target_ir = validate_target_ir_artifact(&request.target_ir)?; + let expected = expected_target_ir(&core, &request.target_profile.reference.digest) + .map_err(|()| unsupported_semantics(OPERATION_COORDINATE))?; + let failure = relation_failure(&target_ir, &expected); + let (outcome, diagnostics) = match failure { + None => ("accepted", Vec::new()), + Some(failure) => ( + "rejected", + vec![Diagnostic { + code: failure.code.to_owned(), + severity: DiagnosticSeverity::Error, + message: failure.message.to_owned(), + repair: None, + }], + ), + }; + let report = build_report(&request.target_ir.reference, outcome).map_err(|()| { + invalid_artifact( + REPORT_ROLE, + "the canonical verifier report could not be constructed", + ) + })?; + Ok(VerificationSuccessV1 { + outputs: vec![VerificationOutputArtifact { + role: REPORT_ROLE.to_owned(), + kind: VerificationOutputKind::VerifierReport, + artifact: Artifact { + domain: REPORT_DOMAIN.to_owned(), + bytes: report, + }, + logical_path: None, + }], + diagnostics, + }) +} + +fn validate_target_profile(profile: &BoundArtifact) -> Result<(), ProviderRefusalV1> { + validate_binding(profile).map_err(|()| { + invalid_artifact( + "target-profile.echo-dpo", + "target-profile binding is invalid", + ) + })?; + if profile.reference.coordinate != TARGET_PROFILE_COORDINATE + || profile.artifact.domain != TARGET_PROFILE_DOMAIN + || profile.artifact.bytes != TARGET_PROFILE_BYTES + { + return Err(refusal( + ProviderRefusalKind::UnsupportedTargetProfile, + &profile.reference.coordinate, + "echo.verifier.unsupported-target-profile", + "the verifier accepts only the exact checked Echo DPO target profile", + )); + } + Ok(()) +} + +fn validate_semantic_closure(inputs: &[SemanticInput]) -> Result<(), ProviderRefusalV1> { + const EXPECTED: [(&str, SemanticInputKind, &str, &str, &[u8]); 3] = [ + ( + "authority-facts.echo-dpo", + SemanticInputKind::AuthorityFacts, + "echo.dpo-authority-facts@1", + AUTHORITY_DOMAIN, + TARGET_AUTHORITY_BYTES, + ), + ( + "authority-facts.echo-lawpack", + SemanticInputKind::AuthorityFacts, + "echo.dpo-lawpack-authority-facts@1", + AUTHORITY_DOMAIN, + LAWPACK_AUTHORITY_BYTES, + ), + ( + "lawpack.echo-dpo", + SemanticInputKind::Lawpack, + "echo.dpo-lawpack@1", + LAWPACK_DOMAIN, + LAWPACK_BYTES, + ), + ]; + if inputs.len() != 4 { + return Err(unsupported_semantics("semantic-inputs")); + } + for ((role, kind, coordinate, domain, bytes), input) in EXPECTED.into_iter().zip(inputs) { + if input.role != role || input.kind != kind { + return Err(unsupported_semantics(&input.role)); + } + validate_binding(&input.artifact) + .map_err(|()| invalid_artifact(&input.role, "artifact binding is invalid"))?; + if input.artifact.reference.coordinate != coordinate + || input.artifact.artifact.domain != domain + || input.artifact.artifact.bytes != bytes + { + return Err(invalid_artifact( + &input.role, + "artifact does not equal the checked provider closure", + )); + } + } + let lowerability = &inputs[3]; + if lowerability.role != "lowerability.echo-dpo" + || lowerability.kind != SemanticInputKind::LowerabilityFacts + || lowerability.artifact.reference.coordinate != LOWERABILITY_COORDINATE + || lowerability.artifact.artifact.domain != LOWERABILITY_DOMAIN + { + return Err(unsupported_semantics(&lowerability.role)); + } + let value = validate_binding(&lowerability.artifact) + .map_err(|()| invalid_artifact(&lowerability.role, "artifact binding is invalid"))?; + if value != expected_lowerability().map_err(|()| unsupported_semantics(&lowerability.role))? { + return Err(unsupported_semantics(&lowerability.role)); + } + Ok(()) +} + +fn validate_requested_outputs( + requests: &[VerificationOutputRequest], +) -> Result<(), ProviderRefusalV1> { + let Some((request, remaining)) = requests.split_first() else { + return Err(refusal( + ProviderRefusalKind::UnsupportedOutputRole, + "requested-outputs", + "echo.verifier.unsupported-output-role", + "the first verifier requires its verifier-report output", + )); + }; + if request.role != REPORT_ROLE + || request.kind != VerificationOutputKind::VerifierReport + || request.domain != REPORT_DOMAIN + { + return Err(refusal( + ProviderRefusalKind::UnsupportedOutputRole, + &request.role, + "echo.verifier.unsupported-output-role", + "the first verifier serves only verifier-report.echo-dpo", + )); + } + if let Some(unsupported) = remaining.first() { + return Err(refusal( + ProviderRefusalKind::UnsupportedOutputRole, + &unsupported.role, + "echo.verifier.unsupported-output-role", + "the first verifier serves exactly one verifier-report.echo-dpo output", + )); + } + Ok(()) +} + +fn validate_core(core: &BoundArtifact) -> Result { + if core.artifact.domain != CORE_DOMAIN { + return Err(invalid_artifact( + "core.echo-provider", + "Core domain is invalid", + )); + } + let value = validate_binding(core) + .map_err(|()| invalid_artifact("core.echo-provider", "Core binding is invalid"))?; + let api = text_field(&value, "apiVersion") + .ok_or_else(|| invalid_artifact("core.echo-provider", "Core apiVersion is absent"))?; + if api != CORE_ABI { + return Err(refusal( + ProviderRefusalKind::UnsupportedCoreAbi, + api, + "echo.verifier.unsupported-core-abi", + "the verifier accepts only edict.core/v1", + )); + } + if !has_exact_fields( + &value, + &[ + "apiVersion", + "coordinate", + "imports", + "types", + "intents", + "requiredCoreCapabilities", + ], + ) { + return Err(unsupported_semantics("core.echo-provider")); + } + let coordinate = text_field(&value, "coordinate") + .filter(|coordinate| !coordinate.is_empty()) + .ok_or_else(|| invalid_artifact("core.echo-provider", "Core coordinate is invalid"))?; + if coordinate != core.reference.coordinate { + return Err(invalid_artifact( + "core.echo-provider", + "Core coordinate does not equal its bound reference", + )); + } + if coordinate != CORE_COORDINATE { + return Err(unsupported_semantics(coordinate)); + } + if !matches!(array_field(&value, "imports"), Some(values) if values.is_empty()) + || map_field(&value, "types") != expected_core_types().ok().as_ref() + || !matches!(array_field(&value, "requiredCoreCapabilities"), Some(values) if values.is_empty()) + { + return Err(unsupported_semantics(coordinate)); + } + validate_core_intent(&value)?; + Ok(value) +} + +fn validate_core_intent(core: &CanonicalValueV1) -> Result<(), ProviderRefusalV1> { + let intents = map_field(core, "intents") + .and_then(as_map) + .ok_or_else(|| invalid_artifact("core.echo-provider", "Core intents are invalid"))?; + let [(key, intent)] = intents.as_slice() else { + return Err(unsupported_semantics(CORE_COORDINATE)); + }; + if as_text(key) != Some("t") + || !has_exact_fields( + intent, + &[ + "input", + "output", + "requiredOperationProfile", + "inputConstraints", + "coreEvaluationBudget", + "body", + ], + ) + || text_field(intent, "input") != Some(OPERATION_INPUT_TYPE) + || text_field(intent, "output") != Some(OPERATION_OUTPUT_TYPE) + || text_field(intent, "requiredOperationProfile") != Some(OPERATION_PROFILE) + || !matches!(array_field(intent, "inputConstraints"), Some(values) if values.is_empty()) + || map_field(intent, "coreEvaluationBudget") != expected_core_budget().ok().as_ref() + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let body = map_field(intent, "body") + .filter(|body| has_exact_fields(body, &["locals", "nodes", "result"])) + .ok_or_else(|| unsupported_semantics(OPERATION_COORDINATE))?; + let locals = array_field(body, "locals") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core locals are invalid"))?; + let expected_input = reviewed_input_local(); + let expected_receipt = reviewed_receipt_local(); + let expected_reason = reviewed_reason_local(); + if locals + != &[ + expected_input.clone(), + expected_receipt.clone(), + expected_reason.clone(), + ] + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let nodes = array_field(body, "nodes") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core nodes are invalid"))?; + let [node] = nodes.as_slice() else { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + }; + validate_core_effect(node, &expected_input, &expected_receipt, &expected_reason)?; + let result = map_field(body, "result") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core result is absent"))?; + validate_supported_expr( + result, + &[&expected_input, &expected_receipt], + &[], + MAX_EXPRESSION_DEPTH, + )?; + if reviewed_expr_shape( + result, + &[&expected_input, &expected_receipt], + MAX_EXPRESSION_DEPTH, + ) != Some(ReviewedValueShape::RecordLiteralWithId) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + Ok(()) +} + +fn validate_core_effect( + node: &CanonicalValueV1, + input_local: &CanonicalValueV1, + receipt_local: &CanonicalValueV1, + reason_local: &CanonicalValueV1, +) -> Result<(), ProviderRefusalV1> { + if !has_exact_fields( + node, + &["kind", "binding", "effect", "input", "obstructionMap"], + ) || text_field(node, "kind") != Some("effect") + || map_field(node, "binding") != Some(receipt_local) + || text_field(node, "effect") != Some(SEMANTIC_EFFECT) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let input = map_field(node, "input") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect input is absent"))?; + validate_supported_expr(input, &[input_local], &[], MAX_EXPRESSION_DEPTH)?; + if reviewed_expr_shape(input, &[input_local], MAX_EXPRESSION_DEPTH) + != Some(ReviewedValueShape::StringScalar) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let obstruction_map = map_field(node, "obstructionMap") + .and_then(as_map) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "obstruction map is invalid"))?; + let [(failure, arm)] = obstruction_map.as_slice() else { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + }; + if as_text(failure) != Some(FAILURE_COORDINATE) + || !has_exact_fields(arm, &["binder", "value"]) + || map_field(arm, "binder") != Some(reason_local) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let value = map_field(arm, "value") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "obstruction value is absent"))?; + validate_supported_expr( + value, + &[input_local, reason_local], + &[DOMAIN_OBSTRUCTION], + MAX_EXPRESSION_DEPTH, + )?; + if text_field(value, "callee") != Some(DOMAIN_OBSTRUCTION) + || !matches!(array_field(value, "typeArgs"), Some(values) if values.is_empty()) + || !matches!(array_field(value, "args"), Some(values) if values.is_empty()) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + Ok(()) +} + +fn validate_target_ir_artifact( + target_ir: &BoundArtifact, +) -> Result { + if target_ir.reference.coordinate.is_empty() || target_ir.artifact.domain != TARGET_IR_DOMAIN { + return Err(invalid_artifact( + "target-ir.echo-dpo", + "Target IR identity or outer domain is invalid", + )); + } + let value = validate_binding(target_ir) + .map_err(|()| invalid_artifact("target-ir.echo-dpo", "Target IR binding is invalid"))?; + if !validate_target_ir_shape(&value) { + return Err(invalid_artifact( + "target-ir.echo-dpo", + "Target IR structure is invalid", + )); + } + Ok(value) +} + +fn validate_target_ir_shape(value: &CanonicalValueV1) -> bool { + if !has_exact_fields( + value, + &[ + "kind", + "domain", + "targetProfile", + "sourceCoreCoordinate", + "intents", + ], + ) || text_field(value, "kind") != Some("targetIrArtifact") + || text_field(value, "domain").is_none_or(str::is_empty) + || text_field(value, "sourceCoreCoordinate").is_none_or(str::is_empty) + || !map_field(value, "targetProfile").is_some_and(validate_resource_ref_value) + { + return false; + } + map_field(value, "intents") + .and_then(as_map) + .is_some_and(|intents| { + intents.iter().all(|(name, intent)| { + as_text(name).is_some_and(|name| !name.is_empty()) + && validate_target_intent_shape(intent) + }) + }) +} + +fn validate_target_intent_shape(value: &CanonicalValueV1) -> bool { + if !has_exact_fields( + value, + &[ + "operationProfile", + "inputConstraints", + "coreEvaluationBudget", + "requirements", + "steps", + "result", + ], + ) || text_field(value, "operationProfile").is_none_or(str::is_empty) + || !array_field(value, "inputConstraints") + .is_some_and(|constraints| constraints.iter().all(validate_input_constraint_shape)) + || !map_field(value, "coreEvaluationBudget").is_some_and(validate_budget_shape) + || !array_field(value, "requirements") + .is_some_and(|requirements| requirements.iter().all(validate_requirement_shape)) + || !array_field(value, "steps").is_some_and(|steps| steps.iter().all(validate_step_shape)) + { + return false; + } + map_field(value, "result").is_some_and(validate_target_expr_shape) +} + +fn validate_input_constraint_shape(value: &CanonicalValueV1) -> bool { + has_exact_fields(value, &["coordinate", "source", "predicate"]) + && text_field(value, "coordinate").is_some_and(|coordinate| !coordinate.is_empty()) + && matches!(text_field(value, "source"), Some("where" | "compiler")) + && map_field(value, "predicate") + .is_some_and(|predicate| preflight_predicate(predicate, MAX_EXPRESSION_DEPTH).is_ok()) +} + +fn validate_budget_shape(value: &CanonicalValueV1) -> bool { + has_exact_fields( + value, + &["maxSteps", "maxAllocatedBytes", "maxOutputBytes"], + ) && ["maxSteps", "maxAllocatedBytes", "maxOutputBytes"] + .into_iter() + .all(|field| { + matches!(map_field(value, field), Some(CanonicalValueV1::Integer(value)) if *value >= 0) + }) +} + +fn validate_requirement_shape(value: &CanonicalValueV1) -> bool { + has_exact_fields(value, &["id", "predicate", "onFailure"]) + && text_field(value, "id").is_some_and(|id| !id.is_empty()) + && map_field(value, "predicate") + .is_some_and(|predicate| preflight_predicate(predicate, MAX_EXPRESSION_DEPTH).is_ok()) + && map_field(value, "onFailure").is_some_and(|on_failure| { + preflight_require_failure(on_failure, MAX_EXPRESSION_DEPTH).is_ok() + }) +} + +fn validate_step_shape(value: &CanonicalValueV1) -> bool { + if !has_exact_fields( + value, + &[ + "id", + "binding", + "effect", + "targetIntrinsic", + "input", + "obstructionFailures", + "obstructionArms", + ], + ) || text_field(value, "id").is_none_or(str::is_empty) + || !map_field(value, "binding").is_some_and(validate_local) + || text_field(value, "effect").is_none_or(str::is_empty) + || text_field(value, "targetIntrinsic").is_none_or(str::is_empty) + || !map_field(value, "input").is_some_and(validate_target_expr_shape) + || !array_field(value, "obstructionFailures").is_some_and(|failures| { + failures + .iter() + .all(|failure| as_text(failure).is_some_and(is_failure_ident)) + }) + { + return false; + } + map_field(value, "obstructionArms") + .and_then(as_map) + .is_some_and(|arms| { + arms.iter().all(|(failure, arm)| { + as_text(failure).is_some_and(is_failure_ident) + && has_exact_fields(arm, &["binder", "value"]) + && map_field(arm, "binder").is_some_and(validate_local) + && map_field(arm, "value").is_some_and(validate_target_expr_shape) + }) + }) +} + +fn validate_target_expr_shape(value: &CanonicalValueV1) -> bool { + matches!( + preflight_expr(value, MAX_EXPRESSION_DEPTH), + Ok(()) | Err(ExpressionPreflightError::Unsupported) + ) +} + +fn expected_target_ir( + core: &CanonicalValueV1, + target_profile_digest: &Digest, +) -> Result { + let intent = map_field(map_field(core, "intents").ok_or(())?, "t").ok_or(())?; + let body = map_field(intent, "body").ok_or(())?; + let nodes = array_field(body, "nodes").ok_or(())?; + let [node] = nodes.as_slice() else { + return Err(()); + }; + let obstruction_map = map_field(node, "obstructionMap").ok_or(())?; + let expected_step = canonical_sorted_map([ + ("id", canonical_text("t.step.0")), + ("binding", map_field(node, "binding").ok_or(())?.clone()), + ("effect", canonical_text(SEMANTIC_EFFECT)), + ("targetIntrinsic", canonical_text(TARGET_INTRINSIC)), + ("input", map_field(node, "input").ok_or(())?.clone()), + ( + "obstructionFailures", + CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]), + ), + ("obstructionArms", obstruction_map.clone()), + ])?; + let expected_intent = canonical_sorted_map([ + ("operationProfile", canonical_text(OPERATION_PROFILE)), + ( + "inputConstraints", + map_field(intent, "inputConstraints").ok_or(())?.clone(), + ), + ( + "coreEvaluationBudget", + map_field(intent, "coreEvaluationBudget").ok_or(())?.clone(), + ), + ("requirements", CanonicalValueV1::Array(Vec::new())), + ("steps", CanonicalValueV1::Array(vec![expected_step])), + ("result", map_field(body, "result").ok_or(())?.clone()), + ])?; + canonical_sorted_map([ + ("kind", canonical_text("targetIrArtifact")), + ("domain", canonical_text(INNER_TARGET_IR_DOMAIN)), + ( + "targetProfile", + canonical_sorted_map([ + ("id", canonical_text(TARGET_PROFILE_COORDINATE)), + ("digest", digest_value(target_profile_digest).ok_or(())?), + ])?, + ), + ("sourceCoreCoordinate", canonical_text(CORE_COORDINATE)), + ("intents", canonical_sorted_map([("t", expected_intent)])?), + ]) +} + +struct RelationFailure { + code: &'static str, + message: &'static str, +} + +fn relation_failure( + target_ir: &CanonicalValueV1, + expected: &CanonicalValueV1, +) -> Option { + if text_field(target_ir, "domain") != text_field(expected, "domain") { + return Some(RelationFailure { + code: "echo.verifier.target-domain-mismatch", + message: "Target IR names a domain outside the reviewed Echo target", + }); + } + if map_field(target_ir, "targetProfile") != map_field(expected, "targetProfile") { + return Some(RelationFailure { + code: "echo.verifier.target-profile-mismatch", + message: "Target IR does not bind the exact supplied Echo target profile", + }); + } + + let target_intents = map_field(target_ir, "intents").and_then(as_map); + let expected_intents = map_field(expected, "intents").and_then(as_map); + match (target_intents, expected_intents) { + (Some(target), Some(expected)) if target.len() > expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.introduced-claim", + message: "Target IR introduces an operation absent from Core", + }); + } + (Some(target), Some(expected)) if target.len() < expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.silent-loss", + message: "Target IR drops an operation required by Core", + }); + } + _ => {} + } + + let target_intent = map_field(target_ir, "intents").and_then(|value| map_field(value, "t")); + let expected_intent = map_field(expected, "intents").and_then(|value| map_field(value, "t")); + let (Some(target_intent), Some(expected_intent)) = (target_intent, expected_intent) else { + return Some(RelationFailure { + code: "echo.verifier.silent-loss", + message: "Target IR omits the reviewed Core operation", + }); + }; + + let target_requirements = array_field(target_intent, "requirements"); + let expected_requirements = array_field(expected_intent, "requirements"); + match (target_requirements, expected_requirements) { + (Some(target), Some(expected)) if target.len() > expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.introduced-claim", + message: "Target IR introduces a guard absent from Core", + }); + } + (Some(target), Some(expected)) if target.len() < expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.silent-loss", + message: "Target IR drops a guard required by Core", + }); + } + _ => {} + } + + if map_field(target_intent, "coreEvaluationBudget") + != map_field(expected_intent, "coreEvaluationBudget") + { + return Some(RelationFailure { + code: "echo.verifier.budget-mismatch", + message: "Target IR does not preserve the exact Core evaluation budget", + }); + } + + let target_steps = array_field(target_intent, "steps"); + let expected_steps = array_field(expected_intent, "steps"); + match (target_steps, expected_steps) { + (Some(target), Some(expected)) if target.len() > expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.introduced-claim", + message: "Target IR introduces an effect absent from Core", + }); + } + (Some(target), Some(expected)) if target.len() < expected.len() => { + return Some(RelationFailure { + code: "echo.verifier.silent-loss", + message: "Target IR drops an effect required by Core", + }); + } + _ => {} + } + let target_step = target_steps.and_then(|steps| steps.first()); + let expected_step = expected_steps.and_then(|steps| steps.first()); + let (Some(target_step), Some(expected_step)) = (target_step, expected_step) else { + return Some(RelationFailure { + code: "echo.verifier.silent-loss", + message: "Target IR omits the reviewed Core effect", + }); + }; + + if text_field(target_step, "targetIntrinsic") != Some(TARGET_INTRINSIC) { + return Some(RelationFailure { + code: "echo.verifier.target-intrinsic-mismatch", + message: "Target IR uses an intrinsic outside the reviewed Echo capability", + }); + } + if map_field(target_step, "input") != map_field(expected_step, "input") { + return Some(RelationFailure { + code: "echo.verifier.effect-input-mismatch", + message: "Target IR does not preserve the exact Core effect input", + }); + } + if map_field(target_step, "obstructionFailures") + != map_field(expected_step, "obstructionFailures") + || map_field(target_step, "obstructionArms") != map_field(expected_step, "obstructionArms") + { + return Some(RelationFailure { + code: "echo.verifier.obstruction-mismatch", + message: "Target IR does not preserve the exact Core obstruction relation", + }); + } + if map_field(target_intent, "result") != map_field(expected_intent, "result") { + return Some(RelationFailure { + code: "echo.verifier.result-mismatch", + message: "Target IR does not preserve the exact Core result expression", + }); + } + (target_ir != expected).then_some(RelationFailure { + code: "echo.verifier.semantic-relation-mismatch", + message: "Target IR does not exactly preserve the reviewed Core obligations", + }) +} + +fn build_report(target_ir: &ResourceRef, outcome: &str) -> Result, ()> { + let report = canonical_sorted_map([ + ("apiVersion", canonical_text(REPORT_DOMAIN)), + ("targetIr", resource_ref_value(target_ir)?), + ("outcome", canonical_text(outcome)), + ( + "diagnosticAbi", + resource_ref_value(&ResourceRef { + coordinate: DIAGNOSTIC_ABI.coordinate.to_owned(), + digest: Digest { + algorithm: DigestAlgorithm::Sha256, + bytes: DIAGNOSTIC_ABI.framed_sha256.to_vec(), + }, + })?, + ), + ("diagnosticBytes", CanonicalValueV1::Bytes(Vec::new())), + ])?; + encode_canonical_cbor_v1(&report).map_err(|_| ()) +} + +fn resource_ref_value(reference: &ResourceRef) -> Result { + canonical_sorted_map([ + ("id", canonical_text(&reference.coordinate)), + ("digest", digest_value(&reference.digest).ok_or(())?), + ]) +} + +fn digest_value(digest: &Digest) -> Option { + (digest.algorithm == DigestAlgorithm::Sha256 && digest.bytes.len() == 32).then(|| { + CanonicalValueV1::Array(vec![ + canonical_text("sha256"), + CanonicalValueV1::Bytes(digest.bytes.clone()), + ]) + }) +} + +fn validate_resource_ref_value(value: &CanonicalValueV1) -> bool { + has_exact_fields(value, &["id", "digest"]) + && text_field(value, "id").is_some_and(|id| !id.is_empty()) + && map_field(value, "digest").is_some_and(|digest| { + matches!( + digest, + CanonicalValueV1::Array(values) + if matches!(values.as_slice(), + [CanonicalValueV1::Text(algorithm), CanonicalValueV1::Bytes(bytes)] + if algorithm == "sha256" && bytes.len() == 32) + ) + }) +} + +fn validate_binding(bound: &BoundArtifact) -> Result { + if bound.reference.coordinate.is_empty() + || bound.reference.digest.algorithm != DigestAlgorithm::Sha256 + || bound.reference.digest.bytes.len() != 32 + || bound.artifact.domain.is_empty() + { + return Err(()); + } + let value = decode_canonical_cbor_v1(&bound.artifact.bytes).map_err(|_| ())?; + let actual = digest_canonical_value_v1(&bound.artifact.domain, &value).map_err(|_| ())?; + if actual != digest_review(&bound.reference.digest).ok_or(())? { + return Err(()); + } + Ok(value) +} + +fn digest_review(digest: &Digest) -> Option { + if digest.algorithm != DigestAlgorithm::Sha256 || digest.bytes.len() != 32 { + return None; + } + let mut value = String::with_capacity(71); + value.push_str("sha256:"); + for byte in &digest.bytes { + write!(value, "{byte:02x}").ok()?; + } + Some(value) +} + +fn expected_lowerability() -> Result { + let guard_kinds = || CanonicalValueV1::Array(vec![canonical_text("precommit-atomic")]); + let obstructions = || CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]); + let footprints = || CanonicalValueV1::Array(vec![canonical_text("target.replace.footprint")]); + let costs = || CanonicalValueV1::Array(vec![canonical_text("target.replace.cost")]); + canonical_sorted_map([ + ("apiVersion", canonical_text(LOWERABILITY_DOMAIN)), + ("operationProfile", canonical_text(OPERATION_PROFILE)), + ( + "semanticEffects", + CanonicalValueV1::Array(vec![canonical_sorted_map([ + ("coordinate", canonical_text(SEMANTIC_EFFECT)), + ("writeClass", canonical_text("replace")), + ("guardKinds", guard_kinds()), + ("obstructionCoordinates", obstructions()), + ("footprintObligations", footprints()), + ("costObligations", costs()), + ])?]), + ), + ( + "requiredWriteClasses", + CanonicalValueV1::Array(vec![canonical_text("replace")]), + ), + ("guardKinds", guard_kinds()), + ("atomicity", canonical_text("atomic")), + ("postconditionSupport", CanonicalValueV1::Bool(true)), + ("obstructionCoordinates", obstructions()), + ("footprintObligations", footprints()), + ("costObligations", costs()), + ("opticContract", canonical_text("replace-point")), + ]) +} + +fn expected_core_types() -> Result { + canonical_sorted_map([ + ("Input", expected_record_type("a.b@1.Input.id")?), + ("Output", expected_record_type("a.b@1.Output.id")?), + ("Receipt", expected_record_type("a.b@1.Receipt.id")?), + ("Input.id", expected_string_type()?), + ("Output.id", expected_string_type()?), + ("Receipt.id", expected_string_type()?), + ]) +} + +fn expected_record_type(field_type: &str) -> Result { + canonical_sorted_map([ + ("kind", canonical_text("Record")), + ( + "fields", + canonical_sorted_map([("id", canonical_text(field_type))])?, + ), + ]) +} + +fn expected_string_type() -> Result { + canonical_sorted_map([ + ("kind", canonical_text("String")), + ("max", CanonicalValueV1::Integer(16)), + ("canonical", canonical_text("raw-utf8")), + ]) +} + +fn expected_core_budget() -> Result { + canonical_sorted_map([ + ("maxSteps", CanonicalValueV1::Integer(8)), + ("maxAllocatedBytes", CanonicalValueV1::Integer(1024)), + ("maxOutputBytes", CanonicalValueV1::Integer(256)), + ]) +} + +fn reviewed_input_local() -> CanonicalValueV1 { + local("arg.0", "$arg0", OPERATION_INPUT_TYPE) +} + +fn reviewed_receipt_local() -> CanonicalValueV1 { + local("local.0", "$local0", OPERATION_RECEIPT_TYPE) +} + +fn reviewed_reason_local() -> CanonicalValueV1 { + local("obstruction.0", "$obstruction0", FAILURE_PAYLOAD_TYPE) +} + +fn local(id: &str, alpha_name: &str, ty: &str) -> CanonicalValueV1 { + CanonicalValueV1::Map(vec![ + (canonical_text("id"), canonical_text(id)), + (canonical_text("type"), canonical_text(ty)), + (canonical_text("alphaName"), canonical_text(alpha_name)), + ]) +} + +#[derive(Clone, Copy)] +enum ExpressionPreflightError { + InvalidDiscriminator, + InvalidShape, + Unsupported, + DepthExceeded, +} + +fn validate_supported_expr( + value: &CanonicalValueV1, + scope: &[&CanonicalValueV1], + allowed_callees: &[&str], + remaining_depth: usize, +) -> Result<(), ProviderRefusalV1> { + preflight_expr(value, remaining_depth).map_err(expression_preflight_refusal)?; + if validate_expr(value, scope, allowed_callees, remaining_depth) { + Ok(()) + } else { + Err(unsupported_semantics(OPERATION_COORDINATE)) + } +} + +fn preflight_expr( + value: &CanonicalValueV1, + remaining_depth: usize, +) -> Result<(), ExpressionPreflightError> { + if remaining_depth == 0 { + return Err(ExpressionPreflightError::DepthExceeded); + } + let kind = text_field(value, "kind").ok_or(ExpressionPreflightError::InvalidDiscriminator)?; + match kind { + "local" => { + if has_exact_fields(value, &["kind", "ref"]) + && map_field(value, "ref").is_some_and(validate_local) + { + Ok(()) + } else { + Err(ExpressionPreflightError::InvalidShape) + } + } + "const" => { + if !has_exact_fields(value, &["kind", "value"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + preflight_core_value( + map_field(value, "value").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ) + } + "record" => { + if !has_exact_fields(value, &["kind", "fields"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + let fields = map_field(value, "fields") + .and_then(as_map) + .ok_or(ExpressionPreflightError::InvalidShape)?; + let mut unsupported = false; + for (key, field) in fields { + if as_text(key).is_none_or(str::is_empty) { + return Err(ExpressionPreflightError::InvalidShape); + } + note_preflight_result( + &mut unsupported, + preflight_expr(field, remaining_depth - 1), + )?; + } + finish_preflight(unsupported) + } + "field" => { + if !has_exact_fields(value, &["kind", "base", "field"]) + || text_field(value, "field").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + preflight_expr( + map_field(value, "base").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ) + } + "call" => { + if !has_exact_fields(value, &["kind", "callee", "typeArgs", "args"]) + || text_field(value, "callee").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + let type_args = + array_field(value, "typeArgs").ok_or(ExpressionPreflightError::InvalidShape)?; + if !type_args + .iter() + .all(|argument| as_text(argument).is_some_and(|argument| !argument.is_empty())) + { + return Err(ExpressionPreflightError::InvalidShape); + } + let args = array_field(value, "args").ok_or(ExpressionPreflightError::InvalidShape)?; + let mut unsupported = false; + for argument in args { + note_preflight_result( + &mut unsupported, + preflight_expr(argument, remaining_depth - 1), + )?; + } + finish_preflight(unsupported) + } + "variant" => { + if !(has_exact_fields(value, &["kind", "type", "case"]) + || has_exact_fields(value, &["kind", "type", "case", "payload"])) + || text_field(value, "type").is_none_or(str::is_empty) + || text_field(value, "case").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + if let Some(payload) = map_field(value, "payload") { + require_valid_expression_shape(preflight_expr(payload, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "match" => { + if !has_exact_fields(value, &["kind", "scrutinee", "arms"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + require_valid_expression_shape(preflight_expr( + map_field(value, "scrutinee").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ))?; + let arms = array_field(value, "arms") + .filter(|arms| !arms.is_empty()) + .ok_or(ExpressionPreflightError::InvalidShape)?; + for arm in arms { + if !(has_exact_fields(arm, &["case", "body"]) + || has_exact_fields(arm, &["case", "binder", "body"])) + || text_field(arm, "case").is_none_or(str::is_empty) + || map_field(arm, "binder").is_some_and(|binder| !validate_local(binder)) + { + return Err(ExpressionPreflightError::InvalidShape); + } + require_valid_expression_shape(preflight_expr( + map_field(arm, "body").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "list" => { + if !has_exact_fields(value, &["kind", "values"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + for item in + array_field(value, "values").ok_or(ExpressionPreflightError::InvalidShape)? + { + require_valid_expression_shape(preflight_expr(item, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "map" => { + if !has_exact_fields(value, &["kind", "entries"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + for entry in + array_field(value, "entries").ok_or(ExpressionPreflightError::InvalidShape)? + { + let CanonicalValueV1::Array(pair) = entry else { + return Err(ExpressionPreflightError::InvalidShape); + }; + let [key, entry_value] = pair.as_slice() else { + return Err(ExpressionPreflightError::InvalidShape); + }; + require_valid_expression_shape(preflight_expr(key, remaining_depth - 1))?; + require_valid_expression_shape(preflight_expr(entry_value, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "if" => { + if !has_exact_fields(value, &["kind", "predicate", "then", "else"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + preflight_predicate( + map_field(value, "predicate").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + )?; + for branch in ["then", "else"] { + require_valid_expression_shape(preflight_expr( + map_field(value, branch).ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ))?; + } + Err(ExpressionPreflightError::Unsupported) + } + _ => Err(ExpressionPreflightError::InvalidDiscriminator), + } +} + +fn note_preflight_result( + unsupported: &mut bool, + result: Result<(), ExpressionPreflightError>, +) -> Result<(), ExpressionPreflightError> { + match result { + Ok(()) => Ok(()), + Err(ExpressionPreflightError::Unsupported) => { + *unsupported = true; + Ok(()) + } + Err(error) => Err(error), + } +} + +fn finish_preflight(unsupported: bool) -> Result<(), ExpressionPreflightError> { + if unsupported { + Err(ExpressionPreflightError::Unsupported) + } else { + Ok(()) + } +} + +fn require_valid_expression_shape( + result: Result<(), ExpressionPreflightError>, +) -> Result<(), ExpressionPreflightError> { + match result { + Ok(()) | Err(ExpressionPreflightError::Unsupported) => Ok(()), + Err(error) => Err(error), + } +} + +fn preflight_predicate( + value: &CanonicalValueV1, + remaining_depth: usize, +) -> Result<(), ExpressionPreflightError> { + if remaining_depth == 0 { + return Err(ExpressionPreflightError::DepthExceeded); + } + let kind = text_field(value, "kind").ok_or(ExpressionPreflightError::InvalidDiscriminator)?; + match kind { + "true" | "false" if has_exact_fields(value, &["kind"]) => Ok(()), + "true" | "false" => Err(ExpressionPreflightError::InvalidShape), + "not" => { + if !has_exact_fields(value, &["kind", "value"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + preflight_predicate( + map_field(value, "value").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ) + } + "all" | "any" => { + if !has_exact_fields(value, &["kind", "values"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + let values = array_field(value, "values") + .filter(|values| !values.is_empty()) + .ok_or(ExpressionPreflightError::InvalidShape)?; + for predicate in values { + preflight_predicate(predicate, remaining_depth - 1)?; + } + Ok(()) + } + "compare" => { + if !has_exact_fields(value, &["kind", "op", "left", "right"]) + || !matches!( + text_field(value, "op"), + Some("==" | "!=" | "<" | "<=" | ">" | ">=") + ) + { + return Err(ExpressionPreflightError::InvalidShape); + } + for operand in ["left", "right"] { + require_valid_expression_shape(preflight_expr( + map_field(value, operand).ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + ))?; + } + Ok(()) + } + "call" => { + if !has_exact_fields(value, &["kind", "predicate", "args"]) + || text_field(value, "predicate").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + for argument in + array_field(value, "args").ok_or(ExpressionPreflightError::InvalidShape)? + { + require_valid_expression_shape(preflight_expr(argument, remaining_depth - 1))?; + } + Ok(()) + } + "obstruction" => { + if !has_exact_fields(value, &["kind", "coordinate", "payload"]) + || text_field(value, "coordinate") + .is_none_or(|coordinate| !is_failure_ident(coordinate)) + { + return Err(ExpressionPreflightError::InvalidShape); + } + require_valid_expression_shape(preflight_expr( + map_field(value, "payload").ok_or(ExpressionPreflightError::InvalidShape)?, + remaining_depth - 1, + )) + } + _ => Err(ExpressionPreflightError::InvalidDiscriminator), + } +} + +fn preflight_require_failure( + value: &CanonicalValueV1, + remaining_depth: usize, +) -> Result<(), ExpressionPreflightError> { + if remaining_depth == 0 { + return Err(ExpressionPreflightError::DepthExceeded); + } + if !matches!( + text_field(value, "kind"), + Some("terminal" | "continueObstructed") + ) { + return Err(ExpressionPreflightError::InvalidDiscriminator); + } + if !has_exact_fields(value, &["kind", "reason"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + let reason = map_field(value, "reason").ok_or(ExpressionPreflightError::InvalidShape)?; + if !has_exact_fields(reason, &["reasonKind", "payload"]) + || text_field(reason, "reasonKind").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + let payload = map_field(reason, "payload") + .and_then(as_map) + .ok_or(ExpressionPreflightError::InvalidShape)?; + for (key, expression) in payload { + if as_text(key).is_none_or(str::is_empty) { + return Err(ExpressionPreflightError::InvalidShape); + } + require_valid_expression_shape(preflight_expr(expression, remaining_depth - 1))?; + } + Ok(()) +} + +fn preflight_core_value( + value: &CanonicalValueV1, + remaining_depth: usize, +) -> Result<(), ExpressionPreflightError> { + if remaining_depth == 0 { + return Err(ExpressionPreflightError::DepthExceeded); + } + let kind = text_field(value, "kind").ok_or(ExpressionPreflightError::InvalidDiscriminator)?; + match kind { + "string" + if has_exact_fields(value, &["kind", "value"]) + && matches!(map_field(value, "value"), Some(CanonicalValueV1::Text(_))) => + { + Ok(()) + } + "null" if has_exact_fields(value, &["kind"]) => Err(ExpressionPreflightError::Unsupported), + "bool" + if has_exact_fields(value, &["kind", "value"]) + && matches!(map_field(value, "value"), Some(CanonicalValueV1::Bool(_))) => + { + Err(ExpressionPreflightError::Unsupported) + } + "int" + if has_exact_fields(value, &["kind", "width", "value"]) + && text_field(value, "width").is_some() + && matches!( + map_field(value, "value"), + Some(CanonicalValueV1::Integer(_)) + ) => + { + Err(ExpressionPreflightError::Unsupported) + } + "bytes" + if has_exact_fields(value, &["kind", "value"]) + && matches!(map_field(value, "value"), Some(CanonicalValueV1::Bytes(_))) => + { + Err(ExpressionPreflightError::Unsupported) + } + "string" | "null" | "bool" | "int" | "bytes" => Err(ExpressionPreflightError::InvalidShape), + "record" => { + if !has_exact_fields(value, &["kind", "fields"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + let fields = map_field(value, "fields") + .and_then(as_map) + .ok_or(ExpressionPreflightError::InvalidShape)?; + for (key, field) in fields { + if as_text(key).is_none_or(str::is_empty) { + return Err(ExpressionPreflightError::InvalidShape); + } + require_valid_expression_shape(preflight_core_value(field, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "variant" => { + if !(has_exact_fields(value, &["kind", "type", "case"]) + || has_exact_fields(value, &["kind", "type", "case", "payload"])) + || text_field(value, "type").is_none_or(str::is_empty) + || text_field(value, "case").is_none_or(str::is_empty) + { + return Err(ExpressionPreflightError::InvalidShape); + } + if let Some(payload) = map_field(value, "payload") { + require_valid_expression_shape(preflight_core_value(payload, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "list" => { + if !has_exact_fields(value, &["kind", "values"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + for item in + array_field(value, "values").ok_or(ExpressionPreflightError::InvalidShape)? + { + require_valid_expression_shape(preflight_core_value(item, remaining_depth - 1))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "map" => { + if !has_exact_fields(value, &["kind", "entries"]) { + return Err(ExpressionPreflightError::InvalidShape); + } + for entry in + array_field(value, "entries").ok_or(ExpressionPreflightError::InvalidShape)? + { + let CanonicalValueV1::Array(pair) = entry else { + return Err(ExpressionPreflightError::InvalidShape); + }; + let [key, entry_value] = pair.as_slice() else { + return Err(ExpressionPreflightError::InvalidShape); + }; + require_valid_expression_shape(preflight_core_value(key, remaining_depth - 1))?; + require_valid_expression_shape(preflight_core_value( + entry_value, + remaining_depth - 1, + ))?; + } + Err(ExpressionPreflightError::Unsupported) + } + "capability" => { + if !has_exact_fields(value, &["kind", "receipt"]) + || !map_field(value, "receipt").is_some_and(|receipt| { + matches!( + receipt, + CanonicalValueV1::Array(values) + if matches!(values.as_slice(), + [CanonicalValueV1::Text(algorithm), CanonicalValueV1::Bytes(bytes)] + if algorithm == "sha256" && bytes.len() == 32) + ) + }) + { + return Err(ExpressionPreflightError::InvalidShape); + } + Err(ExpressionPreflightError::Unsupported) + } + _ => Err(ExpressionPreflightError::InvalidDiscriminator), + } +} + +fn expression_preflight_refusal(error: ExpressionPreflightError) -> ProviderRefusalV1 { + match error { + ExpressionPreflightError::InvalidDiscriminator => refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + OPERATION_COORDINATE, + "echo.verifier.invalid-expression-discriminator", + "Core expression has a missing or unknown kind discriminator", + ), + ExpressionPreflightError::InvalidShape => refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + OPERATION_COORDINATE, + "echo.verifier.invalid-expression-shape", + "Core expression does not match the selected kind shape", + ), + ExpressionPreflightError::DepthExceeded => refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + OPERATION_COORDINATE, + "echo.verifier.expression-depth-exceeded", + "Core expression exceeds the verifier recursion bound", + ), + ExpressionPreflightError::Unsupported => unsupported_semantics(OPERATION_COORDINATE), + } +} + +fn validate_expr( + value: &CanonicalValueV1, + scope: &[&CanonicalValueV1], + allowed_callees: &[&str], + remaining_depth: usize, +) -> bool { + if remaining_depth == 0 { + return false; + } + match text_field(value, "kind") { + Some("local") => { + has_exact_fields(value, &["kind", "ref"]) + && map_field(value, "ref").is_some_and(|reference| { + validate_local(reference) && scope.contains(&reference) + }) + } + Some("const") => { + has_exact_fields(value, &["kind", "value"]) + && map_field(value, "value") + .is_some_and(|inner| validate_core_value(inner, remaining_depth - 1)) + } + Some("record") => { + if !has_exact_fields(value, &["kind", "fields"]) { + return false; + } + map_field(value, "fields") + .and_then(as_map) + .is_some_and(|fields| { + fields.iter().all(|(key, field)| { + as_text(key).is_some_and(|key| !key.is_empty()) + && validate_expr(field, scope, allowed_callees, remaining_depth - 1) + }) + }) + } + Some("field") => { + has_exact_fields(value, &["kind", "base", "field"]) + && text_field(value, "field").is_some_and(|field| !field.is_empty()) + && map_field(value, "base").is_some_and(|base| { + validate_expr(base, scope, allowed_callees, remaining_depth - 1) + }) + } + Some("call") => { + has_exact_fields(value, &["kind", "callee", "typeArgs", "args"]) + && text_field(value, "callee") + .is_some_and(|callee| allowed_callees.contains(&callee)) + && matches!(array_field(value, "typeArgs"), Some(values) if values.is_empty()) + && matches!(array_field(value, "args"), Some(values) if values.is_empty()) + } + _ => false, + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ReviewedValueShape { + StringScalar, + InputRecord, + OutputRecord, + ReceiptRecord, + RecordLiteralWithId, +} + +fn reviewed_expr_shape( + value: &CanonicalValueV1, + scope: &[&CanonicalValueV1], + remaining_depth: usize, +) -> Option { + if remaining_depth == 0 { + return None; + } + match text_field(value, "kind")? { + "local" => { + let reference = map_field(value, "ref")?; + if !scope.contains(&reference) { + return None; + } + match text_field(reference, "type")? { + OPERATION_INPUT_TYPE => Some(ReviewedValueShape::InputRecord), + OPERATION_OUTPUT_TYPE => Some(ReviewedValueShape::OutputRecord), + OPERATION_RECEIPT_TYPE => Some(ReviewedValueShape::ReceiptRecord), + INPUT_ID_TYPE | OUTPUT_ID_TYPE | RECEIPT_ID_TYPE => { + Some(ReviewedValueShape::StringScalar) + } + _ => None, + } + } + "const" + if matches!( + map_field(value, "value").and_then(|value| text_field(value, "kind")), + Some("string") + ) => + { + Some(ReviewedValueShape::StringScalar) + } + "field" => { + let base = map_field(value, "base")?; + let base_shape = reviewed_expr_shape(base, scope, remaining_depth - 1); + (text_field(value, "field") == Some("id") + && matches!( + base_shape, + Some( + ReviewedValueShape::InputRecord + | ReviewedValueShape::OutputRecord + | ReviewedValueShape::ReceiptRecord + | ReviewedValueShape::RecordLiteralWithId + ) + )) + .then_some(ReviewedValueShape::StringScalar) + } + "record" => { + let fields = map_field(value, "fields").and_then(as_map)?; + let [(field, expression)] = fields.as_slice() else { + return None; + }; + (as_text(field) == Some("id") + && reviewed_expr_shape(expression, scope, remaining_depth - 1) + == Some(ReviewedValueShape::StringScalar)) + .then_some(ReviewedValueShape::RecordLiteralWithId) + } + _ => None, + } +} + +fn validate_core_value(value: &CanonicalValueV1, remaining_depth: usize) -> bool { + remaining_depth > 0 + && match text_field(value, "kind") { + Some("string") => { + has_exact_fields(value, &["kind", "value"]) + && map_field(value, "value").is_some_and( + |value| matches!(value, CanonicalValueV1::Text(value) if value.chars().count() <= 16), + ) + } + _ => false, + } +} + +fn validate_local(value: &CanonicalValueV1) -> bool { + has_exact_fields(value, &["id", "alphaName", "type"]) + && text_field(value, "id").is_some_and(|value| !value.is_empty()) + && text_field(value, "alphaName").is_some_and(|value| !value.is_empty()) + && text_field(value, "type").is_some_and(|value| !value.is_empty()) +} + +fn is_failure_ident(value: &str) -> bool { + let mut bytes = value.bytes(); + let Some(first) = bytes.next() else { + return false; + }; + (first.is_ascii_alphabetic() || first == b'_') + && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') +} + +fn has_exact_fields(value: &CanonicalValueV1, fields: &[&str]) -> bool { + as_map(value).is_some_and(|entries| { + entries.len() == fields.len() + && fields.iter().all(|field| map_field(value, field).is_some()) + }) +} + +fn canonical_sorted_map<'a>( + entries: impl IntoIterator, +) -> Result { + let mut entries = entries + .into_iter() + .map(|(key, value)| { + let key = canonical_text(key); + let encoded = encode_canonical_cbor_v1(&key).map_err(|_| ())?; + Ok((encoded, key, value)) + }) + .collect::, ()>>()?; + entries.sort_by(|left, right| left.0.cmp(&right.0)); + Ok(CanonicalValueV1::Map( + entries + .into_iter() + .map(|(_, key, value)| (key, value)) + .collect(), + )) +} + +fn canonical_text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn map_field<'a>(value: &'a CanonicalValueV1, field: &str) -> Option<&'a CanonicalValueV1> { + as_map(value)?.iter().find_map(|(key, value)| { + (matches!(key, CanonicalValueV1::Text(key) if key == field)).then_some(value) + }) +} + +fn text_field<'a>(value: &'a CanonicalValueV1, field: &str) -> Option<&'a str> { + map_field(value, field).and_then(as_text) +} + +fn array_field<'a>(value: &'a CanonicalValueV1, field: &str) -> Option<&'a Vec> { + match map_field(value, field)? { + CanonicalValueV1::Array(values) => Some(values), + _ => None, + } +} + +fn as_map(value: &CanonicalValueV1) -> Option<&Vec<(CanonicalValueV1, CanonicalValueV1)>> { + match value { + CanonicalValueV1::Map(entries) => Some(entries), + _ => None, + } +} + +fn as_text(value: &CanonicalValueV1) -> Option<&str> { + match value { + CanonicalValueV1::Text(value) => Some(value), + _ => None, + } +} + +fn refusal( + kind: ProviderRefusalKind, + subject: impl Into, + code: &str, + message: &str, +) -> ProviderRefusalV1 { + ProviderRefusalV1 { + kind, + subject: Some(subject.into()), + diagnostics: vec![Diagnostic { + code: code.to_owned(), + severity: DiagnosticSeverity::Error, + message: message.to_owned(), + repair: None, + }], + } +} + +fn invalid_artifact(subject: &str, message: &str) -> ProviderRefusalV1 { + refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + subject, + "echo.verifier.invalid-semantic-artifact", + message, + ) +} + +fn unsupported_semantics(subject: &str) -> ProviderRefusalV1 { + refusal( + ProviderRefusalKind::UnsupportedSemantics, + subject, + "echo.verifier.unsupported-semantics", + "the semantic closure is outside the first reviewed Echo verifier boundary", + ) +} diff --git a/crates/echo-edict-provider-verifier/src/semantic_resources.rs b/crates/echo-edict-provider-verifier/src/semantic_resources.rs new file mode 100644 index 00000000..36356c76 --- /dev/null +++ b/crates/echo-edict-provider-verifier/src/semantic_resources.rs @@ -0,0 +1,1827 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Closed-world admission for the exact generated semantic resources reviewed +//! by the first Echo provider verifier. + +use std::fmt::Write as _; + +use echo_edict_canonical::{decode_canonical_cbor_v1, digest_canonical_value_v1, CanonicalValueV1}; + +use super::DIAGNOSTIC_ABI; + +const PROFILE: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo@1", + domain: "edict.target-profile/v1", + bytes: include_bytes!("../resources/target-profile.echo-dpo.cbor"), + framed_sha256: "f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0", +}; +const LAWPACK: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo-lawpack@1", + domain: "edict.lawpack/v1", + bytes: include_bytes!("../resources/lawpack.echo-dpo.cbor"), + framed_sha256: "47006be4c2b569f13759078c560fe508e158b429cd9d4c5e47d9a7ea86b983b2", +}; +const GENERATED_PROFILE: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.registration/v1", + domain: "echo.generated-artifact-profile/v1", + bytes: include_bytes!("../resources/generated-artifact-profile.echo-dpo-registration.cbor"), + framed_sha256: "3377304d8634681821cd958427e0b8baccc37b7b08bfb342d988a08571eb83ab", +}; +const LAWPACK_EXPORTS: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo-lawpack.exports@1", + domain: "echo.dpo-lawpack.exports@1", + bytes: include_bytes!("../resources/resource.lawpack-exports.cbor"), + framed_sha256: "b08f2118b018e50bd7f72d77a820424e2a1b46a118c847875da2df973e17ab19", +}; +const LAWPACK_ADAPTER: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo-lawpack.adapter.echo-dpo@1", + domain: "echo.dpo-lawpack.adapter.echo-dpo@1", + bytes: include_bytes!("../resources/resource.lawpack-target-adapter.cbor"), + framed_sha256: "4d4206b6dffb4abb531b44e59345f5f1ac6825253e131b851e6430eeb3480283", +}; +const LAWPACK_VERIFIER: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo-lawpack.verifier@1", + domain: "echo.dpo-lawpack.verifier@1", + bytes: include_bytes!("../resources/resource.lawpack-verifier.cbor"), + framed_sha256: "bd01c4d86a5216cfc60cc69531e7640652d6c2404810e250462c2eae9ef35298", +}; +const INTRINSICS: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.intrinsics/v1", + domain: "echo.dpo.intrinsics/v1", + bytes: include_bytes!("../resources/resource.target-intrinsics.cbor"), + framed_sha256: "fcf0500f386f4f8ca0003a93f4e63cb98a3480d1cf0b4557b5c726970532c2f5", +}; +const FOOTPRINT: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.footprint/v1", + domain: "echo.dpo.footprint/v1", + bytes: include_bytes!("../resources/resource.target-footprint-algebra.cbor"), + framed_sha256: "f47bb65867e78099ddcfd6ae7af83870df8823f974a496a111ed94e5d785c769", +}; +const COST: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.cost/v1", + domain: "echo.dpo.cost/v1", + bytes: include_bytes!("../resources/resource.target-cost-algebra.cbor"), + framed_sha256: "486940acd1d4bc15cea40db29347619a07202d9c0f1e63c4016c26bccf0b52a8", +}; +const OBSTRUCTIONS: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.obstructions/v1", + domain: "echo.dpo.obstructions/v1", + bytes: include_bytes!("../resources/resource.target-obstruction-taxonomy.cbor"), + framed_sha256: "4f5a139d606053c3807f50710076ea9abeb0e611c12a530f5e5b4b31911c3e64", +}; +const OPERATION_PROFILES: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.operation-profiles/v1", + domain: "echo.dpo.operation-profiles/v1", + bytes: include_bytes!("../resources/resource.target-operation-profiles.cbor"), + framed_sha256: "53256c51f6c817a77cc8694458bf9d3891abd15b9c94f79ca97d920d3c5f0416", +}; +const VERIFIER: ResourceSpec = ResourceSpec { + coordinate: "echo.dpo.verifier/v1", + domain: "echo.dpo.verifier/v1", + bytes: include_bytes!("../resources/resource.target-verifier-contract.cbor"), + framed_sha256: "ca96b190728de3d668072ec1bd37d24e5197e7bd9bd54f70966d8c566b9b67f2", +}; +const TARGET_IR: ResourceSpec = ResourceSpec { + coordinate: "echo.span-ir/v1", + domain: "echo.span-ir/v1", + bytes: include_bytes!("../resources/resource.target-ir.cbor"), + framed_sha256: "0057167e68f50c99dcce087b3e1cd677d17c5d1dc238bdb52d89469e1472fc2f", +}; + +const OPERATION: &str = "a.b@1.t"; +const EFFECT: &str = "target.replace"; +const CAPABILITY: &str = "echo.dpo@1.replace"; +const FOOTPRINT_OBLIGATION: &str = "target.replace.footprint"; +const COST_OBLIGATION: &str = "target.replace.cost"; +const FAILURE: &str = "rejected"; +const FAILURE_PAYLOAD: &str = "target.replace.rejected"; +const DOMAIN_OBSTRUCTION: &str = "domain.WriteRejected"; +const OPERATION_PROFILE: &str = "continuum.profile.write/v1"; +const OPTIC_CONTRACT: &str = "replace-point"; + +#[derive(Clone, Copy)] +struct ResourceSpec { + coordinate: &'static str, + domain: &'static str, + bytes: &'static [u8], + framed_sha256: &'static str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct ReviewedSemanticClosure; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum SemanticResourceErrorKind { + InvalidArtifact, + ReferenceMismatch, + SemanticMismatch, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct SemanticResourceError { + kind: SemanticResourceErrorKind, + subject: &'static str, +} + +impl SemanticResourceError { + pub(super) const fn kind(self) -> SemanticResourceErrorKind { + self.kind + } + + pub(super) const fn subject(self) -> &'static str { + self.subject + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ResourceSet { + profile: CanonicalValueV1, + lawpack: CanonicalValueV1, + generated_profile: CanonicalValueV1, + lawpack_exports: CanonicalValueV1, + lawpack_adapter: CanonicalValueV1, + lawpack_verifier: CanonicalValueV1, + intrinsics: CanonicalValueV1, + footprint: CanonicalValueV1, + cost: CanonicalValueV1, + obstructions: CanonicalValueV1, + operation_profiles: CanonicalValueV1, + verifier: CanonicalValueV1, + target_ir: CanonicalValueV1, +} + +pub(super) fn admit_packaged_semantic_resources( +) -> Result { + let resources = ResourceSet::decode_packaged()?; + resources.validate_references()?; + resources.validate_semantics()?; + Ok(ReviewedSemanticClosure) +} + +impl ResourceSet { + fn decode_packaged() -> Result { + Ok(Self { + profile: decode_pinned(PROFILE)?, + lawpack: decode_pinned(LAWPACK)?, + generated_profile: decode_pinned(GENERATED_PROFILE)?, + lawpack_exports: decode_pinned(LAWPACK_EXPORTS)?, + lawpack_adapter: decode_pinned(LAWPACK_ADAPTER)?, + lawpack_verifier: decode_pinned(LAWPACK_VERIFIER)?, + intrinsics: decode_pinned(INTRINSICS)?, + footprint: decode_pinned(FOOTPRINT)?, + cost: decode_pinned(COST)?, + obstructions: decode_pinned(OBSTRUCTIONS)?, + operation_profiles: decode_pinned(OPERATION_PROFILES)?, + verifier: decode_pinned(VERIFIER)?, + target_ir: decode_pinned(TARGET_IR)?, + }) + } + + fn validate_references(&self) -> Result<(), SemanticResourceError> { + assert_ref( + field(&self.profile, "targetIr", "target-profile.targetIr")?, + TARGET_IR, + &self.target_ir, + "target-profile.targetIr", + )?; + assert_external_ref( + field( + &self.profile, + "diagnosticAbi", + "target-profile.diagnosticAbi", + )?, + DIAGNOSTIC_ABI.coordinate, + &DIAGNOSTIC_ABI.framed_sha256, + "target-profile.diagnosticAbi", + )?; + assert_ref( + field(&self.profile, "intrinsics", "target-profile.intrinsics")?, + INTRINSICS, + &self.intrinsics, + "target-profile.intrinsics", + )?; + assert_ref( + field( + &self.profile, + "footprintAlgebra", + "target-profile.footprintAlgebra", + )?, + FOOTPRINT, + &self.footprint, + "target-profile.footprintAlgebra", + )?; + assert_ref( + field(&self.profile, "costAlgebra", "target-profile.costAlgebra")?, + COST, + &self.cost, + "target-profile.costAlgebra", + )?; + assert_ref( + field( + &self.profile, + "obstructionTaxonomy", + "target-profile.obstructionTaxonomy", + )?, + OBSTRUCTIONS, + &self.obstructions, + "target-profile.obstructionTaxonomy", + )?; + assert_ref( + field( + &self.profile, + "operationProfiles", + "target-profile.operationProfiles", + )?, + OPERATION_PROFILES, + &self.operation_profiles, + "target-profile.operationProfiles", + )?; + assert_ref( + field(&self.profile, "verifier", "target-profile.verifier")?, + VERIFIER, + &self.verifier, + "target-profile.verifier", + )?; + let generated = exact_array( + field( + &self.profile, + "generatedArtifactProfiles", + "target-profile.generatedArtifactProfiles", + )?, + 1, + "target-profile.generatedArtifactProfiles", + )?; + assert_ref( + &generated[0], + GENERATED_PROFILE, + &self.generated_profile, + "target-profile.generatedArtifactProfiles[0]", + )?; + + assert_ref( + field(&self.lawpack, "exports", "lawpack.exports")?, + LAWPACK_EXPORTS, + &self.lawpack_exports, + "lawpack.exports", + )?; + assert_ref( + field( + field(&self.lawpack, "verifier", "lawpack.verifier")?, + "ruleset", + "lawpack.verifier.ruleset", + )?, + LAWPACK_VERIFIER, + &self.lawpack_verifier, + "lawpack.verifier.ruleset", + )?; + let adapters = exact_array( + field(&self.lawpack, "targetAdapters", "lawpack.targetAdapters")?, + 1, + "lawpack.targetAdapters", + )?; + let adapter = &adapters[0]; + assert_ref( + field(adapter, "adapter", "lawpack.targetAdapters[0].adapter")?, + LAWPACK_ADAPTER, + &self.lawpack_adapter, + "lawpack.targetAdapters[0].adapter", + )?; + assert_ref( + field( + adapter, + "acceptedTargetProfile", + "lawpack.targetAdapters[0].acceptedTargetProfile", + )?, + PROFILE, + &self.profile, + "lawpack.targetAdapters[0].acceptedTargetProfile", + )?; + assert_ref( + field( + adapter, + "acceptedTargetIr", + "lawpack.targetAdapters[0].acceptedTargetIr", + )?, + TARGET_IR, + &self.target_ir, + "lawpack.targetAdapters[0].acceptedTargetIr", + )?; + Ok(()) + } + + #[allow(clippy::too_many_lines)] + fn validate_semantics(&self) -> Result<(), SemanticResourceError> { + expect_text( + field(&self.profile, "apiVersion", "target-profile.apiVersion")?, + PROFILE.domain, + "target-profile.apiVersion", + )?; + expect_text( + field(&self.profile, "id", "target-profile.id")?, + "echo.dpo", + "target-profile.id", + )?; + expect_text( + field( + &self.profile, + "intrinsicNamespace", + "target-profile.intrinsicNamespace", + )?, + PROFILE.coordinate, + "target-profile.intrinsicNamespace", + )?; + expect_text( + field( + &self.profile, + "applicationModel", + "target-profile.applicationModel", + )?, + "atomic", + "target-profile.applicationModel", + )?; + expect_text( + field( + &self.profile, + "readConsistency", + "target-profile.readConsistency", + )?, + "application-snapshot", + "target-profile.readConsistency", + )?; + expect_text( + field( + &self.profile, + "guardEvaluation", + "target-profile.guardEvaluation", + )?, + "precommit-atomic", + "target-profile.guardEvaluation", + )?; + expect_text( + field( + &self.profile, + "obstructionRollback", + "target-profile.obstructionRollback", + )?, + "no-visible-effects", + "target-profile.obstructionRollback", + )?; + expect_bool( + field(&self.profile, "multiTarget", "target-profile.multiTarget")?, + false, + "target-profile.multiTarget", + )?; + expect_bool( + field( + &self.profile, + "postconditionSupport", + "target-profile.postconditionSupport", + )?, + true, + "target-profile.postconditionSupport", + )?; + let accepted_core = exact_array( + field( + &self.profile, + "acceptedCoreAbi", + "target-profile.acceptedCoreAbi", + )?, + 1, + "target-profile.acceptedCoreAbi", + )?; + expect_text( + &accepted_core[0], + "edict.core/v1", + "target-profile.acceptedCoreAbi[0]", + )?; + + let lawpack_core = exact_array( + field(&self.lawpack, "acceptedCoreAbi", "lawpack.acceptedCoreAbi")?, + 1, + "lawpack.acceptedCoreAbi", + )?; + expect_text( + &lawpack_core[0], + "edict.core/v1", + "lawpack.acceptedCoreAbi[0]", + )?; + exact_array( + field(&self.lawpack, "dependencies", "lawpack.dependencies")?, + 0, + "lawpack.dependencies", + )?; + + expect_text( + field( + &self.generated_profile, + "apiVersion", + "generated.apiVersion", + )?, + GENERATED_PROFILE.domain, + "generated.apiVersion", + )?; + expect_text( + field( + &self.generated_profile, + "targetProfile", + "generated.targetProfile", + )?, + PROFILE.coordinate, + "generated.targetProfile", + )?; + validate_generated_types(&self.generated_profile)?; + let operations = exact_map( + field( + &self.generated_profile, + "operations", + "generated.operations", + )?, + 1, + "generated.operations", + )?; + let operation = entry(operations, OPERATION, "generated.operations")?; + expect_text( + field(operation, "inputType", "generated.operation.inputType")?, + "a.b@1.Input", + "generated.operation.inputType", + )?; + expect_text( + field(operation, "outputType", "generated.operation.outputType")?, + "a.b@1.Output", + "generated.operation.outputType", + )?; + expect_text( + field(operation, "budget", "generated.operation.budget")?, + "p.tiny", + "generated.operation.budget", + )?; + expect_text( + field( + operation, + "invocationKind", + "generated.operation.invocationKind", + )?, + "mutation", + "generated.operation.invocationKind", + )?; + expect_text( + field(operation, "effect", "generated.operation.effect")?, + EFFECT, + "generated.operation.effect", + )?; + expect_text( + field( + field( + operation, + "implementation", + "generated.operation.implementation", + )?, + "coordinate", + "generated.operation.implementation.coordinate", + )?, + CAPABILITY, + "generated.operation.implementation.coordinate", + )?; + expect_text( + field( + field( + operation, + "implementation", + "generated.operation.implementation", + )?, + "kind", + "generated.operation.implementation.kind", + )?, + "native", + "generated.operation.implementation.kind", + )?; + expect_text( + field( + operation, + "operationProfile", + "generated.operation.operationProfile", + )?, + OPERATION_PROFILE, + "generated.operation.operationProfile", + )?; + expect_text( + field( + operation, + "opticContract", + "generated.operation.opticContract", + )?, + OPTIC_CONTRACT, + "generated.operation.opticContract", + )?; + let generated_mappings = exact_map( + field( + operation, + "obstructionMappings", + "generated.operation.obstructionMappings", + )?, + 1, + "generated.operation.obstructionMappings", + )?; + expect_text( + entry( + generated_mappings, + FAILURE, + "generated.operation.obstructionMappings", + )?, + DOMAIN_OBSTRUCTION, + "generated.operation.obstructionMappings.rejected", + )?; + + let exported_types = exact_array( + field(&self.lawpack_exports, "types", "lawpack-exports.types")?, + 1, + "lawpack-exports.types", + )?; + expect_text( + field( + &exported_types[0], + "coordinate", + "lawpack-exports.type.coordinate", + )?, + "a.b@1.Id", + "lawpack-exports.type.coordinate", + )?; + expect_text( + field( + &exported_types[0], + "definition", + "lawpack-exports.type.definition", + )?, + "String", + "lawpack-exports.type.definition", + )?; + exact_array( + field( + &self.lawpack_exports, + "constants", + "lawpack-exports.constants", + )?, + 0, + "lawpack-exports.constants", + )?; + exact_array( + field( + &self.lawpack_exports, + "pureFunctions", + "lawpack-exports.pureFunctions", + )?, + 0, + "lawpack-exports.pureFunctions", + )?; + exact_map( + field( + &self.lawpack_exports, + "operationProfiles", + "lawpack-exports.operationProfiles", + )?, + 0, + "lawpack-exports.operationProfiles", + )?; + let effects = exact_array( + field(&self.lawpack_exports, "effects", "lawpack-exports.effects")?, + 1, + "lawpack-exports.effects", + )?; + let exported_effect = &effects[0]; + expect_text( + field( + exported_effect, + "coordinate", + "lawpack-exports.effect.coordinate", + )?, + EFFECT, + "lawpack-exports.effect.coordinate", + )?; + expect_text( + field( + exported_effect, + "inputType", + "lawpack-exports.effect.inputType", + )?, + "a.b@1.Id", + "lawpack-exports.effect.inputType", + )?; + expect_text( + field( + exported_effect, + "outputType", + "lawpack-exports.effect.outputType", + )?, + "a.b@1.Receipt", + "lawpack-exports.effect.outputType", + )?; + expect_text( + field( + exported_effect, + "executionClass", + "lawpack-exports.effect.executionClass", + )?, + "runtime", + "lawpack-exports.effect.executionClass", + )?; + expect_text( + field( + exported_effect, + "effectKindHint", + "lawpack-exports.effect.effectKindHint", + )?, + "replace", + "lawpack-exports.effect.effectKindHint", + )?; + expect_bool( + field( + exported_effect, + "guardSupport", + "lawpack-exports.effect.guardSupport", + )?, + true, + "lawpack-exports.effect.guardSupport", + )?; + exact_array( + field( + exported_effect, + "typeParameters", + "lawpack-exports.effect.typeParameters", + )?, + 0, + "lawpack-exports.effect.typeParameters", + )?; + expect_text( + field( + exported_effect, + "footprintObligation", + "lawpack-exports.effect.footprintObligation", + )?, + FOOTPRINT_OBLIGATION, + "lawpack-exports.effect.footprintObligation", + )?; + expect_text( + field( + exported_effect, + "costObligation", + "lawpack-exports.effect.costObligation", + )?, + COST_OBLIGATION, + "lawpack-exports.effect.costObligation", + )?; + let exported_failures = exact_map( + field( + exported_effect, + "effectFailures", + "lawpack-exports.effect.effectFailures", + )?, + 1, + "lawpack-exports.effect.effectFailures", + )?; + let exported_failure = entry( + exported_failures, + FAILURE, + "lawpack-exports.effect.effectFailures", + )?; + expect_text( + field( + exported_failure, + "payloadType", + "lawpack-exports.effect.failure.payloadType", + )?, + FAILURE_PAYLOAD, + "lawpack-exports.effect.failure.payloadType", + )?; + expect_text( + field( + exported_failure, + "authorityClass", + "lawpack-exports.effect.failure.authorityClass", + )?, + "domainMappable", + "lawpack-exports.effect.failure.authorityClass", + )?; + let exported_obstructions = exact_array( + field( + &self.lawpack_exports, + "obstructions", + "lawpack-exports.obstructions", + )?, + 1, + "lawpack-exports.obstructions", + )?; + expect_text( + field( + &exported_obstructions[0], + "coordinate", + "lawpack-exports.obstruction.coordinate", + )?, + DOMAIN_OBSTRUCTION, + "lawpack-exports.obstruction.coordinate", + )?; + expect_text( + field( + &exported_obstructions[0], + "payloadSchema", + "lawpack-exports.obstruction.payloadSchema", + )?, + "domain.WriteRejected.Payload", + "lawpack-exports.obstruction.payloadSchema", + )?; + expect_text( + field( + &exported_obstructions[0], + "authorityClass", + "lawpack-exports.obstruction.authorityClass", + )?, + "domainMappable", + "lawpack-exports.obstruction.authorityClass", + )?; + + expect_text( + field( + &self.lawpack_adapter, + "targetProfile", + "lawpack-adapter.targetProfile", + )?, + PROFILE.coordinate, + "lawpack-adapter.targetProfile", + )?; + expect_text( + field( + &self.lawpack_adapter, + "targetIrDomain", + "lawpack-adapter.targetIrDomain", + )?, + TARGET_IR.domain, + "lawpack-adapter.targetIrDomain", + )?; + let implementations = exact_map( + field( + &self.lawpack_adapter, + "effectImplementations", + "lawpack-adapter.effectImplementations", + )?, + 1, + "lawpack-adapter.effectImplementations", + )?; + let implementation = entry( + implementations, + EFFECT, + "lawpack-adapter.effectImplementations", + )?; + expect_text( + field( + implementation, + "capability", + "lawpack-adapter.effect.capability", + )?, + CAPABILITY, + "lawpack-adapter.effect.capability", + )?; + expect_text( + field(implementation, "kind", "lawpack-adapter.effect.kind")?, + "native", + "lawpack-adapter.effect.kind", + )?; + expect_text( + field( + implementation, + "writeClass", + "lawpack-adapter.effect.writeClass", + )?, + "replace", + "lawpack-adapter.effect.writeClass", + )?; + + expect_text( + field( + &self.intrinsics, + "apiVersion", + "target-intrinsics.apiVersion", + )?, + "edict.target-profile.intrinsics/v1", + "target-intrinsics.apiVersion", + )?; + let intrinsic_map = exact_map( + field( + &self.intrinsics, + "intrinsics", + "target-intrinsics.intrinsics", + )?, + 1, + "target-intrinsics.intrinsics", + )?; + let intrinsic = entry(intrinsic_map, CAPABILITY, "target-intrinsics.intrinsics")?; + expect_text( + field( + intrinsic, + "intrinsicClass", + "target-intrinsics.intrinsic.intrinsicClass", + )?, + "effect", + "target-intrinsics.intrinsic.intrinsicClass", + )?; + let argument_types = exact_array( + field( + intrinsic, + "argumentTypes", + "target-intrinsics.intrinsic.argumentTypes", + )?, + 1, + "target-intrinsics.intrinsic.argumentTypes", + )?; + expect_text( + &argument_types[0], + "a.b@1.Id", + "target-intrinsics.intrinsic.argumentTypes[0]", + )?; + expect_text( + field( + intrinsic, + "returnType", + "target-intrinsics.intrinsic.returnType", + )?, + "a.b@1.Receipt", + "target-intrinsics.intrinsic.returnType", + )?; + expect_text( + field( + intrinsic, + "effectKind", + "target-intrinsics.intrinsic.effectKind", + )?, + "replace", + "target-intrinsics.intrinsic.effectKind", + )?; + expect_text( + field( + intrinsic, + "writeClass", + "target-intrinsics.intrinsic.writeClass", + )?, + "replace", + "target-intrinsics.intrinsic.writeClass", + )?; + expect_bool( + field( + intrinsic, + "guardSupport", + "target-intrinsics.intrinsic.guardSupport", + )?, + true, + "target-intrinsics.intrinsic.guardSupport", + )?; + expect_bool( + field( + intrinsic, + "canParticipateInAtomicGuard", + "target-intrinsics.intrinsic.canParticipateInAtomicGuard", + )?, + true, + "target-intrinsics.intrinsic.canParticipateInAtomicGuard", + )?; + exact_array( + field( + intrinsic, + "typeParameters", + "target-intrinsics.intrinsic.typeParameters", + )?, + 0, + "target-intrinsics.intrinsic.typeParameters", + )?; + expect_text( + field( + intrinsic, + "footprintTemplate", + "target-intrinsics.intrinsic.footprintTemplate", + )?, + FOOTPRINT_OBLIGATION, + "target-intrinsics.intrinsic.footprintTemplate", + )?; + expect_text( + field( + intrinsic, + "costTemplate", + "target-intrinsics.intrinsic.costTemplate", + )?, + COST_OBLIGATION, + "target-intrinsics.intrinsic.costTemplate", + )?; + let intrinsic_failures = exact_map( + field( + intrinsic, + "effectFailures", + "target-intrinsics.intrinsic.effectFailures", + )?, + 1, + "target-intrinsics.intrinsic.effectFailures", + )?; + let intrinsic_failure = entry( + intrinsic_failures, + FAILURE, + "target-intrinsics.intrinsic.effectFailures", + )?; + expect_text( + field( + intrinsic_failure, + "payloadType", + "target-intrinsics.intrinsic.failure.payloadType", + )?, + FAILURE_PAYLOAD, + "target-intrinsics.intrinsic.failure.payloadType", + )?; + expect_text( + field( + intrinsic_failure, + "authorityClass", + "target-intrinsics.intrinsic.failure.authorityClass", + )?, + "domainMappable", + "target-intrinsics.intrinsic.failure.authorityClass", + )?; + + validate_algebra( + &self.footprint, + "footprintTemplate", + FOOTPRINT_OBLIGATION, + Some("replace"), + FOOTPRINT.domain, + "target-footprint", + )?; + validate_algebra( + &self.cost, + "costTemplate", + COST_OBLIGATION, + None, + COST.domain, + "target-cost", + )?; + + let profiles = exact_map( + field( + &self.operation_profiles, + "profiles", + "target-operation-profiles.profiles", + )?, + 1, + "target-operation-profiles.profiles", + )?; + expect_text( + field( + &self.operation_profiles, + "apiVersion", + "target-operation-profiles.apiVersion", + )?, + "edict.target-profile.operation-profiles/v1", + "target-operation-profiles.apiVersion", + )?; + let profile = entry( + profiles, + OPERATION_PROFILE, + "target-operation-profiles.profiles", + )?; + expect_text( + field( + profile, + "effectPredicate", + "target-operation-profile.effectPredicate", + )?, + "echo.dpo.operation-mode.replace-only/v1", + "target-operation-profile.effectPredicate", + )?; + let optic_template = field( + profile, + "opticTemplate", + "target-operation-profile.opticTemplate", + )?; + expect_text( + field( + optic_template, + "opticKind", + "target-operation-profile.opticTemplate.opticKind", + )?, + "affectReintegration", + "target-operation-profile.opticTemplate.opticKind", + )?; + expect_text( + field( + optic_template, + "boundaryKind", + "target-operation-profile.opticTemplate.boundaryKind", + )?, + "affect", + "target-operation-profile.opticTemplate.boundaryKind", + )?; + expect_text( + field( + optic_template, + "supportPolicy", + "target-operation-profile.opticTemplate.supportPolicy", + )?, + "continuum.support.carry-or-obstruct/v1", + "target-operation-profile.opticTemplate.supportPolicy", + )?; + expect_text( + field( + optic_template, + "lossDisposition", + "target-operation-profile.opticTemplate.lossDisposition", + )?, + "continuum.support.reject-on-loss/v1", + "target-operation-profile.opticTemplate.lossDisposition", + )?; + let aperture = field( + optic_template, + "apertureRequirement", + "target-operation-profile.apertureRequirement", + )?; + expect_text( + field(aperture, "kind", "target-operation-profile.aperture.kind")?, + "abstractFootprintObligation", + "target-operation-profile.aperture.kind", + )?; + expect_text( + field(aperture, "ref", "target-operation-profile.aperture.ref")?, + FOOTPRINT_OBLIGATION, + "target-operation-profile.aperture.ref", + )?; + + expect_text( + field(&self.verifier, "class", "target-verifier.class")?, + "declarative", + "target-verifier.class", + )?; + expect_text( + field(&self.verifier, "apiVersion", "target-verifier.apiVersion")?, + VERIFIER.domain, + "target-verifier.apiVersion", + )?; + expect_text( + field( + &self.verifier, + "targetProfile", + "target-verifier.targetProfile", + )?, + PROFILE.coordinate, + "target-verifier.targetProfile", + )?; + expect_text( + field( + &self.verifier, + "targetIrDomain", + "target-verifier.targetIrDomain", + )?, + TARGET_IR.domain, + "target-verifier.targetIrDomain", + )?; + assert_null_set( + field( + &self.verifier, + "capabilities", + "target-verifier.capabilities", + )?, + CAPABILITY, + "target-verifier.capabilities", + )?; + assert_null_set( + field( + &self.verifier, + "operationProfiles", + "target-verifier.operationProfiles", + )?, + OPERATION_PROFILE, + "target-verifier.operationProfiles", + )?; + let optic_contracts = exact_map( + field( + &self.verifier, + "opticContracts", + "target-verifier.opticContracts", + )?, + 1, + "target-verifier.opticContracts", + )?; + expect_text( + entry( + optic_contracts, + OPERATION_PROFILE, + "target-verifier.opticContracts", + )?, + OPTIC_CONTRACT, + "target-verifier.opticContracts.write", + )?; + + expect_text( + field(&self.target_ir, "class", "target-ir.class")?, + "declarative", + "target-ir.class", + )?; + expect_text( + field(&self.target_ir, "apiVersion", "target-ir.apiVersion")?, + TARGET_IR.domain, + "target-ir.apiVersion", + )?; + assert_null_set( + field(&self.target_ir, "capabilities", "target-ir.capabilities")?, + CAPABILITY, + "target-ir.capabilities", + )?; + expect_text( + field(&self.target_ir, "targetProfile", "target-ir.targetProfile")?, + PROFILE.coordinate, + "target-ir.targetProfile", + )?; + expect_text( + field(&self.target_ir, "domain", "target-ir.domain")?, + TARGET_IR.domain, + "target-ir.domain", + )?; + + expect_text( + field(&self.lawpack_verifier, "class", "lawpack-verifier.class")?, + "declarative", + "lawpack-verifier.class", + )?; + expect_text( + field( + &self.lawpack_verifier, + "apiVersion", + "lawpack-verifier.apiVersion", + )?, + "echo.edict-provider.lawpack-verifier/v1", + "lawpack-verifier.apiVersion", + )?; + let operation_obstructions = exact_map( + field( + &self.lawpack_verifier, + "operationObstructions", + "lawpack-verifier.operationObstructions", + )?, + 1, + "lawpack-verifier.operationObstructions", + )?; + let obstruction_rule = entry( + operation_obstructions, + OPERATION, + "lawpack-verifier.operationObstructions", + )?; + expect_text( + field( + obstruction_rule, + "effect", + "lawpack-verifier.operation.effect", + )?, + EFFECT, + "lawpack-verifier.operation.effect", + )?; + let failure_mappings = exact_map( + field( + obstruction_rule, + "failureMappings", + "lawpack-verifier.operation.failureMappings", + )?, + 1, + "lawpack-verifier.operation.failureMappings", + )?; + expect_text( + entry( + failure_mappings, + FAILURE, + "lawpack-verifier.operation.failureMappings", + )?, + DOMAIN_OBSTRUCTION, + "lawpack-verifier.operation.failureMappings.rejected", + )?; + + expect_text( + field(&self.obstructions, "class", "target-obstructions.class")?, + "declarative", + "target-obstructions.class", + )?; + expect_text( + field( + &self.obstructions, + "apiVersion", + "target-obstructions.apiVersion", + )?, + OBSTRUCTIONS.domain, + "target-obstructions.apiVersion", + )?; + let taxonomy_failures = exact_map( + field( + &self.obstructions, + "effectFailures", + "target-obstructions.effectFailures", + )?, + 1, + "target-obstructions.effectFailures", + )?; + let taxonomy_failure = entry( + taxonomy_failures, + FAILURE_PAYLOAD, + "target-obstructions.effectFailures", + )?; + expect_text( + field( + taxonomy_failure, + "payloadType", + "target-obstructions.effectFailure.payloadType", + )?, + FAILURE_PAYLOAD, + "target-obstructions.effectFailure.payloadType", + )?; + expect_text( + field( + taxonomy_failure, + "authorityClass", + "target-obstructions.effectFailure.authorityClass", + )?, + "domainMappable", + "target-obstructions.effectFailure.authorityClass", + )?; + let domain_obstructions = exact_map( + field( + &self.obstructions, + "domainObstructions", + "target-obstructions.domainObstructions", + )?, + 1, + "target-obstructions.domainObstructions", + )?; + let domain_obstruction = entry( + domain_obstructions, + DOMAIN_OBSTRUCTION, + "target-obstructions.domainObstructions", + )?; + expect_text( + field( + domain_obstruction, + "payloadSchema", + "target-obstructions.domainObstruction.payloadSchema", + )?, + "domain.WriteRejected.Payload", + "target-obstructions.domainObstruction.payloadSchema", + )?; + expect_text( + field( + domain_obstruction, + "authorityClass", + "target-obstructions.domainObstruction.authorityClass", + )?, + "domainMappable", + "target-obstructions.domainObstruction.authorityClass", + )?; + Ok(()) + } +} + +fn decode_pinned(spec: ResourceSpec) -> Result { + let value = decode_canonical_cbor_v1(spec.bytes).map_err(|_| invalid(spec.coordinate))?; + let digest = + digest_canonical_value_v1(spec.domain, &value).map_err(|_| invalid(spec.coordinate))?; + if digest != format!("sha256:{}", spec.framed_sha256) { + return Err(invalid(spec.coordinate)); + } + Ok(value) +} + +fn assert_ref( + reference: &CanonicalValueV1, + spec: ResourceSpec, + resource: &CanonicalValueV1, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let map = super::as_map(reference).ok_or_else(|| reference_mismatch(subject))?; + if map.len() != 2 { + return Err(reference_mismatch(subject)); + } + let id = super::map_field(reference, "id") + .and_then(super::as_text) + .ok_or_else(|| reference_mismatch(subject))?; + if id != spec.coordinate { + return Err(reference_mismatch(subject)); + } + let digest = match super::map_field(reference, "digest") { + Some(CanonicalValueV1::Array(digest)) if digest.len() == 2 => digest, + _ => return Err(reference_mismatch(subject)), + }; + if super::as_text(&digest[0]) != Some("sha256") { + return Err(reference_mismatch(subject)); + } + let CanonicalValueV1::Bytes(bytes) = &digest[1] else { + return Err(reference_mismatch(subject)); + }; + if bytes.len() != 32 { + return Err(reference_mismatch(subject)); + } + let expected = + digest_canonical_value_v1(spec.domain, resource).map_err(|_| invalid(subject))?; + let mut actual = String::with_capacity(71); + actual.push_str("sha256:"); + for byte in bytes { + write!(&mut actual, "{byte:02x}").map_err(|_| invalid(subject))?; + } + if actual != expected { + return Err(reference_mismatch(subject)); + } + Ok(()) +} + +fn assert_external_ref( + reference: &CanonicalValueV1, + coordinate: &str, + framed_sha256: &[u8; 32], + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let map = super::as_map(reference).ok_or_else(|| reference_mismatch(subject))?; + if map.len() != 2 + || super::map_field(reference, "id").and_then(super::as_text) != Some(coordinate) + { + return Err(reference_mismatch(subject)); + } + let digest = match super::map_field(reference, "digest") { + Some(CanonicalValueV1::Array(digest)) if digest.len() == 2 => digest, + _ => return Err(reference_mismatch(subject)), + }; + if super::as_text(&digest[0]) != Some("sha256") + || !matches!(&digest[1], CanonicalValueV1::Bytes(bytes) if bytes == framed_sha256) + { + return Err(reference_mismatch(subject)); + } + Ok(()) +} + +fn validate_algebra( + resource: &CanonicalValueV1, + template_field: &'static str, + obligation: &'static str, + write_class: Option<&'static str>, + api_version: &'static str, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + expect_text(field(resource, "class", subject)?, "declarative", subject)?; + expect_text( + field(resource, "apiVersion", subject)?, + api_version, + subject, + )?; + let capabilities = exact_map(field(resource, "capabilities", subject)?, 1, subject)?; + let capability = entry(capabilities, CAPABILITY, subject)?; + expect_text(field(capability, "effect", subject)?, EFFECT, subject)?; + expect_text( + field(capability, template_field, subject)?, + obligation, + subject, + )?; + expect_text( + field(capability, "semanticObligation", subject)?, + obligation, + subject, + )?; + if let Some(write_class) = write_class { + expect_text( + field(capability, "writeClass", subject)?, + write_class, + subject, + )?; + } + Ok(()) +} + +fn validate_generated_types( + generated_profile: &CanonicalValueV1, +) -> Result<(), SemanticResourceError> { + let types = exact_map( + field(generated_profile, "types", "generated.types")?, + 6, + "generated.types", + )?; + let id = exact_map( + entry(types, "a.b@1.Id", "generated.types")?, + 3, + "generated.types.Id", + )?; + expect_text( + entry(id, "kind", "generated.types.Id")?, + "coreStringAlias", + "generated.types.Id.kind", + )?; + expect_text( + entry(id, "canonical", "generated.types.Id")?, + "raw-utf8", + "generated.types.Id.canonical", + )?; + expect_integer( + entry(id, "maxScalarValues", "generated.types.Id")?, + 16, + "generated.types.Id.maxScalarValues", + )?; + validate_generated_record(types, "a.b@1.Input", Some("a.b@1.Id"))?; + validate_generated_record(types, "a.b@1.Output", Some("a.b@1.Id"))?; + validate_generated_record(types, "a.b@1.Receipt", Some("a.b@1.Id"))?; + validate_generated_record(types, FAILURE_PAYLOAD, None)?; + validate_generated_record(types, "domain.WriteRejected.Payload", None) +} + +fn validate_generated_record( + types: &[(CanonicalValueV1, CanonicalValueV1)], + coordinate: &'static str, + field_type: Option<&'static str>, +) -> Result<(), SemanticResourceError> { + let record = exact_map(entry(types, coordinate, coordinate)?, 2, coordinate)?; + expect_text(entry(record, "kind", coordinate)?, "record", coordinate)?; + let fields = exact_array( + entry(record, "fields", coordinate)?, + usize::from(field_type.is_some()), + coordinate, + )?; + if let Some(field_type) = field_type { + let field = exact_map(&fields[0], 2, coordinate)?; + expect_text(entry(field, "name", coordinate)?, "id", coordinate)?; + expect_text(entry(field, "type", coordinate)?, field_type, coordinate)?; + } + Ok(()) +} + +fn field<'a>( + value: &'a CanonicalValueV1, + name: &str, + subject: &'static str, +) -> Result<&'a CanonicalValueV1, SemanticResourceError> { + super::map_field(value, name).ok_or_else(|| invalid(subject)) +} + +fn exact_map<'a>( + value: &'a CanonicalValueV1, + len: usize, + subject: &'static str, +) -> Result<&'a [(CanonicalValueV1, CanonicalValueV1)], SemanticResourceError> { + let map = super::as_map(value).ok_or_else(|| invalid(subject))?; + if map.len() != len { + return Err(semantic_mismatch(subject)); + } + Ok(map) +} + +fn exact_array<'a>( + value: &'a CanonicalValueV1, + len: usize, + subject: &'static str, +) -> Result<&'a Vec, SemanticResourceError> { + let CanonicalValueV1::Array(array) = value else { + return Err(invalid(subject)); + }; + if array.len() != len { + return Err(semantic_mismatch(subject)); + } + Ok(array) +} + +fn entry<'a>( + map: &'a [(CanonicalValueV1, CanonicalValueV1)], + key: &str, + subject: &'static str, +) -> Result<&'a CanonicalValueV1, SemanticResourceError> { + map.iter() + .find_map(|(candidate, value)| (super::as_text(candidate) == Some(key)).then_some(value)) + .ok_or_else(|| semantic_mismatch(subject)) +} + +fn expect_text( + value: &CanonicalValueV1, + expected: &str, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let actual = super::as_text(value).ok_or_else(|| invalid(subject))?; + if actual != expected { + return Err(semantic_mismatch(subject)); + } + Ok(()) +} + +fn expect_bool( + value: &CanonicalValueV1, + expected: bool, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let CanonicalValueV1::Bool(actual) = value else { + return Err(invalid(subject)); + }; + if *actual != expected { + return Err(semantic_mismatch(subject)); + } + Ok(()) +} + +fn expect_integer( + value: &CanonicalValueV1, + expected: i128, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let CanonicalValueV1::Integer(actual) = value else { + return Err(invalid(subject)); + }; + if *actual != expected { + return Err(semantic_mismatch(subject)); + } + Ok(()) +} + +fn assert_null_set( + value: &CanonicalValueV1, + expected: &str, + subject: &'static str, +) -> Result<(), SemanticResourceError> { + let map = exact_map(value, 1, subject)?; + if !matches!(entry(map, expected, subject)?, CanonicalValueV1::Null) { + return Err(invalid(subject)); + } + Ok(()) +} + +const fn invalid(subject: &'static str) -> SemanticResourceError { + SemanticResourceError { + kind: SemanticResourceErrorKind::InvalidArtifact, + subject, + } +} + +const fn reference_mismatch(subject: &'static str) -> SemanticResourceError { + SemanticResourceError { + kind: SemanticResourceErrorKind::ReferenceMismatch, + subject, + } +} + +const fn semantic_mismatch(subject: &'static str) -> SemanticResourceError { + SemanticResourceError { + kind: SemanticResourceErrorKind::SemanticMismatch, + subject, + } +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::panic)] +mod tests { + use echo_edict_canonical::{digest_canonical_value_v1, encode_canonical_cbor_v1}; + use sha2::{Digest as _, Sha256}; + + use super::*; + + #[test] + fn packaged_semantic_resource_closure_is_admitted_deterministically() { + let first = admit_packaged_semantic_resources() + .expect("the exact packaged semantic resource closure is admitted"); + let second = admit_packaged_semantic_resources() + .expect("repeated semantic resource admission succeeds"); + assert_eq!(first, second); + } + + #[test] + fn packaged_raw_and_domain_framed_identities_are_distinct_and_pinned() { + let expected_raw = [ + ( + PROFILE, + "95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb", + ), + ( + LAWPACK, + "df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad", + ), + ( + GENERATED_PROFILE, + "8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3", + ), + ( + LAWPACK_EXPORTS, + "e80f3597830f30079bae7428cc6fe1ca27e9f2626614284338c5f5c7bfd9f8b8", + ), + ( + LAWPACK_ADAPTER, + "d0cf6a5310c7aa3d5bf291e40221a50a9be676d0e7f9947b71de0c52f03f9a41", + ), + ( + LAWPACK_VERIFIER, + "a0ca53486f2e1181027cab255792150735a68fc01a93669ab90b58e7c32bd36e", + ), + ( + INTRINSICS, + "d4080b044ad95d88763f605d25d678dad0d44bbfdc32c212334a962eaf456588", + ), + ( + FOOTPRINT, + "c08b853b22fa1920cb8971b60a6622e4d8f400c68b8027a9c4f246c5cb4a222c", + ), + ( + COST, + "29cc941cc96ffdb420a24a377146f0e9d5409a89f84cdbb2ad597e78dfde3503", + ), + ( + OBSTRUCTIONS, + "5eb85dc3fbb53890f4f5accf6a7a3c03889bd25bf00e7c2a23c43e497652328e", + ), + ( + OPERATION_PROFILES, + "3d560e8df1bfb38e8ba9b70b0a4bc843c848a7900e3af76d8f798db51b6b1077", + ), + ( + VERIFIER, + "9cef12fac8190156e152e7c8ab5adeed077bf2bdadf3add2225ce90ab4be8710", + ), + ( + TARGET_IR, + "f0ead7a5dcb41532d5df9067a9735d7078456b59cc267b3a9f6bb6437431000c", + ), + ]; + for (spec, raw_sha256) in expected_raw { + assert_eq!(hex::encode(Sha256::digest(spec.bytes)), raw_sha256); + let value = decode_canonical_cbor_v1(spec.bytes).expect("resource is canonical"); + assert_eq!( + digest_canonical_value_v1(spec.domain, &value) + .expect("resource has a framed digest"), + format!("sha256:{}", spec.framed_sha256) + ); + assert_ne!(raw_sha256, spec.framed_sha256); + } + } + + #[test] + fn malformed_shape_is_distinct_from_semantic_and_reference_disagreement() { + let malformed = expect_text(&CanonicalValueV1::Integer(1), "expected", "ordinary-field") + .expect_err("wrong CBOR type is malformed"); + assert_eq!(malformed.kind(), SemanticResourceErrorKind::InvalidArtifact); + + let disagreement = expect_text(&text("other"), "expected", "ordinary-field") + .expect_err("well-typed contradiction is semantic"); + assert_eq!( + disagreement.kind(), + SemanticResourceErrorKind::SemanticMismatch + ); + + let mut entries = vec![ + (text("id"), text(INTRINSICS.coordinate)), + ( + text("digest"), + CanonicalValueV1::Array(vec![text("sha512"), CanonicalValueV1::Bytes(vec![0; 32])]), + ), + ]; + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + let resource = decode_canonical_cbor_v1(INTRINSICS.bytes).expect("resource decodes"); + let reference = assert_ref( + &CanonicalValueV1::Map(entries), + INTRINSICS, + &resource, + "resource-reference", + ) + .expect_err("wrong digest algorithm is a reference mismatch"); + assert_eq!( + reference.kind(), + SemanticResourceErrorKind::ReferenceMismatch + ); + } + + #[test] + fn diagnostic_abi_reference_cannot_diverge_from_the_report_identity() { + let mut resources = ResourceSet::decode_packaged().expect("resources decode"); + set_ref_digest( + field_mut(&mut resources.profile, "diagnosticAbi"), + vec![0; 32], + ); + let profile_digest = digest_bytes(PROFILE.domain, &resources.profile); + let adapters = array_mut(field_mut(&mut resources.lawpack, "targetAdapters")); + set_ref_digest( + field_mut(&mut adapters[0], "acceptedTargetProfile"), + profile_digest, + ); + + let error = resources + .validate_references() + .expect_err("a rebound diagnostic ABI must fail closed"); + assert_eq!(error.kind(), SemanticResourceErrorKind::ReferenceMismatch); + assert_eq!(error.subject(), "target-profile.diagnosticAbi"); + } + + #[test] + fn rebound_intrinsic_capability_cannot_cross_the_adapter_boundary() { + let mut resources = ResourceSet::decode_packaged().expect("resources decode"); + rename_map_key( + field_mut(&mut resources.intrinsics, "intrinsics"), + CAPABILITY, + "echo.dpo@1.other", + ); + rebind_profile_field(&mut resources, "intrinsics", INTRINSICS.domain); + let error = resources + .validate_references() + .and_then(|()| resources.validate_semantics()) + .expect_err("rebound but contradictory intrinsic must fail"); + assert_eq!(error.kind(), SemanticResourceErrorKind::SemanticMismatch); + assert_eq!(error.subject(), "target-intrinsics.intrinsics"); + } + + #[test] + fn rebound_footprint_template_cannot_cross_the_algebra_boundary() { + let mut resources = ResourceSet::decode_packaged().expect("resources decode"); + let intrinsics = field_mut(&mut resources.intrinsics, "intrinsics"); + let intrinsic = field_mut(intrinsics, CAPABILITY); + *field_mut(intrinsic, "footprintTemplate") = text("target.replace.other-footprint"); + rebind_profile_field(&mut resources, "intrinsics", INTRINSICS.domain); + let error = resources + .validate_references() + .and_then(|()| resources.validate_semantics()) + .expect_err("rebound but contradictory footprint must fail"); + assert_eq!(error.kind(), SemanticResourceErrorKind::SemanticMismatch); + assert_eq!( + error.subject(), + "target-intrinsics.intrinsic.footprintTemplate" + ); + } + + #[test] + fn rebound_obstruction_mapping_cannot_cross_the_taxonomy_boundary() { + let mut resources = ResourceSet::decode_packaged().expect("resources decode"); + let operations = field_mut(&mut resources.lawpack_verifier, "operationObstructions"); + let operation = field_mut(operations, OPERATION); + let mappings = field_mut(operation, "failureMappings"); + *field_mut(mappings, FAILURE) = text("domain.Other"); + rebind_lawpack_ruleset(&mut resources); + let error = resources + .validate_references() + .and_then(|()| resources.validate_semantics()) + .expect_err("rebound but contradictory obstruction must fail"); + assert_eq!(error.kind(), SemanticResourceErrorKind::SemanticMismatch); + assert_eq!( + error.subject(), + "lawpack-verifier.operation.failureMappings.rejected" + ); + } + + fn rebind_profile_field(resources: &mut ResourceSet, name: &str, domain: &str) { + let canonical = canonical_round_trip(resources.intrinsics.clone()); + resources.intrinsics = canonical; + let digest = digest_bytes(domain, &resources.intrinsics); + set_ref_digest(field_mut(&mut resources.profile, name), digest); + let profile_digest = digest_bytes(PROFILE.domain, &resources.profile); + let adapters = array_mut(field_mut(&mut resources.lawpack, "targetAdapters")); + set_ref_digest( + field_mut(&mut adapters[0], "acceptedTargetProfile"), + profile_digest, + ); + } + + fn rebind_lawpack_ruleset(resources: &mut ResourceSet) { + resources.lawpack_verifier = canonical_round_trip(resources.lawpack_verifier.clone()); + let digest = digest_bytes(LAWPACK_VERIFIER.domain, &resources.lawpack_verifier); + let verifier = field_mut(&mut resources.lawpack, "verifier"); + set_ref_digest(field_mut(verifier, "ruleset"), digest); + } + + fn canonical_round_trip(value: CanonicalValueV1) -> CanonicalValueV1 { + let bytes = encode_canonical_cbor_v1(&value).expect("mutated resource encodes"); + decode_canonical_cbor_v1(&bytes).expect("mutated resource remains canonical") + } + + fn digest_bytes(domain: &str, value: &CanonicalValueV1) -> Vec { + let digest = digest_canonical_value_v1(domain, value).expect("mutated resource digests"); + hex::decode(digest.strip_prefix("sha256:").expect("digest is SHA-256")) + .expect("digest hexadecimal decodes") + } + + fn set_ref_digest(reference: &mut CanonicalValueV1, bytes: Vec) { + *field_mut(reference, "digest") = + CanonicalValueV1::Array(vec![text("sha256"), CanonicalValueV1::Bytes(bytes)]); + } + + fn rename_map_key(value: &mut CanonicalValueV1, old: &str, new: &str) { + let CanonicalValueV1::Map(entries) = value else { + panic!("expected map"); + }; + let (key, _) = entries + .iter_mut() + .find(|(key, _)| super::super::as_text(key) == Some(old)) + .expect("map key exists"); + *key = text(new); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + } + + fn field_mut<'a>(value: &'a mut CanonicalValueV1, name: &str) -> &'a mut CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("expected map"); + }; + entries + .iter_mut() + .find_map(|(key, value)| (super::super::as_text(key) == Some(name)).then_some(value)) + .expect("map field exists") + } + + fn array_mut(value: &mut CanonicalValueV1) -> &mut Vec { + let CanonicalValueV1::Array(values) = value else { + panic!("expected array"); + }; + values + } + + fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) + } +} diff --git a/crates/echo-edict-provider-verifier/tests/fixtures/edict-core.hex b/crates/echo-edict-provider-verifier/tests/fixtures/edict-core.hex new file mode 100644 index 00000000..78b4a161 --- /dev/null +++ b/crates/echo-edict-provider-verifier/tests/fixtures/edict-core.hex @@ -0,0 +1 @@ +a6657479706573a665496e707574a2646b696e64665265636f7264666669656c6473a16269646e612e6240312e496e7075742e6964664f7574707574a2646b696e64665265636f7264666669656c6473a16269646f612e6240312e4f75747075742e69646752656365697074a2646b696e64665265636f7264666669656c6473a162696470612e6240312e526563656970742e696468496e7075742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d75746638694f75747075742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d757466386a526563656970742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d7574663867696d706f7274738067696e74656e7473a16174a664626f6479a3656e6f64657381a5646b696e646665666665637465696e707574a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c64626964666566666563746e7461726765742e7265706c6163656762696e64696e67a3626964676c6f63616c2e3064747970656d612e6240312e5265636569707469616c7068614e616d6567246c6f63616c306e6f62737472756374696f6e4d6170a16872656a6563746564a26576616c7565a4646172677380646b696e646463616c6c6663616c6c656574646f6d61696e2e577269746552656a6563746564687479706541726773806662696e646572a36269646d6f62737472756374696f6e2e306474797065777461726765742e7265706c6163652e72656a656374656469616c7068614e616d656d246f62737472756374696f6e30666c6f63616c7383a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730a3626964676c6f63616c2e3064747970656d612e6240312e5265636569707469616c7068614e616d6567246c6f63616c30a36269646d6f62737472756374696f6e2e306474797065777461726765742e7265706c6163652e72656a656374656469616c7068614e616d656d246f62737472756374696f6e3066726573756c74a2646b696e64667265636f7264666669656c6473a1626964a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c6462696465696e7075746b612e6240312e496e707574666f75747075746c612e6240312e4f757470757470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073086e6d61784f75747075744279746573190100716d6178416c6c6f63617465644279746573190400781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e77726974652f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e61746565612e62403178187265717569726564436f72654361706162696c697469657380 diff --git a/crates/echo-edict-provider-verifier/tests/fixtures/edict-target-ir.hex b/crates/echo-edict-provider-verifier/tests/fixtures/edict-target-ir.hex new file mode 100644 index 00000000..a3acc0ff --- /dev/null +++ b/crates/echo-edict-provider-verifier/tests/fixtures/edict-target-ir.hex @@ -0,0 +1 @@ +a5646b696e64707461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763167696e74656e7473a16174a665737465707381a762696468742e737465702e3065696e707574a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c64626964666566666563746e7461726765742e7265706c6163656762696e64696e67a3626964676c6f63616c2e3064747970656d612e6240312e5265636569707469616c7068614e616d6567246c6f63616c306f6f62737472756374696f6e41726d73a16872656a6563746564a26576616c7565a4646172677380646b696e646463616c6c6663616c6c656574646f6d61696e2e577269746552656a6563746564687479706541726773806662696e646572a36269646d6f62737472756374696f6e2e306474797065777461726765742e7265706c6163652e72656a656374656469616c7068614e616d656d246f62737472756374696f6e306f746172676574496e7472696e736963726563686f2e64706f40312e7265706c616365736f62737472756374696f6e4661696c75726573816872656a656374656466726573756c74a2646b696e64667265636f7264666669656c6473a1626964a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c646269646c726571756972656d656e74738070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e77726974652f763174636f72654576616c756174696f6e427564676574a3686d61785374657073086e6d61784f75747075744279746573190100716d6178416c6c6f636174656442797465731904006d74617267657450726f66696c65a26269646a6563686f2e64706f40316664696765737482667368613235365820f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db074736f75726365436f7265436f6f7264696e61746565612e624031 diff --git a/crates/echo-edict-provider-verifier/tests/verifier_contract.rs b/crates/echo-edict-provider-verifier/tests/verifier_contract.rs new file mode 100644 index 00000000..801b3ba9 --- /dev/null +++ b/crates/echo-edict-provider-verifier/tests/verifier_contract.rs @@ -0,0 +1,1157 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Executable contract for the first pure Echo Edict provider verifier. + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, +}; +use echo_edict_provider_verifier::{ + verify, Artifact, BoundArtifact, DiagnosticSeverity, Digest, DigestAlgorithm, + ProtocolVersionV1, ProviderRefusalKind, ResourceRef, ResponseLimitsV1, SemanticInput, + SemanticInputKind, VerificationOutputKind, VerificationOutputRequest, VerificationRequestV1, + VerificationSuccessV1, +}; +use sha2::{Digest as _, Sha256}; + +const TARGET_PROFILE: &[u8] = include_bytes!("../resources/target-profile.echo-dpo.cbor"); +const LAWPACK: &[u8] = include_bytes!("../resources/lawpack.echo-dpo.cbor"); +const TARGET_AUTHORITY: &[u8] = include_bytes!("../resources/authority-facts.echo-dpo.cbor"); +const LAWPACK_AUTHORITY: &[u8] = include_bytes!("../resources/authority-facts.echo-lawpack.cbor"); + +const CORE_DOMAIN: &str = "edict.core.module/v1"; +const TARGET_PROFILE_DOMAIN: &str = "edict.target-profile/v1"; +const LAWPACK_DOMAIN: &str = "edict.lawpack/v1"; +const AUTHORITY_DOMAIN: &str = "edict.authority-facts/v1"; +const LOWERABILITY_DOMAIN: &str = "edict.lowering-requirements/v1"; +const TARGET_IR_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const REPORT_DOMAIN: &str = "echo.verifier-report/v1"; +const REPORT_ROLE: &str = "verifier-report.echo-dpo"; +const DIAGNOSTIC_ABI_DIGEST: &str = + "28fd72a98223153982ca084c29dbb1b2d430623967ab3b6db9d7fee668e614b9"; + +#[test] +fn vendored_wit_is_the_frozen_edict_verifier_contract() { + let bytes = include_bytes!("../wit/edict-target-provider.wit"); + assert_eq!(bytes.len(), 7392); + assert_eq!( + hex::encode(Sha256::digest(bytes)), + "2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e" + ); +} + +fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn map(entries: impl IntoIterator) -> CanonicalValueV1 { + let mut entries = entries + .into_iter() + .map(|(key, value)| (text(key), value)) + .collect::>(); + entries.sort_by_cached_key(|(key, _)| canonical_bytes(key)); + CanonicalValueV1::Map(entries) +} + +fn canonical_bytes(value: &CanonicalValueV1) -> Vec { + encode_canonical_cbor_v1(value).expect("test value has a canonical encoding") +} + +fn bound(coordinate: &str, domain: &str, bytes: impl Into>) -> BoundArtifact { + let bytes = bytes.into(); + let value = decode_canonical_cbor_v1(&bytes).expect("fixture is canonical CBOR"); + let digest = digest_canonical_value_v1(domain, &value) + .expect("fixture has a domain-framed digest") + .strip_prefix("sha256:") + .expect("fixture digest uses sha256") + .to_owned(); + BoundArtifact { + reference: ResourceRef { + coordinate: coordinate.to_owned(), + digest: Digest { + algorithm: DigestAlgorithm::Sha256, + bytes: hex::decode(digest).expect("fixture digest is hexadecimal"), + }, + }, + artifact: Artifact { + domain: domain.to_owned(), + bytes, + }, + } +} + +fn lowerability_value() -> CanonicalValueV1 { + let guard_kinds = || CanonicalValueV1::Array(vec![text("precommit-atomic")]); + let obstructions = || CanonicalValueV1::Array(vec![text("rejected")]); + let footprints = || CanonicalValueV1::Array(vec![text("target.replace.footprint")]); + let costs = || CanonicalValueV1::Array(vec![text("target.replace.cost")]); + map([ + ("apiVersion", text(LOWERABILITY_DOMAIN)), + ("operationProfile", text("continuum.profile.write/v1")), + ( + "semanticEffects", + CanonicalValueV1::Array(vec![map([ + ("coordinate", text("target.replace")), + ("writeClass", text("replace")), + ("guardKinds", guard_kinds()), + ("obstructionCoordinates", obstructions()), + ("footprintObligations", footprints()), + ("costObligations", costs()), + ])]), + ), + ( + "requiredWriteClasses", + CanonicalValueV1::Array(vec![text("replace")]), + ), + ("guardKinds", guard_kinds()), + ("atomicity", text("atomic")), + ("postconditionSupport", CanonicalValueV1::Bool(true)), + ("obstructionCoordinates", obstructions()), + ("footprintObligations", footprints()), + ("costObligations", costs()), + ("opticContract", text("replace-point")), + ]) +} + +fn limits(max_total_response_bytes: u64) -> ResponseLimitsV1 { + ResponseLimitsV1 { + max_output_count: 8, + max_diagnostic_count: 8, + max_total_response_bytes, + } +} + +fn request_with_target_ir( + target_ir_bytes: impl Into>, + response_limits: ResponseLimitsV1, +) -> VerificationRequestV1 { + VerificationRequestV1 { + protocol_version: ProtocolVersionV1 { + major: 1, + minor: 0, + patch: 0, + }, + core: bound( + "a.b@1", + CORE_DOMAIN, + hex::decode(include_str!("fixtures/edict-core.hex").trim()) + .expect("Core fixture hex is valid"), + ), + target_profile: bound("echo.dpo@1", TARGET_PROFILE_DOMAIN, TARGET_PROFILE.to_vec()), + target_ir: bound("echo.target-ir@1", TARGET_IR_DOMAIN, target_ir_bytes.into()), + semantic_inputs: vec![ + SemanticInput { + role: "authority-facts.echo-dpo".to_owned(), + kind: SemanticInputKind::AuthorityFacts, + artifact: bound( + "echo.dpo-authority-facts@1", + AUTHORITY_DOMAIN, + TARGET_AUTHORITY.to_vec(), + ), + }, + SemanticInput { + role: "authority-facts.echo-lawpack".to_owned(), + kind: SemanticInputKind::AuthorityFacts, + artifact: bound( + "echo.dpo-lawpack-authority-facts@1", + AUTHORITY_DOMAIN, + LAWPACK_AUTHORITY.to_vec(), + ), + }, + SemanticInput { + role: "lawpack.echo-dpo".to_owned(), + kind: SemanticInputKind::Lawpack, + artifact: bound("echo.dpo-lawpack@1", LAWPACK_DOMAIN, LAWPACK.to_vec()), + }, + SemanticInput { + role: "lowerability.echo-dpo".to_owned(), + kind: SemanticInputKind::LowerabilityFacts, + artifact: bound( + "echo.dpo-lowerability@1", + LOWERABILITY_DOMAIN, + canonical_bytes(&lowerability_value()), + ), + }, + ], + requested_outputs: vec![VerificationOutputRequest { + role: REPORT_ROLE.to_owned(), + kind: VerificationOutputKind::VerifierReport, + domain: REPORT_DOMAIN.to_owned(), + }], + limits: response_limits, + } +} + +fn request() -> VerificationRequestV1 { + request_with_target_ir( + hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) + .expect("Target IR fixture hex is valid"), + limits(64 * 1024), + ) +} + +fn core_value() -> CanonicalValueV1 { + decode_canonical_cbor_v1( + &hex::decode(include_str!("fixtures/edict-core.hex").trim()) + .expect("Core fixture hex is valid"), + ) + .expect("Core fixture is canonical") +} + +fn target_ir_value() -> CanonicalValueV1 { + decode_canonical_cbor_v1( + &hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) + .expect("Target IR fixture hex is valid"), + ) + .expect("Target IR fixture is canonical") +} + +fn bind_core(request: &mut VerificationRequestV1, core: &CanonicalValueV1) { + request.core = bound("a.b@1", CORE_DOMAIN, canonical_bytes(core)); +} + +fn bind_target_ir(request: &mut VerificationRequestV1, target_ir: &CanonicalValueV1) { + request.target_ir = bound( + "echo.target-ir@1", + TARGET_IR_DOMAIN, + canonical_bytes(target_ir), + ); +} + +fn map_field<'a>(value: &'a CanonicalValueV1, field: &str) -> &'a CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("value is not a map"); + }; + entries + .iter() + .find_map(|(key, value)| (key == &text(field)).then_some(value)) + .unwrap_or_else(|| panic!("map field {field} is absent")) +} + +fn map_field_mut<'a>(value: &'a mut CanonicalValueV1, field: &str) -> &'a mut CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("value is not a map"); + }; + entries + .iter_mut() + .find_map(|(key, value)| (key == &text(field)).then_some(value)) + .unwrap_or_else(|| panic!("map field {field} is absent")) +} + +fn array_mut(value: &mut CanonicalValueV1) -> &mut Vec { + let CanonicalValueV1::Array(values) = value else { + panic!("value is not an array"); + }; + values +} + +fn map_entries_mut(value: &mut CanonicalValueV1) -> &mut Vec<(CanonicalValueV1, CanonicalValueV1)> { + let CanonicalValueV1::Map(entries) = value else { + panic!("value is not a map"); + }; + entries +} + +fn insert_map_field(value: &mut CanonicalValueV1, field: &str, inserted: CanonicalValueV1) { + let entries = map_entries_mut(value); + entries.push((text(field), inserted)); + entries.sort_by_cached_key(|(key, _)| canonical_bytes(key)); +} + +fn remove_map_field(value: &mut CanonicalValueV1, field: &str) -> CanonicalValueV1 { + let entries = map_entries_mut(value); + let index = entries + .iter() + .position(|(key, _)| key == &text(field)) + .unwrap_or_else(|| panic!("map field {field} is absent")); + entries.remove(index).1 +} + +fn map_field_index(value: &CanonicalValueV1, field: &str) -> usize { + let CanonicalValueV1::Map(entries) = value else { + panic!("value is not a map"); + }; + entries + .iter() + .position(|(key, _)| key == &text(field)) + .unwrap_or_else(|| panic!("map field {field} is absent")) +} + +fn core_result_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + let intent = map_field_mut(map_field_mut(core, "intents"), "t"); + map_field_mut(map_field_mut(intent, "body"), "result") +} + +fn core_effect_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + let intent = map_field_mut(map_field_mut(core, "intents"), "t"); + let nodes = array_mut(map_field_mut(map_field_mut(intent, "body"), "nodes")); + &mut nodes[0] +} + +fn target_intent_mut(target_ir: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + map_field_mut(map_field_mut(target_ir, "intents"), "t") +} + +fn target_step_mut(target_ir: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + let steps = array_mut(map_field_mut(target_intent_mut(target_ir), "steps")); + &mut steps[0] +} + +fn assert_rejected(request: VerificationRequestV1, expected_code: &str) { + let target_ir_reference = request.target_ir.reference.clone(); + let success = verify(request).expect("semantic disagreement produces a verifier report"); + assert_eq!(success.diagnostics.len(), 1); + assert_eq!(success.diagnostics[0].severity, DiagnosticSeverity::Error); + assert_eq!(success.diagnostics[0].code, expected_code); + assert_report_common(&report(&success), &target_ir_reference, "rejected"); +} + +fn assert_accepted(success: &VerificationSuccessV1, target_ir_reference: &ResourceRef) { + assert!(success.diagnostics.is_empty()); + assert_report_common(&report(success), target_ir_reference, "accepted"); +} + +fn text_value(value: &CanonicalValueV1) -> &str { + let CanonicalValueV1::Text(value) = value else { + panic!("value is not text"); + }; + value +} + +fn report(success: &echo_edict_provider_verifier::VerificationSuccessV1) -> CanonicalValueV1 { + assert_eq!(success.outputs.len(), 1); + let output = &success.outputs[0]; + assert_eq!(output.role, REPORT_ROLE); + assert_eq!(output.kind, VerificationOutputKind::VerifierReport); + assert_eq!(output.artifact.domain, REPORT_DOMAIN); + assert_eq!(output.logical_path, None); + decode_canonical_cbor_v1(&output.artifact.bytes).expect("report is canonical CBOR") +} + +fn assert_report_common( + report: &CanonicalValueV1, + target_ir_reference: &ResourceRef, + expected_outcome: &str, +) { + let CanonicalValueV1::Map(fields) = report else { + panic!("report is not a map"); + }; + assert_eq!(fields.len(), 5); + assert_eq!( + text_value(map_field(report, "apiVersion")), + "echo.verifier-report/v1" + ); + assert_eq!( + map_field(report, "targetIr"), + &map([ + ("id", text(&target_ir_reference.coordinate)), + ( + "digest", + CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes(target_ir_reference.digest.bytes.clone()), + ]), + ), + ]) + ); + assert_eq!(text_value(map_field(report, "outcome")), expected_outcome); + let diagnostic_abi = map_field(report, "diagnosticAbi"); + assert_eq!( + text_value(map_field(diagnostic_abi, "id")), + "edict.diagnostics/v1" + ); + assert_eq!( + map_field(diagnostic_abi, "digest"), + &CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes( + hex::decode(DIAGNOSTIC_ABI_DIGEST).expect("diagnostic digest is valid") + ), + ]) + ); + assert_eq!( + map_field(report, "diagnosticBytes"), + &CanonicalValueV1::Bytes(Vec::new()) + ); +} + +#[test] +fn exact_one_operation_relation_is_accepted() { + let request = request(); + let target_ir_reference = request.target_ir.reference.clone(); + let success = verify(request).expect("the exact reviewed relation verifies"); + + assert!(success.diagnostics.is_empty()); + assert_report_common(&report(&success), &target_ir_reference, "accepted"); +} + +#[test] +fn report_diagnostic_abi_matches_the_admitted_target_profile() { + let target_profile = decode_canonical_cbor_v1(TARGET_PROFILE).expect("profile is canonical"); + let success = verify(request()).expect("the exact reviewed relation verifies"); + let report = report(&success); + + assert_eq!( + map_field(&report, "diagnosticAbi"), + map_field(&target_profile, "diagnosticAbi") + ); +} + +#[test] +fn packaged_semantic_resource_bytes_are_pinned() { + let resources = [ + ( + TARGET_PROFILE, + "95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb", + ), + ( + LAWPACK, + "df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad", + ), + ( + TARGET_AUTHORITY, + "d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86", + ), + ( + LAWPACK_AUTHORITY, + "3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96", + ), + ]; + + for (bytes, expected_sha256) in resources { + assert_eq!(hex::encode(Sha256::digest(bytes)), expected_sha256); + } +} + +#[test] +fn changed_target_intrinsic_is_rejected() { + let mut target_ir = decode_canonical_cbor_v1( + &hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) + .expect("Target IR fixture hex is valid"), + ) + .expect("Target IR fixture is canonical"); + let intent = map_field_mut(map_field_mut(&mut target_ir, "intents"), "t"); + let CanonicalValueV1::Array(steps) = map_field_mut(intent, "steps") else { + panic!("steps is not an array"); + }; + *map_field_mut(&mut steps[0], "targetIntrinsic") = text("echo.dpo@1.unreviewed"); + + let request = request_with_target_ir(canonical_bytes(&target_ir), limits(64 * 1024)); + let target_ir_reference = request.target_ir.reference.clone(); + let success = verify(request).expect("semantic disagreement produces a verifier report"); + + assert_eq!(success.diagnostics.len(), 1); + assert_eq!( + success.diagnostics[0].code, + "echo.verifier.target-intrinsic-mismatch" + ); + assert_eq!(success.diagnostics[0].severity, DiagnosticSeverity::Error); + assert_report_common(&report(&success), &target_ir_reference, "rejected"); +} + +#[test] +fn output_overclaim_names_the_first_unsupported_role() { + let mut case = request(); + case.requested_outputs.push(VerificationOutputRequest { + role: "verifier-report.unreviewed".to_owned(), + kind: VerificationOutputKind::VerifierReport, + domain: REPORT_DOMAIN.to_owned(), + }); + + let refusal = verify(case).expect_err("an undeclared second output must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedOutputRole); + assert_eq!( + refusal.subject.as_deref(), + Some("verifier-report.unreviewed") + ); +} + +#[test] +fn repeated_verification_is_byte_identical() { + let first = verify(request()).expect("first verification succeeds"); + let second = verify(request_with_target_ir( + hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) + .expect("Target IR fixture hex is valid"), + limits(1024 * 1024), + )) + .expect("second verification succeeds"); + + assert_eq!(first, second); + assert_eq!( + first.outputs[0].artifact.bytes, + second.outputs[0].artifact.bytes + ); +} + +#[test] +fn target_domain_and_profile_bindings_are_exact() { + let mut inner_domain = target_ir_value(); + *map_field_mut(&mut inner_domain, "domain") = text("echo.span-ir/v2"); + let mut case = request(); + bind_target_ir(&mut case, &inner_domain); + assert_rejected(case, "echo.verifier.target-domain-mismatch"); + + let mut profile_id = target_ir_value(); + *map_field_mut(map_field_mut(&mut profile_id, "targetProfile"), "id") = + text("echo.dpo@1.rogue"); + let mut case = request(); + bind_target_ir(&mut case, &profile_id); + assert_rejected(case, "echo.verifier.target-profile-mismatch"); + + let mut profile_digest = target_ir_value(); + let digest = map_field_mut( + map_field_mut(&mut profile_digest, "targetProfile"), + "digest", + ); + array_mut(digest)[1] = CanonicalValueV1::Bytes(vec![0; 32]); + let mut case = request(); + bind_target_ir(&mut case, &profile_digest); + assert_rejected(case, "echo.verifier.target-profile-mismatch"); + + let mut rogue_request = request(); + rogue_request.target_profile.reference.coordinate = "echo.dpo@1.rogue".to_owned(); + let refusal = verify(rogue_request).expect_err("an unauthorized supplied profile must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedTargetProfile); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.unsupported-target-profile" + ); +} + +#[test] +fn copied_budget_disagreement_is_rejected() { + let mut target_ir = target_ir_value(); + let budget = map_field_mut(target_intent_mut(&mut target_ir), "coreEvaluationBudget"); + *map_field_mut(budget, "maxSteps") = CanonicalValueV1::Integer(9); + let mut request = request(); + bind_target_ir(&mut request, &target_ir); + + assert_rejected(request, "echo.verifier.budget-mismatch"); +} + +#[test] +fn obstruction_relation_is_exact() { + let mut failures_dropped = target_ir_value(); + *map_field_mut( + target_step_mut(&mut failures_dropped), + "obstructionFailures", + ) = CanonicalValueV1::Array(Vec::new()); + let mut case = request(); + bind_target_ir(&mut case, &failures_dropped); + assert_rejected(case, "echo.verifier.obstruction-mismatch"); + + let mut arms_dropped = target_ir_value(); + *map_field_mut(target_step_mut(&mut arms_dropped), "obstructionArms") = + CanonicalValueV1::Map(Vec::new()); + let mut case = request(); + bind_target_ir(&mut case, &arms_dropped); + assert_rejected(case, "echo.verifier.obstruction-mismatch"); + + let mut both_dropped = target_ir_value(); + let step = target_step_mut(&mut both_dropped); + *map_field_mut(step, "obstructionFailures") = CanonicalValueV1::Array(Vec::new()); + *map_field_mut(step, "obstructionArms") = CanonicalValueV1::Map(Vec::new()); + let mut case = request(); + bind_target_ir(&mut case, &both_dropped); + assert_rejected(case, "echo.verifier.obstruction-mismatch"); + + let mut changed_arm = target_ir_value(); + let arms = map_field_mut(target_step_mut(&mut changed_arm), "obstructionArms"); + let arm = map_field_mut(arms, "rejected"); + *map_field_mut(map_field_mut(arm, "value"), "callee") = text("domain.Other"); + let mut case = request(); + bind_target_ir(&mut case, &changed_arm); + assert_rejected(case, "echo.verifier.obstruction-mismatch"); + + let mut extra_arm = target_ir_value(); + let step = target_step_mut(&mut extra_arm); + array_mut(map_field_mut(step, "obstructionFailures")).push(text("other")); + let existing_arm = map_field(map_field(step, "obstructionArms"), "rejected").clone(); + insert_map_field( + map_field_mut(step, "obstructionArms"), + "other", + existing_arm, + ); + let mut case = request(); + bind_target_ir(&mut case, &extra_arm); + assert_rejected(case, "echo.verifier.obstruction-mismatch"); +} + +#[test] +fn target_only_claims_are_rejected() { + let mut extra_intent = target_ir_value(); + let existing_intent = map_field(map_field(&extra_intent, "intents"), "t").clone(); + insert_map_field( + map_field_mut(&mut extra_intent, "intents"), + "u", + existing_intent, + ); + let mut case = request(); + bind_target_ir(&mut case, &extra_intent); + assert_rejected(case, "echo.verifier.introduced-claim"); + + let mut extra_step = target_ir_value(); + let steps = array_mut(map_field_mut(target_intent_mut(&mut extra_step), "steps")); + steps.push(steps[0].clone()); + let mut case = request(); + bind_target_ir(&mut case, &extra_step); + assert_rejected(case, "echo.verifier.introduced-claim"); + + let mut extra_requirement = target_ir_value(); + let requirement = map([ + ("id", text("guard.0")), + ("predicate", map([("kind", text("true"))])), + ( + "onFailure", + map([ + ("kind", text("terminal")), + ( + "reason", + map([ + ("reasonKind", text("domain.GuardRejected")), + ("payload", CanonicalValueV1::Map(Vec::new())), + ]), + ), + ]), + ), + ]); + array_mut(map_field_mut( + target_intent_mut(&mut extra_requirement), + "requirements", + )) + .push(requirement); + let mut case = request(); + bind_target_ir(&mut case, &extra_requirement); + assert_rejected(case, "echo.verifier.introduced-claim"); + + let mut unsupported_result = target_ir_value(); + let reviewed_result = map_field(target_intent_mut(&mut unsupported_result), "result").clone(); + *map_field_mut(target_intent_mut(&mut unsupported_result), "result") = map([ + ("kind", text("if")), + ("predicate", map([("kind", text("true"))])), + ("then", reviewed_result.clone()), + ("else", reviewed_result), + ]); + let mut case = request(); + bind_target_ir(&mut case, &unsupported_result); + assert_rejected(case, "echo.verifier.result-mismatch"); +} + +#[test] +fn silent_loss_is_rejected() { + let mut missing_intent = target_ir_value(); + remove_map_field(map_field_mut(&mut missing_intent, "intents"), "t"); + let mut case = request(); + bind_target_ir(&mut case, &missing_intent); + assert_rejected(case, "echo.verifier.silent-loss"); + + let mut missing_step = target_ir_value(); + array_mut(map_field_mut(target_intent_mut(&mut missing_step), "steps")).clear(); + let mut case = request(); + bind_target_ir(&mut case, &missing_step); + assert_rejected(case, "echo.verifier.silent-loss"); +} + +#[test] +fn well_formed_input_constraint_is_semantic_disagreement_not_malformed() { + let mut target_ir = target_ir_value(); + array_mut(map_field_mut( + target_intent_mut(&mut target_ir), + "inputConstraints", + )) + .push(map([ + ("coordinate", text("constraint.0")), + ("source", text("where")), + ("predicate", map([("kind", text("true"))])), + ])); + + let mut case = request(); + bind_target_ir(&mut case, &target_ir); + assert_rejected(case, "echo.verifier.semantic-relation-mismatch"); +} + +#[test] +fn canonical_decoding_does_not_admit_a_malformed_target_ir() { + let mut missing_kind = target_ir_value(); + remove_map_field(&mut missing_kind, "kind"); + let mut invalid_profile_ref = target_ir_value(); + *map_field_mut( + map_field_mut(&mut invalid_profile_ref, "targetProfile"), + "digest", + ) = text("not-a-digest"); + let mut invalid_step = target_ir_value(); + *map_field_mut(target_intent_mut(&mut invalid_step), "steps") = + CanonicalValueV1::Array(vec![CanonicalValueV1::Null]); + let mut malformed_target_irs = vec![ + CanonicalValueV1::Text("not-target-ir".to_owned()), + missing_kind, + invalid_profile_ref, + invalid_step, + ]; + for malformed_expr in [ + map([("kind", text("unknown"))]), + map([("kind", text("variant"))]), + map([("kind", text("match"))]), + map([("kind", text("list"))]), + map([("kind", text("map"))]), + map([("kind", text("if"))]), + map([ + ("kind", text("if")), + ("predicate", map([("kind", text("true"))])), + ( + "then", + map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("valid"))]), + ), + ]), + ), + ("else", map([("kind", text("variant"))])), + ]), + ] { + let mut invalid_result = target_ir_value(); + *map_field_mut(target_intent_mut(&mut invalid_result), "result") = malformed_expr; + malformed_target_irs.push(invalid_result); + } + let malformed_requirement = |predicate, on_failure| { + let mut target_ir = target_ir_value(); + array_mut(map_field_mut( + target_intent_mut(&mut target_ir), + "requirements", + )) + .push(map([ + ("id", text("guard.0")), + ("predicate", predicate), + ("onFailure", on_failure), + ])); + target_ir + }; + let valid_on_failure = map([ + ("kind", text("terminal")), + ( + "reason", + map([ + ("reasonKind", text("domain.GuardRejected")), + ("payload", CanonicalValueV1::Map(Vec::new())), + ]), + ), + ]); + malformed_target_irs.push(malformed_requirement( + map([("kind", text("if"))]), + valid_on_failure, + )); + malformed_target_irs.push(malformed_requirement( + map([("kind", text("true"))]), + map([("kind", text("terminal"))]), + )); + let malformed_constraint = |predicate| { + let mut target_ir = target_ir_value(); + array_mut(map_field_mut( + target_intent_mut(&mut target_ir), + "inputConstraints", + )) + .push(map([ + ("coordinate", text("constraint.0")), + ("source", text("where")), + ("predicate", predicate), + ])); + target_ir + }; + malformed_target_irs.push(malformed_constraint(map([("kind", text("if"))]))); + let mut deep_predicate = map([("kind", text("true"))]); + for _ in 0..72 { + deep_predicate = map([("kind", text("not")), ("value", deep_predicate)]); + } + malformed_target_irs.push(malformed_constraint(deep_predicate)); + let mut invalid_failure_start = target_ir_value(); + *map_field_mut( + target_step_mut(&mut invalid_failure_start), + "obstructionFailures", + ) = CanonicalValueV1::Array(vec![text("1bad")]); + malformed_target_irs.push(invalid_failure_start); + let mut invalid_failure_continuation = target_ir_value(); + let CanonicalValueV1::Map(arms) = map_field_mut( + target_step_mut(&mut invalid_failure_continuation), + "obstructionArms", + ) else { + panic!("obstruction arms are a map"); + }; + arms[0].0 = text("bad-name"); + malformed_target_irs.push(invalid_failure_continuation); + let mut invalid_obstruction_predicate = target_ir_value(); + let reviewed_result = map_field( + target_intent_mut(&mut invalid_obstruction_predicate), + "result", + ) + .clone(); + *map_field_mut( + target_intent_mut(&mut invalid_obstruction_predicate), + "result", + ) = map([ + ("kind", text("if")), + ( + "predicate", + map([ + ("kind", text("obstruction")), + ("coordinate", text("bad-name")), + ( + "payload", + map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("payload"))]), + ), + ]), + ), + ]), + ), + ("then", reviewed_result.clone()), + ("else", reviewed_result), + ]); + malformed_target_irs.push(invalid_obstruction_predicate); + + for malformed in malformed_target_irs { + let mut case = request(); + bind_target_ir(&mut case, &malformed); + let refusal = verify(case).expect_err("malformed Target IR must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.invalid-semantic-artifact" + ); + } +} + +#[test] +fn semantic_guard_and_footprint_declarations_are_pinned() { + let profile = decode_canonical_cbor_v1(TARGET_PROFILE).expect("target profile is canonical"); + assert_eq!( + text_value(map_field(&profile, "guardEvaluation")), + "precommit-atomic" + ); + assert_eq!( + text_value(map_field(map_field(&profile, "footprintAlgebra"), "id")), + "echo.dpo.footprint/v1" + ); + let lowerability = lowerability_value(); + assert_eq!( + map_field(&lowerability, "guardKinds"), + &CanonicalValueV1::Array(vec![text("precommit-atomic")]) + ); + assert_eq!( + map_field(&lowerability, "footprintObligations"), + &CanonicalValueV1::Array(vec![text("target.replace.footprint")]) + ); + + let mut changed_guard = lowerability_value(); + *map_field_mut(&mut changed_guard, "guardKinds") = + CanonicalValueV1::Array(vec![text("postcommit")]); + let mut case = request(); + case.semantic_inputs[3].artifact = bound( + "echo.dpo-lowerability@1", + LOWERABILITY_DOMAIN, + canonical_bytes(&changed_guard), + ); + let refusal = verify(case).expect_err("changed guard obligations must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.unsupported-semantics" + ); + + let mut changed_footprint = lowerability_value(); + *map_field_mut(&mut changed_footprint, "footprintObligations") = + CanonicalValueV1::Array(vec![text("target.replace.unreviewed-footprint")]); + let mut case = request(); + case.semantic_inputs[3].artifact = bound( + "echo.dpo-lowerability@1", + LOWERABILITY_DOMAIN, + canonical_bytes(&changed_footprint), + ); + let refusal = verify(case).expect_err("changed footprint obligations must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.unsupported-semantics" + ); +} + +#[test] +fn effect_input_relation_is_dynamic_and_exact() { + let alternate_input = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("changed"))]), + ), + ]); + let mut core = core_value(); + *map_field_mut(core_effect_mut(&mut core), "input") = alternate_input.clone(); + let mut correlated_target = target_ir_value(); + *map_field_mut(target_step_mut(&mut correlated_target), "input") = alternate_input; + let mut correlated = request(); + bind_core(&mut correlated, &core); + bind_target_ir(&mut correlated, &correlated_target); + let target_ir_reference = correlated.target_ir.reference.clone(); + let success = verify(correlated).expect("a correlated effect input change must verify"); + assert_accepted(&success, &target_ir_reference); + + let mut target_only = request(); + bind_target_ir(&mut target_only, &correlated_target); + assert_rejected(target_only, "echo.verifier.effect-input-mismatch"); +} + +#[test] +fn string_bound_counts_unicode_scalar_values() { + let authored = "🦀".repeat(16); + let bounded_input = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text(&authored))]), + ), + ]); + let mut core = core_value(); + *map_field_mut(core_effect_mut(&mut core), "input") = bounded_input.clone(); + let mut target_ir = target_ir_value(); + *map_field_mut(target_step_mut(&mut target_ir), "input") = bounded_input; + let mut case = request(); + bind_core(&mut case, &core); + bind_target_ir(&mut case, &target_ir); + let target_ir_reference = case.target_ir.reference.clone(); + + let success = verify(case).expect("sixteen Unicode scalar values remain within the bound"); + assert_accepted(&success, &target_ir_reference); + + let overlong = "🦀".repeat(17); + let overlong_input = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text(&overlong))]), + ), + ]); + let mut core = core_value(); + *map_field_mut(core_effect_mut(&mut core), "input") = overlong_input.clone(); + let mut target_ir = target_ir_value(); + *map_field_mut(target_step_mut(&mut target_ir), "input") = overlong_input; + let mut case = request(); + bind_core(&mut case, &core); + bind_target_ir(&mut case, &target_ir); + + let refusal = verify(case).expect_err("seventeen Unicode scalar values must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.unsupported-semantics" + ); +} + +#[test] +fn type_wrong_correlated_artifacts_refuse_instead_of_agreeing() { + let mut wrong_effect_core = core_value(); + let input_local = { + let intent = map_field(map_field(&wrong_effect_core, "intents"), "t"); + let body = map_field(intent, "body"); + let CanonicalValueV1::Array(locals) = map_field(body, "locals") else { + panic!("Core locals are not an array"); + }; + locals[0].clone() + }; + let record_typed_input = map([("kind", text("local")), ("ref", input_local)]); + *map_field_mut(core_effect_mut(&mut wrong_effect_core), "input") = record_typed_input.clone(); + let mut wrong_effect_target = target_ir_value(); + *map_field_mut(target_step_mut(&mut wrong_effect_target), "input") = record_typed_input; + let mut wrong_effect = request(); + bind_core(&mut wrong_effect, &wrong_effect_core); + bind_target_ir(&mut wrong_effect, &wrong_effect_target); + let refusal = verify(wrong_effect).expect_err("a record cannot become an effect scalar"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + + let scalar_result = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("wrong-shape"))]), + ), + ]); + let mut wrong_result_core = core_value(); + *core_result_mut(&mut wrong_result_core) = scalar_result.clone(); + let mut wrong_result_target = target_ir_value(); + *map_field_mut(target_intent_mut(&mut wrong_result_target), "result") = scalar_result; + let mut wrong_result = request(); + bind_core(&mut wrong_result, &wrong_result_core); + bind_target_ir(&mut wrong_result, &wrong_result_target); + let refusal = verify(wrong_result).expect_err("a scalar cannot become the output record"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + + for local_index in [0, 1] { + let mut wrong_nominal_core = core_value(); + let local = { + let intent = map_field(map_field(&wrong_nominal_core, "intents"), "t"); + let body = map_field(intent, "body"); + let CanonicalValueV1::Array(locals) = map_field(body, "locals") else { + panic!("Core locals are not an array"); + }; + locals[local_index].clone() + }; + let nominally_wrong = map([("kind", text("local")), ("ref", local)]); + *core_result_mut(&mut wrong_nominal_core) = nominally_wrong.clone(); + let mut wrong_nominal_target = target_ir_value(); + *map_field_mut(target_intent_mut(&mut wrong_nominal_target), "result") = nominally_wrong; + let mut case = request(); + bind_core(&mut case, &wrong_nominal_core); + bind_target_ir(&mut case, &wrong_nominal_target); + let refusal = verify(case).expect_err("Input and Receipt are not Output records"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + } +} + +#[test] +fn relation_is_derived_from_the_supplied_core_result() { + let mut core = core_value(); + let intent = map_field_mut(map_field_mut(&mut core, "intents"), "t"); + let body = map_field_mut(intent, "body"); + let (input_local, receipt_local) = { + let CanonicalValueV1::Array(locals) = map_field(body, "locals") else { + panic!("Core locals are not an array"); + }; + (locals[0].clone(), locals[1].clone()) + }; + let result_from = |local| { + map([ + ("kind", text("record")), + ( + "fields", + map([( + "id", + map([ + ("kind", text("field")), + ("base", map([("kind", text("local")), ("ref", local)])), + ("field", text("id")), + ]), + )]), + ), + ]) + }; + let original_result = map_field(body, "result").clone(); + let input_result = result_from(input_local); + let alternate_result = if input_result == original_result { + result_from(receipt_local) + } else { + input_result + }; + *map_field_mut(body, "result") = alternate_result.clone(); + + let mut correlated_target = target_ir_value(); + *map_field_mut(target_intent_mut(&mut correlated_target), "result") = alternate_result; + let mut correlated_request = request(); + bind_core(&mut correlated_request, &core); + bind_target_ir(&mut correlated_request, &correlated_target); + let target_ir_reference = correlated_request.target_ir.reference.clone(); + let success = verify(correlated_request).expect("a correlated result change must verify"); + assert_accepted(&success, &target_ir_reference); + + let mut one_sided_request = request(); + bind_target_ir(&mut one_sided_request, &correlated_target); + assert_rejected(one_sided_request, "echo.verifier.result-mismatch"); +} + +#[test] +fn recursive_expression_dispatch_is_bounded_and_order_independent() { + let core = core_value(); + let result = map_field( + map_field(map_field(map_field(&core, "intents"), "t"), "body"), + "result", + ); + assert!(map_field_index(result, "kind") < map_field_index(result, "fields")); + let CanonicalValueV1::Map(fields) = map_field(result, "fields") else { + panic!("result fields are not a map"); + }; + let nested_field = &fields[0].1; + assert!(map_field_index(nested_field, "base") < map_field_index(nested_field, "kind")); + verify(request()).expect("both canonical map orders must verify"); + + for (malformed, expected_code) in [ + ( + map([("value", text("missing discriminator"))]), + "echo.verifier.invalid-expression-discriminator", + ), + ( + map([("kind", text("unknown"))]), + "echo.verifier.invalid-expression-discriminator", + ), + ( + map([ + ("kind", text("local")), + ("fields", CanonicalValueV1::Map(Vec::new())), + ]), + "echo.verifier.invalid-expression-shape", + ), + ] { + let mut malformed_core = core_value(); + *core_result_mut(&mut malformed_core) = malformed; + let mut request = request(); + bind_core(&mut request, &malformed_core); + let refusal = verify(request).expect_err("a malformed expression must refuse"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!(refusal.diagnostics[0].code, expected_code); + } + + let mut deep_child = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("leaf"))]), + ), + ]); + for _ in 0..72 { + deep_child = map([ + ("kind", text("field")), + ("base", deep_child), + ("field", text("x")), + ]); + } + let malicious_outer = map([("kind", text("unknown")), ("base", deep_child.clone())]); + let mut malicious_core = core_value(); + *core_result_mut(&mut malicious_core) = malicious_outer; + let mut mismatching_request = request(); + bind_core(&mut mismatching_request, &malicious_core); + let refusal = + verify(mismatching_request).expect_err("the outer discriminator must fail closed"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.invalid-expression-discriminator" + ); + + let mut malicious_core = core_value(); + *core_result_mut(&mut malicious_core) = deep_child; + let mut deep_request = request(); + bind_core(&mut deep_request, &malicious_core); + let refusal = verify(deep_request).expect_err("the recursive field chain must fail closed"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!( + refusal.diagnostics[0].code, + "echo.verifier.expression-depth-exceeded" + ); +} + +#[test] +fn native_result_is_invariant_under_all_host_limits() { + let expected = verify(request()).expect("baseline verification succeeds"); + let mut zero_limits = request(); + zero_limits.limits = ResponseLimitsV1 { + max_output_count: 0, + max_diagnostic_count: 0, + max_total_response_bytes: 0, + }; + + assert_eq!( + verify(zero_limits).expect("limits are enforced by the host"), + expected + ); +} diff --git a/crates/echo-edict-provider-verifier/wit/edict-target-provider.wit b/crates/echo-edict-provider-verifier/wit/edict-target-provider.wit new file mode 100644 index 00000000..0efa5903 --- /dev/null +++ b/crates/echo-edict-provider-verifier/wit/edict-target-provider.wit @@ -0,0 +1,221 @@ +// edict-target-provider.wit +// +// Runtime-neutral component boundary for provider-owned lowerer and verifier +// components. The transport carries opaque artifact bytes. The artifact domain +// selects the separately versioned canonical schema; this WIT package does not +// redefine Core, lawpack, target-profile, authority, lowerability, Target IR, +// or verifier-report semantics. +// +// Every input artifact is bound to a host-verified resource reference. Output +// artifacts deliberately carry no digest: a future Edict host must validate +// their declared role and domain, check canonicality under the owning schema, +// and compute their authoritative digest after component invocation. + +package edict:target-provider@1.0.0; + +interface protocol { + // This value is carried in each request so pure envelope validation can + // reject a protocol mismatch before component execution. It must agree with + // this package's semantic version. + record protocol-version-v1 { + major: u32, + minor: u32, + patch: u32, + } + + enum digest-algorithm { + sha256, + } + + record digest { + algorithm: digest-algorithm, + bytes: list, + } + + record resource-ref { + coordinate: string, + digest: digest, + } + + record artifact { + domain: string, + bytes: list, + } + + record bound-artifact { + reference: resource-ref, + artifact: artifact, + } + + // The target profile and Core are explicit request fields. This list carries + // the deterministic closure of every other semantic input needed by the + // selected component. Auxiliary kinds remain role- and domain-constrained by + // the host; they are not ambient discovery handles. + variant semantic-input-kind { + lawpack, + authority-facts, + lowerability-facts, + auxiliary(string), + } + + record semantic-input { + role: string, + kind: semantic-input-kind, + artifact: bound-artifact, + } + + // Every role in this package is nonempty. Every semantic-inputs list is + // strictly ascending by the UTF-8 bytes of its unique role. The future host + // rejects empty, duplicate, or out-of-order roles. + + // The separate output vocabularies make lowerer and verifier authority + // structural. A lowerer cannot claim verifier evidence, and a verifier + // cannot claim Target IR or generated artifacts. + enum lowering-output-kind { + target-ir, + generated-artifact, + review-payload, + } + + record lowering-output-request { + role: string, + kind: lowering-output-kind, + domain: string, + } + + // Every requested-outputs list and every successful outputs list is strictly + // ascending by the UTF-8 bytes of its unique role. Success contains exactly + // one output for every request and no others. Each output's role, kind, and + // artifact.domain must equal its request's role, kind, and domain. + + // No digest field is permitted here. A provider cannot notarize its own + // bytes; the future host validates an optional logical package-relative path + // and recomputes output identity after validation. A logical path is + // nonempty UTF-8, uses `/` separators, has no leading slash, backslash, colon, + // empty segment, `.` segment, or `..` segment, and is never resolved through + // a symlink by the validator. Present logical paths within one response are + // unique by exact, case-sensitive UTF-8 bytes with no Unicode normalization. + record lowering-output-artifact { + role: string, + kind: lowering-output-kind, + artifact: artifact, + logical-path: option, + } + + enum verification-output-kind { + verifier-report, + } + + record verification-output-request { + role: string, + kind: verification-output-kind, + domain: string, + } + + record verification-output-artifact { + role: string, + kind: verification-output-kind, + artifact: artifact, + logical-path: option, + } + + // These bounds apply to both success and refusal. Max-output-count counts the + // success outputs list (zero for refusal), and max-diagnostic-count counts the + // diagnostics list on either result arm. Max-total-response-bytes is the + // checked-u64 sum of every provider-authored list length and every + // provider-authored string's UTF-8 byte length in the selected result arm: + // output role/domain/artifact bytes/logical path, diagnostic + // code/message/repair, and refusal subject. Enum values, numeric values, and + // canonical-ABI framing add zero. Sum overflow or a value above any bound is + // a host-owned response-limit failure. A zero bound permits only zero counted + // items or bytes. A provider's canonical result is independent of these + // limits. For requests otherwise identical, if that result fits both the old + // and new bounds, the selected result arm and every provider-authored field + // must be byte-identical. A provider must not substitute, truncate, reorder, + // or change result arms to fit a smaller bound. Component memory, fuel, and + // interruption remain host-only. + record response-limits-v1 { + max-output-count: u32, + max-diagnostic-count: u32, + max-total-response-bytes: u64, + } + + enum diagnostic-severity { + error, + warning, + info, + } + + record diagnostic { + code: string, + severity: diagnostic-severity, + message: string, + repair: option, + } + + // Every diagnostics list is strictly ascending by the tuple (code UTF-8 + // bytes, severity declaration index, message UTF-8 bytes, repair), with none + // before some and present repair ordered by UTF-8 bytes. Exact duplicates are + // therefore invalid. + + enum provider-refusal-kind { + unsupported-core-abi, + unsupported-target-profile, + unsupported-semantics, + unsupported-output-role, + invalid-semantic-artifact, + } + + // Provider refusal is target-owned evidence. Component loading, denied + // imports, traps, exhaustion, malformed envelopes, binding mismatches, and + // replay mismatches are distinct host-owned failure categories. + record provider-refusal-v1 { + kind: provider-refusal-kind, + subject: option, + diagnostics: list, + } + + record lowering-request-v1 { + protocol-version: protocol-version-v1, + core: bound-artifact, + target-profile: bound-artifact, + semantic-inputs: list, + requested-outputs: list, + limits: response-limits-v1, + } + + record verification-request-v1 { + protocol-version: protocol-version-v1, + core: bound-artifact, + target-profile: bound-artifact, + target-ir: bound-artifact, + semantic-inputs: list, + requested-outputs: list, + limits: response-limits-v1, + } + + record lowering-success-v1 { + outputs: list, + diagnostics: list, + } + + record verification-success-v1 { + outputs: list, + diagnostics: list, + } + + type lowering-result-v1 = result; + type verification-result-v1 = result; +} + +world lowerer { + use protocol.{lowering-request-v1, lowering-result-v1}; + + export lower: func(request: lowering-request-v1) -> lowering-result-v1; +} + +world verifier { + use protocol.{verification-request-v1, verification-result-v1}; + + export verify: func(request: verification-request-v1) -> verification-result-v1; +} diff --git a/det-policy.yaml b/det-policy.yaml index bb92a2d3..fd4135b8 100644 --- a/det-policy.yaml +++ b/det-policy.yaml @@ -46,7 +46,11 @@ crates: echo-edict-provider-lowerer: class: DET_CRITICAL owner_role: "Tooling Engineer" - paths: ["crates/echo-edict-provider-lowerer/**", "schemas/edict-provider/components/v1/**", "tests/edict-provider-host-v1/**"] + paths: ["crates/echo-edict-provider-lowerer/**", "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm", "tests/edict-provider-host-v1/**"] + echo-edict-provider-verifier: + class: DET_CRITICAL + owner_role: "Tooling Engineer" + paths: ["crates/echo-edict-provider-verifier/**", "schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm", "tests/edict-provider-host-v1/**"] echo-runtime-schema: class: DET_CRITICAL owner_role: "Architect" diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index e914c2b5..945d7355 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -218,6 +218,23 @@ crossing proves translation only. Package admission, installation, invocation, commitment, observation, and receipt authority remain separate Echo runtime crossings. +The first executable verifier independently implements the frozen +`edict:target-provider/verifier@1.0.0` world. It compares the exact Core and +Target IR relation under the target profile and ordered semantic inputs rather +than trusting the lowerer's construction. The pinned Edict host preflights the +request artifacts and declared output schema before invoking the checked +verifier. After invocation, it schema-validates each returned accepted or +well-formed rejected report before authoring its manifest. An unsupported +output-role overclaim remains a typed provider refusal with neither response nor +manifest. Independent fresh-store replay and separate host processes reproduce +all three completed outcomes identically. The report binds its named Target IR +reference; the +host-authored manifest separately binds the Core, target profile, Target IR, +semantic inputs, output request, output bytes, and domain-framed output digest. +This crossing proves provider semantic verification and host replay only. It +does not install a package, authorize or execute an operation, observe a +consequence, or mint an Echo receipt. + The package-root projection pins `echo.edict-provider@1` to exact component world `edict:target-provider@1.0.0`. Generated provenance is a generic Edict `generationProvenance` package member whose document contract remains owned by @@ -305,8 +322,9 @@ crosses the Edict provider WIT boundary and is validated through the declared This source contract does not claim that Echo currently executes the declared replace operation. Runtime mutation, admission, receipts, and presence-sensitive -replace enforcement remain runtime implementation authority and must be proven -by the lowerer, verifier, and admitted-execution slices. +replace enforcement remain runtime implementation authority and must still be +proven by admitted execution. The completed lowerer and verifier witnesses prove +their translation, semantic-verification, and Edict-host replay crossings only. Applications own: diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index 5f7c31d4..829f3fb8 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -29,9 +29,11 @@ each semantic fact. In the first compatibility closure: outer `edict.target-ir.artifact/v1` provider domain and `target-ir-artifact` root; - runtime GraphQL owns none of these first-operation facts; and -- runtime implementation authority for actual replace execution remains - unresolved until the lowerer and verifier slices prove presence-sensitive - replace behavior. +- the checked lowerer and verifier prove the bounded provider translation and + semantic-verification crossings, while runtime authority for replace package + admission, installation, scheduling, execution, presence-sensitive + enforcement, commitment, receipts, readings, and observations remains + unresolved and Echo-owned. The source deliberately selects native lowerability and declares no semantic effect-to-effect direct adapter. Its lawpack projection separately declares the @@ -152,7 +154,15 @@ The first checked executable lowerer now lives under WIT request, accepts the exact generated mutation closure, and has exact Target IR byte parity with Edict's built-in Echo wrapper. Unsupported reads and other semantics produce typed refusals instead of invented artifacts. The checked -component remains uninstalled package material; neither it nor the generated +verifier lives beside it and independently checks the exact Core-to-Target-IR +relation under the generated target profile and semantic closure. Through the +pinned Edict host, exact request artifacts and the declared report schema are +preflighted before invocation. Returned accepted and well-formed rejected +reports are then admitted and receive host-authored manifests, while an +unsupported output-role overclaim remains a typed refusal with neither response +nor manifest. Independent fresh-store replay and separate host processes +reproduce all three completed outcomes identically. Both checked components +remain uninstalled package material; neither they nor the generated authority-facts documents are runtime Echo authority. External Edict contract inputs come from the checked @@ -170,8 +180,9 @@ enforces both exact `edict.canonical-cbor/v1` bytes and that named root. This is generation-time artifact validation, not runtime installation or authority; Echo must still explicitly admit any package, operation, or consequence. -Issue #655, after the lowerer and verifier components exist, assembles those -outputs and generates the package-root `edict.provider-manifest/v1` for +Issue #655 consumes these checked lowerer and verifier components, assembles +them with the generated semantic artifacts, and generates the package-root +`edict.provider-manifest/v1` for `echo.edict-provider@1`, implementing exact world `edict:target-provider@1.0.0`. The manifest is declared separately and is never listed inside its own artifact inventory. Runtime `reviewPayload` invocation diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index 622e1980..a36c394d 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -3,10 +3,10 @@ # Echo Edict Provider Components v1 -This directory checks the first executable Echo provider component for the -frozen Edict target-provider world. Issue #655 will bind this exact component -into the digest-locked provider package; its presence here does not claim that -Echo has installed, admitted, authorized, or executed it. +This directory checks the first executable Echo provider components for the +frozen Edict target-provider worlds. Issue #655 will bind these exact components +into the digest-locked provider package; their presence here does not claim that +Echo has installed, admitted, authorized, or executed them. `lowerer.echo-dpo.component.wasm` implements `edict:target-provider/lowerer@1.0.0`. It was built from @@ -31,6 +31,19 @@ non-callable protocol instance and equality-bounded request/result type aliases; its only callable world export is `lower`. It has no core, WASI, or ambient capability imports. +`verifier.echo-dpo.component.wasm` implements +`edict:target-provider/verifier@1.0.0`. It uses the same immutable builder, +authenticated Rust/Cargo identities, frozen WIT bytes, path-remapping law, and +`wit-component` version recorded above. Its checked component is 188,736 bytes +with SHA-256 +`e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b`. +Its sole contract attestation is the top-level custom section +`edict:target-provider-contract` containing +`edict:target-provider/verifier@1.0.0`. Its only imports are the frozen WIT's +non-callable protocol instance and equality-bounded verification request/result +type aliases; its only callable world export is `verify`. It has no core, WASI, +or ambient capability imports. + In the designated immutable `linux/amd64` Rust 1.90.0 builder above, rebuild and check the artifact without rewriting it: @@ -38,17 +51,24 @@ check the artifact without rewriting it: cargo +1.90.0 xtask provider-lowerer-component check \ --target-dir target/provider-lowerer-component \ --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm + +cargo +1.90.0 xtask provider-verifier-component check \ + --target-dir target/provider-verifier-component \ + --output schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm ``` Two independently provisioned containers with fresh target and controlled Cargo -home directories must produce byte-identical component bytes. Builds on other -hosts are local structural and semantic witnesses; Rust/LLVM cross-host code -generation is not claimed to be byte-identical. The standalone Edict-host -witness audits and invokes the checked -portable component and separately proves invocation parity, typed refusal, host -failure separation, replay equality, and cross-process determinism. Exact -component identity is build evidence for this translation crossing only; it is -not runtime Echo authority. +home directories must produce byte-identical bytes for each component +independently. Builds on other hosts are local structural and semantic witnesses; +Rust/LLVM cross-host code generation is not claimed to be byte-identical. The +standalone witness audits and invokes both checked components through the pinned +Edict host. Before verifier invocation, the host preflights the exact request +artifacts and declared report schema; afterward, it admits and manifests each +returned accepted or well-formed rejected report. A typed output-role refusal +has neither response nor manifest. Fresh-store replay and separate host +processes reproduce each completed verifier outcome identically. Exact +component identity is build evidence for its translation or verification +crossing only; neither host replay nor identity is runtime Echo authority. After two designated-builder candidates compare exactly, promote either explicit candidate through the same structural admission boundary: @@ -59,14 +79,20 @@ cargo +1.90.0 xtask provider-lowerer-component promote \ --candidate-b /explicit/build-b/lowerer.echo-dpo.component.wasm \ --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm \ --write + +cargo +1.90.0 xtask provider-verifier-component promote \ + --candidate-a /explicit/build-a/verifier.echo-dpo.component.wasm \ + --candidate-b /explicit/build-b/verifier.echo-dpo.component.wasm \ + --output schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm \ + --write ``` Promotion performs no discovery. It requires two distinct underlying files, -exact byte equality, the repository-approved SHA-256 identity above, and complete -component/world admission. It then synchronizes a same-directory temporary file -and atomically replaces a regular output; symlink and non-regular outputs fail -closed. The one-build `designated-build` command refuses this checked repository -path, so a refresh must cross the promotion boundary. The command does not infer -how the two files were produced. G4's two separately provisioned designated -container jobs and exact source checkouts—or equivalent explicit operator -evidence—establish independent build provenance. +exact byte equality, the corresponding repository-approved SHA-256 identity +above, and complete component/world admission. It then synchronizes a +same-directory temporary file and atomically replaces a regular output; symlink +and non-regular outputs fail closed. Each one-build `designated-build` command +refuses its checked repository path, so a refresh must cross the promotion +boundary. Neither command infers how the two files were produced. G4's two +separately provisioned designated container jobs and exact source checkouts—or +equivalent explicit operator evidence—establish independent build provenance. diff --git a/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm new file mode 100644 index 00000000..0fe4b1ac Binary files /dev/null and b/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm differ diff --git a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json index 490f121c..9bdeefb2 100644 --- a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json +++ b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:9bac584554dfedf119f835b056bb56e7051b7e7dfe6275f2e863a66bd49638be","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:5325aca06a7b00900e8be3ecd3e420138cc1ff94cf615b5be4552fdfb9392447","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json index d0564d71..3bbaeaf3 100644 --- a/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json +++ b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:9bac584554dfedf119f835b056bb56e7051b7e7dfe6275f2e863a66bd49638be","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:83f38ea22697ef30dc61a13e705e40e897d67a15e207a685af62952840f37ae9","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:5325aca06a7b00900e8be3ecd3e420138cc1ff94cf615b5be4552fdfb9392447","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:a76347f1e54e22b21c4298fab750e83ea7611b7a37f889d78434c53cf4527cb1","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file diff --git a/scripts/ban-globals.sh b/scripts/ban-globals.sh index 46ba4bfe..43241a2e 100755 --- a/scripts/ban-globals.sh +++ b/scripts/ban-globals.sh @@ -18,13 +18,13 @@ set -euo pipefail # ./scripts/ban-globals.sh # # Optional env: -# BAN_GLOBALS_PATHS="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer" +# BAN_GLOBALS_PATHS="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer crates/echo-edict-provider-verifier" # BAN_GLOBALS_ALLOWLIST=".ban-globals-allowlist" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -PATHS_DEFAULT="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer" +PATHS_DEFAULT="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer crates/echo-edict-provider-verifier" PATHS="${BAN_GLOBALS_PATHS:-$PATHS_DEFAULT}" ALLOWLIST="${BAN_GLOBALS_ALLOWLIST:-.ban-globals-allowlist}" diff --git a/scripts/ban-nondeterminism.sh b/scripts/ban-nondeterminism.sh index fc7bed56..b37e09e4 100755 --- a/scripts/ban-nondeterminism.sh +++ b/scripts/ban-nondeterminism.sh @@ -9,7 +9,7 @@ set -euo pipefail # ./scripts/ban-nondeterminism.sh # # Optional env: -# DETERMINISM_PATHS="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer" +# DETERMINISM_PATHS="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer crates/echo-edict-provider-verifier" # DETERMINISM_ALLOWLIST=".ban-nondeterminism-allowlist" # # Every waiver is rule-scoped to an exact path and must explain why the @@ -19,7 +19,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" source scripts/lib/determinism-scan.sh -PATHS_DEFAULT="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer" +PATHS_DEFAULT="crates/warp-core crates/warp-math crates/warp-wasm crates/echo-wasm-abi crates/echo-edict-canonical crates/echo-edict-provider-lowerer crates/echo-edict-provider-verifier" PATHS="${DETERMINISM_PATHS:-$PATHS_DEFAULT}" ALLOWLIST="${DETERMINISM_ALLOWLIST:-.ban-nondeterminism-allowlist}" diff --git a/scripts/tests/classify_changes.test.cjs b/scripts/tests/classify_changes.test.cjs index 6548e179..f481e754 100644 --- a/scripts/tests/classify_changes.test.cjs +++ b/scripts/tests/classify_changes.test.cjs @@ -136,6 +136,10 @@ test('every workspace member has an explicit non-catch-all policy path', () => { const missing = members.filter((member) => !policyYaml.includes(`"${member}/**"`)); assert.deepEqual(missing, [], `workspace members without explicit policy paths: ${missing}`); + assert.match( + policyYaml, + /echo-edict-provider-verifier:\n\s+class: DET_CRITICAL\n\s+owner_role: "Tooling Engineer"\n\s+paths: \["crates\/echo-edict-provider-verifier\/\*\*", "schemas\/edict-provider\/components\/v1\/verifier\.echo-dpo\.component\.wasm", "tests\/edict-provider-host-v1\/\*\*"\]\n/, + ); assert.match(policyYaml, /echo-runtime-schema:\n\s+class: DET_CRITICAL\n/); assert.match(policyYaml, /echo-file-aperture:\n\s+class: DET_IMPORTANT\n/); assert.match(policyYaml, /echo-trace:\n\s+class: DET_IMPORTANT\n/); diff --git a/scripts/verify-edict-provider-host-v1.sh b/scripts/verify-edict-provider-host-v1.sh index 5f0f7af1..799721f9 100755 --- a/scripts/verify-edict-provider-host-v1.sh +++ b/scripts/verify-edict-provider-host-v1.sh @@ -9,6 +9,7 @@ readonly ROOT readonly MANIFEST="$ROOT/tests/edict-provider-host-v1/Cargo.toml" readonly HOST_TARGET_DIR="$ROOT/target/edict-provider-host-v1" readonly COMPONENT_TARGET_DIR="$ROOT/target/provider-lowerer-local" +readonly VERIFIER_COMPONENT_TARGET_DIR="$ROOT/target/provider-verifier-local" component="${ECHO_PROVIDER_LOWERER_COMPONENT:-schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm}" if [[ "$component" != /* ]]; then @@ -16,22 +17,39 @@ if [[ "$component" != /* ]]; then fi readonly component +verifier_component="${ECHO_PROVIDER_VERIFIER_COMPONENT:-schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm}" +if [[ "$verifier_component" != /* ]]; then + verifier_component="$ROOT/$verifier_component" +fi +readonly verifier_component + cd "$ROOT" cargo +1.90.0 xtask provider-lowerer-component build \ --target-dir "$COMPONENT_TARGET_DIR" cargo +1.90.0 xtask provider-lowerer-component audit \ --input "$component" +cargo +1.90.0 xtask provider-verifier-component build \ + --target-dir "$VERIFIER_COMPONENT_TARGET_DIR" +cargo +1.90.0 xtask provider-verifier-component audit \ + --input "$verifier_component" cargo +1.94.0 fmt --manifest-path "$MANIFEST" --all -- --check ECHO_PROVIDER_LOWERER_COMPONENT="$component" \ + ECHO_PROVIDER_VERIFIER_COMPONENT="$verifier_component" \ CARGO_TARGET_DIR="$HOST_TARGET_DIR" \ cargo +1.94.0 test \ --manifest-path "$MANIFEST" \ --locked \ --test host_contract +CARGO_TARGET_DIR="$HOST_TARGET_DIR" \ + cargo +1.94.0 test \ + --manifest-path "$MANIFEST" \ + --locked \ + --test verifier_resource_sync + CARGO_TARGET_DIR="$HOST_TARGET_DIR" \ cargo +1.94.0 clippy \ --manifest-path "$MANIFEST" \ diff --git a/scripts/verify-local.sh b/scripts/verify-local.sh index e8e542f7..ad807c1d 100755 --- a/scripts/verify-local.sh +++ b/scripts/verify-local.sh @@ -259,6 +259,7 @@ readonly FULL_CRITICAL_PREFIXES=( "crates/echo-wasm-abi/" "crates/echo-edict-canonical/" "crates/echo-edict-provider-lowerer/" + "crates/echo-edict-provider-verifier/" "schemas/edict-provider/components/v1/" "tests/edict-provider-host-v1/" "crates/echo-scene-port/" @@ -315,6 +316,7 @@ readonly FULL_LOCAL_PACKAGES=( "echo-wasm-abi" "echo-edict-canonical" "echo-edict-provider-lowerer" + "echo-edict-provider-verifier" "echo-scene-port" "echo-scene-codec" "echo-graph" @@ -328,6 +330,7 @@ readonly FULL_LOCAL_TEST_PACKAGES=( "warp-math" "echo-edict-canonical" "echo-edict-provider-lowerer" + "echo-edict-provider-verifier" "echo-graph" "echo-scene-port" "echo-scene-codec" @@ -346,6 +349,7 @@ readonly FULL_LOCAL_CLIPPY_CORE_PACKAGES=( readonly FULL_LOCAL_CLIPPY_SUPPORT_PACKAGES=( "echo-edict-canonical" "echo-edict-provider-lowerer" + "echo-edict-provider-verifier" "echo-scene-port" "echo-scene-codec" "echo-graph" @@ -364,6 +368,7 @@ readonly FULL_LOCAL_RUSTDOC_PACKAGES=( "warp-wasm" "echo-edict-canonical" "echo-edict-provider-lowerer" + "echo-edict-provider-verifier" ) readonly FAST_CLIPPY_LIB_ONLY_PACKAGES=( @@ -649,7 +654,12 @@ list_changed_critical_crates() { while IFS= read -r file; do [[ -z "$file" ]] && continue case "$file" in - crates/echo-edict-provider-lowerer/wit/*|schemas/edict-provider/components/v1/*|tests/edict-provider-host-v1/*|scripts/verify-edict-provider-host-v1.sh) + crates/echo-edict-provider-verifier/wit/*|crates/echo-edict-provider-verifier/resources/*|schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm|tests/edict-provider-host-v1/*|scripts/verify-edict-provider-host-v1.sh|xtask/src/provider_lowerer_component.rs) + printf '%s\n' "echo-edict-provider-verifier" + ;; + esac + case "$file" in + crates/echo-edict-provider-lowerer/wit/*|schemas/edict-provider/components/v1/*|tests/edict-provider-host-v1/*|scripts/verify-edict-provider-host-v1.sh|xtask/src/provider_lowerer_component.rs) printf '%s\n' "echo-edict-provider-lowerer" ;; crates/*/Cargo.toml|crates/*/build.rs|crates/*/src/*|crates/*/tests/*) @@ -1534,7 +1544,8 @@ prepare_full_scope() { FULL_SCOPE_WARP_MATH_RUN_PRNG=0 FULL_SCOPE_RUN_EDICT_PROVIDER_HOST=0 - if array_contains "echo-edict-provider-lowerer" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"}; then + if array_contains "echo-edict-provider-lowerer" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"} \ + || array_contains "echo-edict-provider-verifier" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"}; then FULL_SCOPE_RUN_EDICT_PROVIDER_HOST=1 fi @@ -1826,12 +1837,20 @@ run_full_lane_edict_provider_host() { } run_full_lane_edict_provider_wasm_clippy() { - echo "[verify-local][clippy-edict-provider-wasm] strict wasm32 lowerer adapter" + echo "[verify-local][clippy-edict-provider-wasm] strict selected wasm32 provider adapters" rustup target add wasm32-unknown-unknown --toolchain "$PINNED" - lane_cargo "full-clippy-edict-provider-wasm" clippy \ - -p echo-edict-provider-lowerer \ - --target wasm32-unknown-unknown \ - --lib -- -D warnings -D missing_docs + if array_contains "echo-edict-provider-lowerer" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"}; then + lane_cargo "full-clippy-edict-provider-wasm" clippy \ + -p echo-edict-provider-lowerer \ + --target wasm32-unknown-unknown \ + --lib -- -D warnings -D missing_docs + fi + if array_contains "echo-edict-provider-verifier" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"}; then + lane_cargo "full-clippy-edict-provider-wasm" clippy \ + -p echo-edict-provider-verifier \ + --target wasm32-unknown-unknown \ + --lib -- -D warnings -D missing_docs + fi } run_full_checks_sequential() { diff --git a/tests/edict-provider-host-v1/tests/host_contract.rs b/tests/edict-provider-host-v1/tests/host_contract.rs index 732b90cb..f8ab9758 100644 --- a/tests/edict-provider-host-v1/tests/host_contract.rs +++ b/tests/edict-provider-host-v1/tests/host_contract.rs @@ -25,13 +25,16 @@ use edict_syntax::{ validate_provider_lowering_request, BuiltinLowererRequest, BuiltinTargetLowerer, CanonicalValue, CompilerContext, CoreBudget, CoreModule, ProviderArtifact, ProviderArtifactBinding, ProviderArtifactKind, ProviderArtifactRef, ProviderArtifactSource, - ProviderBoundArtifact, ProviderDigest, ProviderDigestAlgorithm, ProviderInvocationKind, - ProviderInvocationValidationFailureKind, ProviderLoweringInvocationContract, - ProviderLoweringOutputKind, ProviderLoweringOutputRequest, ProviderLoweringRequest, - ProviderRefusalKind, ProviderResourceRef, ProviderResponseLimits, ProviderSchemaBinding, - ProviderSchemaFormat, ProviderSemanticInput, ProviderSemanticInputBinding, - ProviderSemanticInputKind, ResourceRef, TargetEffectLowering, TargetIrLoweringFacts, - TargetProviderManifest, ValidatedProviderLoweringRequest, WriteClass, + ProviderBoundArtifact, ProviderDiagnosticSeverity, ProviderDigest, ProviderDigestAlgorithm, + ProviderInvocationKind, ProviderInvocationValidationFailureKind, + ProviderLoweringInvocationContract, ProviderLoweringOutputKind, ProviderLoweringOutputRequest, + ProviderLoweringRequest, ProviderOutputManifest, ProviderRefusalKind, ProviderResourceRef, + ProviderResponseLimits, ProviderSchemaBinding, ProviderSchemaFormat, ProviderSemanticInput, + ProviderSemanticInputBinding, ProviderSemanticInputKind, + ProviderVerificationInvocationContract, ProviderVerificationOutputKind, + ProviderVerificationOutputRequest, ProviderVerificationRequest, ProviderVerificationSuccess, + ResourceRef, TargetEffectLowering, TargetIrLoweringFacts, TargetProviderManifest, + ValidatedProviderLoweringRequest, ValidatedProviderVerificationRequest, WriteClass, AUTHORITY_FACTS_API_VERSION, CORE_DIGEST_FRAME, CORE_MODULE_DIGEST_DOMAIN, ECHO_DPO_TARGET_PROFILE, ECHO_SPAN_IR_DOMAIN, MAX_CANONICAL_NESTING_DEPTH, PROVIDER_LAWPACK_ARTIFACT_DOMAIN, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, TARGET_PROFILE_API_VERSION, @@ -55,6 +58,11 @@ const ECHO_SOURCE: &str = "package a.b@1;\n\ const LOWERABILITY_DOMAIN: &str = "edict.lowering-requirements/v1"; const TARGET_IR_ROLE: &str = "target-ir.echo-dpo"; const LOWERER_ROLE: &str = "lowerer.echo-dpo"; +const VERIFIER_ROLE: &str = "verifier.echo-dpo"; +const VERIFIER_REPORT_ROLE: &str = "verifier-report.echo-dpo"; +const VERIFIER_REPORT_DOMAIN: &str = "echo.verifier-report/v1"; +const DIAGNOSTIC_ABI_DIGEST: &str = + "28fd72a98223153982ca084c29dbb1b2d430623967ab3b6db9d7fee668e614b9"; const SCHEMA_ROLE: &str = "schema.echo-provider-artifacts"; const RAW_TARGET_IR_SHA256: &str = "41ae7a1d95e5068cb09ec581f16a90cc6e26a80f83ec073e86d5108c3a61ea41"; @@ -102,6 +110,13 @@ struct LowerHarness { schema: &'static ProviderArtifactSchemaRegistry, } +struct VerifyHarness { + host: ProviderComponentHost, + prepared: PreparedProviderComponent<'static>, + request: ValidatedProviderVerificationRequest<'static>, + schema: &'static ProviderArtifactSchemaRegistry, +} + fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") @@ -128,6 +143,25 @@ fn echo_component_bytes() -> &'static [u8] { Box::leak(bytes.into_boxed_slice()) } +fn echo_verifier_component_path() -> PathBuf { + let configured = std::env::var_os("ECHO_PROVIDER_VERIFIER_COMPONENT").map(PathBuf::from); + let path = configured.unwrap_or_else(|| { + PathBuf::from("schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm") + }); + if path.is_absolute() { + path + } else { + repo_root().join(path) + } +} + +fn echo_verifier_component_bytes() -> &'static [u8] { + let path = echo_verifier_component_path(); + let bytes = std::fs::read(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + Box::leak(bytes.into_boxed_slice()) +} + fn hex(bytes: &[u8]) -> String { let mut output = String::with_capacity(bytes.len() * 2); for byte in bytes { @@ -136,6 +170,23 @@ fn hex(bytes: &[u8]) -> String { output } +fn decode_hex(value: &str) -> Vec { + assert_eq!(value.len() % 2, 0, "hexadecimal input has whole bytes"); + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let digit = |byte: u8| match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => panic!("invalid hexadecimal digit"), + }; + (digit(pair[0]) << 4) | digit(pair[1]) + }) + .collect() +} + fn raw_sha256(bytes: &[u8]) -> String { hex(&Sha256::digest(bytes)) } @@ -358,6 +409,81 @@ fn echo_registry( )) } +fn echo_verifier_manifest(component_bytes: &'static [u8]) -> &'static TargetProviderManifest { + let component = raw_resource("echo.dpo.verifier/component@1", component_bytes); + let schema = raw_resource("echo.provider-artifacts.cddl@1", SCHEMA_BYTES); + Box::leak(Box::new(TargetProviderManifest { + api_version: TARGET_PROVIDER_MANIFEST_API_VERSION.to_owned(), + provider_abi: TARGET_PROVIDER_ABI.to_owned(), + provider: locked_test_resource("echo.edict-provider-host-witness@1", '1'), + artifacts: vec![ + ProviderArtifactRef { + role: VERIFIER_ROLE.to_owned(), + artifact_kind: ProviderArtifactKind::Verifier, + resource: component.clone(), + source: ProviderArtifactSource::Component { component }, + }, + ProviderArtifactRef { + role: SCHEMA_ROLE.to_owned(), + artifact_kind: ProviderArtifactKind::ArtifactSchema, + resource: schema, + source: ProviderArtifactSource::Generated { + semantic_source: locked_test_resource( + "echo.edict-provider-host-witness.schema-source@1", + '2', + ), + generator: locked_test_resource( + "echo.edict-provider-host-witness.schema-generator@1", + '3', + ), + }, + }, + ], + schema_bindings: [ + (VERIFIER_REPORT_DOMAIN, "verifier-report"), + (AUTHORITY_FACTS_API_VERSION, "authority-facts"), + (CORE_MODULE_DIGEST_DOMAIN, "core-module"), + (PROVIDER_LAWPACK_ARTIFACT_DOMAIN, "lawpack-manifest"), + (LOWERABILITY_DOMAIN, "lowering-requirements"), + (TARGET_IR_ARTIFACT_DIGEST_DOMAIN, "target-ir-artifact"), + (TARGET_PROFILE_API_VERSION, "target-profile-manifest"), + ] + .into_iter() + .map(|(domain, root_rule)| ProviderSchemaBinding { + domain: domain.to_owned(), + schema_role: SCHEMA_ROLE.to_owned(), + format: ProviderSchemaFormat::SelfContainedCddlV1, + root_rule: root_rule.to_owned(), + }) + .collect(), + })) +} + +fn echo_verifier_registry( + manifest: &'static TargetProviderManifest, +) -> &'static ProviderArtifactSchemaRegistry { + let proof = bind_target_provider_manifest(manifest).expect("Echo verifier manifest validates"); + Box::leak(Box::new( + ProviderArtifactSchemaRegistry::from_manifest( + &proof, + [ResolvedProviderSchemaArtifact { + role: SCHEMA_ROLE.to_owned(), + bytes: Arc::from(SCHEMA_BYTES), + }], + [ + VERIFIER_REPORT_DOMAIN, + AUTHORITY_FACTS_API_VERSION, + CORE_MODULE_DIGEST_DOMAIN, + PROVIDER_LAWPACK_ARTIFACT_DOMAIN, + LOWERABILITY_DOMAIN, + TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + TARGET_PROFILE_API_VERSION, + ], + ) + .expect("Echo verifier schema registry constructs"), + )) +} + fn echo_request_from_core_bytes( core_bytes: &[u8], output_role: &str, @@ -438,6 +564,125 @@ fn echo_request( echo_request_from_core_bytes(&core_bytes, output_role) } +fn echo_verification_request( + core: &CoreModule, + target_ir_bytes: &[u8], +) -> ( + ProviderVerificationInvocationContract, + ProviderVerificationRequest, +) { + let core_bytes = encode_core_module(core).expect("Core module encodes canonically"); + let core_artifact = bound_artifact("a.b@1", CORE_MODULE_DIGEST_DOMAIN, &core_bytes); + let target_profile_artifact = bound_artifact( + ECHO_DPO_TARGET_PROFILE, + TARGET_PROFILE_API_VERSION, + TARGET_PROFILE_BYTES, + ); + let target_ir_artifact = bound_artifact( + "echo.target-ir@1", + TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + target_ir_bytes, + ); + let lowerability = lowerability_bytes(); + let semantic_inputs = vec![ + semantic_input( + "authority-facts.echo-dpo", + ProviderSemanticInputKind::AuthorityFacts, + "echo.dpo-authority-facts@1", + AUTHORITY_FACTS_API_VERSION, + TARGET_AUTHORITY_BYTES, + ), + semantic_input( + "authority-facts.echo-lawpack", + ProviderSemanticInputKind::AuthorityFacts, + "echo.dpo-lawpack-authority-facts@1", + AUTHORITY_FACTS_API_VERSION, + LAWPACK_AUTHORITY_BYTES, + ), + semantic_input( + "lawpack.echo-dpo", + ProviderSemanticInputKind::Lawpack, + "echo.dpo-lawpack@1", + PROVIDER_LAWPACK_ARTIFACT_DOMAIN, + LAWPACK_BYTES, + ), + semantic_input( + "lowerability.echo-dpo", + ProviderSemanticInputKind::LowerabilityFacts, + "echo.dpo-lowerability@1", + LOWERABILITY_DOMAIN, + &lowerability, + ), + ]; + let contract = ProviderVerificationInvocationContract { + core: artifact_binding(&core_artifact), + target_profile: artifact_binding(&target_profile_artifact), + target_ir: artifact_binding(&target_ir_artifact), + semantic_inputs: semantic_inputs + .iter() + .map(|input| ProviderSemanticInputBinding { + role: input.role.clone(), + kind: input.kind.clone(), + artifact: artifact_binding(&input.artifact), + }) + .collect(), + }; + let request = ProviderVerificationRequest { + protocol_version: TARGET_PROVIDER_PROTOCOL_VERSION, + core: core_artifact, + target_profile: target_profile_artifact, + target_ir: target_ir_artifact, + semantic_inputs, + requested_outputs: vec![ProviderVerificationOutputRequest { + role: VERIFIER_REPORT_ROLE.to_owned(), + kind: ProviderVerificationOutputKind::VerifierReport, + domain: VERIFIER_REPORT_DOMAIN.to_owned(), + }], + limits: ProviderResponseLimits { + max_output_count: 8, + max_diagnostic_count: 8, + max_total_response_bytes: 64 * 1024, + }, + }; + (contract, request) +} + +fn echo_verifier_harness_with_request( + contract: ProviderVerificationInvocationContract, + request: ProviderVerificationRequest, +) -> VerifyHarness { + let component = echo_verifier_component_bytes(); + let manifest = echo_verifier_manifest(component); + let manifest_proof = Box::leak(Box::new( + bind_target_provider_manifest(manifest).expect("Echo verifier manifest validates"), + )); + let selected = select_provider_component( + manifest_proof, + VERIFIER_ROLE, + ProviderInvocationKind::Verification, + ) + .expect("Echo verifier selects"); + let resolved = ResolvedProviderComponent::new(selected, Arc::from(component)); + let host = ProviderComponentHost::new().expect("host configures"); + let prepared = host.prepare(&resolved).expect("Echo verifier prepares"); + let schema = echo_verifier_registry(manifest); + let contract = Box::leak(Box::new(contract)); + let request = Box::leak(Box::new(request)); + let request = edict_syntax::validate_provider_verification_request(schema, contract, request) + .expect("Echo verification request passes complete schema admission"); + VerifyHarness { + host, + prepared, + request, + schema, + } +} + +fn echo_verifier_harness(core: &CoreModule, target_ir_bytes: &[u8]) -> VerifyHarness { + let (contract, request) = echo_verification_request(core, target_ir_bytes); + echo_verifier_harness_with_request(contract, request) +} + fn canonical_map_field_mut<'a>( value: &'a mut CanonicalValue, field: &str, @@ -455,6 +700,113 @@ fn canonical_core_intent_mut(core: &mut CanonicalValue) -> &mut CanonicalValue { canonical_map_field_mut(canonical_map_field_mut(core, "intents"), "t") } +fn target_ir_with_mismatched_intrinsic(core: &CoreModule) -> Vec { + let (target_ir_bytes, _) = oracle_target_ir(core); + let mut target_ir = + decode_canonical_cbor(&target_ir_bytes).expect("oracle Target IR decodes canonically"); + let intent = canonical_map_field_mut(canonical_map_field_mut(&mut target_ir, "intents"), "t"); + let CanonicalValue::Array(steps) = canonical_map_field_mut(intent, "steps") else { + panic!("Target IR steps are not an array"); + }; + let step = steps + .first_mut() + .expect("reviewed Target IR has one effect step"); + *canonical_map_field_mut(step, "targetIntrinsic") = text("echo.dpo@1.unreviewed"); + encode_canonical_cbor(&target_ir).expect("mismatched Target IR encodes canonically") +} + +fn resource_ref_value(reference: &ProviderResourceRef) -> CanonicalValue { + assert_eq!( + reference.digest.algorithm, + ProviderDigestAlgorithm::Sha256, + "reviewed resource reference uses sha256" + ); + map([ + ("id", text(&reference.coordinate)), + ( + "digest", + CanonicalValue::Array(vec![ + text("sha256"), + CanonicalValue::Bytes(reference.digest.bytes.clone()), + ]), + ), + ]) +} + +fn expected_verifier_report_bytes( + target_ir_reference: &ProviderResourceRef, + outcome: &str, +) -> Vec { + encode_canonical_cbor(&map([ + ("apiVersion", text(VERIFIER_REPORT_DOMAIN)), + ("targetIr", resource_ref_value(target_ir_reference)), + ("outcome", text(outcome)), + ( + "diagnosticAbi", + resource_ref_value(&ProviderResourceRef { + coordinate: "edict.diagnostics/v1".to_owned(), + digest: ProviderDigest { + algorithm: ProviderDigestAlgorithm::Sha256, + bytes: decode_hex(DIAGNOSTIC_ABI_DIGEST), + }, + }), + ), + ("diagnosticBytes", CanonicalValue::Bytes(Vec::new())), + ])) + .expect("independent verifier report oracle encodes canonically") +} + +fn assert_admitted_verifier_success( + harness: &VerifyHarness, + response: &ProviderVerificationSuccess, + manifest: &ProviderOutputManifest, + expected_outcome: &str, +) { + assert_eq!(response.outputs.len(), 1); + let output = &response.outputs[0]; + assert_eq!(output.role, VERIFIER_REPORT_ROLE); + assert_eq!(output.kind, ProviderVerificationOutputKind::VerifierReport); + assert_eq!(output.artifact.domain, VERIFIER_REPORT_DOMAIN); + assert_eq!(output.logical_path, None); + + let request = harness.request.request(); + let contract = harness.request.contract(); + let expected_bytes = + expected_verifier_report_bytes(&request.target_ir.reference, expected_outcome); + assert_eq!(output.artifact.bytes, expected_bytes); + + assert_eq!(manifest.invocation(), ProviderInvocationKind::Verification); + assert_eq!( + manifest.protocol_version(), + TARGET_PROVIDER_PROTOCOL_VERSION + ); + assert_eq!(manifest.inputs().core(), &contract.core); + assert_eq!(manifest.inputs().target_profile(), &contract.target_profile); + assert_eq!(manifest.inputs().target_ir(), Some(&contract.target_ir)); + assert_eq!( + manifest.inputs().semantic_inputs(), + contract.semantic_inputs.as_slice() + ); + assert_eq!(manifest.requested_outputs().len(), 1); + let requested = &manifest.requested_outputs()[0]; + assert_eq!(requested.role, VERIFIER_REPORT_ROLE); + assert_eq!( + requested.kind, + ProviderVerificationOutputKind::VerifierReport + ); + assert_eq!(requested.domain, VERIFIER_REPORT_DOMAIN); + assert_eq!(manifest.outputs().len(), 1); + let entry = &manifest.outputs()[0]; + assert_eq!(entry.role, VERIFIER_REPORT_ROLE); + assert_eq!(entry.kind, ProviderVerificationOutputKind::VerifierReport); + assert_eq!(entry.domain, VERIFIER_REPORT_DOMAIN); + assert_eq!(entry.logical_path, None); + assert_eq!( + entry.digest, + provider_digest(VERIFIER_REPORT_DOMAIN, &expected_bytes) + ); +} + fn echo_harness_with_request( contract: ProviderLoweringInvocationContract, request: ProviderLoweringRequest, @@ -693,6 +1045,123 @@ fn echo_observation() -> String { ) } +fn echo_verifier_observation() -> String { + let core = echo_core(); + let (accepted_target_ir, _) = oracle_target_ir(&core); + let accepted = echo_verifier_harness(&core, &accepted_target_ir); + let accepted_outcome = accepted + .host + .invoke_verifier( + &accepted.prepared, + &accepted.request, + accepted.schema, + host_limits(), + ) + .expect("accepted verifier observation crosses complete Edict host admission"); + assert!(accepted_outcome.refusal().is_none()); + let accepted_response = accepted_outcome + .response() + .expect("accepted verifier observation returns a response"); + let accepted_manifest = accepted_outcome + .manifest() + .expect("accepted verifier observation has a host-authored manifest"); + assert_admitted_verifier_success(&accepted, accepted_response, accepted_manifest, "accepted"); + + let rejected_target_ir = target_ir_with_mismatched_intrinsic(&core); + let rejected = echo_verifier_harness(&core, &rejected_target_ir); + let rejected_outcome = rejected + .host + .invoke_verifier( + &rejected.prepared, + &rejected.request, + rejected.schema, + host_limits(), + ) + .expect("rejected verifier observation crosses complete Edict host admission"); + assert!(rejected_outcome.refusal().is_none()); + let rejected_response = rejected_outcome + .response() + .expect("rejected verifier observation returns a response"); + let rejected_manifest = rejected_outcome + .manifest() + .expect("rejected verifier observation has a host-authored manifest"); + assert_admitted_verifier_success(&rejected, rejected_response, rejected_manifest, "rejected"); + let [rejected_diagnostic] = rejected_response.diagnostics.as_slice() else { + panic!("rejected verifier observation has one diagnostic"); + }; + assert_eq!( + rejected_diagnostic.code, + "echo.verifier.target-intrinsic-mismatch" + ); + assert_eq!( + rejected_diagnostic.severity, + ProviderDiagnosticSeverity::Error + ); + assert_eq!( + rejected_diagnostic.message, + "Target IR uses an intrinsic outside the reviewed Echo capability" + ); + assert_eq!(rejected_diagnostic.repair, None); + + let (refused_contract, mut refused_request) = + echo_verification_request(&core, &accepted_target_ir); + refused_request + .requested_outputs + .push(ProviderVerificationOutputRequest { + role: "verifier-report.unreviewed".to_owned(), + kind: ProviderVerificationOutputKind::VerifierReport, + domain: VERIFIER_REPORT_DOMAIN.to_owned(), + }); + let refused = echo_verifier_harness_with_request(refused_contract, refused_request); + let refused_outcome = refused + .host + .invoke_verifier( + &refused.prepared, + &refused.request, + refused.schema, + host_limits(), + ) + .expect("refused verifier observation crosses the WIT transport"); + assert!(refused_outcome.response().is_none()); + assert!(refused_outcome.manifest().is_none()); + let refusal = refused_outcome + .refusal() + .expect("refused verifier observation preserves a typed refusal"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedOutputRole); + assert_eq!( + refusal.subject.as_deref(), + Some("verifier-report.unreviewed") + ); + let [refusal_diagnostic] = refusal.diagnostics.as_slice() else { + panic!("refused verifier observation has one diagnostic"); + }; + assert_eq!( + refusal_diagnostic.code, + "echo.verifier.unsupported-output-role" + ); + assert_eq!( + refusal_diagnostic.severity, + ProviderDiagnosticSeverity::Error + ); + assert_eq!( + refusal_diagnostic.message, + "the first verifier serves exactly one verifier-report.echo-dpo output" + ); + assert_eq!(refusal_diagnostic.repair, None); + + format!( + "{}:{}:{}:{}:{}:{}:{}:{}", + raw_sha256(echo_verifier_component_bytes()), + raw_sha256(&accepted_response.outputs[0].artifact.bytes), + hex(&accepted_manifest.outputs()[0].digest.bytes), + raw_sha256(&rejected_response.outputs[0].artifact.bytes), + hex(&rejected_manifest.outputs()[0].digest.bytes), + rejected_diagnostic.code, + "unsupported-output-role", + refusal_diagnostic.code, + ) +} + fn oracle_target_ir(core: &CoreModule) -> (Vec, ProviderDigest) { let facts = TargetIrLoweringFacts { target_profile: ResourceRef { @@ -1082,3 +1551,194 @@ fn independent_processes_reproduce_the_same_echo_component_observation() { let second = run_child(); assert_eq!(first, second); } + +#[test] +fn echo_verifier_component_admits_the_exact_relation_through_the_actual_host() { + let core = echo_core(); + let (target_ir_bytes, _) = oracle_target_ir(&core); + let harness = echo_verifier_harness(&core, &target_ir_bytes); + let outcome = harness + .host + .invoke_verifier( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("accepted Echo relation crosses complete Edict host admission"); + assert!(outcome.refusal().is_none()); + let response = outcome + .response() + .expect("accepted Echo relation returns a verifier report"); + assert!(response.diagnostics.is_empty()); + let manifest = outcome + .manifest() + .expect("host authors the accepted verifier output manifest"); + assert_admitted_verifier_success(&harness, response, manifest, "accepted"); +} + +#[test] +fn echo_verifier_component_admits_a_well_formed_semantic_rejection() { + let core = echo_core(); + let target_ir_bytes = target_ir_with_mismatched_intrinsic(&core); + let harness = echo_verifier_harness(&core, &target_ir_bytes); + let outcome = harness + .host + .invoke_verifier( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("well-formed semantic disagreement crosses complete Edict host admission"); + assert!(outcome.refusal().is_none()); + let response = outcome + .response() + .expect("semantic disagreement returns a rejected verifier report"); + assert_eq!(response.diagnostics.len(), 1); + let diagnostic = &response.diagnostics[0]; + assert_eq!(diagnostic.code, "echo.verifier.target-intrinsic-mismatch"); + assert_eq!(diagnostic.severity, ProviderDiagnosticSeverity::Error); + assert_eq!( + diagnostic.message, + "Target IR uses an intrinsic outside the reviewed Echo capability" + ); + assert_eq!(diagnostic.repair, None); + let manifest = outcome + .manifest() + .expect("host authors the rejected verifier output manifest"); + assert_admitted_verifier_success(&harness, response, manifest, "rejected"); +} + +#[test] +fn echo_verifier_component_preserves_a_typed_output_overclaim_refusal() { + let core = echo_core(); + let (target_ir_bytes, _) = oracle_target_ir(&core); + let (contract, mut request) = echo_verification_request(&core, &target_ir_bytes); + request + .requested_outputs + .push(ProviderVerificationOutputRequest { + role: "verifier-report.unreviewed".to_owned(), + kind: ProviderVerificationOutputKind::VerifierReport, + domain: VERIFIER_REPORT_DOMAIN.to_owned(), + }); + let harness = echo_verifier_harness_with_request(contract, request); + let outcome = harness + .host + .invoke_verifier( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("typed verifier refusal crosses the WIT transport"); + assert!(outcome.response().is_none()); + assert!(outcome.manifest().is_none()); + let refusal = outcome + .refusal() + .expect("output overclaim returns a typed provider refusal"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedOutputRole); + assert_eq!( + refusal.subject.as_deref(), + Some("verifier-report.unreviewed") + ); + assert_eq!(refusal.diagnostics.len(), 1); + let diagnostic = &refusal.diagnostics[0]; + assert_eq!(diagnostic.code, "echo.verifier.unsupported-output-role"); + assert_eq!(diagnostic.severity, ProviderDiagnosticSeverity::Error); + assert_eq!( + diagnostic.message, + "the first verifier serves exactly one verifier-report.echo-dpo output" + ); + assert_eq!(diagnostic.repair, None); +} + +#[test] +fn echo_verifier_component_replays_all_completed_outcome_classes_identically() { + let core = echo_core(); + let (accepted_target_ir, _) = oracle_target_ir(&core); + let accepted = echo_verifier_harness(&core, &accepted_target_ir); + let rejected_target_ir = target_ir_with_mismatched_intrinsic(&core); + let rejected = echo_verifier_harness(&core, &rejected_target_ir); + let (refusal_contract, mut refusal_request) = + echo_verification_request(&core, &accepted_target_ir); + refusal_request + .requested_outputs + .push(ProviderVerificationOutputRequest { + role: "verifier-report.unreviewed".to_owned(), + kind: ProviderVerificationOutputKind::VerifierReport, + domain: VERIFIER_REPORT_DOMAIN.to_owned(), + }); + let refused = echo_verifier_harness_with_request(refusal_contract, refusal_request); + + for (harness, expects_refusal) in [(&accepted, false), (&rejected, false), (&refused, true)] { + let outcome = harness + .host + .invoke_verifier( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("completed verifier outcome crosses the Edict host"); + let replay = harness + .host + .replay_verifier( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("verifier outcome replays identically in fresh stores"); + let ProviderReplayObservation::Completed(replayed) = replay.observation() else { + panic!("accepted, rejected, and refused outcomes must all be completed observations"); + }; + assert_eq!(replayed, &outcome); + if expects_refusal { + assert!(outcome.response().is_none()); + assert!(outcome.manifest().is_none()); + assert!(outcome.refusal().is_some()); + } else { + assert!(outcome.response().is_some()); + assert!(outcome.manifest().is_some()); + assert!(outcome.refusal().is_none()); + } + } +} + +#[test] +#[ignore = "child entrypoint exercised by the verifier independent-process witness"] +fn emit_echo_verifier_host_observation() { + println!("{OBSERVATION_MARKER}{}", echo_verifier_observation()); +} + +#[test] +fn independent_processes_reproduce_the_same_echo_verifier_observation() { + let executable = std::env::current_exe().expect("current test executable is discoverable"); + let run_child = || { + let output = Command::new(&executable) + .arg("emit_echo_verifier_host_observation") + .args(["--exact", "--ignored", "--nocapture", "--test-threads=1"]) + .output() + .expect("child verifier host process launches"); + assert!( + output.status.success(), + "child verifier host witness failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).expect("child output is UTF-8"); + stdout + .lines() + .find_map(|line| { + line.split_once(OBSERVATION_MARKER) + .map(|(_, observation)| observation) + }) + .unwrap_or_else(|| panic!("child omitted stable verifier observation:\n{stdout}")) + .to_owned() + }; + + let first = run_child(); + let second = run_child(); + assert_eq!(first, second); +} diff --git a/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs b/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs new file mode 100644 index 00000000..158a1a23 --- /dev/null +++ b/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::panic)] +//! Workspace crossing from the CDDL-admitted generated corpus to the +//! publishable verifier crate's package-local semantic resources. + +#[test] +fn verifier_package_resources_equal_the_checked_generated_corpus() { + let pairs: &[(&str, &[u8], &[u8])] = &[ + ( + "generated-artifact-profile.echo-dpo-registration.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor" + ), + ), + ( + "resource.lawpack-exports.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.lawpack-exports.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.lawpack-exports.cbor" + ), + ), + ( + "resource.lawpack-target-adapter.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.lawpack-target-adapter.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.lawpack-target-adapter.cbor" + ), + ), + ( + "resource.lawpack-verifier.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.lawpack-verifier.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.lawpack-verifier.cbor" + ), + ), + ( + "resource.target-intrinsics.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-intrinsics.cbor" + ), + ), + ( + "resource.target-footprint-algebra.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-footprint-algebra.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-footprint-algebra.cbor" + ), + ), + ( + "resource.target-cost-algebra.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-cost-algebra.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-cost-algebra.cbor" + ), + ), + ( + "resource.target-obstruction-taxonomy.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-obstruction-taxonomy.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-obstruction-taxonomy.cbor" + ), + ), + ( + "resource.target-operation-profiles.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-operation-profiles.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-operation-profiles.cbor" + ), + ), + ( + "resource.target-verifier-contract.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-verifier-contract.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-verifier-contract.cbor" + ), + ), + ( + "resource.target-ir.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/resource.target-ir.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor" + ), + ), + ]; + + for (name, packaged, generated) in pairs { + assert_eq!(packaged, generated, "semantic resource drift: {name}"); + } +} diff --git a/tests/hooks/test_verify_local.sh b/tests/hooks/test_verify_local.sh index 21f68582..80af6011 100755 --- a/tests/hooks/test_verify_local.sh +++ b/tests/hooks/test_verify_local.sh @@ -1895,6 +1895,63 @@ else printf '%s\n' "$fake_edict_host_script_output" fi +assert_verifier_local_route() { + local description="$1" + local changed_file="$2" + local preserve_lowerer_route="${3:-0}" + local output + output="$(run_fake_verify full "$changed_file")" + + if printf '%s\n' "$output" | grep -q 'critical local gate (targeted-rust)'; then + pass "$description selects the targeted Rust scope" + else + fail "$description should select the targeted Rust scope" + printf '%s\n' "$output" + fi + if printf '%s\n' "$output" | grep -q -- 'clippy -p echo-edict-provider-verifier --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs'; then + pass "$description runs verifier wasm-target strict Clippy" + else + fail "$description should run verifier wasm-target strict Clippy" + printf '%s\n' "$output" + fi + if printf '%s\n' "$output" | grep -q '^edict-provider-host-v1$'; then + pass "$description runs the isolated Edict host witness" + else + fail "$description should run the isolated Edict host witness" + printf '%s\n' "$output" + fi + if [[ "$preserve_lowerer_route" == "1" ]]; then + if printf '%s\n' "$output" | grep -q -- 'clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs'; then + pass "$description preserves the lowerer wasm-target route" + else + fail "$description should preserve the lowerer wasm-target route" + printf '%s\n' "$output" + fi + fi +} + +assert_verifier_local_route \ + "verifier crate changes" \ + crates/echo-edict-provider-verifier/src/lib.rs +assert_verifier_local_route \ + "verifier semantic-resource changes" \ + crates/echo-edict-provider-verifier/resources/resource.target-ir.cbor +assert_verifier_local_route \ + "verifier WIT changes" \ + crates/echo-edict-provider-verifier/wit/edict-target-provider.wit +assert_verifier_local_route \ + "checked verifier component changes" \ + schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm \ + 1 +assert_verifier_local_route \ + "shared Edict host changes" \ + tests/edict-provider-host-v1/tests/host_contract.rs \ + 1 +assert_verifier_local_route \ + "shared provider component-builder changes" \ + xtask/src/provider_lowerer_component.rs \ + 1 + fake_warp_wasm_readme_output="$(run_fake_verify full crates/warp-wasm/README.md)" if printf '%s\n' "$fake_warp_wasm_readme_output" | grep -q 'critical local gate (tooling-only)'; then pass "non-rust critical crate docs stay off the Rust smoke lanes" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 0f2b5fd2..39db4c92 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -50,6 +50,8 @@ enum Commands { PrSnapshot(PrSnapshotArgs), /// Build or check the exact Echo Edict provider lowerer component. ProviderLowererComponent(ProviderLowererComponentArgs), + /// Build or check the exact Echo Edict provider verifier component. + ProviderVerifierComponent(ProviderLowererComponentArgs), /// List, reply to, or resolve PR review threads via `gh`. PrThreads(PrThreadsArgs), /// Run the high-signal local gate before opening a PR. @@ -435,6 +437,7 @@ fn main() -> Result<()> { Commands::PrStatus(args) => run_pr_status(args), Commands::PrSnapshot(args) => run_pr_snapshot(args), Commands::ProviderLowererComponent(args) => run_provider_lowerer_component(args), + Commands::ProviderVerifierComponent(args) => run_provider_verifier_component(args), Commands::PrThreads(args) => run_pr_threads(args), Commands::PrPreflight(args) => run_pr_preflight(args), Commands::Dind(args) => run_dind(args), @@ -447,37 +450,207 @@ fn main() -> Result<()> { } fn run_provider_lowerer_component(args: ProviderLowererComponentArgs) -> Result<()> { + run_provider_component(ProviderComponentKind::Lowerer, args) +} + +fn run_provider_verifier_component(args: ProviderLowererComponentArgs) -> Result<()> { + run_provider_component(ProviderComponentKind::Verifier, args) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ProviderComponentKind { + Lowerer, + Verifier, +} + +impl ProviderComponentKind { + const fn label(self) -> &'static str { + match self { + Self::Lowerer => "lowerer", + Self::Verifier => "verifier", + } + } + + const fn checked_repository_path(self) -> &'static str { + match self { + Self::Lowerer => provider_lowerer_component::CHECKED_COMPONENT_REPOSITORY_PATH, + Self::Verifier => { + provider_lowerer_component::VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH + } + } + } + + const fn other_checked_repository_path(self) -> &'static str { + match self { + Self::Lowerer => provider_lowerer_component::VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH, + Self::Verifier => provider_lowerer_component::CHECKED_COMPONENT_REPOSITORY_PATH, + } + } + + fn pinned_toolchain( + self, + ) -> provider_lowerer_component::Result { + match self { + Self::Lowerer => provider_lowerer_component::pinned_rust_toolchain(), + Self::Verifier => provider_lowerer_component::pinned_verifier_rust_toolchain(), + } + } + + fn require_checked_builder( + self, + ) -> provider_lowerer_component::Result { + match self { + Self::Lowerer => provider_lowerer_component::require_checked_builder(), + Self::Verifier => provider_lowerer_component::require_verifier_checked_builder(), + } + } + + fn ensure_designated_candidate_output( + self, + output_path: &Path, + checked_path: &Path, + ) -> provider_lowerer_component::Result<()> { + match self { + Self::Lowerer => provider_lowerer_component::ensure_designated_candidate_output( + output_path, + checked_path, + ), + Self::Verifier => { + provider_lowerer_component::ensure_verifier_designated_candidate_output( + output_path, + checked_path, + ) + } + } + } + + fn ensure_reserved_checked_output( + self, + output_path: &Path, + reserved_path: &Path, + ) -> provider_lowerer_component::Result<()> { + match self { + Self::Lowerer => provider_lowerer_component::ensure_reserved_checked_output( + output_path, + reserved_path, + ), + Self::Verifier => provider_lowerer_component::ensure_verifier_reserved_checked_output( + output_path, + reserved_path, + ), + } + } + + fn require_checked_identity( + self, + component: &provider_lowerer_component::ProviderLowererComponent, + ) -> provider_lowerer_component::Result<()> { + match self { + Self::Lowerer => { + provider_lowerer_component::require_lowerer_component_identity(component) + } + Self::Verifier => { + provider_lowerer_component::require_verifier_component_identity(component) + } + } + } + + fn build( + self, + repository_root: &Path, + target_directory: &Path, + toolchain: &provider_lowerer_component::PinnedRustToolchain, + ) -> provider_lowerer_component::Result + { + match self { + Self::Lowerer => provider_lowerer_component::build_component( + repository_root, + target_directory, + toolchain, + ), + Self::Verifier => provider_lowerer_component::build_verifier_component( + repository_root, + target_directory, + toolchain, + ), + } + } + + fn read( + self, + input_path: &Path, + ) -> provider_lowerer_component::Result + { + match self { + Self::Lowerer => provider_lowerer_component::read_component(input_path), + Self::Verifier => provider_lowerer_component::read_verifier_component(input_path), + } + } + + fn sync_output( + self, + output_path: &Path, + bytes: &[u8], + mode: provider_lowerer_component::ComponentOutputMode, + ) -> provider_lowerer_component::Result { + match self { + Self::Lowerer => provider_lowerer_component::sync_output(output_path, bytes, mode), + Self::Verifier => { + provider_lowerer_component::sync_verifier_output(output_path, bytes, mode) + } + } + } + + fn promote( + self, + candidate_a: &Path, + candidate_b: &Path, + output_path: &Path, + ) -> provider_lowerer_component::Result<( + provider_lowerer_component::ProviderLowererComponent, + provider_lowerer_component::ComponentOutputStatus, + )> { + match self { + Self::Lowerer => provider_lowerer_component::promote_reproducible_candidates( + candidate_a, + candidate_b, + output_path, + ), + Self::Verifier => provider_lowerer_component::promote_verifier_reproducible_candidates( + candidate_a, + candidate_b, + output_path, + ), + } + } +} + +fn run_provider_component( + kind: ProviderComponentKind, + args: ProviderLowererComponentArgs, +) -> Result<()> { let repository_root = find_repo_root()?; match args.command { ProviderLowererComponentCommand::Build(args) => { - let toolchain = provider_lowerer_component::pinned_rust_toolchain()?; - let component = provider_lowerer_component::build_component( - &repository_root, - &args.target_dir, - &toolchain, - )?; - print_provider_component("local-build", None, &component, None); + let toolchain = kind.pinned_toolchain()?; + let component = kind.build(&repository_root, &args.target_dir, &toolchain)?; + print_provider_component(kind, "local-build", None, &component, None); } ProviderLowererComponentCommand::DesignatedBuild(args) => { let output_path = repository_path(&repository_root, args.output); - let checked_path = - repository_root.join(provider_lowerer_component::CHECKED_COMPONENT_REPOSITORY_PATH); - provider_lowerer_component::ensure_designated_candidate_output( - &output_path, - &checked_path, - )?; - let builder = provider_lowerer_component::require_checked_builder()?; - let component = provider_lowerer_component::build_component( - &repository_root, - &args.target_dir, - &builder, - )?; - let status = provider_lowerer_component::sync_output( + let checked_path = repository_root.join(kind.checked_repository_path()); + let other_checked_path = repository_root.join(kind.other_checked_repository_path()); + kind.ensure_designated_candidate_output(&output_path, &checked_path)?; + kind.ensure_reserved_checked_output(&output_path, &other_checked_path)?; + let builder = kind.require_checked_builder()?; + let component = kind.build(&repository_root, &args.target_dir, &builder)?; + let status = kind.sync_output( &output_path, component.bytes(), provider_lowerer_component::ComponentOutputMode::Write, )?; print_provider_component( + kind, &format!("designated-build:{}", builder.host()), Some(&output_path), &component, @@ -485,19 +658,17 @@ fn run_provider_lowerer_component(args: ProviderLowererComponentArgs) -> Result< ); } ProviderLowererComponentCommand::Check(args) => { - let builder = provider_lowerer_component::require_checked_builder()?; let output_path = repository_path(&repository_root, args.output); - let component = provider_lowerer_component::build_component( - &repository_root, - &args.target_dir, - &builder, - )?; - let status = provider_lowerer_component::sync_output( + let builder = kind.require_checked_builder()?; + let component = kind.build(&repository_root, &args.target_dir, &builder)?; + kind.require_checked_identity(&component)?; + let status = kind.sync_output( &output_path, component.bytes(), provider_lowerer_component::ComponentOutputMode::Check, )?; print_provider_component( + kind, &format!("checked-build:{}", builder.host()), Some(&output_path), &component, @@ -506,22 +677,26 @@ fn run_provider_lowerer_component(args: ProviderLowererComponentArgs) -> Result< } ProviderLowererComponentCommand::Audit(args) => { let input_path = repository_path(&repository_root, args.input); - let component = provider_lowerer_component::read_component(&input_path)?; - print_provider_component("audited", Some(&input_path), &component, None); + let component = kind.read(&input_path)?; + print_provider_component(kind, "audited", Some(&input_path), &component, None); } ProviderLowererComponentCommand::Promote(args) => { if !args.write { - bail!("provider lowerer promotion requires --write"); + bail!("provider {} promotion requires --write", kind.label()); } let candidate_a = repository_path(&repository_root, args.candidate_a); let candidate_b = repository_path(&repository_root, args.candidate_b); let output_path = repository_path(&repository_root, args.output); - let (component, status) = provider_lowerer_component::promote_reproducible_candidates( - &candidate_a, - &candidate_b, - &output_path, - )?; - print_provider_component("promoted", Some(&output_path), &component, Some(status)); + let other_checked_path = repository_root.join(kind.other_checked_repository_path()); + kind.ensure_reserved_checked_output(&output_path, &other_checked_path)?; + let (component, status) = kind.promote(&candidate_a, &candidate_b, &output_path)?; + print_provider_component( + kind, + "promoted", + Some(&output_path), + &component, + Some(status), + ); } } Ok(()) @@ -536,6 +711,7 @@ fn repository_path(repository_root: &Path, path: PathBuf) -> PathBuf { } fn print_provider_component( + kind: ProviderComponentKind, action: &str, path: Option<&Path>, component: &provider_lowerer_component::ProviderLowererComponent, @@ -548,13 +724,15 @@ fn print_provider_component( }; if let Some(path) = path { println!( - "provider lowerer component: {action}{status}; sha256:{}; {}", + "provider {} component: {action}{status}; sha256:{}; {}", + kind.label(), component.sha256_hex(), path.display() ); } else { println!( - "provider lowerer component: {action}{status}; sha256:{}", + "provider {} component: {action}{status}; sha256:{}", + kind.label(), component.sha256_hex() ); } @@ -6011,6 +6189,16 @@ mod tests { } } + fn require_anyhow_error( + result: Result, + message: &str, + ) -> std::result::Result> { + match result { + Err(error) => Ok(error), + Ok(_) => Err(std::io::Error::other(message).into()), + } + } + fn command_program_and_args(command: &Command) -> (String, Vec) { let program = command.get_program().to_string_lossy().into_owned(); let args = command @@ -6020,6 +6208,278 @@ mod tests { (program, args) } + #[test] + fn provider_component_cli_routes_verifier_and_lowerer_independently( + ) -> std::result::Result<(), Box> { + for arguments in [ + vec![ + "xtask", + "provider-verifier-component", + "build", + "--target-dir", + "target/verifier-local", + ], + vec![ + "xtask", + "provider-verifier-component", + "designated-build", + "--output", + "target/verifier-a.wasm", + "--target-dir", + "target/verifier-a", + ], + vec![ + "xtask", + "provider-verifier-component", + "check", + "--output", + "target/verifier-checked.wasm", + "--target-dir", + "target/verifier-check", + ], + vec![ + "xtask", + "provider-verifier-component", + "audit", + "--input", + "target/verifier-a.wasm", + ], + vec![ + "xtask", + "provider-verifier-component", + "promote", + "--candidate-a", + "target/verifier-a.wasm", + "--candidate-b", + "target/verifier-b.wasm", + "--output", + "target/verifier-checked.wasm", + "--write", + ], + ] { + let parsed = Cli::try_parse_from(arguments)?; + assert!(matches!( + parsed.command, + Commands::ProviderVerifierComponent(_) + )); + } + + let missing_write = Cli::try_parse_from([ + "xtask", + "provider-verifier-component", + "promote", + "--candidate-a", + "target/verifier-a.wasm", + "--candidate-b", + "target/verifier-b.wasm", + "--output", + "target/verifier-checked.wasm", + ]); + assert!(missing_write.is_err()); + + let checked_verifier = + "schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm"; + let verifier_audit = Cli::try_parse_from([ + "xtask", + "provider-verifier-component", + "audit", + "--input", + checked_verifier, + ])?; + let Commands::ProviderVerifierComponent(args) = verifier_audit.command else { + unreachable!("verifier audit parsed into the wrong command family"); + }; + run_provider_verifier_component(args)?; + + let checked_lowerer = + "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; + let lowerer_as_verifier_audit = Cli::try_parse_from([ + "xtask", + "provider-verifier-component", + "audit", + "--input", + checked_lowerer, + ])?; + let Commands::ProviderVerifierComponent(args) = lowerer_as_verifier_audit.command else { + unreachable!("verifier audit parsed into the wrong command family"); + }; + let error = require_anyhow_error( + run_provider_verifier_component(args), + "checked lowerer bytes must not authenticate as a verifier", + )?; + let component_error = error + .downcast_ref::() + .ok_or_else(|| std::io::Error::other("verifier audit returned the wrong error type"))?; + assert_eq!( + component_error.kind(), + provider_lowerer_component::ProviderLowererComponentErrorKind::ComponentImportForbidden + ); + assert_eq!(component_error.subject(), "lowering-request-v1"); + + let lowerer_audit = Cli::try_parse_from([ + "xtask", + "provider-lowerer-component", + "audit", + "--input", + checked_lowerer, + ])?; + let Commands::ProviderLowererComponent(args) = lowerer_audit.command else { + unreachable!("lowerer audit parsed into the wrong command family"); + }; + run_provider_lowerer_component(args)?; + Ok(()) + } + + #[test] + fn provider_component_routes_protect_each_others_checked_outputs( + ) -> std::result::Result<(), Box> { + let repository_root = find_repo_root()?; + let lowerer_checked = + repository_root.join(provider_lowerer_component::CHECKED_COMPONENT_REPOSITORY_PATH); + let verifier_checked = repository_root + .join(provider_lowerer_component::VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH); + let lowerer_before = fs::read(&lowerer_checked)?; + let verifier_before = fs::read(&verifier_checked)?; + + let lowerer_hard_link = unique_temp_path("provider-lowerer-checked-hard-link"); + fs::hard_link(&lowerer_checked, &lowerer_hard_link)?; + + let verifier_designated = Cli::try_parse_from(vec![ + "xtask".to_owned(), + "provider-verifier-component".to_owned(), + "designated-build".to_owned(), + "--output".to_owned(), + lowerer_hard_link.display().to_string(), + "--target-dir".to_owned(), + unique_temp_path("verifier-cross-designated-target") + .display() + .to_string(), + ])?; + let Commands::ProviderVerifierComponent(args) = verifier_designated.command else { + unreachable!("verifier designated build parsed into the wrong command family"); + }; + let error = require_anyhow_error( + run_provider_verifier_component(args), + "verifier designated build must reject a lowerer checked-output hard link", + )?; + let component_error = error + .downcast_ref::() + .ok_or_else(|| std::io::Error::other("cross-output guard returned the wrong error"))?; + assert_eq!( + component_error.kind(), + provider_lowerer_component::ProviderLowererComponentErrorKind::ReservedCheckedOutputForbidden + ); + + let normalized_verifier_checked = verifier_checked + .parent() + .ok_or_else(|| std::io::Error::other("verifier checked path has no parent"))? + .join("..") + .join("v1") + .join("verifier.echo-dpo.component.wasm"); + let lowerer_designated = Cli::try_parse_from(vec![ + "xtask".to_owned(), + "provider-lowerer-component".to_owned(), + "designated-build".to_owned(), + "--output".to_owned(), + normalized_verifier_checked.display().to_string(), + "--target-dir".to_owned(), + unique_temp_path("lowerer-cross-designated-target") + .display() + .to_string(), + ])?; + let Commands::ProviderLowererComponent(args) = lowerer_designated.command else { + unreachable!("lowerer designated build parsed into the wrong command family"); + }; + let error = require_anyhow_error( + run_provider_lowerer_component(args), + "lowerer designated build must reject the normalized verifier checked path", + )?; + let component_error = error + .downcast_ref::() + .ok_or_else(|| std::io::Error::other("cross-output guard returned the wrong error"))?; + assert_eq!( + component_error.kind(), + provider_lowerer_component::ProviderLowererComponentErrorKind::ReservedCheckedOutputForbidden + ); + + for (command, output) in [ + ( + "provider-verifier-component", + lowerer_hard_link.display().to_string(), + ), + ( + "provider-lowerer-component", + normalized_verifier_checked.display().to_string(), + ), + ] { + let promote = Cli::try_parse_from(vec![ + "xtask".to_owned(), + command.to_owned(), + "promote".to_owned(), + "--candidate-a".to_owned(), + unique_temp_path("cross-promote-missing-a") + .display() + .to_string(), + "--candidate-b".to_owned(), + unique_temp_path("cross-promote-missing-b") + .display() + .to_string(), + "--output".to_owned(), + output, + "--write".to_owned(), + ])?; + let error = match promote.command { + Commands::ProviderLowererComponent(args) => require_anyhow_error( + run_provider_lowerer_component(args), + "lowerer promotion must reject the verifier checked path", + )?, + Commands::ProviderVerifierComponent(args) => require_anyhow_error( + run_provider_verifier_component(args), + "verifier promotion must reject the lowerer checked path", + )?, + _ => unreachable!("promotion parsed into an unrelated command family"), + }; + let component_error = error + .downcast_ref::() + .ok_or_else(|| { + std::io::Error::other("cross-output guard returned the wrong error") + })?; + assert_eq!( + component_error.kind(), + provider_lowerer_component::ProviderLowererComponentErrorKind::ReservedCheckedOutputForbidden + ); + } + + assert_eq!(fs::read(&lowerer_checked)?, lowerer_before); + assert_eq!(fs::read(&verifier_checked)?, verifier_before); + fs::remove_file(lowerer_hard_link)?; + Ok(()) + } + + #[test] + fn verifier_audit_read_failures_keep_the_verifier_prefix( + ) -> std::result::Result<(), Box> { + let missing = unique_temp_path("provider-verifier-definitely-missing"); + let audit = Cli::try_parse_from(vec![ + "xtask".to_owned(), + "provider-verifier-component".to_owned(), + "audit".to_owned(), + "--input".to_owned(), + missing.display().to_string(), + ])?; + let Commands::ProviderVerifierComponent(args) = audit.command else { + unreachable!("verifier audit parsed into the wrong command family"); + }; + let error = require_anyhow_error( + run_provider_verifier_component(args), + "a missing verifier audit input must fail", + )?; + assert!(error + .to_string() + .starts_with("provider-verifier-component:component-read-failed:")); + Ok(()) + } + #[test] fn man_pages_render_top_level_and_subcommands() { let pages = assert_ok(render_man_pages(), "man pages should render"); diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index 8c901f83..a2865174 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Deterministic build and structural admission for the Echo Edict lowerer component. +//! Deterministic build and structural admission for Echo Edict provider components. use sha2::{Digest, Sha256}; use std::borrow::Cow; @@ -24,7 +24,7 @@ pub(crate) const CONTRACT_SECTION_NAME: &str = "edict:target-provider-contract"; /// Exact Edict lowerer contract coordinate carried by the attestation. pub(crate) const LOWERER_CONTRACT_COORDINATE: &str = "edict:target-provider/lowerer@1.0.0"; -/// Exact type-only protocol instance that the lowerer component may import. +/// Exact type-only protocol instance that a provider component may import. pub(crate) const PROTOCOL_INSTANCE_COORDINATE: &str = "edict:target-provider/protocol@1.0.0"; const PROTOCOL_TYPE_IMPORTS: [&str; 2] = ["lowering-request-v1", "lowering-result-v1"]; @@ -35,6 +35,16 @@ const LOWERER_PACKAGE: &str = "echo-edict-provider-lowerer"; const LOWERER_CORE_WASM: &str = "echo_edict_provider_lowerer.wasm"; const LOWER_EXPORT: &str = "lower"; +const VERIFIER_CONTRACT_COORDINATE: &str = "edict:target-provider/verifier@1.0.0"; +const VERIFIER_PROTOCOL_TYPE_IMPORTS: [&str; 2] = + ["verification-request-v1", "verification-result-v1"]; +const VERIFIER_WORLD_NAME: &str = "verifier"; +const VERIFIER_WIT_SOURCE: &str = + include_str!("../../crates/echo-edict-provider-verifier/wit/edict-target-provider.wit"); +const VERIFIER_PACKAGE: &str = "echo-edict-provider-verifier"; +const VERIFIER_CORE_WASM: &str = "echo_edict_provider_verifier.wasm"; +const VERIFY_EXPORT: &str = "verify"; + /// Exact host triple designated to produce the checked component bytes. pub(crate) const CHECKED_COMPONENT_BUILDER_HOST: &str = "x86_64-unknown-linux-gnu"; const PINNED_RUST_TOOLCHAIN: &str = "1.90.0"; @@ -47,6 +57,74 @@ pub(crate) const APPROVED_CHECKED_COMPONENT_SHA256: &str = pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; +/// Repository path reserved for the approved verifier component. +pub(crate) const VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH: &str = + "schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm"; +/// Approved SHA-256 identity of the checked verifier component. +pub(crate) const APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256: &str = + "e13eda6e02d5a46d2aecdec0546d53a7bf66f2580f8d5ec06e5d76710716a27b"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ProviderComponentSpec { + package: &'static str, + core_wasm: &'static str, + contract_coordinate: &'static str, + protocol_type_imports: [&'static str; 2], + world_name: &'static str, + wit_source: &'static str, + callable_export: &'static str, + checked_repository_path: &'static str, + approved_sha256: Option<&'static str>, +} + +const LOWERER_SPEC: ProviderComponentSpec = ProviderComponentSpec { + package: LOWERER_PACKAGE, + core_wasm: LOWERER_CORE_WASM, + contract_coordinate: LOWERER_CONTRACT_COORDINATE, + protocol_type_imports: PROTOCOL_TYPE_IMPORTS, + world_name: LOWERER_WORLD_NAME, + wit_source: LOWERER_WIT_SOURCE, + callable_export: LOWER_EXPORT, + checked_repository_path: CHECKED_COMPONENT_REPOSITORY_PATH, + approved_sha256: Some(APPROVED_CHECKED_COMPONENT_SHA256), +}; + +const VERIFIER_SPEC: ProviderComponentSpec = ProviderComponentSpec { + package: VERIFIER_PACKAGE, + core_wasm: VERIFIER_CORE_WASM, + contract_coordinate: VERIFIER_CONTRACT_COORDINATE, + protocol_type_imports: VERIFIER_PROTOCOL_TYPE_IMPORTS, + world_name: VERIFIER_WORLD_NAME, + wit_source: VERIFIER_WIT_SOURCE, + callable_export: VERIFY_EXPORT, + checked_repository_path: VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH, + approved_sha256: Some(APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256), +}; + +fn export_missing_kind(spec: &ProviderComponentSpec) -> ProviderLowererComponentErrorKind { + if spec.callable_export == VERIFY_EXPORT { + ProviderLowererComponentErrorKind::VerifyExportMissing + } else { + ProviderLowererComponentErrorKind::LowerExportMissing + } +} + +fn export_duplicate_kind(spec: &ProviderComponentSpec) -> ProviderLowererComponentErrorKind { + if spec.callable_export == VERIFY_EXPORT { + ProviderLowererComponentErrorKind::VerifyExportDuplicate + } else { + ProviderLowererComponentErrorKind::LowerExportDuplicate + } +} + +fn export_invalid_kind(spec: &ProviderComponentSpec) -> ProviderLowererComponentErrorKind { + if spec.callable_export == VERIFY_EXPORT { + ProviderLowererComponentErrorKind::VerifyExportInvalid + } else { + ProviderLowererComponentErrorKind::LowerExportInvalid + } +} + /// Stable classification for a component build or audit failure. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ProviderLowererComponentErrorKind { @@ -54,7 +132,7 @@ pub(crate) enum ProviderLowererComponentErrorKind { InvalidPath, /// Cargo could not be started. BuildInvocationFailed, - /// Cargo completed without producing a successful lowerer build. + /// Cargo completed without producing a successful provider-component build. BuildFailed, /// The expected core WebAssembly artifact could not be read. CoreWasmReadFailed, @@ -78,8 +156,12 @@ pub(crate) enum ProviderLowererComponentErrorKind { ExpectedDigestInvalid, /// Reproducible candidates do not have the explicitly expected digest. CandidateDigestMismatch, + /// No reviewed checked identity has been approved for this component. + CheckedIdentityUnapproved, /// A one-build candidate command targeted the checked repository artifact. CheckedOutputRequiresPromotion, + /// A component command targeted another component's reserved checked artifact. + ReservedCheckedOutputForbidden, /// The core module could not be componentized. ComponentEncodingFailed, /// The component byte stream is malformed or fails WebAssembly validation. @@ -106,7 +188,7 @@ pub(crate) enum ProviderLowererComponentErrorKind { ProtocolImportClosureInvalid, /// The candidate component's decoded WIT world could not be authenticated. WorldContractInvalid, - /// The candidate's complete decoded WIT world differs from the frozen lowerer world. + /// The candidate's complete decoded WIT world differs from its frozen selected world. WorldContractMismatch, /// The required `lower` export is absent. LowerExportMissing, @@ -114,6 +196,12 @@ pub(crate) enum ProviderLowererComponentErrorKind { LowerExportDuplicate, /// A top-level export is not the exact callable `lower` export. LowerExportInvalid, + /// The required `verify` export is absent. + VerifyExportMissing, + /// More than one `verify` export is present. + VerifyExportDuplicate, + /// A top-level export is not the exact callable `verify` export. + VerifyExportInvalid, /// An explicit checked output does not exist. OutputMissing, /// An explicit checked output could not be read. @@ -143,7 +231,9 @@ impl ProviderLowererComponentErrorKind { Self::CandidateAliased => "candidate-aliased", Self::ExpectedDigestInvalid => "expected-digest-invalid", Self::CandidateDigestMismatch => "candidate-digest-mismatch", + Self::CheckedIdentityUnapproved => "checked-identity-unapproved", Self::CheckedOutputRequiresPromotion => "checked-output-requires-promotion", + Self::ReservedCheckedOutputForbidden => "reserved-checked-output-forbidden", Self::ComponentEncodingFailed => "component-encoding-failed", Self::ComponentInvalid => "component-invalid", Self::ComponentEncodingInvalid => "component-encoding-invalid", @@ -161,6 +251,9 @@ impl ProviderLowererComponentErrorKind { Self::LowerExportMissing => "lower-export-missing", Self::LowerExportDuplicate => "lower-export-duplicate", Self::LowerExportInvalid => "lower-export-invalid", + Self::VerifyExportMissing => "verify-export-missing", + Self::VerifyExportDuplicate => "verify-export-duplicate", + Self::VerifyExportInvalid => "verify-export-invalid", Self::OutputMissing => "output-missing", Self::OutputReadFailed => "output-read-failed", Self::OutputDrift => "output-drift", @@ -173,6 +266,7 @@ impl ProviderLowererComponentErrorKind { /// Stable, typed error returned by component build and audit operations. #[derive(Debug)] pub(crate) struct ProviderLowererComponentError { + component_label: &'static str, kind: ProviderLowererComponentErrorKind, subject: String, reference: Option, @@ -186,6 +280,7 @@ impl ProviderLowererComponentError { reference: Option, ) -> Self { Self { + component_label: "lowerer", kind, subject: subject.into(), reference, @@ -198,6 +293,11 @@ impl ProviderLowererComponentError { self } + fn for_spec(mut self, spec: &ProviderComponentSpec) -> Self { + self.component_label = spec.world_name; + self + } + /// Returns the stable failure classification. #[cfg(test)] pub(crate) fn kind(&self) -> ProviderLowererComponentErrorKind { @@ -221,7 +321,8 @@ impl fmt::Display for ProviderLowererComponentError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, - "provider-lowerer-component:{}: {}", + "provider-{}-component:{}: {}", + self.component_label, self.kind.code(), self.subject )?; @@ -237,7 +338,7 @@ impl std::error::Error for ProviderLowererComponentError {} /// Result alias for deterministic component operations. pub(crate) type Result = std::result::Result; -/// Exact lowerer component bytes and their SHA-256 identity. +/// Exact provider component bytes and their SHA-256 identity. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct ProviderLowererComponent { bytes: Vec, @@ -291,6 +392,11 @@ pub(crate) fn pinned_rust_toolchain() -> Result { }) } +/// Resolves the same pinned toolchain with verifier-specific failure context. +pub(crate) fn pinned_verifier_rust_toolchain() -> Result { + pinned_rust_toolchain().map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + /// Authenticates the pinned compiler host before an exact checked-byte build. pub(crate) fn require_checked_builder() -> Result { let toolchain = pinned_rust_toolchain()?; @@ -298,6 +404,11 @@ pub(crate) fn require_checked_builder() -> Result { Ok(toolchain) } +/// Authenticates the designated verifier builder host and pinned toolchain. +pub(crate) fn require_verifier_checked_builder() -> Result { + require_checked_builder().map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + fn resolve_rustup_tool(tool: &str) -> Result { let output = Command::new("rustup") .args(["which", "--toolchain", PINNED_RUST_TOOLCHAIN, tool]) @@ -459,6 +570,13 @@ pub(crate) fn read_component(input_path: &Path) -> Result Result { + read_component_bytes(input_path) + .and_then(authenticated_verifier_component) + .map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + fn read_component_bytes(input_path: &Path) -> Result> { fs::read(input_path).map_err(|error| { ProviderLowererComponentError::new( @@ -474,30 +592,72 @@ pub(crate) fn ensure_designated_candidate_output( output_path: &Path, checked_path: &Path, ) -> Result<()> { - let aliases_checked = match same_file::is_same_file(output_path, checked_path) { + ensure_output_does_not_alias( + output_path, + checked_path, + ProviderLowererComponentErrorKind::CheckedOutputRequiresPromotion, + ) +} + +fn ensure_output_does_not_alias( + output_path: &Path, + reserved_path: &Path, + kind: ProviderLowererComponentErrorKind, +) -> Result<()> { + let aliases_reserved = match same_file::is_same_file(output_path, reserved_path) { Ok(aliases) => aliases, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - normalized_destination(output_path)? == normalized_destination(checked_path)? + normalized_destination(output_path)? == normalized_destination(reserved_path)? } Err(error) => { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::OutputReadFailed, output_path.display().to_string(), - Some(checked_path.display().to_string()), + Some(reserved_path.display().to_string()), ) .with_detail(error.to_string())); } }; - if aliases_checked { + if aliases_reserved { return Err(ProviderLowererComponentError::new( - ProviderLowererComponentErrorKind::CheckedOutputRequiresPromotion, + kind, output_path.display().to_string(), - Some(checked_path.display().to_string()), + Some(reserved_path.display().to_string()), )); } Ok(()) } +/// Refuses an output that aliases another component's reserved checked path. +pub(crate) fn ensure_reserved_checked_output( + output_path: &Path, + reserved_path: &Path, +) -> Result<()> { + ensure_output_does_not_alias( + output_path, + reserved_path, + ProviderLowererComponentErrorKind::ReservedCheckedOutputForbidden, + ) +} + +/// Refuses a verifier output that aliases another component's reserved checked path. +pub(crate) fn ensure_verifier_reserved_checked_output( + output_path: &Path, + reserved_path: &Path, +) -> Result<()> { + ensure_reserved_checked_output(output_path, reserved_path) + .map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + +/// Refuses a one-build verifier candidate that aliases its checked path. +pub(crate) fn ensure_verifier_designated_candidate_output( + output_path: &Path, + checked_path: &Path, +) -> Result<()> { + ensure_designated_candidate_output(output_path, checked_path) + .map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + fn normalized_destination(path: &Path) -> Result { match fs::canonicalize(path) { Ok(path) => Ok(path), @@ -537,6 +697,14 @@ fn normalized_destination(path: &Path) -> Result { pub(crate) fn read_reproducible_candidates( first_path: &Path, second_path: &Path, +) -> Result { + read_reproducible_candidates_for(&LOWERER_SPEC, first_path, second_path) +} + +fn read_reproducible_candidates_for( + spec: &ProviderComponentSpec, + first_path: &Path, + second_path: &Path, ) -> Result { let first = read_component_bytes(first_path)?; let second = read_component_bytes(second_path)?; @@ -555,8 +723,9 @@ pub(crate) fn read_reproducible_candidates( Some(second_path.display().to_string()), )); } - ensure_reproducible_candidates(&first, &second, APPROVED_CHECKED_COMPONENT_SHA256)?; - authenticated_component(first) + let approved_sha256 = require_approved_checked_digest(spec)?; + ensure_reproducible_candidates_for(spec, &first, &second, approved_sha256)?; + authenticated_component_for(spec, first) } /// Audits, authenticates, and intentionally writes two reproducible candidates. @@ -570,15 +739,98 @@ pub(crate) fn promote_reproducible_candidates( Ok((component, status)) } +/// Fails closed until an exact verifier identity is approved, then promotes it. +pub(crate) fn promote_verifier_reproducible_candidates( + first_path: &Path, + second_path: &Path, + output_path: &Path, +) -> Result<(ProviderLowererComponent, ComponentOutputStatus)> { + require_verifier_checked_identity()?; + let component = read_reproducible_candidates_for(&VERIFIER_SPEC, first_path, second_path) + .map_err(|error| error.for_spec(&VERIFIER_SPEC))?; + let status = sync_output(output_path, component.bytes(), ComponentOutputMode::Write) + .map_err(|error| error.for_spec(&VERIFIER_SPEC))?; + Ok((component, status)) +} + +/// Requires the verifier's checked identity to have been explicitly approved. +pub(crate) fn require_verifier_checked_identity() -> Result<&'static str> { + require_approved_checked_digest(&VERIFIER_SPEC).map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + +/// Binds one already authenticated lowerer component to the approved identity. +pub(crate) fn require_lowerer_component_identity( + component: &ProviderLowererComponent, +) -> Result<()> { + require_component_identity_for(&LOWERER_SPEC, component) + .map_err(|error| error.for_spec(&LOWERER_SPEC)) +} + +/// Binds one already authenticated verifier component to the approved identity. +pub(crate) fn require_verifier_component_identity( + component: &ProviderLowererComponent, +) -> Result<()> { + require_component_identity_for(&VERIFIER_SPEC, component) + .map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + +fn require_component_identity_for( + spec: &ProviderComponentSpec, + component: &ProviderLowererComponent, +) -> Result<()> { + let approved_sha256 = require_approved_checked_digest(spec)?; + if approved_sha256.len() != 64 + || !approved_sha256 + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ExpectedDigestInvalid, + approved_sha256, + Some("lowercase-sha256".to_owned()), + )); + } + + let observed_sha256 = component.sha256_hex(); + if observed_sha256 != approved_sha256 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CandidateDigestMismatch, + approved_sha256, + Some(observed_sha256), + )); + } + Ok(()) +} + +fn require_approved_checked_digest(spec: &ProviderComponentSpec) -> Result<&'static str> { + spec.approved_sha256.ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CheckedIdentityUnapproved, + spec.contract_coordinate, + Some(spec.checked_repository_path.to_owned()), + ) + }) +} + +#[cfg(test)] fn ensure_reproducible_candidates( first: &[u8], second: &[u8], expected_sha256: &str, +) -> Result<()> { + ensure_reproducible_candidates_for(&LOWERER_SPEC, first, second, expected_sha256) +} + +fn ensure_reproducible_candidates_for( + spec: &ProviderComponentSpec, + first: &[u8], + second: &[u8], + expected_sha256: &str, ) -> Result<()> { if first != second { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::CandidateMismatch, - LOWERER_CONTRACT_COORDINATE, + spec.contract_coordinate, Some(format!( "first:{};second:{}", digest_hex(&sha256(first)), @@ -597,10 +849,11 @@ fn ensure_reproducible_candidates( Some("lowercase-sha256".to_owned()), )); } - if expected_sha256 != APPROVED_CHECKED_COMPONENT_SHA256 { + let approved_sha256 = require_approved_checked_digest(spec)?; + if expected_sha256 != approved_sha256 { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::CandidateDigestMismatch, - APPROVED_CHECKED_COMPONENT_SHA256, + approved_sha256, Some(expected_sha256.to_owned()), )); } @@ -615,7 +868,7 @@ fn ensure_reproducible_candidates( Ok(()) } -/// Structural facts established by [`audit_component`]. +/// Structural facts established by a specification-selected component audit. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct ComponentAudit { /// Number of exact top-level contract attestations. @@ -624,8 +877,8 @@ pub(crate) struct ComponentAudit { pub(crate) protocol_imports: u32, /// Number of exact equality-bounded protocol type aliases (zero or two). pub(crate) protocol_type_imports: u32, - /// Number of exact callable `lower` exports. - pub(crate) lower_exports: u32, + /// Number of exact callable exports selected by the component specification. + pub(crate) callable_exports: u32, } /// Whether an explicit output should be checked or updated. @@ -655,6 +908,25 @@ pub(crate) fn build_component( repository_root: &Path, target_directory: &Path, toolchain: &PinnedRustToolchain, +) -> Result { + build_component_for(&LOWERER_SPEC, repository_root, target_directory, toolchain) +} + +/// Builds, componentizes, attests, and audits the provider verifier. +pub(crate) fn build_verifier_component( + repository_root: &Path, + target_directory: &Path, + toolchain: &PinnedRustToolchain, +) -> Result { + build_component_for(&VERIFIER_SPEC, repository_root, target_directory, toolchain) + .map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + +fn build_component_for( + spec: &ProviderComponentSpec, + repository_root: &Path, + target_directory: &Path, + toolchain: &PinnedRustToolchain, ) -> Result { let target_directory = absolute_target_directory(repository_root, target_directory); let cargo_home = target_directory.join("cargo-home"); @@ -668,7 +940,7 @@ pub(crate) fn build_component( .args([ "build", "-p", - LOWERER_PACKAGE, + spec.package, "--target", "wasm32-unknown-unknown", "--release", @@ -686,7 +958,7 @@ pub(crate) fn build_component( .map_err(|error| { ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::BuildInvocationFailed, - LOWERER_PACKAGE, + spec.package, Some(toolchain.cargo.display().to_string()), ) .with_detail(error.to_string()) @@ -695,7 +967,7 @@ pub(crate) fn build_component( if !output.status.success() { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::BuildFailed, - LOWERER_PACKAGE, + spec.package, Some("wasm32-unknown-unknown/release".to_owned()), ) .with_detail(String::from_utf8_lossy(&output.stderr))); @@ -704,17 +976,21 @@ pub(crate) fn build_component( let core_path = target_directory .join("wasm32-unknown-unknown") .join("release") - .join(LOWERER_CORE_WASM); + .join(spec.core_wasm); let core_bytes = fs::read(&core_path).map_err(|error| { ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::CoreWasmReadFailed, core_path.display().to_string(), - Some(LOWERER_CORE_WASM.to_owned()), + Some(spec.core_wasm.to_owned()), ) .with_detail(error.to_string()) })?; - componentize(&core_bytes) + if spec == &LOWERER_SPEC { + componentize(&core_bytes) + } else { + componentize_for(spec, &core_bytes) + } } fn encoded_build_rustflags( @@ -764,6 +1040,13 @@ fn bind_pinned_toolchain(command: &mut Command, toolchain: &PinnedRustToolchain) /// Componentizes explicit core Wasm bytes and appends the exact attestation. pub(crate) fn componentize(core_bytes: &[u8]) -> Result { + componentize_for(&LOWERER_SPEC, core_bytes) +} + +fn componentize_for( + spec: &ProviderComponentSpec, + core_bytes: &[u8], +) -> Result { let encoder = ComponentEncoder::default() .validate(true) .merge_imports_based_on_semver(false) @@ -771,7 +1054,7 @@ pub(crate) fn componentize(core_bytes: &[u8]) -> Result Result) -> Result { @@ -796,22 +1083,46 @@ fn authenticated_component(bytes: Vec) -> Result { Ok(ProviderLowererComponent { bytes, sha256 }) } +fn authenticated_verifier_component(bytes: Vec) -> Result { + audit_verifier_component(&bytes)?; + let sha256 = sha256(&bytes); + Ok(ProviderLowererComponent { bytes, sha256 }) +} + +fn authenticated_component_for( + spec: &ProviderComponentSpec, + bytes: Vec, +) -> Result { + audit_component_for(spec, &bytes)?; + let sha256 = sha256(&bytes); + Ok(ProviderLowererComponent { bytes, sha256 }) +} + /// Audits exact attestation, import, export, and WebAssembly validity claims. pub(crate) fn audit_component(bytes: &[u8]) -> Result { + audit_component_for(&LOWERER_SPEC, bytes) +} + +/// Audits exact verifier attestation, imports, export, and WebAssembly validity. +pub(crate) fn audit_verifier_component(bytes: &[u8]) -> Result { + audit_component_for(&VERIFIER_SPEC, bytes).map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + +fn audit_component_for(spec: &ProviderComponentSpec, bytes: &[u8]) -> Result { let mut depth = 0_u32; let mut outer_is_component = false; let mut contract_attestations = 0_u32; let mut protocol_imports = 0_u32; - let mut protocol_type_imports = [false; PROTOCOL_TYPE_IMPORTS.len()]; - let mut lower_exports = 0_u32; + let mut protocol_type_imports = [false; 2]; + let mut callable_exports = 0_u32; let mut top_level_types = Vec::new(); for payload in Parser::new(0).parse_all(bytes) { - let payload = payload.map_err(|error| component_invalid(error.to_string()))?; + let payload = payload.map_err(|error| component_invalid_for(spec, error.to_string()))?; match payload { Payload::Version { encoding, .. } => { depth = depth.checked_add(1).ok_or_else(|| { - component_invalid("component nesting depth overflow".to_owned()) + component_invalid_for(spec, "component nesting depth overflow".to_owned()) })?; if depth == 1 { outer_is_component = encoding == Encoding::Component; @@ -819,7 +1130,7 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { } Payload::End(_) => { depth = depth.checked_sub(1).ok_or_else(|| { - component_invalid("component nesting depth underflow".to_owned()) + component_invalid_for(spec, "component nesting depth underflow".to_owned()) })?; } Payload::CustomSection(section) if section.name() == CONTRACT_SECTION_NAME => { @@ -835,14 +1146,14 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ContractAttestationDuplicate, CONTRACT_SECTION_NAME, - Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + Some(spec.contract_coordinate.to_owned()), )); } - if section.data() != LOWERER_CONTRACT_COORDINATE.as_bytes() { + if section.data() != spec.contract_coordinate.as_bytes() { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ContractAttestationMismatch, CONTRACT_SECTION_NAME, - Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + Some(spec.contract_coordinate.to_owned()), )); } } @@ -851,7 +1162,7 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { .into_imports() .next() .transpose() - .map_err(|error| component_invalid(error.to_string()))?; + .map_err(|error| component_invalid_for(spec, error.to_string()))?; if let Some(import) = import { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::CoreImportForbidden, @@ -862,14 +1173,15 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { } Payload::ComponentTypeSection(section) if depth == 1 => { for component_type in section { - let component_type = - component_type.map_err(|error| component_invalid(error.to_string()))?; + let component_type = component_type + .map_err(|error| component_invalid_for(spec, error.to_string()))?; top_level_types.push(type_only_instance(&component_type)); } } Payload::ComponentImportSection(section) => { for import in section { - let import = import.map_err(|error| component_invalid(error.to_string()))?; + let import = + import.map_err(|error| component_invalid_for(spec, error.to_string()))?; if depth != 1 || import.name.implements.is_some() { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ComponentImportForbidden, @@ -905,7 +1217,8 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { continue; } - let Some(alias_index) = PROTOCOL_TYPE_IMPORTS + let Some(alias_index) = spec + .protocol_type_imports .iter() .position(|name| *name == import.name.name) else { @@ -934,22 +1247,23 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { } Payload::ComponentExportSection(section) if depth == 1 => { for export in section { - let export = export.map_err(|error| component_invalid(error.to_string()))?; - if export.name.name != LOWER_EXPORT + let export = + export.map_err(|error| component_invalid_for(spec, error.to_string()))?; + if export.name.name != spec.callable_export || export.name.implements.is_some() || export.kind != ComponentExternalKind::Func { return Err(ProviderLowererComponentError::new( - ProviderLowererComponentErrorKind::LowerExportInvalid, + export_invalid_kind(spec), export.name.name, Some(export.kind.desc().to_owned()), )); } - lower_exports += 1; - if lower_exports > 1 { + callable_exports += 1; + if callable_exports > 1 { return Err(ProviderLowererComponentError::new( - ProviderLowererComponentErrorKind::LowerExportDuplicate, - LOWER_EXPORT, + export_duplicate_kind(spec), + spec.callable_export, None, )); } @@ -962,7 +1276,7 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { if !outer_is_component { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ComponentEncodingInvalid, - LOWERER_PACKAGE, + spec.package, Some("component-model".to_owned()), )); } @@ -970,7 +1284,7 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { return Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ContractAttestationMissing, CONTRACT_SECTION_NAME, - Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + Some(spec.contract_coordinate.to_owned()), )); } let protocol_type_import_count = protocol_type_imports @@ -988,24 +1302,24 @@ pub(crate) fn audit_component(bytes: &[u8]) -> Result { )), )); } - if lower_exports != 1 { + if callable_exports != 1 { return Err(ProviderLowererComponentError::new( - ProviderLowererComponentErrorKind::LowerExportMissing, - LOWER_EXPORT, + export_missing_kind(spec), + spec.callable_export, None, )); } Validator::new_with_features(WasmFeatures::all()) .validate_all(bytes) - .map_err(|error| component_invalid(error.to_string()))?; - authenticate_component_world(bytes)?; + .map_err(|error| component_invalid_for(spec, error.to_string()))?; + authenticate_component_world_for(spec, bytes)?; Ok(ComponentAudit { contract_attestations, protocol_imports, protocol_type_imports: protocol_type_import_count, - lower_exports, + callable_exports, }) } @@ -1052,6 +1366,15 @@ pub(crate) fn sync_output( } } +/// Checks or writes explicit verifier component bytes without path discovery. +pub(crate) fn sync_verifier_output( + output_path: &Path, + bytes: &[u8], + mode: ComponentOutputMode, +) -> Result { + sync_output(output_path, bytes, mode).map_err(|error| error.for_spec(&VERIFIER_SPEC)) +} + fn write_output(output_path: &Path, bytes: &[u8]) -> Result { write_output_with(output_path, |temporary| temporary.write_all(bytes)) } @@ -1169,9 +1492,13 @@ fn create_temporary_sibling(output_path: &Path) -> Result<(PathBuf, File)> { } fn append_contract_attestation(bytes: &mut Vec) { + append_contract_attestation_for(&LOWERER_SPEC, bytes); +} + +fn append_contract_attestation_for(spec: &ProviderComponentSpec, bytes: &mut Vec) { CustomSection { name: Cow::Borrowed(CONTRACT_SECTION_NAME), - data: Cow::Borrowed(LOWERER_CONTRACT_COORDINATE.as_bytes()), + data: Cow::Borrowed(spec.contract_coordinate.as_bytes()), } .append_to_component(bytes); } @@ -1192,76 +1519,99 @@ fn type_only_instance(component_type: &ComponentType<'_>) -> bool { export_count > 0 } -fn authenticate_component_world(bytes: &[u8]) -> Result<()> { +fn authenticate_component_world_for(spec: &ProviderComponentSpec, bytes: &[u8]) -> Result<()> { let (mut expected_resolve, expected_world) = - parse_lowerer_world(LOWERER_WIT_SOURCE, "frozen-wit")?; + parse_component_world(spec, spec.wit_source, "frozen-wit")?; let expected_package = expected_resolve.worlds[expected_world] .package .map(|package| expected_resolve.packages[package].name.clone()) - .ok_or_else(|| world_contract_invalid("frozen-wit", "lowerer world has no package"))?; + .ok_or_else(|| { + world_contract_invalid_for(spec, "frozen-wit", "selected world has no package") + })?; canonicalize_world_type_closure(&mut expected_resolve, expected_world); - let expected = encode_world_contract(&expected_resolve, expected_world, "frozen-wit")?; + let expected = encode_world_contract(spec, &expected_resolve, expected_world, "frozen-wit")?; let decoded = wit_component::decode(bytes).map_err(|error| { - world_contract_invalid("candidate-component", "component WIT decode failed") + world_contract_invalid_for(spec, "candidate-component", "component WIT decode failed") .with_detail(error.to_string()) })?; let DecodedWasm::Component(mut observed_resolve, observed_world) = decoded else { - return Err(world_contract_invalid( + return Err(world_contract_invalid_for( + spec, "candidate-component", "decoded bytes are a WIT package rather than a component", )); }; - normalize_decoded_world_identity(&mut observed_resolve, observed_world, expected_package)?; + normalize_decoded_world_identity( + spec, + &mut observed_resolve, + observed_world, + expected_package, + )?; canonicalize_world_type_closure(&mut observed_resolve, observed_world); - let observed = encode_world_contract(&observed_resolve, observed_world, "candidate-component")?; - compare_world_contracts(&expected, &observed) + let observed = encode_world_contract( + spec, + &observed_resolve, + observed_world, + "candidate-component", + )?; + compare_world_contracts(spec, &expected, &observed) } #[cfg(test)] fn authenticate_wit_source_for_test(source: &str) -> Result<()> { + authenticate_wit_source_for_spec(&LOWERER_SPEC, source) +} + +#[cfg(test)] +fn authenticate_wit_source_for_spec(spec: &ProviderComponentSpec, source: &str) -> Result<()> { let (mut expected_resolve, expected_world) = - parse_lowerer_world(LOWERER_WIT_SOURCE, "frozen-wit")?; + parse_component_world(spec, spec.wit_source, "frozen-wit")?; canonicalize_world_type_closure(&mut expected_resolve, expected_world); - let expected = encode_world_contract(&expected_resolve, expected_world, "frozen-wit")?; - let (mut observed_resolve, observed_world) = parse_lowerer_world(source, "candidate-wit")?; + let expected = encode_world_contract(spec, &expected_resolve, expected_world, "frozen-wit")?; + let (mut observed_resolve, observed_world) = + parse_component_world(spec, source, "candidate-wit")?; canonicalize_world_type_closure(&mut observed_resolve, observed_world); - let observed = encode_world_contract(&observed_resolve, observed_world, "candidate-wit")?; - compare_world_contracts(&expected, &observed) + let observed = encode_world_contract(spec, &observed_resolve, observed_world, "candidate-wit")?; + compare_world_contracts(spec, &expected, &observed) } -fn parse_lowerer_world(source: &str, reference: &str) -> Result<(Resolve, WorldId)> { +fn parse_component_world( + spec: &ProviderComponentSpec, + source: &str, + reference: &str, +) -> Result<(Resolve, WorldId)> { let mut resolve = Resolve::default(); let package = resolve .push_str("edict-target-provider.wit", source) .map_err(|error| { - world_contract_invalid(reference, "WIT parse failed").with_detail(error.to_string()) + world_contract_invalid_for(spec, reference, "WIT parse failed") + .with_detail(error.to_string()) })?; let world = resolve - .select_world(&[package], Some(LOWERER_WORLD_NAME)) + .select_world(&[package], Some(spec.world_name)) .map_err(|error| { - world_contract_invalid(reference, "lowerer world selection failed") + world_contract_invalid_for(spec, reference, "component world selection failed") .with_detail(error.to_string()) })?; Ok((resolve, world)) } fn normalize_decoded_world_identity( + spec: &ProviderComponentSpec, resolve: &mut Resolve, world: WorldId, package_name: PackageName, ) -> Result<()> { let package = resolve.worlds[world].package.ok_or_else(|| { - world_contract_invalid("candidate-component", "decoded world has no package") + world_contract_invalid_for(spec, "candidate-component", "decoded world has no package") })?; - let previous_world_name = std::mem::replace( - &mut resolve.worlds[world].name, - LOWERER_WORLD_NAME.to_owned(), - ); + let previous_world_name = + std::mem::replace(&mut resolve.worlds[world].name, spec.world_name.to_owned()); let package_worlds = &mut resolve.packages[package].worlds; if package_worlds.get(&previous_world_name) == Some(&world) { package_worlds.shift_remove(&previous_world_name); - package_worlds.insert(LOWERER_WORLD_NAME.to_owned(), world); + package_worlds.insert(spec.world_name.to_owned(), world); } resolve.packages[package].name = package_name; Ok(()) @@ -1301,20 +1651,29 @@ fn canonicalize_world_type_closure(resolve: &mut Resolve, world: WorldId) { } } -fn encode_world_contract(resolve: &Resolve, world: WorldId, reference: &str) -> Result> { +fn encode_world_contract( + spec: &ProviderComponentSpec, + resolve: &Resolve, + world: WorldId, + reference: &str, +) -> Result> { wit_component::metadata::encode(resolve, world, StringEncoding::UTF8, None).map_err(|error| { - world_contract_invalid(reference, "canonical world encoding failed") + world_contract_invalid_for(spec, reference, "canonical world encoding failed") .with_detail(error.to_string()) }) } -fn compare_world_contracts(expected: &[u8], observed: &[u8]) -> Result<()> { +fn compare_world_contracts( + spec: &ProviderComponentSpec, + expected: &[u8], + observed: &[u8], +) -> Result<()> { if expected == observed { return Ok(()); } Err(ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::WorldContractMismatch, - LOWERER_CONTRACT_COORDINATE, + spec.contract_coordinate, Some(format!( "expected:{};observed:{}", digest_hex(&sha256(expected)), @@ -1323,13 +1682,14 @@ fn compare_world_contracts(expected: &[u8], observed: &[u8]) -> Result<()> { )) } -fn world_contract_invalid( +fn world_contract_invalid_for( + spec: &ProviderComponentSpec, reference: impl Into, detail: impl Into, ) -> ProviderLowererComponentError { ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::WorldContractInvalid, - LOWERER_CONTRACT_COORDINATE, + spec.contract_coordinate, Some(reference.into()), ) .with_detail(detail) @@ -1353,10 +1713,13 @@ fn path_text(path: &Path) -> Result<&str> { }) } -fn component_invalid(detail: String) -> ProviderLowererComponentError { +fn component_invalid_for( + spec: &ProviderComponentSpec, + detail: String, +) -> ProviderLowererComponentError { ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::ComponentInvalid, - LOWERER_PACKAGE, + spec.package, Some("wasmparser".to_owned()), ) .with_detail(detail) @@ -1378,6 +1741,288 @@ mod tests { static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); + fn require_component_error( + result: Result, + message: &str, + ) -> std::result::Result> { + match result { + Err(error) => Ok(error), + Ok(_) => Err(std::io::Error::other(message).into()), + } + } + + #[test] + fn provider_verifier_spec_is_exact_and_approved() { + assert_eq!(VERIFIER_SPEC.package, "echo-edict-provider-verifier"); + assert_eq!(VERIFIER_SPEC.core_wasm, "echo_edict_provider_verifier.wasm"); + assert_eq!(VERIFIER_SPEC.world_name, "verifier"); + assert_eq!( + VERIFIER_SPEC.contract_coordinate, + "edict:target-provider/verifier@1.0.0" + ); + assert_eq!( + VERIFIER_SPEC.protocol_type_imports, + ["verification-request-v1", "verification-result-v1"] + ); + assert_eq!(VERIFIER_SPEC.callable_export, "verify"); + assert_eq!( + VERIFIER_SPEC.checked_repository_path, + "schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + ); + assert_eq!( + VERIFIER_SPEC.approved_sha256, + Some(APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256) + ); + assert_eq!( + sha256(VERIFIER_SPEC.wit_source.as_bytes()), + sha256(LOWERER_SPEC.wit_source.as_bytes()) + ); + } + + #[test] + fn provider_checked_identities_are_exact() -> std::result::Result<(), Box> + { + let lowerer = authenticated_component_for( + &LOWERER_SPEC, + include_bytes!( + "../../schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + ) + .to_vec(), + )?; + require_lowerer_component_identity(&lowerer)?; + assert_eq!( + require_approved_checked_digest(&VERIFIER_SPEC)?, + APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256 + ); + assert_eq!( + require_verifier_checked_identity()?, + APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256 + ); + Ok(()) + } + + #[test] + fn authenticated_verifier_requires_a_well_formed_approved_digest( + ) -> std::result::Result<(), Box> { + let component = authenticated_component_for( + &VERIFIER_SPEC, + include_bytes!( + "../../schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + ) + .to_vec(), + )?; + + let stale_spec = ProviderComponentSpec { + approved_sha256: Some( + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ..VERIFIER_SPEC + }; + let stale = require_component_error( + require_component_identity_for(&stale_spec, &component), + "a stale approved verifier digest must reject the authenticated component", + )?; + assert_eq!( + stale.kind(), + ProviderLowererComponentErrorKind::CandidateDigestMismatch + ); + + let malformed_spec = ProviderComponentSpec { + approved_sha256: Some("not-a-lowercase-sha256"), + ..VERIFIER_SPEC + }; + let malformed = require_component_error( + require_component_identity_for(&malformed_spec, &component), + "a malformed approved verifier digest must reject the authenticated component", + )?; + assert_eq!( + malformed.kind(), + ProviderLowererComponentErrorKind::ExpectedDigestInvalid + ); + Ok(()) + } + + #[test] + fn wrong_verifier_signature_cannot_receive_the_frozen_attestation( + ) -> std::result::Result<(), Box> { + let source = VERIFIER_SPEC.wit_source.replacen( + "export verify: func(request: verification-request-v1) -> verification-result-v1;", + "export verify: func(request: verification-result-v1) -> verification-request-v1;", + 1, + ); + + let error = require_component_error( + authenticate_wit_source_for_spec(&VERIFIER_SPEC, &source), + "a wrong verifier signature must fail world authentication", + )?; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractMismatch + ); + assert_eq!(error.subject(), VERIFIER_SPEC.contract_coordinate); + Ok(()) + } + + #[test] + fn lowerer_public_identity_remains_the_merged_spec() { + assert_eq!( + LOWERER_SPEC.contract_coordinate, + LOWERER_CONTRACT_COORDINATE + ); + assert_eq!( + LOWERER_SPEC.approved_sha256, + Some(APPROVED_CHECKED_COMPONENT_SHA256) + ); + assert_eq!( + LOWERER_SPEC.checked_repository_path, + CHECKED_COMPONENT_REPOSITORY_PATH + ); + assert_eq!(LOWERER_SPEC.callable_export, "lower"); + } + + #[test] + fn verifier_spec_drives_exact_attestation_and_export_admission( + ) -> std::result::Result<(), Box> { + let mut bytes = Component::new().finish(); + append_contract_attestation_for(&VERIFIER_SPEC, &mut bytes); + + let mut sections = Vec::new(); + for payload in Parser::new(0).parse_all(&bytes) { + match payload? { + Payload::CustomSection(section) if section.name() == CONTRACT_SECTION_NAME => { + sections.push(section.data().to_vec()); + } + _ => {} + } + } + assert_eq!(sections, [VERIFIER_CONTRACT_COORDINATE.as_bytes()]); + + let lowerer_error = require_component_error( + audit_component(&bytes), + "the verifier attestation must not authenticate as a lowerer", + )?; + assert_eq!( + lowerer_error.kind(), + ProviderLowererComponentErrorKind::ContractAttestationMismatch + ); + assert_eq!(lowerer_error.subject(), CONTRACT_SECTION_NAME); + assert_eq!(lowerer_error.reference(), Some(LOWERER_CONTRACT_COORDINATE)); + + let verifier_error = require_component_error( + audit_verifier_component(&bytes), + "a verifier without its callable export must fail", + )?; + assert_eq!( + verifier_error.kind(), + ProviderLowererComponentErrorKind::VerifyExportMissing + ); + assert_eq!(verifier_error.subject(), VERIFY_EXPORT); + assert_eq!(verifier_error.reference(), None); + Ok(()) + } + + #[test] + fn verifier_world_rejects_world_export_and_alias_drift( + ) -> std::result::Result<(), Box> { + authenticate_wit_source_for_spec(&VERIFIER_SPEC, VERIFIER_SPEC.wit_source)?; + + let renamed_world = + VERIFIER_SPEC + .wit_source + .replacen("world verifier {", "world verifier-drift {", 1); + let error = require_component_error( + authenticate_wit_source_for_spec(&VERIFIER_SPEC, &renamed_world), + "a renamed verifier world must fail closed", + )?; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractInvalid + ); + assert_eq!(error.subject(), VERIFIER_CONTRACT_COORDINATE); + + let renamed_export = VERIFIER_SPEC.wit_source.replacen( + " export verify: func(", + " export verify-drift: func(", + 1, + ); + let error = require_component_error( + authenticate_wit_source_for_spec(&VERIFIER_SPEC, &renamed_export), + "a renamed verifier export must fail world equality", + )?; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractMismatch + ); + assert_eq!(error.subject(), VERIFIER_CONTRACT_COORDINATE); + + let lowerer_alias_closure = VERIFIER_SPEC.wit_source.replacen( + concat!( + " use protocol.{verification-request-v1, verification-result-v1};\n\n", + " export verify: func(request: verification-request-v1) -> verification-result-v1;", + ), + concat!( + " use protocol.{lowering-request-v1, lowering-result-v1};\n\n", + " export verify: func(request: lowering-request-v1) -> lowering-result-v1;", + ), + 1, + ); + let error = require_component_error( + authenticate_wit_source_for_spec(&VERIFIER_SPEC, &lowerer_alias_closure), + "the lowerer alias closure must not authenticate as the verifier", + )?; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractMismatch + ); + assert_eq!(error.subject(), VERIFIER_CONTRACT_COORDINATE); + Ok(()) + } + + #[test] + fn checked_verifier_component_promotion_is_exact_and_idempotent( + ) -> std::result::Result<(), Box> { + let bytes = include_bytes!( + "../../schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + ); + assert_eq!( + digest_hex(&sha256(bytes)), + APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256 + ); + let first = temporary_output("verifier-promotion-valid-a"); + let second = temporary_output("verifier-promotion-valid-b"); + let output = temporary_output("verifier-promotion-output"); + fs::write(&first, bytes)?; + fs::write(&second, bytes)?; + + let (component, status) = + promote_verifier_reproducible_candidates(&first, &second, &output)?; + assert_eq!(status, ComponentOutputStatus::Written); + assert_eq!(component.bytes(), bytes); + let (_, status) = promote_verifier_reproducible_candidates(&first, &second, &output)?; + assert_eq!(status, ComponentOutputStatus::Current); + assert_eq!(fs::read(&output)?, bytes); + + fs::remove_file(first)?; + fs::remove_file(second)?; + fs::remove_file(output)?; + Ok(()) + } + + #[test] + fn lowerer_componentize_wrapper_preserves_its_error_surface( + ) -> std::result::Result<(), Box> { + let error = require_component_error( + componentize(b"not-core-wasm"), + "invalid core Wasm must still fail componentization", + )?; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::ComponentEncodingFailed + ); + assert_eq!(error.subject(), LOWERER_PACKAGE); + Ok(()) + } + #[test] fn appends_one_exact_top_level_attestation( ) -> std::result::Result<(), Box> { @@ -2131,6 +2776,18 @@ mod tests { " GIT_CONFIG_KEY_0: safe.directory\n", " GIT_CONFIG_VALUE_0: ${{ github.workspace }}", ))); + assert!(ci.contains(" build-edict-provider-verifier:")); + assert!(ci.contains("name: Build and check Edict provider verifier")); + assert!(ci.contains("cargo xtask provider-verifier-component check")); + assert!(ci.contains("--target-dir target/provider-verifier-component")); + assert!(ci.contains( + "--output schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + )); + assert!(ci.contains( + "cargo +1.90.0 clippy -p echo-edict-provider-verifier --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs" + )); + assert!(ci.matches(designated_image).count() >= 2); + assert!(ci.matches("options: --platform linux/amd64").count() >= 2); let determinism = include_str!("../../.github/workflows/det-gates.yml"); assert!(determinism.contains(designated_image)); @@ -2155,6 +2812,45 @@ mod tests { assert!(determinism.contains( "cmp build2.lowerer.component.wasm schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" )); + assert!(determinism.contains("cargo xtask provider-verifier-component designated-build")); + assert!(determinism.contains("--target-dir target/provider-verifier-repro")); + assert!(determinism.contains("--output target/verifier.echo-dpo.component.wasm")); + assert!(determinism.contains( + "sha256sum target/verifier.echo-dpo.component.wasm > \"verifier-hash${CANDIDATE}.txt\"" + )); + assert!(determinism.contains( + "cp target/verifier.echo-dpo.component.wasm \"build${CANDIDATE}.verifier.component.wasm\"" + )); + assert!(determinism.contains("verifier-hash${{ matrix.candidate }}.txt")); + assert!(determinism.contains("build${{ matrix.candidate }}.verifier.component.wasm")); + assert!(determinism.contains("diff verifier-hash1.txt verifier-hash2.txt")); + assert!(determinism + .contains("cmp build1.verifier.component.wasm build2.verifier.component.wasm")); + assert!(determinism.contains("cargo xtask provider-verifier-component promote")); + assert!(determinism.contains("--candidate-a build1.verifier.component.wasm")); + assert!(determinism.contains("--candidate-b build2.verifier.component.wasm")); + assert!(determinism.contains("--output target/verifier.echo-dpo.promoted.component.wasm")); + assert!(determinism.contains( + "cmp build1.verifier.component.wasm target/verifier.echo-dpo.promoted.component.wasm" + )); + assert!(determinism.contains( + "cmp build1.verifier.component.wasm schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + )); + assert!(determinism.contains( + "cmp build2.verifier.component.wasm schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm" + )); + assert!(determinism.contains(concat!( + " verifier-hash1.txt\n", + " verifier-hash2.txt\n", + " build1.verifier.component.wasm\n", + " build2.verifier.component.wasm", + ))); + assert!(determinism.contains(concat!( + " gathered-artifacts/build-repro-artifacts/verifier-hash1.txt\n", + " gathered-artifacts/build-repro-artifacts/verifier-hash2.txt\n", + " gathered-artifacts/build-repro-artifacts/build1.verifier.component.wasm\n", + " gathered-artifacts/build-repro-artifacts/build2.verifier.component.wasm", + ))); let host = include_str!("../../scripts/verify-edict-provider-host-v1.sh"); assert!(host.contains("provider-lowerer-component build"));