From 0abc4182bad1bc463602e61f6c86c1592f1d1e5f Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 12:09:01 -0700 Subject: [PATCH 1/6] Implement Echo Edict provider lowerer --- .github/workflows/ci.yml | 48 + .github/workflows/det-gates.yml | 25 +- CHANGELOG.md | 17 + Cargo.lock | 178 ++- Cargo.toml | 3 + crates/echo-edict-canonical/Cargo.toml | 20 + crates/echo-edict-canonical/README.md | 29 + crates/echo-edict-canonical/src/lib.rs | 521 +++++++ .../tests/canonical_contract.rs | 269 ++++ crates/echo-edict-provider-lowerer/Cargo.toml | 27 + crates/echo-edict-provider-lowerer/README.md | 46 + .../src/component.rs | 176 +++ crates/echo-edict-provider-lowerer/src/lib.rs | 835 ++++++++++ .../tests/lowerer_contract.rs | 754 +++++++++ .../wit/edict-target-provider.wit | 221 +++ crates/echo-wesley-gen/Cargo.toml | 1 + .../echo-wesley-gen/src/provider_canonical.rs | 520 +------ crates/echo-wesley-gen/src/provider_corpus.rs | 10 +- .../tests/provider_artifact_corpus.rs | 4 +- det-policy.yaml | 8 + .../application-contract-hosting.md | 17 + schemas/edict-provider/README.md | 8 + .../edict-provider/components/v1/README.md | 38 + .../v1/lowerer.echo-dpo.component.wasm | Bin 0 -> 112718 bytes .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- scripts/ban-globals.sh | 4 +- scripts/ban-nondeterminism.sh | 4 +- scripts/verify-edict-provider-host-v1.sh | 40 + scripts/verify-local.sh | 43 + tests/edict-provider-host-v1/Cargo.lock | 1372 +++++++++++++++++ tests/edict-provider-host-v1/Cargo.toml | 40 + .../fixtures/edict-7cd8858c/ORIGIN.toml | 18 + .../edict-7cd8858c/lowerer.component.wasm | Bin 0 -> 28007 bytes .../malformed-lowerer.component.wasm | Bin 0 -> 2165 bytes .../rust-toolchain.toml | 7 + tests/edict-provider-host-v1/src/lib.rs | 3 + .../tests/host_contract.rs | 999 ++++++++++++ tests/hooks/test_verify_local.sh | 73 + xtask/Cargo.toml | 4 + xtask/src/main.rs | 44 + xtask/src/provider_lowerer_component.rs | 1018 ++++++++++++ 42 files changed, 6920 insertions(+), 528 deletions(-) create mode 100644 crates/echo-edict-canonical/Cargo.toml create mode 100644 crates/echo-edict-canonical/README.md create mode 100644 crates/echo-edict-canonical/src/lib.rs create mode 100644 crates/echo-edict-canonical/tests/canonical_contract.rs create mode 100644 crates/echo-edict-provider-lowerer/Cargo.toml create mode 100644 crates/echo-edict-provider-lowerer/README.md create mode 100644 crates/echo-edict-provider-lowerer/src/component.rs create mode 100644 crates/echo-edict-provider-lowerer/src/lib.rs create mode 100644 crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs create mode 100644 crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit create mode 100644 schemas/edict-provider/components/v1/README.md create mode 100644 schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm create mode 100755 scripts/verify-edict-provider-host-v1.sh create mode 100644 tests/edict-provider-host-v1/Cargo.lock create mode 100644 tests/edict-provider-host-v1/Cargo.toml create mode 100644 tests/edict-provider-host-v1/fixtures/edict-7cd8858c/ORIGIN.toml create mode 100644 tests/edict-provider-host-v1/fixtures/edict-7cd8858c/lowerer.component.wasm create mode 100644 tests/edict-provider-host-v1/fixtures/edict-7cd8858c/malformed-lowerer.component.wasm create mode 100644 tests/edict-provider-host-v1/rust-toolchain.toml create mode 100644 tests/edict-provider-host-v1/src/lib.rs create mode 100644 tests/edict-provider-host-v1/tests/host_contract.rs create mode 100644 xtask/src/provider_lowerer_component.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23900ecc..5e4b3a11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ jobs: -p warp-geom \ -p warp-wasm \ -p echo-wasm-abi \ + -p echo-edict-canonical \ + -p echo-edict-provider-lowerer \ -p echo-runtime-schema \ -p echo-dry-tests \ -p echo-graph \ @@ -175,6 +177,52 @@ jobs: . - name: cargo test (workspace sans warp-core) run: cargo test --workspace --exclude warp-core + - name: package publishable Edict canonical leaf + run: cargo package -p echo-edict-canonical --locked + + build-edict-provider-lowerer: + name: Build and check Edict provider lowerer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: false + - uses: dtolnay/rust-toolchain@1.90.0 + with: + components: clippy + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . + - name: build, audit, and check exact component bytes + run: | + cargo xtask provider-lowerer-component \ + --target-dir target/provider-lowerer-component \ + --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm + - 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 + + edict-provider-host-v1: + name: Edict provider host contract (Rust 1.94) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: false + - uses: dtolnay/rust-toolchain@1.90.0 + with: + targets: wasm32-unknown-unknown + - uses: dtolnay/rust-toolchain@1.94.0 + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . + tests/edict-provider-host-v1 -> target/edict-provider-host-v1 + - name: invoke and replay through the exact Edict host + run: scripts/verify-edict-provider-host-v1.sh build-echo-cas-wasm: name: Build echo-cas (wasm32) diff --git a/.github/workflows/det-gates.yml b/.github/workflows/det-gates.yml index 0a76bd5e..5c173506 100644 --- a/.github/workflows/det-gates.yml +++ b/.github/workflows/det-gates.yml @@ -228,7 +228,7 @@ jobs: timeout-minutes: 20 steps: - name: Setup Rust (Global) - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.90.0 with: targets: wasm32-unknown-unknown - name: Checkout Build 1 @@ -244,6 +244,12 @@ jobs: cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash1.txt cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build1.wasm + cargo xtask provider-lowerer-component \ + --target-dir target/provider-lowerer-repro \ + --output target/lowerer.echo-dpo.component.wasm \ + --write + sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash1.txt + cp target/lowerer.echo-dpo.component.wasm ../build1.lowerer.component.wasm - name: Checkout Build 2 uses: actions/checkout@v4 with: @@ -257,10 +263,19 @@ jobs: cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash2.txt cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build2.wasm + cargo xtask provider-lowerer-component \ + --target-dir target/provider-lowerer-repro \ + --output target/lowerer.echo-dpo.component.wasm \ + --write + sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash2.txt + cp target/lowerer.echo-dpo.component.wasm ../build2.lowerer.component.wasm - name: Compare hashes run: | diff hash1.txt hash2.txt || (echo "Reproducibility failure: Hashes differ!" && exit 1) + diff lowerer-hash1.txt lowerer-hash2.txt || (echo "Lowerer reproducibility failure: Hashes differ!" && exit 1) + cmp build1.lowerer.component.wasm build2.lowerer.component.wasm echo "Hashes match: $(cat hash1.txt)" + echo "Lowerer hashes match: $(cat lowerer-hash1.txt)" - name: Upload build artifacts if: always() uses: actions/upload-artifact@v4 @@ -271,6 +286,10 @@ jobs: hash2.txt build1.wasm build2.wasm + lowerer-hash1.txt + lowerer-hash2.txt + build1.lowerer.component.wasm + build2.lowerer.component.wasm validate-evidence: name: Evidence artifact presence @@ -349,6 +368,10 @@ jobs: gathered-artifacts/build-repro-artifacts/hash2.txt gathered-artifacts/build-repro-artifacts/build1.wasm gathered-artifacts/build-repro-artifacts/build2.wasm + gathered-artifacts/build-repro-artifacts/lowerer-hash1.txt + 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 ) fi diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d43b1e..41c2173e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ ### Added +- Echo now provides the exact `edict:target-provider/lowerer@1.0.0` + Component Model implementation for the first checked provider closure. The + pure lowerer accepts only explicit digest-bound Core, target-profile, + authority, lawpack, lowerability, and output-role inputs; produces canonical + `echo.span-ir/v1` Target IR with byte-for-byte parity to Edict's built-in Echo + wrapper; and returns typed refusals for unsupported ABI, profiles, semantics, + reads, rebound operations, unresolved authored optics, changed type bindings, + intrinsics, and output roles. A deterministic build boundary pins the frozen + WIT bytes, rejects ambient or callable imports, checks the exact decoded world + type graph and contract attestation, and reproduces the checked component + byte-for-byte across fresh target directories. These artifacts describe and + translate provider semantics; they confer no Echo runtime authority. +- `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 + re-export, while executable provider components use the same codec without + depending on generator or Wesley APIs. - `echo-wesley-gen` now checks in the first exact 22-file Edict provider artifact corpus: five canonical-CBOR primaries, fourteen canonical-CBOR resources, the self-contained CDDL, Wesley provenance JSON, and diff --git a/Cargo.lock b/Cargo.lock index a5ad3413..474fed16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,24 @@ dependencies = [ "warp-core", ] +[[package]] +name = "echo-edict-canonical" +version = "0.1.0" +dependencies = [ + "hex", + "sha2", +] + +[[package]] +name = "echo-edict-provider-lowerer" +version = "0.1.0" +dependencies = [ + "echo-edict-canonical", + "hex", + "sha2", + "wit-bindgen 0.58.0", +] + [[package]] name = "echo-file-aperture" version = "0.1.0" @@ -728,6 +746,7 @@ dependencies = [ "cddl-cat", "ciborium", "clap", + "echo-edict-canonical", "hex", "prettyplease", "proc-macro2", @@ -970,6 +989,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "hashlink" @@ -1041,6 +1065,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.14.0" @@ -1123,6 +1153,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.178" @@ -1661,6 +1697,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -2073,6 +2115,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2201,7 +2249,7 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", ] [[package]] @@ -2249,6 +2297,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f998ccc6e012f7b86865eb2a106c8a0422017a1a88977ce01a69f2244be2e57" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -2542,6 +2625,95 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a43552cfa071f246cfd99e5dbb23710dfe7336b3259e09339818483359470749" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4738d1c9a78e97bc7f664bfafd5d8e67d7bb26faa5c41e6d628e8bbdad3ec351" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130ce1f531bc9f9a75922244aa773bf5e2117fda1ef4a86b9f98d6b8135eb46" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.111", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07296369e4d598e7e79b64eef66f724d83324ea671bcf677d78fc5cf92604ae5" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a5e60173c413659c689f0581b0cf5d1a2404077568f9ffdce748a9eb2fc913" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "xtask" version = "0.1.0" @@ -2559,6 +2731,10 @@ dependencies = [ "time", "warp-cli", "warp-core", + "wasm-encoder", + "wasmparser", + "wit-component", + "wit-parser", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0be635ad..59381ddd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ members = [ "crates/echo-dind-tests", "crates/echo-wasm-abi", "crates/echo-registry-api", + "crates/echo-edict-canonical", + "crates/echo-edict-provider-lowerer", "crates/echo-wesley-gen", "crates/echo-dry-tests", "crates/echo-cas", @@ -39,6 +41,7 @@ echo-file-aperture = { version = "0.1.0", path = "crates/echo-file-aperture" } echo-graph = { version = "0.1.0", path = "crates/echo-graph" } echo-runtime-schema = { version = "0.1.0", path = "crates/echo-runtime-schema", default-features = false } echo-registry-api = { version = "0.1.0", path = "crates/echo-registry-api" } +echo-edict-canonical = { version = "0.1.0", path = "crates/echo-edict-canonical" } echo-scene-codec = { version = "0.1.0", path = "crates/echo-scene-codec" } echo-scene-port = { version = "0.1.0", path = "crates/echo-scene-port" } echo-wasm-abi = { version = "0.1.0", path = "crates/echo-wasm-abi" } diff --git a/crates/echo-edict-canonical/Cargo.toml b/crates/echo-edict-canonical/Cargo.toml new file mode 100644 index 00000000..922f345e --- /dev/null +++ b/crates/echo-edict-canonical/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-edict-canonical" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +description = "Pure Echo implementation of Edict canonical CBOR and artifact digest framing." +license = "Apache-2.0" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "edict", "cbor", "canonical", "deterministic"] +categories = ["encoding", "development-tools"] + +[dependencies] +hex = "0.4" +sha2 = "0.10" + +[lints] +workspace = true diff --git a/crates/echo-edict-canonical/README.md b/crates/echo-edict-canonical/README.md new file mode 100644 index 00000000..5e663009 --- /dev/null +++ b/crates/echo-edict-canonical/README.md @@ -0,0 +1,29 @@ + + + +# Echo Edict Canonical Codec + +`echo-edict-canonical` is Echo's pure implementation of the frozen +`edict.canonical-cbor/v1` value and byte contract plus +`edict.digest/v1` domain-framed SHA-256 identities. + +The crate is intentionally smaller than either Echo's runtime codecs or the +provider generator. Both `echo-wesley-gen` and executable provider components +use this boundary so canonical bytes and digest propositions do not diverge. +It admits nulls, booleans, the CBOR major-zero and major-one integer range, +definite-length byte and UTF-8 text strings, arrays, and maps. Encoding sorts +maps by canonical key bytes and rejects duplicate canonical keys. Decoding +rejects indefinite-length, tagged, floating-point, simple, non-minimal, +trailing, malformed, duplicate-key, and noncanonical inputs. Both directions +enforce Edict's exact 128-container nesting boundary. + +Failures expose stable `CanonicalValueErrorKind` values rather than Rust debug +spellings. The implementation is deterministic and pure: it performs no +filesystem, registry, environment, or network discovery. Version `0.1.0` is a +publishable leaf consumed by `echo-wesley-gen`; the checked provider component +uses the same implementation without making its build crate part of the public +registry dependency graph. + +This codec authenticates neither an artifact's schema nor its authority. A +caller must separately validate the decoded value against the owning admitted +CDDL root and must not treat a domain-framed digest as runtime Echo admission. diff --git a/crates/echo-edict-canonical/src/lib.rs b/crates/echo-edict-canonical/src/lib.rs new file mode 100644 index 00000000..81cac630 --- /dev/null +++ b/crates/echo-edict-canonical/src/lib.rs @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Exact `edict.canonical-cbor/v1` values, bytes, and artifact digests. +//! +//! This crate is a pure compatibility boundary for the Edict-owned provider +//! contract. It deliberately does not reuse Echo's WASM ABI codec: that codec +//! admits a different value model. The implementation accepts only the +//! definite-length Edict v1 subset, enforces the published nesting bound, and +//! validates decoded bytes by exact canonical re-encoding. + +use std::collections::BTreeSet; +use std::fmt; +use std::str; + +use sha2::{Digest, Sha256}; + +/// Coordinate of the canonical encoding profile implemented by this module. +pub const EDICT_CANONICAL_CBOR_V1: &str = "edict.canonical-cbor/v1"; + +/// Domain marker at the head of every Edict v1 artifact digest frame. +pub const EDICT_DIGEST_FRAME_V1: &str = "edict.digest/v1"; + +/// Maximum child nesting depth accepted by the Edict v1 encoder and decoder. +/// +/// The root value is at depth zero. A scalar wrapped in exactly 128 containers +/// is accepted; one additional container is rejected. +pub const MAX_CANONICAL_NESTING_DEPTH_V1: usize = 128; + +/// Stable failure categories for Edict canonical values and byte streams. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalValueErrorKind { + /// A value or collection length is outside the supported value model. + UnsupportedValue, + /// An integer is outside the CBOR major-zero or major-one `u64` range. + InvalidInteger, + /// The byte stream ended before one complete value was decoded. + UnexpectedEof, + /// Bytes remained after one complete canonical value. + TrailingData, + /// The byte stream used an unsupported CBOR major or additional-info form. + UnsupportedCbor, + /// The decoded value did not re-encode to the exact supplied bytes. + NonCanonical, + /// The value exceeded the published 128-level nesting bound. + NestingLimitExceeded, + /// A map contained two keys with identical canonical encodings. + DuplicateMapKey, + /// A CBOR text string was not valid UTF-8. + InvalidUtf8, +} + +impl CanonicalValueErrorKind { + const fn label(self) -> &'static str { + match self { + Self::UnsupportedValue => "unsupported-value", + Self::InvalidInteger => "invalid-integer", + Self::UnexpectedEof => "unexpected-eof", + Self::TrailingData => "trailing-data", + Self::UnsupportedCbor => "unsupported-cbor", + Self::NonCanonical => "noncanonical", + Self::NestingLimitExceeded => "nesting-limit-exceeded", + Self::DuplicateMapKey => "duplicate-map-key", + Self::InvalidUtf8 => "invalid-utf8", + } + } +} + +/// Structured canonical-value failure with a stable kind and diagnostic detail. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CanonicalValueError { + kind: CanonicalValueErrorKind, + detail: String, +} + +impl CanonicalValueError { + /// Returns the stable machine-readable failure category. + #[must_use] + pub const fn kind(&self) -> CanonicalValueErrorKind { + self.kind + } + + /// Returns deterministic diagnostic detail for the failed boundary. + #[must_use] + pub fn detail(&self) -> &str { + &self.detail + } + + fn new(kind: CanonicalValueErrorKind, detail: impl Into) -> Self { + Self { + kind, + detail: detail.into(), + } + } +} + +impl fmt::Display for CanonicalValueError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}: {}", self.kind.label(), self.detail) + } +} + +impl std::error::Error for CanonicalValueError {} + +/// Value tree admitted by `edict.canonical-cbor/v1`. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum CanonicalValueV1 { + /// CBOR null. + Null, + /// CBOR false or true. + Bool(bool), + /// CBOR major-zero or major-one integer stored in a convenient host type. + Integer(i128), + /// Definite-length byte string. + Bytes(Vec), + /// Definite-length UTF-8 text string, without Unicode normalization. + Text(String), + /// Definite-length ordered array. + Array(Vec), + /// Definite-length map sorted during encoding by canonical key bytes. + Map(Vec<(CanonicalValueV1, CanonicalValueV1)>), +} + +/// Encodes one value using the exact `edict.canonical-cbor/v1` byte contract. +/// +/// # Errors +/// +/// Returns a stable failure when an integer or collection cannot be represented, +/// a map repeats a canonical key, or the value exceeds the nesting bound. +pub fn encode_canonical_cbor_v1(value: &CanonicalValueV1) -> Result, CanonicalValueError> { + let mut output = Vec::new(); + encode_value(value, &mut output, 0)?; + Ok(output) +} + +/// Decodes and authenticates one `edict.canonical-cbor/v1` value. +/// +/// Decoding is followed by exact canonical re-encoding. This rejects +/// non-minimal integers and lengths, unsorted maps, and any other supported +/// value whose supplied bytes differ from the unique Edict encoding. +/// Successful decoding authenticates the encoding only: callers must still +/// validate the value against its owning CDDL root, and Echo must separately +/// admit any runtime artifact or authority. +/// +/// # Errors +/// +/// Returns a stable failure for malformed, unsupported, trailing, +/// noncanonical, duplicate-key, invalid-UTF-8, or over-nested input. +pub fn decode_canonical_cbor_v1(bytes: &[u8]) -> Result { + let mut decoder = Decoder::new(bytes); + let value = decoder.value(0)?; + if decoder.remaining() != 0 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::TrailingData, + "canonical CBOR stream contains bytes after the first value", + )); + } + if encode_canonical_cbor_v1(&value)? != bytes { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NonCanonical, + "decoded value does not re-encode to the supplied bytes", + )); + } + Ok(value) +} + +/// Computes an Edict v1 domain-framed SHA-256 digest for a canonical value. +/// +/// The returned review rendering is `sha256:<64 lowercase hex>`. The preimage +/// is canonical CBOR for `["edict.digest/v1", domain, value]`. Each tuple member +/// is encoded at its own root so digest framing does not consume the artifact's +/// 128-level nesting budget. +/// A digest binds bytes and a caller-selected domain; it does not prove the +/// owning CDDL root or grant Echo runtime authority. +/// +/// # Errors +/// +/// Returns a stable failure for an empty domain or an unencodable value. +pub fn digest_canonical_value_v1( + domain: &str, + value: &CanonicalValueV1, +) -> Result { + if domain.is_empty() { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedValue, + "canonical artifact digest domain is empty", + )); + } + + let mut preimage = vec![0x83]; + preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( + EDICT_DIGEST_FRAME_V1.to_owned(), + ))?); + preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( + domain.to_owned(), + ))?); + preimage.extend(encode_canonical_cbor_v1(value)?); + + Ok(format!("sha256:{}", hex::encode(Sha256::digest(preimage)))) +} + +fn encode_value( + value: &CanonicalValueV1, + output: &mut Vec, + depth: usize, +) -> Result<(), CanonicalValueError> { + check_depth(depth)?; + match value { + CanonicalValueV1::Null => output.push(0xf6), + CanonicalValueV1::Bool(false) => output.push(0xf4), + CanonicalValueV1::Bool(true) => output.push(0xf5), + CanonicalValueV1::Integer(value) => encode_integer(*value, output)?, + CanonicalValueV1::Bytes(bytes) => { + encode_type_value(2, usize_to_u64(bytes.len())?, output); + output.extend_from_slice(bytes); + } + CanonicalValueV1::Text(text) => { + encode_type_value(3, usize_to_u64(text.len())?, output); + output.extend_from_slice(text.as_bytes()); + } + CanonicalValueV1::Array(values) => { + check_container_depth(depth)?; + encode_type_value(4, usize_to_u64(values.len())?, output); + for value in values { + encode_value(value, output, depth + 1)?; + } + } + CanonicalValueV1::Map(entries) => { + check_container_depth(depth)?; + let mut encoded_entries = Vec::with_capacity(entries.len()); + let mut encoded_keys = BTreeSet::new(); + for (key, value) in entries { + let mut key_bytes = Vec::new(); + encode_value(key, &mut key_bytes, depth + 1)?; + if !encoded_keys.insert(key_bytes.clone()) { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::DuplicateMapKey, + "canonical CBOR map contains duplicate keys", + )); + } + encoded_entries.push((key_bytes, value)); + } + encoded_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + encode_type_value(5, usize_to_u64(encoded_entries.len())?, output); + for (key_bytes, value) in encoded_entries { + output.extend_from_slice(&key_bytes); + encode_value(value, output, depth + 1)?; + } + } + } + Ok(()) +} + +fn check_depth(depth: usize) -> Result<(), CanonicalValueError> { + if depth > MAX_CANONICAL_NESTING_DEPTH_V1 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NestingLimitExceeded, + format!( + "canonical value nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" + ), + )); + } + Ok(()) +} + +fn check_container_depth(depth: usize) -> Result<(), CanonicalValueError> { + if depth >= MAX_CANONICAL_NESTING_DEPTH_V1 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NestingLimitExceeded, + format!( + "canonical container nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" + ), + )); + } + Ok(()) +} + +fn encode_integer(value: i128, output: &mut Vec) -> Result<(), CanonicalValueError> { + if value >= 0 { + let value = u64::try_from(value).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "positive integer exceeds the canonical CBOR uint range", + ) + })?; + encode_type_value(0, value, output); + return Ok(()); + } + + let magnitude = (-1i128).checked_sub(value).ok_or_else(|| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "negative integer cannot be converted to the canonical CBOR range", + ) + })?; + let magnitude = u64::try_from(magnitude).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "negative integer exceeds the canonical CBOR negative range", + ) + })?; + encode_type_value(1, magnitude, output); + Ok(()) +} + +fn encode_type_value(major: u8, value: u64, output: &mut Vec) { + let prefix = major << 5; + let bytes = value.to_be_bytes(); + match value { + 0..=23 => output.push(prefix | bytes[7]), + 24..=0xff => { + output.push(prefix | 0x18); + output.push(bytes[7]); + } + 0x100..=0xffff => { + output.push(prefix | 0x19); + output.extend_from_slice(&bytes[6..]); + } + 0x1_0000..=0xffff_ffff => { + output.push(prefix | 0x1a); + output.extend_from_slice(&bytes[4..]); + } + _ => { + output.push(prefix | 0x1b); + output.extend_from_slice(&bytes); + } + } +} + +fn usize_to_u64(value: usize) -> Result { + u64::try_from(value).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedValue, + "canonical collection length does not fit the CBOR uint range", + ) + }) +} + +fn checked_collection_length( + declared: u64, + remaining: u64, +) -> Result +where + HostLength: TryFrom, +{ + if declared > remaining { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR declared length exceeds the remaining bytes", + )); + } + + HostLength::try_from(declared).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR collection length does not fit usize", + ) + }) +} + +struct Decoder<'a> { + bytes: &'a [u8], + position: usize, +} + +impl<'a> Decoder<'a> { + const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, position: 0 } + } + + const fn remaining(&self) -> usize { + self.bytes.len() - self.position + } + + fn value(&mut self, depth: usize) -> Result { + check_depth(depth)?; + let initial = self.byte()?; + let major = initial >> 5; + let additional = initial & 0x1f; + match major { + 0 => Ok(CanonicalValueV1::Integer(i128::from( + self.argument(additional)?, + ))), + 1 => Ok(CanonicalValueV1::Integer( + -1 - i128::from(self.argument(additional)?), + )), + 2 => { + let length = self.length(additional)?; + Ok(CanonicalValueV1::Bytes(self.take(length)?.to_vec())) + } + 3 => { + let length = self.length(additional)?; + let bytes = self.take(length)?; + let text = str::from_utf8(bytes).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidUtf8, + "canonical CBOR text string is not valid UTF-8", + ) + })?; + Ok(CanonicalValueV1::Text(text.to_owned())) + } + 4 => { + check_container_depth(depth)?; + let length = self.length(additional)?; + let mut values = Vec::with_capacity(length); + for _ in 0..length { + values.push(self.value(depth + 1)?); + } + Ok(CanonicalValueV1::Array(values)) + } + 5 => { + check_container_depth(depth)?; + let length = self.length(additional)?; + let mut entries = Vec::with_capacity(length); + let mut encoded_keys = BTreeSet::new(); + for _ in 0..length { + let key = self.value(depth + 1)?; + let mut key_bytes = Vec::new(); + encode_value(&key, &mut key_bytes, depth + 1)?; + if !encoded_keys.insert(key_bytes) { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::DuplicateMapKey, + "canonical CBOR map contains duplicate keys", + )); + } + let value = self.value(depth + 1)?; + entries.push((key, value)); + } + Ok(CanonicalValueV1::Map(entries)) + } + 7 => match additional { + 20 => Ok(CanonicalValueV1::Bool(false)), + 21 => Ok(CanonicalValueV1::Bool(true)), + 22 => Ok(CanonicalValueV1::Null), + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR simple value is unsupported", + )), + }, + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR major type is unsupported", + )), + } + } + + fn argument(&mut self, additional: u8) -> Result { + match additional { + 0..=23 => Ok(u64::from(additional)), + 24 => Ok(u64::from(self.byte()?)), + 25 => Ok(u64::from(u16::from_be_bytes(self.take_array::<2>()?))), + 26 => Ok(u64::from(u32::from_be_bytes(self.take_array::<4>()?))), + 27 => Ok(u64::from_be_bytes(self.take_array::<8>()?)), + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "indefinite or reserved canonical CBOR length is unsupported", + )), + } + } + + fn length(&mut self, additional: u8) -> Result { + let declared = self.argument(additional)?; + let remaining = u64::try_from(self.remaining()).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "remaining canonical CBOR input does not fit the CBOR uint range", + ) + })?; + + checked_collection_length::(declared, remaining) + } + + fn byte(&mut self) -> Result { + let Some(value) = self.bytes.get(self.position).copied() else { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR expected another byte", + )); + }; + self.position += 1; + Ok(value) + } + + fn take(&mut self, length: usize) -> Result<&'a [u8], CanonicalValueError> { + let end = self.position.checked_add(length).ok_or_else(|| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR length overflowed the input position", + ) + })?; + let Some(bytes) = self.bytes.get(self.position..end) else { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR value extends past the input", + )); + }; + self.position = end; + Ok(bytes) + } + + fn take_array(&mut self) -> Result<[u8; LENGTH], CanonicalValueError> { + let mut output = [0u8; LENGTH]; + output.copy_from_slice(self.take(LENGTH)?); + Ok(output) + } +} + +#[cfg(test)] +mod tests { + use super::{checked_collection_length, CanonicalValueError, CanonicalValueErrorKind}; + + #[test] + fn declared_length_bounds_precede_host_width_conversion() { + assert_eq!( + checked_collection_length::(u64::MAX, u64::from(u32::MAX)), + Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR declared length exceeds the remaining bytes" + )) + ); + } +} diff --git a/crates/echo-edict-canonical/tests/canonical_contract.rs b/crates/echo-edict-canonical/tests/canonical_contract.rs new file mode 100644 index 00000000..1e095f0e --- /dev/null +++ b/crates/echo-edict-canonical/tests/canonical_contract.rs @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used)] +//! Public contract witnesses for the independently publishable canonical leaf. + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, + CanonicalValueErrorKind, CanonicalValueV1, MAX_CANONICAL_NESTING_DEPTH_V1, +}; + +fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn map( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + CanonicalValueV1::Map(entries.into_iter().collect()) +} + +fn string_map( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + map(entries.into_iter().map(|(key, value)| (text(key), value))) +} + +fn nested_array(depth: usize) -> CanonicalValueV1 { + (0..depth).fold(CanonicalValueV1::Null, |value, _| { + CanonicalValueV1::Array(vec![value]) + }) +} + +fn nested_array_bytes(depth: usize) -> Vec { + let mut bytes = vec![0x81; depth]; + bytes.push(0xf6); + bytes +} + +fn nested_empty_array(container_count: usize) -> CanonicalValueV1 { + assert!(container_count > 0); + (1..container_count).fold(CanonicalValueV1::Array(Vec::new()), |value, _| { + CanonicalValueV1::Array(vec![value]) + }) +} + +fn nested_empty_array_bytes(container_count: usize) -> Vec { + assert!(container_count > 0); + let mut bytes = vec![0x81; container_count - 1]; + bytes.push(0x80); + bytes +} + +fn nested_empty_map(container_count: usize) -> CanonicalValueV1 { + assert!(container_count > 0); + (1..container_count).fold(CanonicalValueV1::Map(Vec::new()), |value, _| { + CanonicalValueV1::Map(vec![(text("k"), value)]) + }) +} + +fn nested_empty_map_bytes(container_count: usize) -> Vec { + assert!(container_count > 0); + let mut bytes = Vec::with_capacity((container_count - 1) * 3 + 1); + for _ in 1..container_count { + bytes.extend([0xa1, 0x61, b'k']); + } + bytes.push(0xa0); + bytes +} + +#[test] +fn canonical_maps_are_order_independent_and_fail_closed() { + let forward = string_map([("z", text("last")), ("a", text("first"))]); + let reversed = string_map([("a", text("first")), ("z", text("last"))]); + assert_eq!( + encode_canonical_cbor_v1(&forward).expect("forward map encodes"), + encode_canonical_cbor_v1(&reversed).expect("reversed map encodes") + ); + let heterogeneous = map([ + (text(""), CanonicalValueV1::Null), + (CanonicalValueV1::Integer(24), CanonicalValueV1::Null), + ]); + assert_eq!( + encode_canonical_cbor_v1(&heterogeneous).expect("heterogeneous map encodes"), + [0xa2, 0x18, 0x18, 0xf6, 0x60, 0xf6] + ); + + let duplicate = map([(text("same"), text("one")), (text("same"), text("two"))]); + assert_eq!( + encode_canonical_cbor_v1(&duplicate) + .expect_err("duplicate canonical keys reject") + .kind(), + CanonicalValueErrorKind::DuplicateMapKey + ); + assert_eq!( + decode_canonical_cbor_v1(&[0x18, 0x00]) + .expect_err("non-minimal integer rejects") + .kind(), + CanonicalValueErrorKind::NonCanonical + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xf6, 0xf6]) + .expect_err("trailing canonical value rejects") + .kind(), + CanonicalValueErrorKind::TrailingData + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xa2, 0x61, b'a', 0x01, 0x61, b'a', 0x02]) + .expect_err("duplicate decoded map keys reject") + .kind(), + CanonicalValueErrorKind::DuplicateMapKey + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xa2, 0x61, b'b', 0x00, 0x61, b'a', 0x00]) + .expect_err("unsorted decoded map keys reject") + .kind(), + CanonicalValueErrorKind::NonCanonical + ); +} + +#[test] +fn canonical_integer_ranges_and_widths_are_exact() { + let cases: &[(i128, &[u8])] = &[ + (23, &[0x17]), + (24, &[0x18, 0x18]), + (255, &[0x18, 0xff]), + (256, &[0x19, 0x01, 0x00]), + (65_535, &[0x19, 0xff, 0xff]), + (65_536, &[0x1a, 0x00, 0x01, 0x00, 0x00]), + (4_294_967_295, &[0x1a, 0xff, 0xff, 0xff, 0xff]), + ( + 4_294_967_296, + &[0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], + ), + ( + i128::from(u64::MAX), + &[0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ), + ( + -1 - i128::from(u64::MAX), + &[0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ), + ]; + for (integer, expected) in cases { + let value = CanonicalValueV1::Integer(*integer); + let bytes = encode_canonical_cbor_v1(&value).expect("boundary integer encodes"); + assert_eq!(bytes, *expected); + assert_eq!( + decode_canonical_cbor_v1(expected).expect("boundary integer decodes"), + value + ); + } + + for integer in [i128::from(u64::MAX) + 1, -2 - i128::from(u64::MAX)] { + assert_eq!( + encode_canonical_cbor_v1(&CanonicalValueV1::Integer(integer)) + .expect_err("out-of-range integer rejects") + .kind(), + CanonicalValueErrorKind::InvalidInteger + ); + } +} + +#[test] +fn canonical_nesting_bound_includes_empty_containers_but_not_digest_framing() { + let at_limit = nested_array(MAX_CANONICAL_NESTING_DEPTH_V1); + let at_limit_bytes = nested_array_bytes(MAX_CANONICAL_NESTING_DEPTH_V1); + assert_eq!( + encode_canonical_cbor_v1(&at_limit).expect("maximum-depth value encodes"), + at_limit_bytes + ); + assert_eq!( + decode_canonical_cbor_v1(&at_limit_bytes).expect("maximum-depth bytes decode"), + at_limit + ); + digest_canonical_value_v1("test.maximum-depth/v1", &at_limit) + .expect("digest frame preserves the full artifact depth budget"); + + let over_limit = MAX_CANONICAL_NESTING_DEPTH_V1 + 1; + assert_eq!( + encode_canonical_cbor_v1(&nested_array(over_limit)) + .expect_err("over-depth value rejects") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + assert_eq!( + decode_canonical_cbor_v1(&nested_array_bytes(over_limit)) + .expect_err("over-depth bytes reject") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + + let empty_cases = [ + ( + nested_empty_array(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_array(over_limit), + nested_empty_array_bytes(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_array_bytes(over_limit), + ), + ( + nested_empty_map(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_map(over_limit), + nested_empty_map_bytes(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_map_bytes(over_limit), + ), + ]; + for (at_limit, over_limit_value, at_limit_bytes, over_limit_bytes) in empty_cases { + assert_eq!( + encode_canonical_cbor_v1(&at_limit).expect("128 empty containers encode"), + at_limit_bytes + ); + assert_eq!( + decode_canonical_cbor_v1(&at_limit_bytes).expect("128 empty containers decode"), + at_limit + ); + assert_eq!( + encode_canonical_cbor_v1(&over_limit_value) + .expect_err("the 129th empty container rejects on encode") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + assert_eq!( + decode_canonical_cbor_v1(&over_limit_bytes) + .expect_err("the 129th empty container rejects on decode") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + } +} + +#[test] +fn unsupported_cbor_and_digest_domains_fail_closed() { + for bytes in [ + &[0xc0, 0xf6][..], + &[0xf9, 0x00, 0x00], + &[0xf7], + &[0x9f, 0xff], + &[0x1c], + ] { + assert_eq!( + decode_canonical_cbor_v1(bytes) + .expect_err("unsupported CBOR form rejects") + .kind(), + CanonicalValueErrorKind::UnsupportedCbor + ); + } + assert_eq!( + decode_canonical_cbor_v1(&[0x61, 0xff]) + .expect_err("invalid UTF-8 rejects") + .kind(), + CanonicalValueErrorKind::InvalidUtf8 + ); + assert_eq!( + decode_canonical_cbor_v1(&[0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + .expect_err("oversized declared collection rejects before allocation") + .kind(), + CanonicalValueErrorKind::UnexpectedEof + ); + let value = text("domain-separated"); + assert_eq!( + digest_canonical_value_v1("", &value) + .expect_err("empty domain rejects") + .kind(), + CanonicalValueErrorKind::UnsupportedValue + ); + assert_ne!( + digest_canonical_value_v1("test.first/v1", &value).expect("first digest computes"), + digest_canonical_value_v1("test.second/v1", &value).expect("second digest computes") + ); +} diff --git a/crates/echo-edict-provider-lowerer/Cargo.toml b/crates/echo-edict-provider-lowerer/Cargo.toml new file mode 100644 index 00000000..fa8261d5 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/Cargo.toml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-edict-provider-lowerer" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +description = "Pure Echo provider lowerer for Edict's frozen Component Model boundary." +license = "Apache-2.0" +repository = "https://github.com/flyingrobots/echo" +publish = false + +[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-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md new file mode 100644 index 00000000..2f3646b1 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/README.md @@ -0,0 +1,46 @@ + + + +# Echo Edict Provider Lowerer + +`echo-edict-provider-lowerer` implements Echo's exact +`edict:target-provider/lowerer@1.0.0` component for the first checked provider +closure. Its production boundary is pure: callers supply canonical Core, the +exact target profile, the complete semantic closure, requested output roles, +and response limits through the frozen Edict WIT request. + +The first supported operation is the mutating `a.b@1.t` compatibility fixture. +The lowerer emits canonical `edict.target-ir.artifact/v1` bytes whose decoded +inner domain is `echo.span-ir/v1`. It accepts only the exact Core module +coordinate, local intent key, input/output type bindings, Echo DPO target +profile, and #652 lawpack, authority, and lowerability identities. Rebound +operations, authored optics that this crossing cannot yet discharge, changed +type bindings, unsupported Core ABI, target profiles, semantics, and output +roles produce typed provider refusals. Reads remain unsupported and fail +closed; the lowerer never represents a read as a synthetic mutation. + +The native Rust model is also the narrow unit-test boundary. A `wasm32` adapter +generated from [`wit/edict-target-provider.wit`](wit/edict-target-provider.wit) +exports the exact Component Model function and performs total conversions to +and from that model. The component imports only the frozen WIT's type closure; +it imports no filesystem, network, environment, clock, randomness, registry, +logging callback, or WASI capability. + +Build and audit the checked component from the repository root with: + +```sh +cargo +1.90.0 xtask provider-lowerer-component \ + --target-dir target/provider-lowerer-component \ + --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm +``` + +The command checks by default and never rewrites a stale output. Add `--write` +only for an intentional component refresh. Component construction validates the +complete module, exact import/export topology, and exact contract attestation. +Edict-host invocation evidence lives in the isolated Rust 1.94 witness under +`tests/edict-provider-host-v1/` so Wasmtime and unpublished Edict host crates do +not enter Echo's Rust 1.90 workspace dependency graph. + +These bytes implement a provider translation. They do not install a package, +admit runtime authority, execute an operation, or attest an Echo consequence. +Those remain explicit Echo runtime crossings. diff --git a/crates/echo-edict-provider-lowerer/src/component.rs b/crates/echo-edict-provider-lowerer/src/component.rs new file mode 100644 index 00000000..b85fddf7 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/src/component.rs @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! `wasm32` guest adapter for the frozen Edict target-provider lowerer world. + +// The generated canonical-ABI trampoline necessarily contains unsafe exports; +// all authored conversion and lowering code remains safe Rust. +#![allow(unsafe_code)] + +wit_bindgen::generate!({ + path: "wit", + world: "lowerer", +}); + +use edict::target_provider::protocol as wit; + +struct Component; + +impl Guest for Component { + fn lower(request: wit::LoweringRequestV1) -> wit::LoweringResultV1 { + from_model_result(super::lower(into_model_request(request))) + } +} + +fn into_model_request(request: wit::LoweringRequestV1) -> super::LoweringRequestV1 { + super::LoweringRequestV1 { + 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), + 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::LoweringOutputRequest) -> super::LoweringOutputRequest { + super::LoweringOutputRequest { + role: request.role, + kind: into_model_output_kind(request.kind), + domain: request.domain, + } +} + +const fn into_model_output_kind(kind: wit::LoweringOutputKind) -> super::LoweringOutputKind { + match kind { + wit::LoweringOutputKind::TargetIr => super::LoweringOutputKind::TargetIr, + wit::LoweringOutputKind::GeneratedArtifact => super::LoweringOutputKind::GeneratedArtifact, + wit::LoweringOutputKind::ReviewPayload => super::LoweringOutputKind::ReviewPayload, + } +} + +fn from_model_result(result: super::LoweringResultV1) -> wit::LoweringResultV1 { + result.map(from_model_success).map_err(from_model_refusal) +} + +fn from_model_success(success: super::LoweringSuccessV1) -> wit::LoweringSuccessV1 { + wit::LoweringSuccessV1 { + 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::LoweringOutputArtifact) -> wit::LoweringOutputArtifact { + wit::LoweringOutputArtifact { + 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::LoweringOutputKind) -> wit::LoweringOutputKind { + match kind { + super::LoweringOutputKind::TargetIr => wit::LoweringOutputKind::TargetIr, + super::LoweringOutputKind::GeneratedArtifact => wit::LoweringOutputKind::GeneratedArtifact, + super::LoweringOutputKind::ReviewPayload => wit::LoweringOutputKind::ReviewPayload, + } +} + +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-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs new file mode 100644 index 00000000..e00a8f75 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -0,0 +1,835 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure Echo lowering for Edict's frozen target-provider component boundary. +//! +//! This crate translates only explicit, digest-bound canonical artifacts. It +//! performs no discovery or I/O and grants no Echo runtime authority. + +#![deny(unsafe_code)] + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, +}; + +#[cfg(target_arch = "wasm32")] +mod component; + +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 OUTPUT_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const INNER_TARGET_IR_DOMAIN: &str = "echo.span-ir/v1"; +const TARGET_IR_ROLE: &str = "target-ir.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_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 TARGET_PROFILE_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" +); +const LAWPACK_BYTES: &[u8] = + include_bytes!("../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"); +const TARGET_AUTHORITY_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" +); +const LAWPACK_AUTHORITY_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/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 kinds that a lowerer can structurally claim. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LoweringOutputKind { + /// Target-owned intermediate representation. + TargetIr, + /// Generated application artifact. + GeneratedArtifact, + /// Non-authoritative review payload. + ReviewPayload, +} + +/// One requested output role. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LoweringOutputRequest { + /// Unique output role. + pub role: String, + /// Structurally permitted output kind. + pub kind: LoweringOutputKind, + /// Required owning domain. + pub domain: String, +} + +/// One provider-authored output without a provider-authored digest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LoweringOutputArtifact { + /// Requested output role. + pub role: String, + /// Requested output kind. + pub kind: LoweringOutputKind, + /// 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 `edict:target-provider@1.0.0`. +#[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 represented 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 lowering request mirroring the frozen WIT record. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LoweringRequestV1 { + /// 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, + /// Complete explicit semantic closure. + pub semantic_inputs: Vec, + /// Exact requested output roles. + pub requested_outputs: Vec, + /// Host-owned response limits, which cannot alter canonical provider output. + pub limits: ResponseLimitsV1, +} + +/// Successful pure lowering response. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LoweringSuccessV1 { + /// Exactly the requested and supported outputs. + pub outputs: Vec, + /// Deterministically ordered diagnostics. + pub diagnostics: Vec, +} + +/// Result returned by the first provider lowerer. +pub type LoweringResultV1 = Result; + +/// Lowers the explicit canonical Echo provider closure without discovery or I/O. +/// +/// The function emits no authoritative digest or runtime authority. A future +/// Edict host remains responsible for validating the returned bytes against the +/// owning Target IR schema and computing their authoritative digest. +/// +/// # Errors +/// +/// Returns a typed provider refusal when the request crosses an unsupported ABI, +/// profile, semantic, output, or artifact boundary. +pub fn lower(request: LoweringRequestV1) -> LoweringResultV1 { + if request.protocol_version != PROVIDER_ABI { + return Err(refusal( + ProviderRefusalKind::UnsupportedCoreAbi, + format!( + "edict:target-provider@{}.{}.{}", + request.protocol_version.major, + request.protocol_version.minor, + request.protocol_version.patch + ), + "echo.provider.unsupported-protocol", + "the provider accepts only edict:target-provider@1.0.0", + )); + } + + validate_target_profile(&request.target_profile)?; + validate_semantic_closure(&request.semantic_inputs)?; + validate_requested_outputs(&request.requested_outputs)?; + let target_ir = lower_core(&request.core, &request.target_profile.reference.digest)?; + + let outputs = if request.requested_outputs.is_empty() { + Vec::new() + } else { + let bytes = encode_canonical_cbor_v1(&target_ir).map_err(|_| { + refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + TARGET_IR_ROLE, + "echo.provider.target-ir-encoding", + "the lowered Target IR could not be canonically encoded", + ) + })?; + vec![LoweringOutputArtifact { + role: TARGET_IR_ROLE.to_owned(), + kind: LoweringOutputKind::TargetIr, + artifact: Artifact { + domain: OUTPUT_DOMAIN.to_owned(), + bytes, + }, + logical_path: None, + }] + }; + + // Response limits are deliberately host-owned. Canonical provider output is + // invariant under limit changes and the host decides whether it fits. + let _ = request.limits; + Ok(LoweringSuccessV1 { + outputs, + diagnostics: Vec::new(), + }) +} + +fn validate_target_profile(profile: &BoundArtifact) -> Result<(), ProviderRefusalV1> { + validate_binding(profile).map_err(|()| { + refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + "target-profile.echo-dpo", + "echo.provider.invalid-target-profile-artifact", + "the target-profile artifact is not canonically digest-bound", + ) + })?; + 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.provider.unsupported-target-profile", + "the lowerer 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.iter()) + { + 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.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 lowerability.artifact.reference.coordinate != LOWERABILITY_COORDINATE { + return Err(invalid_artifact( + &lowerability.role, + "artifact does not equal the checked provider closure", + )); + } + validate_lowerability(&value).map_err(|()| unsupported_semantics(&lowerability.role)) +} + +fn validate_lowerability(value: &CanonicalValueV1) -> Result<(), ()> { + if value != &expected_lowerability()? { + return Err(()); + } + Ok(()) +} + +fn expected_lowerability() -> Result { + let guard_kinds = || CanonicalValueV1::Array(vec![canonical_text("precommit-atomic")]); + let obstructions = || CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]); + let footprint_obligations = + || CanonicalValueV1::Array(vec![canonical_text("target.replace.footprint")]); + let cost_obligations = || 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", footprint_obligations()), + ("costObligations", cost_obligations()), + ])?]), + ), + ( + "requiredWriteClasses", + CanonicalValueV1::Array(vec![canonical_text("replace")]), + ), + ("guardKinds", guard_kinds()), + ("atomicity", canonical_text("atomic")), + ("postconditionSupport", CanonicalValueV1::Bool(true)), + ("obstructionCoordinates", obstructions()), + ("footprintObligations", footprint_obligations()), + ("costObligations", cost_obligations()), + ("opticContract", canonical_text("replace-point")), + ]) +} + +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_key = encode_canonical_cbor_v1(&key).map_err(|_| ())?; + Ok((encoded_key, 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 validate_requested_outputs(requests: &[LoweringOutputRequest]) -> Result<(), ProviderRefusalV1> { + match requests { + [] => Ok(()), + [request] + if request.role == TARGET_IR_ROLE + && request.kind == LoweringOutputKind::TargetIr + && request.domain == OUTPUT_DOMAIN => + { + Ok(()) + } + [request, remaining @ ..] => { + let unsupported = if request.role == TARGET_IR_ROLE + && request.kind == LoweringOutputKind::TargetIr + && request.domain == OUTPUT_DOMAIN + { + remaining.first().unwrap_or(request) + } else { + request + }; + Err(refusal( + ProviderRefusalKind::UnsupportedOutputRole, + &unsupported.role, + "echo.provider.unsupported-output-role", + "the first lowerer serves only target-ir.echo-dpo", + )) + } + } +} + +fn lower_core( + core: &BoundArtifact, + target_profile_digest: &Digest, +) -> 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_version = text_field(&value, "apiVersion") + .ok_or_else(|| invalid_artifact("core.echo-provider", "Core apiVersion is absent"))?; + if api_version != CORE_ABI { + return Err(refusal( + ProviderRefusalKind::UnsupportedCoreAbi, + api_version, + "echo.provider.unsupported-core-abi", + "the lowerer accepts only edict.core/v1", + )); + } + 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(imports) if imports.is_empty()) + || !matches!(map_field(&value, "types"), Some(CanonicalValueV1::Map(_))) + || !matches!(array_field(&value, "requiredCoreCapabilities"), Some(capabilities) if capabilities.is_empty()) + { + return Err(unsupported_semantics(coordinate)); + } + + let intents = map_field(&value, "intents") + .and_then(as_map) + .ok_or_else(|| invalid_artifact("core.echo-provider", "Core intents map is invalid"))?; + let [(intent_key, intent)] = intents.as_slice() else { + return Err(unsupported_semantics(coordinate)); + }; + let intent_name = as_text(intent_key).ok_or_else(|| unsupported_semantics(coordinate))?; + if intent_name != "t" { + return Err(unsupported_semantics(intent_name)); + } + let lowered_intent = lower_intent(intent_name, intent)?; + let digest_value = target_profile_digest_value(target_profile_digest) + .ok_or_else(|| invalid_artifact("target-profile.echo-dpo", "digest is invalid"))?; + + Ok(canonical_map([ + ("kind", canonical_text("targetIrArtifact")), + ("domain", canonical_text(INNER_TARGET_IR_DOMAIN)), + ( + "targetProfile", + canonical_map([ + ("id", canonical_text(TARGET_PROFILE_COORDINATE)), + ("digest", digest_value), + ]), + ), + ("sourceCoreCoordinate", canonical_text(coordinate)), + ("intents", canonical_map([(intent_name, lowered_intent)])), + ])) +} + +fn lower_intent( + intent_name: &str, + intent: &CanonicalValueV1, +) -> Result { + if text_field(intent, "input") != Some(OPERATION_INPUT_TYPE) + || text_field(intent, "output") != Some(OPERATION_OUTPUT_TYPE) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + if map_field(intent, "optic").is_some() { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + if text_field(intent, "requiredOperationProfile") != Some(OPERATION_PROFILE) { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let input_constraints = array_field(intent, "inputConstraints") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "input constraints are invalid"))?; + let budget = map_field(intent, "coreEvaluationBudget") + .filter(|budget| validate_budget(budget)) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core budget is invalid"))?; + let body = map_field(intent, "body") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core body is absent"))?; + let locals = array_field(body, "locals") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core locals are invalid"))?; + if !locals.iter().all(validate_local) { + return Err(invalid_artifact( + OPERATION_COORDINATE, + "Core local reference is invalid", + )); + } + 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)); + }; + let step = lower_effect_node(intent_name, node)?; + let result = map_field(body, "result") + .filter(|result| validate_expr(result)) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core result is invalid"))?; + + Ok(canonical_map([ + ("operationProfile", canonical_text(OPERATION_PROFILE)), + ( + "inputConstraints", + CanonicalValueV1::Array(input_constraints.clone()), + ), + ("coreEvaluationBudget", budget.clone()), + ("requirements", CanonicalValueV1::Array(Vec::new())), + ("steps", CanonicalValueV1::Array(vec![step])), + ("result", result.clone()), + ])) +} + +fn lower_effect_node( + intent_name: &str, + node: &CanonicalValueV1, +) -> Result { + if text_field(node, "kind") != Some("effect") + || text_field(node, "effect") != Some(SEMANTIC_EFFECT) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + let binding = map_field(node, "binding") + .filter(|binding| validate_local(binding)) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect binding is invalid"))?; + let input = map_field(node, "input") + .filter(|input| validate_expr(input)) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect input is invalid"))?; + 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) || !validate_obstruction_arm(arm) { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + + Ok(canonical_map([ + ("id", canonical_text(&format!("{intent_name}.step.0"))), + ("binding", binding.clone()), + ("effect", canonical_text(SEMANTIC_EFFECT)), + ("targetIntrinsic", canonical_text(TARGET_INTRINSIC)), + ("input", input.clone()), + ( + "obstructionFailures", + CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]), + ), + ( + "obstructionArms", + canonical_map([(FAILURE_COORDINATE, arm.clone())]), + ), + ])) +} + +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 computed = digest_canonical_value_v1(&bound.artifact.domain, &value).map_err(|_| ())?; + if computed != digest_review(&bound.reference.digest) { + return Err(()); + } + Ok(value) +} + +fn digest_review(digest: &Digest) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut review = String::with_capacity(7 + digest.bytes.len() * 2); + review.push_str("sha256:"); + for byte in &digest.bytes { + review.push(char::from(HEX[usize::from(byte >> 4)])); + review.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + review +} + +fn target_profile_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_budget(value: &CanonicalValueV1) -> bool { + ["maxSteps", "maxAllocatedBytes", "maxOutputBytes"] + .into_iter() + .all(|field| matches!(map_field(value, field), Some(CanonicalValueV1::Integer(value)) if *value >= 0)) +} + +fn validate_local(value: &CanonicalValueV1) -> bool { + ["id", "alphaName", "type"] + .into_iter() + .all(|field| text_field(value, field).is_some_and(|value| !value.is_empty())) +} + +fn validate_obstruction_arm(value: &CanonicalValueV1) -> bool { + map_field(value, "binder").is_some_and(validate_local) + && map_field(value, "value").is_some_and(validate_expr) +} + +fn validate_expr(value: &CanonicalValueV1) -> bool { + match text_field(value, "kind") { + Some("local") => map_field(value, "ref").is_some_and(validate_local), + Some("const") => map_field(value, "value").is_some_and(validate_core_value), + Some("record") => map_field(value, "fields") + .and_then(as_map) + .is_some_and(|fields| { + fields.iter().all(|(key, value)| { + as_text(key).is_some_and(|key| !key.is_empty()) && validate_expr(value) + }) + }), + Some("field") => { + text_field(value, "field").is_some_and(|field| !field.is_empty()) + && map_field(value, "base").is_some_and(validate_expr) + } + Some("call") => { + text_field(value, "callee").is_some_and(|callee| !callee.is_empty()) + && array_field(value, "typeArgs") + .is_some_and(|values| values.iter().all(|value| as_text(value).is_some())) + && array_field(value, "args").is_some_and(|values| values.iter().all(validate_expr)) + } + _ => false, + } +} + +fn validate_core_value(value: &CanonicalValueV1) -> bool { + match text_field(value, "kind") { + Some("null") => true, + Some("bool") => matches!(map_field(value, "value"), Some(CanonicalValueV1::Bool(_))), + Some("int") => { + text_field(value, "width").is_some() + && matches!( + map_field(value, "value"), + Some(CanonicalValueV1::Integer(_)) + ) + } + Some("string") => text_field(value, "value").is_some(), + Some("bytes") => matches!(map_field(value, "value"), Some(CanonicalValueV1::Bytes(_))), + _ => false, + } +} + +fn canonical_map<'a>( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + CanonicalValueV1::Map( + entries + .into_iter() + .map(|(key, value)| (canonical_text(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.provider.invalid-semantic-artifact", + message, + ) +} + +fn unsupported_semantics(subject: &str) -> ProviderRefusalV1 { + refusal( + ProviderRefusalKind::UnsupportedSemantics, + subject, + "echo.provider.unsupported-semantics", + "the supplied semantics are outside the exact first Echo lowering closure", + ) +} diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs new file mode 100644 index 00000000..77fb0ad3 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -0,0 +1,754 @@ +// 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 lowerer. + +use echo_edict_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, +}; +use echo_edict_provider_lowerer::{ + lower, Artifact, BoundArtifact, Digest, DigestAlgorithm, LoweringOutputKind, + LoweringOutputRequest, LoweringRequestV1, ProtocolVersionV1, ProviderRefusalKind, ResourceRef, + ResponseLimitsV1, SemanticInput, SemanticInputKind, +}; +use sha2::{Digest as ShaDigest, Sha256}; + +const TARGET_PROFILE: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" +); +const LAWPACK: &[u8] = + include_bytes!("../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"); +const TARGET_AUTHORITY: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" +); +const LAWPACK_AUTHORITY: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/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 OUTER_TARGET_IR_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const INNER_TARGET_IR_DOMAIN: &str = "echo.span-ir/v1"; +const TARGET_IR_ROLE: &str = "target-ir.echo-dpo"; + +const EDICT_ORACLE_CORE_HEX: &str = concat!( + "a6657479706573a665496e707574a2646b696e64665265636f7264666669656c6473a16269646e612e6240312e496e70", + "75742e6964664f7574707574a2646b696e64665265636f7264666669656c6473a16269646f612e6240312e4f75747075", + "742e69646752656365697074a2646b696e64665265636f7264666669656c6473a162696470612e6240312e5265636569", + "70742e696468496e7075742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d75", + "746638694f75747075742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d7574", + "66386a526563656970742e6964a3636d617810646b696e6466537472696e676963616e6f6e6963616c687261772d7574", + "663867696d706f7274738067696e74656e7473a16174a664626f6479a3656e6f64657381a5646b696e64666566666563", + "7465696e707574a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e70757469616c", + "7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c6462696466656666", + "6563746e7461726765742e7265706c6163656762696e64696e67a3626964676c6f63616c2e3064747970656d612e6240", + "312e5265636569707469616c7068614e616d6567246c6f63616c306e6f62737472756374696f6e4d6170a16872656a65", + "63746564a26576616c7565a4646172677380646b696e646463616c6c6663616c6c656574646f6d61696e2e5772697465", + "52656a6563746564687479706541726773806662696e646572a36269646d6f62737472756374696f6e2e306474797065", + "777461726765742e7265706c6163652e72656a656374656469616c7068614e616d656d246f62737472756374696f6e30", + "666c6f63616c7383a3626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461", + "726730a3626964676c6f63616c2e3064747970656d612e6240312e5265636569707469616c7068614e616d6567246c6f", + "63616c30a36269646d6f62737472756374696f6e2e306474797065777461726765742e7265706c6163652e72656a6563", + "74656469616c7068614e616d656d246f62737472756374696f6e3066726573756c74a2646b696e64667265636f726466", + "6669656c6473a1626964a36462617365a263726566a3626964656172672e3064747970656b612e6240312e496e707574", + "69616c7068614e616d65652461726730646b696e64656c6f63616c646b696e64656669656c64656669656c6462696465", + "696e7075746b612e6240312e496e707574666f75747075746c612e6240312e4f757470757470696e707574436f6e7374", + "7261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073086e6d61784f75747075", + "744279746573190100716d6178416c6c6f63617465644279746573190400781872657175697265644f7065726174696f", + "6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e77726974652f76316a61706956657273696f6e6d", + "65646963742e636f72652f76316a636f6f7264696e61746565612e62403178187265717569726564436f726543617061", + "62696c697469657380", +); + +const EDICT_ORACLE_TARGET_IR_HEX: &str = concat!( + "a5646b696e64707461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f76316769", + "6e74656e7473a16174a665737465707381a762696468742e737465702e3065696e707574a36462617365a263726566a3", + "626964656172672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e6465", + "6c6f63616c646b696e64656669656c64656669656c64626964666566666563746e7461726765742e7265706c61636567", + "62696e64696e67a3626964676c6f63616c2e3064747970656d612e6240312e5265636569707469616c7068614e616d65", + "67246c6f63616c306f6f62737472756374696f6e41726d73a16872656a6563746564a26576616c7565a4646172677380", + "646b696e646463616c6c6663616c6c656574646f6d61696e2e577269746552656a656374656468747970654172677380", + "6662696e646572a36269646d6f62737472756374696f6e2e306474797065777461726765742e7265706c6163652e7265", + "6a656374656469616c7068614e616d656d246f62737472756374696f6e306f746172676574496e7472696e7369637265", + "63686f2e64706f40312e7265706c616365736f62737472756374696f6e4661696c75726573816872656a656374656466", + "726573756c74a2646b696e64667265636f7264666669656c6473a1626964a36462617365a263726566a3626964656172", + "672e3064747970656b612e6240312e496e70757469616c7068614e616d65652461726730646b696e64656c6f63616c64", + "6b696e64656669656c64656669656c646269646c726571756972656d656e74738070696e707574436f6e73747261696e", + "747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e77726974652f7631", + "74636f72654576616c756174696f6e427564676574a3686d61785374657073086e6d61784f7574707574427974657319", + "0100716d6178416c6c6f636174656442797465731904006d74617267657450726f66696c65a26269646a6563686f2e64", + "706f40316664696765737482667368613235365820f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255", + "d0d8322db074736f75726365436f7265436f6f7264696e61746565612e624031", +); + +fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn integer(value: u64) -> CanonicalValueV1 { + CanonicalValueV1::Integer(i128::from(value)) +} + +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 string_map( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + map(entries) +} + +fn local(id: &str, alpha_name: &str, ty: &str) -> CanonicalValueV1 { + map([ + ("id", text(id)), + ("alphaName", text(alpha_name)), + ("type", text(ty)), + ]) +} + +fn local_expr(reference: CanonicalValueV1) -> CanonicalValueV1 { + map([("kind", text("local")), ("ref", reference)]) +} + +fn field_expr(base: CanonicalValueV1, field: &str) -> CanonicalValueV1 { + map([ + ("kind", text("field")), + ("base", base), + ("field", text(field)), + ]) +} + +fn record_expr(id: CanonicalValueV1) -> CanonicalValueV1 { + map([ + ("kind", text("record")), + ("fields", string_map([("id", id)])), + ]) +} + +fn call_expr(callee: &str, argument: CanonicalValueV1) -> CanonicalValueV1 { + map([ + ("kind", text("call")), + ("callee", text(callee)), + ("typeArgs", CanonicalValueV1::Array(Vec::new())), + ("args", CanonicalValueV1::Array(vec![argument])), + ]) +} + +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 review_digest = + digest_canonical_value_v1(domain, &value).expect("fixture has a domain-framed digest"); + let digest_bytes = hex::decode( + review_digest + .strip_prefix("sha256:") + .expect("review digest uses sha256"), + ) + .expect("review digest is hexadecimal"); + BoundArtifact { + reference: ResourceRef { + coordinate: coordinate.to_owned(), + digest: Digest { + algorithm: DigestAlgorithm::Sha256, + bytes: digest_bytes, + }, + }, + artifact: Artifact { + domain: domain.to_owned(), + bytes, + }, + } +} + +fn core_value(result: CanonicalValueV1, effect: Option<&str>) -> CanonicalValueV1 { + let input = local("local:0", "input", "a.b@1.Input"); + let receipt = local("local:1", "receipt", "a.b@1.Receipt"); + let reason = local("local:2", "reason", "target.replace.rejected"); + let nodes = effect.map_or_else(Vec::new, |effect| { + vec![map([ + ("kind", text("effect")), + ("binding", receipt.clone()), + ("effect", text(effect)), + ("input", field_expr(local_expr(input.clone()), "id")), + ( + "obstructionMap", + string_map([( + "rejected", + map([ + ("binder", reason.clone()), + ( + "value", + call_expr("domain.WriteRejected", local_expr(reason)), + ), + ]), + )]), + ), + ])] + }); + let intent = map([ + ("input", text("a.b@1.Input")), + ("output", text("a.b@1.Output")), + ( + "requiredOperationProfile", + text("continuum.profile.write/v1"), + ), + ("inputConstraints", CanonicalValueV1::Array(Vec::new())), + ( + "coreEvaluationBudget", + map([ + ("maxSteps", integer(8)), + ("maxAllocatedBytes", integer(1024)), + ("maxOutputBytes", integer(256)), + ]), + ), + ( + "body", + map([ + ("locals", CanonicalValueV1::Array(vec![input, receipt])), + ("nodes", CanonicalValueV1::Array(nodes)), + ("result", result), + ]), + ), + ]); + map([ + ("apiVersion", text("edict.core/v1")), + ("coordinate", text("a.b@1")), + ("imports", CanonicalValueV1::Array(Vec::new())), + ("types", CanonicalValueV1::Map(Vec::new())), + ("intents", string_map([("t", intent)])), + ( + "requiredCoreCapabilities", + CanonicalValueV1::Array(Vec::new()), + ), + ]) +} + +fn ordinary_result() -> CanonicalValueV1 { + record_expr(field_expr( + local_expr(local("local:0", "input", "a.b@1.Input")), + "id", + )) +} + +fn lowerability_value() -> CanonicalValueV1 { + map([ + ("apiVersion", text(LOWERABILITY_DOMAIN)), + ("operationProfile", text("continuum.profile.write/v1")), + ( + "semanticEffects", + CanonicalValueV1::Array(vec![map([ + ("coordinate", text("target.replace")), + ("writeClass", text("replace")), + ( + "guardKinds", + CanonicalValueV1::Array(vec![text("precommit-atomic")]), + ), + ( + "obstructionCoordinates", + CanonicalValueV1::Array(vec![text("rejected")]), + ), + ( + "footprintObligations", + CanonicalValueV1::Array(vec![text("target.replace.footprint")]), + ), + ( + "costObligations", + CanonicalValueV1::Array(vec![text("target.replace.cost")]), + ), + ])]), + ), + ( + "requiredWriteClasses", + CanonicalValueV1::Array(vec![text("replace")]), + ), + ( + "guardKinds", + CanonicalValueV1::Array(vec![text("precommit-atomic")]), + ), + ("atomicity", text("atomic")), + ("postconditionSupport", CanonicalValueV1::Bool(true)), + ( + "obstructionCoordinates", + CanonicalValueV1::Array(vec![text("rejected")]), + ), + ( + "footprintObligations", + CanonicalValueV1::Array(vec![text("target.replace.footprint")]), + ), + ( + "costObligations", + CanonicalValueV1::Array(vec![text("target.replace.cost")]), + ), + ("opticContract", text("replace-point")), + ]) +} + +fn request_with_core(core: CanonicalValueV1) -> LoweringRequestV1 { + LoweringRequestV1 { + protocol_version: ProtocolVersionV1 { + major: 1, + minor: 0, + patch: 0, + }, + core: bound("a.b@1", CORE_DOMAIN, canonical_bytes(&core)), + target_profile: bound("echo.dpo@1", TARGET_PROFILE_DOMAIN, TARGET_PROFILE), + 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, + ), + }, + SemanticInput { + role: "authority-facts.echo-lawpack".to_owned(), + kind: SemanticInputKind::AuthorityFacts, + artifact: bound( + "echo.dpo-lawpack-authority-facts@1", + AUTHORITY_DOMAIN, + LAWPACK_AUTHORITY, + ), + }, + SemanticInput { + role: "lawpack.echo-dpo".to_owned(), + kind: SemanticInputKind::Lawpack, + artifact: bound("echo.dpo-lawpack@1", LAWPACK_DOMAIN, LAWPACK), + }, + 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![LoweringOutputRequest { + role: TARGET_IR_ROLE.to_owned(), + kind: LoweringOutputKind::TargetIr, + domain: OUTER_TARGET_IR_DOMAIN.to_owned(), + }], + limits: ResponseLimitsV1 { + max_output_count: 8, + max_diagnostic_count: 8, + max_total_response_bytes: 64 * 1024, + }, + } +} + +fn request() -> LoweringRequestV1 { + request_with_core(core_value(ordinary_result(), Some("target.replace"))) +} + +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 text_value(value: &CanonicalValueV1) -> &str { + let CanonicalValueV1::Text(value) = value else { + panic!("value is not text"); + }; + value +} + +#[test] +fn minimal_echo_mutation_lowers_from_explicit_semantics() { + let request = request(); + let target_profile_digest = request.target_profile.reference.digest.clone(); + let success = lower(request).expect("supported explicit closure lowers"); + + assert!(success.diagnostics.is_empty()); + assert_eq!(success.outputs.len(), 1); + let output = &success.outputs[0]; + assert_eq!(output.role, TARGET_IR_ROLE); + assert_eq!(output.kind, LoweringOutputKind::TargetIr); + assert_eq!(output.artifact.domain, OUTER_TARGET_IR_DOMAIN); + assert_eq!(output.logical_path, None); + + let artifact = decode_canonical_cbor_v1(&output.artifact.bytes) + .expect("provider output is canonical CBOR"); + assert_eq!(text_value(map_field(&artifact, "kind")), "targetIrArtifact"); + assert_eq!( + text_value(map_field(&artifact, "domain")), + INNER_TARGET_IR_DOMAIN + ); + assert_eq!( + text_value(map_field(&artifact, "sourceCoreCoordinate")), + "a.b@1" + ); + let target_profile = map_field(&artifact, "targetProfile"); + assert_eq!(text_value(map_field(target_profile, "id")), "echo.dpo@1"); + assert_eq!( + map_field(target_profile, "digest"), + &CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes(target_profile_digest.bytes), + ]) + ); + + let intent = map_field(map_field(&artifact, "intents"), "t"); + assert_eq!( + text_value(map_field(intent, "operationProfile")), + "continuum.profile.write/v1" + ); + assert_eq!( + map_field(intent, "coreEvaluationBudget"), + map_field( + map_field( + map_field( + &core_value(ordinary_result(), Some("target.replace")), + "intents" + ), + "t" + ), + "coreEvaluationBudget" + ) + ); + assert_eq!(map_field(intent, "result"), &ordinary_result()); + + let CanonicalValueV1::Array(steps) = map_field(intent, "steps") else { + panic!("steps is not an array"); + }; + assert_eq!(steps.len(), 1); + assert_eq!(text_value(map_field(&steps[0], "id")), "t.step.0"); + assert_eq!(text_value(map_field(&steps[0], "effect")), "target.replace"); + assert_eq!( + text_value(map_field(&steps[0], "targetIntrinsic")), + "echo.dpo@1.replace" + ); + assert_eq!( + map_field(&steps[0], "binding"), + &local("local:1", "receipt", "a.b@1.Receipt") + ); + assert_eq!( + map_field(&steps[0], "input"), + &field_expr(local_expr(local("local:0", "input", "a.b@1.Input")), "id") + ); + assert_eq!( + map_field(map_field(&steps[0], "obstructionArms"), "rejected"), + &map([ + ( + "binder", + local("local:2", "reason", "target.replace.rejected") + ), + ( + "value", + call_expr( + "domain.WriteRejected", + local_expr(local("local:2", "reason", "target.replace.rejected")) + ) + ), + ]) + ); +} + +#[test] +fn reviewed_edict_fixture_has_exact_builtin_wrapper_parity() { + let core_bytes = hex::decode(EDICT_ORACLE_CORE_HEX).expect("oracle Core hex is valid"); + assert_eq!(core_bytes.len(), 1209); + + let mut request = request(); + request.core = bound("a.b@1", CORE_DOMAIN, core_bytes); + assert_eq!( + hex::encode(&request.core.reference.digest.bytes), + "c3dbe413c78a82f6120e64c9a04bc94e2d79505f9e4b8a65c2bc26b408d775de" + ); + + let success = lower(request).expect("reviewed Edict fixture lowers"); + assert!(success.diagnostics.is_empty()); + assert_eq!(success.outputs.len(), 1); + let output = &success.outputs[0]; + let expected = hex::decode(EDICT_ORACLE_TARGET_IR_HEX).expect("oracle Target IR hex is valid"); + assert_eq!(expected.len(), 848); + assert_eq!(output.artifact.bytes, expected); + + let output_value = decode_canonical_cbor_v1(&output.artifact.bytes) + .expect("oracle-parity output is canonical CBOR"); + assert_eq!( + digest_canonical_value_v1(OUTER_TARGET_IR_DOMAIN, &output_value) + .expect("oracle-parity output has a domain-framed digest"), + "sha256:b0d9e218f00a102d1e951c73e5063a9bbe6077e6c7468d171ec08b420e7b47da" + ); +} + +#[test] +fn vendored_wit_is_the_frozen_edict_lowerer_contract() { + let bytes = include_bytes!("../wit/edict-target-provider.wit"); + assert_eq!(bytes.len(), 7392); + assert_eq!( + hex::encode(Sha256::digest(bytes)), + "2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e" + ); +} + +#[test] +fn output_overclaim_identifies_the_first_unsupported_role() { + let mut request = request(); + request.requested_outputs.push(LoweringOutputRequest { + role: "review.echo-dpo".to_owned(), + kind: LoweringOutputKind::ReviewPayload, + domain: "edict.review-payload/v1".to_owned(), + }); + + let refusal = lower(request).expect_err("a second output role is outside this lowerer"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedOutputRole); + assert_eq!(refusal.subject.as_deref(), Some("review.echo-dpo")); +} + +#[test] +fn unrecognized_lowerability_obligation_refuses_instead_of_being_ignored() { + let mut lowerability = lowerability_value(); + *map_field_mut(&mut lowerability, "footprintObligations") = + CanonicalValueV1::Array(vec![text("unexpected.footprint")]); + let mut request = request(); + request.semantic_inputs[3].artifact = bound( + "echo.dpo-lowerability@1", + LOWERABILITY_DOMAIN, + canonical_bytes(&lowerability), + ); + + let refusal = lower(request).expect_err("unrecognized obligations cannot be discharged"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("lowerability.echo-dpo")); +} + +#[test] +fn no_requested_outputs_is_an_empty_success() { + let mut request = request(); + request.requested_outputs.clear(); + let success = lower(request).expect("zero requested roles is valid"); + assert!(success.outputs.is_empty()); + assert!(success.diagnostics.is_empty()); +} + +#[test] +fn provider_output_is_independent_of_host_response_limits() { + let first = lower(request()).expect("baseline lowers"); + let mut changed = request(); + changed.limits = ResponseLimitsV1 { + max_output_count: 0, + max_diagnostic_count: 0, + max_total_response_bytes: 0, + }; + let second = lower(changed).expect("provider does not reinterpret host limits"); + assert_eq!(first, second); +} + +#[test] +fn protocol_target_and_output_mismatches_refuse_with_stable_kinds() { + let mut protocol = request(); + protocol.protocol_version.patch = 1; + assert_eq!( + lower(protocol).expect_err("protocol mismatch refuses").kind, + ProviderRefusalKind::UnsupportedCoreAbi + ); + + let mut profile = request(); + profile.target_profile.reference.coordinate = "echo.other@1".to_owned(); + assert_eq!( + lower(profile).expect_err("profile mismatch refuses").kind, + ProviderRefusalKind::UnsupportedTargetProfile + ); + + let mut profile_domain = request(); + let coordinate = profile_domain.target_profile.reference.coordinate.clone(); + let bytes = profile_domain.target_profile.artifact.bytes.clone(); + profile_domain.target_profile = bound(&coordinate, "wrong.target-profile/v1", bytes); + assert_eq!( + lower(profile_domain) + .expect_err("profile domain mismatch refuses") + .kind, + ProviderRefusalKind::UnsupportedTargetProfile + ); + + let mut output = request(); + output.requested_outputs[0].role = "generated.echo-dpo".to_owned(); + assert_eq!( + lower(output).expect_err("unserved output refuses").kind, + ProviderRefusalKind::UnsupportedOutputRole + ); +} + +#[test] +fn malformed_or_incomplete_semantic_closure_refuses() { + let mut malformed = request(); + malformed.semantic_inputs[0].artifact.artifact.bytes.push(0); + assert_eq!( + lower(malformed) + .expect_err("malformed authority facts refuse") + .kind, + ProviderRefusalKind::InvalidSemanticArtifact + ); + + let mut missing = request(); + missing + .semantic_inputs + .retain(|input| input.role != "authority-facts.echo-lawpack"); + assert_eq!( + lower(missing).expect_err("incomplete closure refuses").kind, + ProviderRefusalKind::UnsupportedSemantics + ); +} + +#[test] +fn unsupported_core_abi_and_semantics_refuse_without_artifacts() { + let mut wrong_abi = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(&mut wrong_abi, "apiVersion") = text("edict.core/v2"); + assert_eq!( + lower(request_with_core(wrong_abi)) + .expect_err("unknown Core ABI refuses") + .kind, + ProviderRefusalKind::UnsupportedCoreAbi + ); + + let read = core_value(ordinary_result(), None); + assert_eq!( + lower(request_with_core(read)) + .expect_err("effect-free reads are not synthetic mutations") + .kind, + ProviderRefusalKind::UnsupportedSemantics + ); +} + +#[test] +fn fully_qualified_core_intent_key_refuses_instead_of_broadening_the_boundary() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let CanonicalValueV1::Map(intents) = map_field_mut(&mut core, "intents") else { + panic!("Core intents is not a map"); + }; + intents[0].0 = text("a.b@1.t"); + + assert_eq!( + lower(request_with_core(core)) + .expect_err("the canonical Core intent key is package-local") + .kind, + ProviderRefusalKind::UnsupportedSemantics + ); +} + +#[test] +fn rebound_core_coordinate_refuses_as_unsupported_semantics() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(&mut core, "coordinate") = text("x.y@1"); + let mut request = request_with_core(core); + request.core.reference.coordinate = "x.y@1".to_owned(); + + let refusal = lower(request).expect_err("a rebound Core module is not the reviewed operation"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("x.y@1")); +} + +#[test] +fn authored_core_optic_refuses_instead_of_being_silently_discarded() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let CanonicalValueV1::Map(intents) = map_field_mut(&mut core, "intents") else { + panic!("Core intents is not a map"); + }; + let CanonicalValueV1::Map(intent) = &mut intents[0].1 else { + panic!("Core intent is not a map"); + }; + intent.push(( + text("optic"), + map([ + ("opticKind", text("affectReintegration")), + ("boundaryKind", text("affect")), + ( + "apertureRequirement", + map([ + ("kind", text("footprintCeiling")), + ("ref", text("echo.dpo@1.replace-footprint")), + ]), + ), + ("supportPolicy", text("echo.dpo@1.replace-support")), + ("lossDisposition", text("refuse")), + ]), + )); + intent.sort_by_cached_key(|(key, _)| canonical_bytes(key)); + + let refusal = lower(request_with_core(core)) + .expect_err("an authored optic cannot disappear across lowering"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); +} + +#[test] +fn unsupported_intent_type_bindings_refuse_instead_of_being_ignored() { + for field in ["input", "output"] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let CanonicalValueV1::Map(intents) = map_field_mut(&mut core, "intents") else { + panic!("Core intents is not a map"); + }; + *map_field_mut(&mut intents[0].1, field) = text("x.y@1.Other"); + + let refusal = lower(request_with_core(core)) + .expect_err("unsupported operation type bindings cannot disappear across lowering"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); + } +} + +#[test] +fn renamed_lowerability_artifact_refuses_as_an_invalid_closure_member() { + let mut request = request(); + request.semantic_inputs[3].artifact.reference.coordinate = + "echo.dpo-renamed-lowerability@1".to_owned(); + + let refusal = lower(request).expect_err("lowerability identity includes its coordinate"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!(refusal.subject.as_deref(), Some("lowerability.echo-dpo")); +} + +#[test] +fn lowering_uses_core_values_instead_of_replaying_static_bytes() { + let baseline = lower(request()).expect("baseline lowers").outputs.remove(0); + let changed_result = record_expr(map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("string")), ("value", text("changed"))]), + ), + ])); + let changed = lower(request_with_core(core_value( + changed_result.clone(), + Some("target.replace"), + ))) + .expect("supported semantic variant lowers") + .outputs + .remove(0); + assert_ne!(baseline.artifact.bytes, changed.artifact.bytes); + let value = decode_canonical_cbor_v1(&changed.artifact.bytes).expect("output is canonical"); + let intent = map_field(map_field(&value, "intents"), "t"); + assert_eq!(map_field(intent, "result"), &changed_result); +} + +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")) +} diff --git a/crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit b/crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit new file mode 100644 index 00000000..0efa5903 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/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/crates/echo-wesley-gen/Cargo.toml b/crates/echo-wesley-gen/Cargo.toml index a75ce68e..a72cd6dd 100644 --- a/crates/echo-wesley-gen/Cargo.toml +++ b/crates/echo-wesley-gen/Cargo.toml @@ -21,6 +21,7 @@ cap-std = "=4.0.2" cddl-cat = { version = "0.7.1", default-features = false, features = ["ciborium"] } ciborium = "0.2.2" clap = { version = "4.4", features = ["derive"] } +echo-edict-canonical = { workspace = true } hex = "0.4" proc-macro2 = "1.0" quote = "1.0" diff --git a/crates/echo-wesley-gen/src/provider_canonical.rs b/crates/echo-wesley-gen/src/provider_canonical.rs index f91f1ba0..0b263e54 100644 --- a/crates/echo-wesley-gen/src/provider_canonical.rs +++ b/crates/echo-wesley-gen/src/provider_canonical.rs @@ -1,521 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Exact `edict.canonical-cbor/v1` values, bytes, and artifact digests. -//! -//! This is a pure compatibility boundary for the Edict-owned provider -//! contract. It deliberately does not reuse Echo's WASM ABI codec: that codec -//! admits a different value model. The implementation accepts only the -//! definite-length Edict v1 subset, enforces the published nesting bound, and -//! validates decoded bytes by exact canonical re-encoding. +//! Compatibility re-export of Echo's pure Edict canonical codec. -use std::collections::BTreeSet; -use std::fmt; -use std::str; - -use sha2::{Digest, Sha256}; - -/// Coordinate of the canonical encoding profile implemented by this module. -pub const EDICT_CANONICAL_CBOR_V1: &str = "edict.canonical-cbor/v1"; - -/// Domain marker at the head of every Edict v1 artifact digest frame. -pub const EDICT_DIGEST_FRAME_V1: &str = "edict.digest/v1"; - -/// Maximum child nesting depth accepted by the Edict v1 encoder and decoder. -/// -/// The root value is at depth zero. A scalar wrapped in exactly 128 containers -/// is accepted; one additional container is rejected. -pub const MAX_CANONICAL_NESTING_DEPTH_V1: usize = 128; - -/// Stable failure categories for Edict canonical values and byte streams. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CanonicalValueErrorKind { - /// A value or collection length is outside the supported value model. - UnsupportedValue, - /// An integer is outside the CBOR major-zero or major-one `u64` range. - InvalidInteger, - /// The byte stream ended before one complete value was decoded. - UnexpectedEof, - /// Bytes remained after one complete canonical value. - TrailingData, - /// The byte stream used an unsupported CBOR major or additional-info form. - UnsupportedCbor, - /// The decoded value did not re-encode to the exact supplied bytes. - NonCanonical, - /// The value exceeded the published 128-level nesting bound. - NestingLimitExceeded, - /// A map contained two keys with identical canonical encodings. - DuplicateMapKey, - /// A CBOR text string was not valid UTF-8. - InvalidUtf8, -} - -impl CanonicalValueErrorKind { - const fn label(self) -> &'static str { - match self { - Self::UnsupportedValue => "unsupported-value", - Self::InvalidInteger => "invalid-integer", - Self::UnexpectedEof => "unexpected-eof", - Self::TrailingData => "trailing-data", - Self::UnsupportedCbor => "unsupported-cbor", - Self::NonCanonical => "noncanonical", - Self::NestingLimitExceeded => "nesting-limit-exceeded", - Self::DuplicateMapKey => "duplicate-map-key", - Self::InvalidUtf8 => "invalid-utf8", - } - } -} - -/// Structured canonical-value failure with a stable kind and diagnostic detail. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct CanonicalValueError { - kind: CanonicalValueErrorKind, - detail: String, -} - -impl CanonicalValueError { - /// Returns the stable machine-readable failure category. - #[must_use] - pub const fn kind(&self) -> CanonicalValueErrorKind { - self.kind - } - - /// Returns deterministic diagnostic detail for the failed boundary. - #[must_use] - pub fn detail(&self) -> &str { - &self.detail - } - - fn new(kind: CanonicalValueErrorKind, detail: impl Into) -> Self { - Self { - kind, - detail: detail.into(), - } - } -} - -impl fmt::Display for CanonicalValueError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "{}: {}", self.kind.label(), self.detail) - } -} - -impl std::error::Error for CanonicalValueError {} - -/// Value tree admitted by `edict.canonical-cbor/v1`. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum CanonicalValueV1 { - /// CBOR null. - Null, - /// CBOR false or true. - Bool(bool), - /// CBOR major-zero or major-one integer stored in a convenient host type. - Integer(i128), - /// Definite-length byte string. - Bytes(Vec), - /// Definite-length UTF-8 text string, without Unicode normalization. - Text(String), - /// Definite-length ordered array. - Array(Vec), - /// Definite-length map sorted during encoding by canonical key bytes. - Map(Vec<(CanonicalValueV1, CanonicalValueV1)>), -} - -/// Encodes one value using the exact `edict.canonical-cbor/v1` byte contract. -/// -/// # Errors -/// -/// Returns a stable failure when an integer or collection cannot be represented, -/// a map repeats a canonical key, or the value exceeds the nesting bound. -pub fn encode_canonical_cbor_v1(value: &CanonicalValueV1) -> Result, CanonicalValueError> { - let mut output = Vec::new(); - encode_value(value, &mut output, 0)?; - Ok(output) -} - -/// Decodes and authenticates one `edict.canonical-cbor/v1` value. -/// -/// Decoding is followed by exact canonical re-encoding. This rejects -/// non-minimal integers and lengths, unsorted maps, and any other supported -/// value whose supplied bytes differ from the unique Edict encoding. -/// Successful decoding authenticates the encoding only: callers must still -/// validate the value against its owning CDDL root, and Echo must separately -/// admit any runtime artifact or authority. -/// -/// # Errors -/// -/// Returns a stable failure for malformed, unsupported, trailing, -/// noncanonical, duplicate-key, invalid-UTF-8, or over-nested input. -pub fn decode_canonical_cbor_v1(bytes: &[u8]) -> Result { - let mut decoder = Decoder::new(bytes); - let value = decoder.value(0)?; - if decoder.remaining() != 0 { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::TrailingData, - "canonical CBOR stream contains bytes after the first value", - )); - } - if encode_canonical_cbor_v1(&value)? != bytes { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::NonCanonical, - "decoded value does not re-encode to the supplied bytes", - )); - } - Ok(value) -} - -/// Computes an Edict v1 domain-framed SHA-256 digest for a canonical value. -/// -/// The returned review rendering is `sha256:<64 lowercase hex>`. The preimage -/// is canonical CBOR for `["edict.digest/v1", domain, value]`. Each tuple member -/// is encoded at its own root so digest framing does not consume the artifact's -/// 128-level nesting budget. -/// A digest binds bytes and a caller-selected domain; it does not prove the -/// owning CDDL root or grant Echo runtime authority. -/// -/// # Errors -/// -/// Returns a stable failure for an empty domain or an unencodable value. -pub fn digest_canonical_value_v1( - domain: &str, - value: &CanonicalValueV1, -) -> Result { - if domain.is_empty() { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedValue, - "canonical artifact digest domain is empty", - )); - } - - let mut preimage = vec![0x83]; - preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( - EDICT_DIGEST_FRAME_V1.to_owned(), - ))?); - preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( - domain.to_owned(), - ))?); - preimage.extend(encode_canonical_cbor_v1(value)?); - - Ok(format!("sha256:{}", hex::encode(Sha256::digest(preimage)))) -} - -fn encode_value( - value: &CanonicalValueV1, - output: &mut Vec, - depth: usize, -) -> Result<(), CanonicalValueError> { - check_depth(depth)?; - match value { - CanonicalValueV1::Null => output.push(0xf6), - CanonicalValueV1::Bool(false) => output.push(0xf4), - CanonicalValueV1::Bool(true) => output.push(0xf5), - CanonicalValueV1::Integer(value) => encode_integer(*value, output)?, - CanonicalValueV1::Bytes(bytes) => { - encode_type_value(2, usize_to_u64(bytes.len())?, output); - output.extend_from_slice(bytes); - } - CanonicalValueV1::Text(text) => { - encode_type_value(3, usize_to_u64(text.len())?, output); - output.extend_from_slice(text.as_bytes()); - } - CanonicalValueV1::Array(values) => { - check_container_depth(depth)?; - encode_type_value(4, usize_to_u64(values.len())?, output); - for value in values { - encode_value(value, output, depth + 1)?; - } - } - CanonicalValueV1::Map(entries) => { - check_container_depth(depth)?; - let mut encoded_entries = Vec::with_capacity(entries.len()); - let mut encoded_keys = BTreeSet::new(); - for (key, value) in entries { - let mut key_bytes = Vec::new(); - encode_value(key, &mut key_bytes, depth + 1)?; - if !encoded_keys.insert(key_bytes.clone()) { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::DuplicateMapKey, - "canonical CBOR map contains duplicate keys", - )); - } - encoded_entries.push((key_bytes, value)); - } - encoded_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - encode_type_value(5, usize_to_u64(encoded_entries.len())?, output); - for (key_bytes, value) in encoded_entries { - output.extend_from_slice(&key_bytes); - encode_value(value, output, depth + 1)?; - } - } - } - Ok(()) -} - -fn check_depth(depth: usize) -> Result<(), CanonicalValueError> { - if depth > MAX_CANONICAL_NESTING_DEPTH_V1 { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::NestingLimitExceeded, - format!( - "canonical value nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" - ), - )); - } - Ok(()) -} - -fn check_container_depth(depth: usize) -> Result<(), CanonicalValueError> { - if depth >= MAX_CANONICAL_NESTING_DEPTH_V1 { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::NestingLimitExceeded, - format!( - "canonical container nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" - ), - )); - } - Ok(()) -} - -fn encode_integer(value: i128, output: &mut Vec) -> Result<(), CanonicalValueError> { - if value >= 0 { - let value = u64::try_from(value).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::InvalidInteger, - "positive integer exceeds the canonical CBOR uint range", - ) - })?; - encode_type_value(0, value, output); - return Ok(()); - } - - let magnitude = (-1i128).checked_sub(value).ok_or_else(|| { - CanonicalValueError::new( - CanonicalValueErrorKind::InvalidInteger, - "negative integer cannot be converted to the canonical CBOR range", - ) - })?; - let magnitude = u64::try_from(magnitude).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::InvalidInteger, - "negative integer exceeds the canonical CBOR negative range", - ) - })?; - encode_type_value(1, magnitude, output); - Ok(()) -} - -fn encode_type_value(major: u8, value: u64, output: &mut Vec) { - let prefix = major << 5; - let bytes = value.to_be_bytes(); - match value { - 0..=23 => output.push(prefix | bytes[7]), - 24..=0xff => { - output.push(prefix | 0x18); - output.push(bytes[7]); - } - 0x100..=0xffff => { - output.push(prefix | 0x19); - output.extend_from_slice(&bytes[6..]); - } - 0x1_0000..=0xffff_ffff => { - output.push(prefix | 0x1a); - output.extend_from_slice(&bytes[4..]); - } - _ => { - output.push(prefix | 0x1b); - output.extend_from_slice(&bytes); - } - } -} - -fn usize_to_u64(value: usize) -> Result { - u64::try_from(value).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedValue, - "canonical collection length does not fit the CBOR uint range", - ) - }) -} - -fn checked_collection_length( - declared: u64, - remaining: u64, -) -> Result -where - HostLength: TryFrom, -{ - if declared > remaining { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnexpectedEof, - "canonical CBOR declared length exceeds the remaining bytes", - )); - } - - HostLength::try_from(declared).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedCbor, - "canonical CBOR collection length does not fit usize", - ) - }) -} - -struct Decoder<'a> { - bytes: &'a [u8], - position: usize, -} - -impl<'a> Decoder<'a> { - const fn new(bytes: &'a [u8]) -> Self { - Self { bytes, position: 0 } - } - - const fn remaining(&self) -> usize { - self.bytes.len() - self.position - } - - fn value(&mut self, depth: usize) -> Result { - check_depth(depth)?; - let initial = self.byte()?; - let major = initial >> 5; - let additional = initial & 0x1f; - match major { - 0 => Ok(CanonicalValueV1::Integer(i128::from( - self.argument(additional)?, - ))), - 1 => Ok(CanonicalValueV1::Integer( - -1 - i128::from(self.argument(additional)?), - )), - 2 => { - let length = self.length(additional)?; - Ok(CanonicalValueV1::Bytes(self.take(length)?.to_vec())) - } - 3 => { - let length = self.length(additional)?; - let bytes = self.take(length)?; - let text = str::from_utf8(bytes).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::InvalidUtf8, - "canonical CBOR text string is not valid UTF-8", - ) - })?; - Ok(CanonicalValueV1::Text(text.to_owned())) - } - 4 => { - check_container_depth(depth)?; - let length = self.length(additional)?; - let mut values = Vec::with_capacity(length); - for _ in 0..length { - values.push(self.value(depth + 1)?); - } - Ok(CanonicalValueV1::Array(values)) - } - 5 => { - check_container_depth(depth)?; - let length = self.length(additional)?; - let mut entries = Vec::with_capacity(length); - let mut encoded_keys = BTreeSet::new(); - for _ in 0..length { - let key = self.value(depth + 1)?; - let mut key_bytes = Vec::new(); - encode_value(&key, &mut key_bytes, depth + 1)?; - if !encoded_keys.insert(key_bytes) { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::DuplicateMapKey, - "canonical CBOR map contains duplicate keys", - )); - } - let value = self.value(depth + 1)?; - entries.push((key, value)); - } - Ok(CanonicalValueV1::Map(entries)) - } - 7 => match additional { - 20 => Ok(CanonicalValueV1::Bool(false)), - 21 => Ok(CanonicalValueV1::Bool(true)), - 22 => Ok(CanonicalValueV1::Null), - _ => Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedCbor, - "canonical CBOR simple value is unsupported", - )), - }, - _ => Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedCbor, - "canonical CBOR major type is unsupported", - )), - } - } - - fn argument(&mut self, additional: u8) -> Result { - match additional { - 0..=23 => Ok(u64::from(additional)), - 24 => Ok(u64::from(self.byte()?)), - 25 => Ok(u64::from(u16::from_be_bytes(self.take_array::<2>()?))), - 26 => Ok(u64::from(u32::from_be_bytes(self.take_array::<4>()?))), - 27 => Ok(u64::from_be_bytes(self.take_array::<8>()?)), - _ => Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedCbor, - "indefinite or reserved canonical CBOR length is unsupported", - )), - } - } - - fn length(&mut self, additional: u8) -> Result { - let declared = self.argument(additional)?; - let remaining = u64::try_from(self.remaining()).map_err(|_| { - CanonicalValueError::new( - CanonicalValueErrorKind::UnsupportedCbor, - "remaining canonical CBOR input does not fit the CBOR uint range", - ) - })?; - - checked_collection_length::(declared, remaining) - } - - fn byte(&mut self) -> Result { - let Some(value) = self.bytes.get(self.position).copied() else { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnexpectedEof, - "canonical CBOR expected another byte", - )); - }; - self.position += 1; - Ok(value) - } - - fn take(&mut self, length: usize) -> Result<&'a [u8], CanonicalValueError> { - let end = self.position.checked_add(length).ok_or_else(|| { - CanonicalValueError::new( - CanonicalValueErrorKind::UnexpectedEof, - "canonical CBOR length overflowed the input position", - ) - })?; - let Some(bytes) = self.bytes.get(self.position..end) else { - return Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnexpectedEof, - "canonical CBOR value extends past the input", - )); - }; - self.position = end; - Ok(bytes) - } - - fn take_array(&mut self) -> Result<[u8; LENGTH], CanonicalValueError> { - let mut output = [0u8; LENGTH]; - output.copy_from_slice(self.take(LENGTH)?); - Ok(output) - } -} - -#[cfg(test)] -mod tests { - use super::{checked_collection_length, CanonicalValueError, CanonicalValueErrorKind}; - - #[test] - fn declared_length_bounds_precede_host_width_conversion() { - assert_eq!( - checked_collection_length::(u64::MAX, u64::from(u32::MAX)), - Err(CanonicalValueError::new( - CanonicalValueErrorKind::UnexpectedEof, - "canonical CBOR declared length exceeds the remaining bytes" - )) - ); - } -} +pub use echo_edict_canonical::*; diff --git a/crates/echo-wesley-gen/src/provider_corpus.rs b/crates/echo-wesley-gen/src/provider_corpus.rs index f2d31339..86e3c215 100644 --- a/crates/echo-wesley-gen/src/provider_corpus.rs +++ b/crates/echo-wesley-gen/src/provider_corpus.rs @@ -349,9 +349,17 @@ pub fn build_provider_generator_source_bundle_v1( /// contract. No filesystem or environment discovery is performed. pub fn checked_provider_generator_source_bundle_v1( ) -> Result { - let sources: [(&str, &[u8]); 14] = [ + let sources: [(&str, &[u8]); 16] = [ ("Cargo.lock", include_bytes!("../../../Cargo.lock")), ("Cargo.toml", include_bytes!("../../../Cargo.toml")), + ( + "crates/echo-edict-canonical/Cargo.toml", + include_bytes!("../../echo-edict-canonical/Cargo.toml"), + ), + ( + "crates/echo-edict-canonical/src/lib.rs", + include_bytes!("../../echo-edict-canonical/src/lib.rs"), + ), ( "crates/echo-wesley-gen/Cargo.toml", include_bytes!("../Cargo.toml"), diff --git a/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs index 5d0d63f0..c18ce001 100644 --- a/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs +++ b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs @@ -38,9 +38,11 @@ const CONTRACT_CDDL: &[u8] = const CONTRACT_MANIFEST: &[u8] = include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); -const GENERATOR_SOURCE_PATHS: [&str; 14] = [ +const GENERATOR_SOURCE_PATHS: [&str; 16] = [ "Cargo.lock", "Cargo.toml", + "crates/echo-edict-canonical/Cargo.toml", + "crates/echo-edict-canonical/src/lib.rs", "crates/echo-wesley-gen/Cargo.toml", "crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs", "crates/echo-wesley-gen/src/lib.rs", diff --git a/det-policy.yaml b/det-policy.yaml index b1e7527f..bb92a2d3 100644 --- a/det-policy.yaml +++ b/det-policy.yaml @@ -39,6 +39,14 @@ crates: class: DET_CRITICAL owner_role: "Architect" paths: ["crates/echo-wasm-abi/**"] + echo-edict-canonical: + class: DET_CRITICAL + owner_role: "Tooling Engineer" + paths: ["crates/echo-edict-canonical/**"] + 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/**"] 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 ee3a0f95..9503b7ab 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -192,6 +192,23 @@ manifest separately binds the exact provider-owned components and their frozen WIT world attestations, preserving independent lowerers and component upgrades without target-profile semantic churn. +The first executable lowerer implements the frozen +`edict:target-provider/lowerer@1.0.0` world as a pure component over explicit +request bytes. It recognizes only the checked mutation closure for local Core +intent `t` (global semantic coordinate `a.b@1.t`) and emits the exact canonical +Target IR bytes produced by Edict's built-in Echo compatibility wrapper. An +effect-free operation is refused as unsupported semantics rather than encoded +as a synthetic mutation. The same fail-closed rule applies to a rebound Core +module, changed operation type binding, or authored Core optic that this first +crossing cannot faithfully discharge. The complete semantic closure includes +the exact lowerability coordinate as well as its canonical bytes and digest. +The component's protocol instance and request/result aliases are type-only +imports; filesystem, network, environment, clock, randomness, registry, +logging callback, WASI, and every other callable import are forbidden. This +crossing proves translation only. Package admission, installation, invocation, +commitment, observation, and receipt authority remain separate Echo runtime +crossings. + 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 diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index ba085b35..5f7c31d4 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -147,6 +147,14 @@ contract documents. They are not executable component bytes. Issue #655 binds the exact lowerer and verifier components, including their frozen WIT world attestations, when it assembles the provider manifest. +The first checked executable lowerer now lives under +[`components/v1/`](components/v1/README.md). It consumes only the frozen Edict +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 +authority-facts documents are runtime Echo authority. + External Edict contract inputs come from the checked [`contracts/v1/`](contracts/v1/README.md) publication merged in [Edict PR #162](https://github.com/flyingrobots/edict/pull/162). Echo passes the diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md new file mode 100644 index 00000000..13c24faf --- /dev/null +++ b/schemas/edict-provider/components/v1/README.md @@ -0,0 +1,38 @@ + + + +# 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. + +`lowerer.echo-dpo.component.wasm` implements +`edict:target-provider/lowerer@1.0.0`. It was built with Rust 1.90.0 from +`echo-edict-provider-lowerer` and componentized with `wit-component` 0.251.0. +The source WIT is the exact 7,392-byte Edict contract with SHA-256 +`2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. + +The checked component is 112,718 bytes with SHA-256 +`ea068940b8ca520585c395c63f18855c243a5b2ce731d601e61e5b508a7c6bf7`. +Its sole contract attestation is the top-level custom section +`edict:target-provider-contract` containing +`edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's +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. + +Check the artifact without rewriting it: + +```sh +cargo +1.90.0 xtask provider-lowerer-component \ + --target-dir target/provider-lowerer-component \ + --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm +``` + +Two independent fresh target directories must produce byte-identical component +bytes. The standalone Edict-host witness 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. diff --git a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..88faa5a73f4506c680b8cee61914f15a7617d0df GIT binary patch literal 112718 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 literal 0 HcmV?d00001 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 42f527a3..c056ce64 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:13cc7656796d3f3b24a35fc3b565893d2e2f6ec018ead60d5bc7a4d5d7f894dc","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:eb725421e9dd6a1af5b54553bef12f46c611346c0dcf7874cbf02548b93470cd","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 2f1ee704..fcde2331 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:13cc7656796d3f3b24a35fc3b565893d2e2f6ec018ead60d5bc7a4d5d7f894dc","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:8ab2000629f7bd7778fb696845309f64837930d0a83689bc18fea2d3ac2c69f9","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:eb725421e9dd6a1af5b54553bef12f46c611346c0dcf7874cbf02548b93470cd","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:37f49b466dbba2c2265e043a259e690c30b914a9d188c89c2138ebe1965385ed","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 f5b8f423..46ba4bfe 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" +# 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_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" +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="${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 e49c2bbe..fc7bed56 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" +# 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_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" +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="${DETERMINISM_PATHS:-$PATHS_DEFAULT}" ALLOWLIST="${DETERMINISM_ALLOWLIST:-.ban-nondeterminism-allowlist}" diff --git a/scripts/verify-edict-provider-host-v1.sh b/scripts/verify-edict-provider-host-v1.sh new file mode 100755 index 00000000..557dbd98 --- /dev/null +++ b/scripts/verify-edict-provider-host-v1.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +set -euo pipefail + +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +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-component" + +component="${ECHO_PROVIDER_LOWERER_COMPONENT:-schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm}" +if [[ "$component" != /* ]]; then + component="$ROOT/$component" +fi +readonly component + +cd "$ROOT" + +cargo +1.90.0 xtask provider-lowerer-component \ + --target-dir "$COMPONENT_TARGET_DIR" \ + --output "$component" + +cargo +1.94.0 fmt --manifest-path "$MANIFEST" --all -- --check + +ECHO_PROVIDER_LOWERER_COMPONENT="$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 clippy \ + --manifest-path "$MANIFEST" \ + --locked \ + --all-targets \ + -- \ + -D warnings diff --git a/scripts/verify-local.sh b/scripts/verify-local.sh index ea0b389a..e8e542f7 100755 --- a/scripts/verify-local.sh +++ b/scripts/verify-local.sh @@ -257,6 +257,10 @@ readonly FULL_CRITICAL_PREFIXES=( "crates/warp-geom/" "crates/warp-wasm/" "crates/echo-wasm-abi/" + "crates/echo-edict-canonical/" + "crates/echo-edict-provider-lowerer/" + "schemas/edict-provider/components/v1/" + "tests/edict-provider-host-v1/" "crates/echo-scene-port/" "crates/echo-scene-codec/" "crates/echo-graph/" @@ -309,6 +313,8 @@ readonly FULL_LOCAL_PACKAGES=( "warp-geom" "warp-wasm" "echo-wasm-abi" + "echo-edict-canonical" + "echo-edict-provider-lowerer" "echo-scene-port" "echo-scene-codec" "echo-graph" @@ -320,6 +326,8 @@ readonly FULL_LOCAL_PACKAGES=( readonly FULL_LOCAL_TEST_PACKAGES=( "warp-geom" "warp-math" + "echo-edict-canonical" + "echo-edict-provider-lowerer" "echo-graph" "echo-scene-port" "echo-scene-codec" @@ -336,6 +344,8 @@ readonly FULL_LOCAL_CLIPPY_CORE_PACKAGES=( ) readonly FULL_LOCAL_CLIPPY_SUPPORT_PACKAGES=( + "echo-edict-canonical" + "echo-edict-provider-lowerer" "echo-scene-port" "echo-scene-codec" "echo-graph" @@ -352,6 +362,8 @@ readonly FULL_LOCAL_RUSTDOC_PACKAGES=( "warp-math" "warp-geom" "warp-wasm" + "echo-edict-canonical" + "echo-edict-provider-lowerer" ) readonly FAST_CLIPPY_LIB_ONLY_PACKAGES=( @@ -370,6 +382,7 @@ FULL_SCOPE_CLIPPY_SUPPORT_PACKAGES=() FULL_SCOPE_CLIPPY_BIN_ONLY_PACKAGES=() FULL_SCOPE_TEST_SUPPORT_PACKAGES=() FULL_SCOPE_RUSTDOC_PACKAGES=() +FULL_SCOPE_RUN_EDICT_PROVIDER_HOST=0 FULL_SCOPE_RUN_WARP_CORE_SMOKE=0 FULL_SCOPE_WARP_WASM_TEST_MODE="none" FULL_SCOPE_ECHO_WASM_ABI_RUN_LIB=0 @@ -636,6 +649,9 @@ 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) + printf '%s\n' "echo-edict-provider-lowerer" + ;; crates/*/Cargo.toml|crates/*/build.rs|crates/*/src/*|crates/*/tests/*) crate="$(printf '%s\n' "$file" | sed -n 's#^crates/\([^/]*\)/.*#\1#p')" [[ -z "$crate" ]] && continue @@ -1516,6 +1532,11 @@ prepare_full_scope() { FULL_SCOPE_ECHO_WASM_ABI_EXTRA_TESTS=() FULL_SCOPE_WARP_CORE_EXTRA_TESTS=() 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 + FULL_SCOPE_RUN_EDICT_PROVIDER_HOST=1 + fi if array_contains "warp-core" ${FULL_SCOPE_SELECTED_CRATES[@]+"${FULL_SCOPE_SELECTED_CRATES[@]}"}; then FULL_SCOPE_RUN_WARP_CORE_SMOKE=1 @@ -1799,6 +1820,20 @@ run_full_lane_guards() { run_docs_lint } +run_full_lane_edict_provider_host() { + echo "[verify-local][edict-provider-host-v1] isolated Edict host contract" + scripts/verify-edict-provider-host-v1.sh +} + +run_full_lane_edict_provider_wasm_clippy() { + echo "[verify-local][clippy-edict-provider-wasm] strict wasm32 lowerer adapter" + 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 +} + run_full_checks_sequential() { echo "[verify-local] critical local gate (${FULL_SCOPE_MODE})" run_timed_step "fmt" run_full_lane_fmt @@ -1818,6 +1853,10 @@ run_full_checks_sequential() { if local_rustdoc_enabled; then run_timed_step "rustdoc" run_full_lane_rustdoc fi + if [[ "$FULL_SCOPE_RUN_EDICT_PROVIDER_HOST" == "1" ]]; then + run_timed_step "clippy-edict-provider-wasm" run_full_lane_edict_provider_wasm_clippy + run_timed_step "edict-provider-host-v1" run_full_lane_edict_provider_host + fi run_timed_step "guards" run_full_lane_guards } @@ -1854,6 +1893,10 @@ run_full_checks_parallel() { if local_rustdoc_enabled && [[ ${#FULL_SCOPE_RUSTDOC_PACKAGES[@]} -gt 0 ]]; then lanes+=("rustdoc" run_full_lane_rustdoc) fi + if [[ "$FULL_SCOPE_RUN_EDICT_PROVIDER_HOST" == "1" ]]; then + lanes+=("clippy-edict-provider-wasm" run_full_lane_edict_provider_wasm_clippy) + lanes+=("edict-provider-host-v1" run_full_lane_edict_provider_host) + fi run_parallel_lanes "${lanes[@]}" } diff --git a/tests/edict-provider-host-v1/Cargo.lock b/tests/edict-provider-host-v1/Cargo.lock new file mode 100644 index 00000000..d0f223da --- /dev/null +++ b/tests/edict-provider-host-v1/Cargo.lock @@ -0,0 +1,1372 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cddl-cat" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0def7310489015a41757b6ae8a0126ad1d91c4a3f77089f862eea7c000638825" +dependencies = [ + "base64", + "ciborium", + "escape8259", + "float-ord", + "hex", + "nom", + "regex", + "serde", + "strum_macros", + "thiserror 1.0.69", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529143118c4eeb58c39ecb02319557d512be6c61348486422974ab8e3906b8a8" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7780677247ad3577e3a6a3ebf43f39b325a11d6393db72b2c9968a910d4d13d" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9645250416cbf92454fe61160e17e026e0ce405906a54500b114f923ddffc9" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck 0.5.0", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" + +[[package]] +name = "cranelift-control" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591abe6f5312bd2c4220f1b3bead56c2ad00257c52668015ba013b85dcf2a17a" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4adbf760207fdbbe130f1191cce01cdef66831a9f648b1f39ff2800d126d45" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8315b21ff018226a42a60a4702c2dd75f6447cac26e9bca622e14c22088c2ff5" + +[[package]] +name = "cranelift-native" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506ef23a60715bde451b06620b14402166ded3b648454fccbf04f3e46a4aa70" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.133.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ed47e602652e3410f9387fc0db70fefadcee4d78a78881421aabcab4e26b89" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "echo-edict-provider-host-v1-witness" +version = "0.0.0" +dependencies = [ + "edict-provider-host-wasmtime", + "edict-provider-schema", + "edict-syntax", + "sha2", +] + +[[package]] +name = "edict-provider-host-wasmtime" +version = "0.11.0-alpha.1" +source = "git+https://github.com/flyingrobots/edict?rev=c75c3f550d049485ba00eae0dc272c6dd6aca11f#c75c3f550d049485ba00eae0dc272c6dd6aca11f" +dependencies = [ + "edict-provider-schema", + "edict-syntax", + "sha2", + "wasmparser", + "wasmtime", +] + +[[package]] +name = "edict-provider-schema" +version = "0.11.0-alpha.1" +source = "git+https://github.com/flyingrobots/edict?rev=c75c3f550d049485ba00eae0dc272c6dd6aca11f#c75c3f550d049485ba00eae0dc272c6dd6aca11f" +dependencies = [ + "cddl-cat", + "ciborium", + "edict-syntax", + "sha2", +] + +[[package]] +name = "edict-syntax" +version = "0.11.0-alpha.1" +source = "git+https://github.com/flyingrobots/edict?rev=c75c3f550d049485ba00eae0dc272c6dd6aca11f#c75c3f550d049485ba00eae0dc272c6dd6aca11f" +dependencies = [ + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b92604caae1a1899b6a5b54967289dd538177c626004c91accf9d0ec7e4a12" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7ac85c0bb3fb351f10d531230aaa5e366b46d7c4e5328e5f02801d6dac1165" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4213d2f019a5e44aa8a61d8826dd33a505bff79f749b14a8bafd67321cb9351" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-environ" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e48f8d4966d62a10b6d70722bc432c1e163890be2801d3b5784589ad36ffc3" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ad5abd5822a22dbf4014475cdfd1fe790707761cd732d74aaa3ba4d5ba489" + +[[package]] +name = "wasmtime-internal-core" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc28372e36eaf8cf70faa83b5779137f7e99c8d18569a125d1580e735cc9e4d" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a433efc6e35112a5457e1dc8bc4d8d39820ac7722267e89bc04e5df641f32124" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a1d3a39d0d210f6b8574ee96a4315e0a14c67f3a1fc3cd5372cb10d2fb4422" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f667288cb4dfa68a4639ffac4d5628535dda64ebdc2b990526efb12b30ba803" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba651d44ab0faad4c58106b3adb45068189fb65ef50f0c404b6d9e3bf81a357" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecc52563b0558af2a7487eb710de07cc4532564b55528876129238e83118cb1" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747f4a074699ba1b4e4d841fb263f9b7df5bd1555181c4752bf5990d21ba676" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "46.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80009f46991622814196d96fac6fc0a938f46b5cba737a8f4e21e24e5a03856f" +dependencies = [ + "anyhow", + "bitflags", + "heck 0.5.0", + "indexmap", + "wit-parser", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-parser" +version = "0.251.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e960732e824fab95099971a09e638979347c94ca48568d3c854c945729196947" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/edict-provider-host-v1/Cargo.toml b/tests/edict-provider-host-v1/Cargo.toml new file mode 100644 index 00000000..567d8f16 --- /dev/null +++ b/tests/edict-provider-host-v1/Cargo.toml @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +[package] +name = "echo-edict-provider-host-v1-witness" +version = "0.0.0" +edition = "2021" +rust-version = "1.94" +license = "Apache-2.0" +publish = false + +[workspace] +resolver = "2" + +[dependencies] +edict-provider-host-wasmtime = { git = "https://github.com/flyingrobots/edict", rev = "c75c3f550d049485ba00eae0dc272c6dd6aca11f" } +edict-provider-schema = { git = "https://github.com/flyingrobots/edict", rev = "c75c3f550d049485ba00eae0dc272c6dd6aca11f" } +edict-syntax = { git = "https://github.com/flyingrobots/edict", rev = "c75c3f550d049485ba00eae0dc272c6dd6aca11f" } +sha2 = "=0.10.9" + +[lints.rust] +missing_docs = "deny" +unsafe_code = "deny" + +[lints.clippy] +all = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +dbg_macro = "deny" +print_stdout = "deny" +print_stderr = "deny" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +too_many_lines = "allow" +multiple_crate_versions = "allow" diff --git a/tests/edict-provider-host-v1/fixtures/edict-7cd8858c/ORIGIN.toml b/tests/edict-provider-host-v1/fixtures/edict-7cd8858c/ORIGIN.toml new file mode 100644 index 00000000..c710cedd --- /dev/null +++ b/tests/edict-provider-host-v1/fixtures/edict-7cd8858c/ORIGIN.toml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +repository = "https://github.com/flyingrobots/edict" +commit = "7cd8858c577fcfb6a05f0f617dfa821bb183c7df" +license = "Apache-2.0" + +[[artifact]] +file = "lowerer.component.wasm" +source = "fixtures/providers/components/lowerer.component.wasm" +bytes = 28007 +sha256 = "bec69c4fa02aa2dfb4f492d4a1c6849ae4ebe81c1725477efb6b0f2e885676aa" + +[[artifact]] +file = "malformed-lowerer.component.wasm" +source = "fixtures/providers/components/malformed-lowerer.component.wasm" +bytes = 2165 +sha256 = "dfcd171918373d18b9dff16778e98b7618eeb4ac85976dd7134b9e201562f41b" diff --git a/tests/edict-provider-host-v1/fixtures/edict-7cd8858c/lowerer.component.wasm b/tests/edict-provider-host-v1/fixtures/edict-7cd8858c/lowerer.component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d9af894b2bbdf7c2a2e02718a51e02c70d47d7d6 GIT binary patch literal 28007 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 literal 0 HcmV?d00001 diff --git a/tests/edict-provider-host-v1/rust-toolchain.toml b/tests/edict-provider-host-v1/rust-toolchain.toml new file mode 100644 index 00000000..2a13a48b --- /dev/null +++ b/tests/edict-provider-host-v1/rust-toolchain.toml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS + +[toolchain] +channel = "1.94.0" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/tests/edict-provider-host-v1/src/lib.rs b/tests/edict-provider-host-v1/src/lib.rs new file mode 100644 index 00000000..c411c4df --- /dev/null +++ b/tests/edict-provider-host-v1/src/lib.rs @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Rust-1.94-isolated witness for Echo's Edict provider component. diff --git a/tests/edict-provider-host-v1/tests/host_contract.rs b/tests/edict-provider-host-v1/tests/host_contract.rs new file mode 100644 index 00000000..8697c75e --- /dev/null +++ b/tests/edict-provider-host-v1/tests/host_contract.rs @@ -0,0 +1,999 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow( + clippy::expect_used, + clippy::panic, + clippy::print_stdout, + clippy::unwrap_used +)] +//! Standalone Rust 1.94 witness for the frozen Edict provider-host contract. + +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; + +use edict_provider_host_wasmtime::{ + PreparedProviderComponent, ProviderComponentHost, ProviderHostFailureKind, ProviderHostLimits, + ProviderHostPhase, ProviderReplayObservation, ResolvedProviderComponent, +}; +use edict_provider_schema::{ProviderArtifactSchemaRegistry, ResolvedProviderSchemaArtifact}; +use edict_syntax::{ + bind_target_provider_manifest, compile_to_core, decode_canonical_cbor, + digest_target_ir_artifact, encode_canonical_cbor, encode_core_module, + encode_target_ir_artifact, lower_with_builtin_lowerer, parse_module, select_provider_component, + 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, + AUTHORITY_FACTS_API_VERSION, CORE_MODULE_DIGEST_DOMAIN, ECHO_DPO_TARGET_PROFILE, + ECHO_SPAN_IR_DOMAIN, PROVIDER_LAWPACK_ARTIFACT_DOMAIN, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + TARGET_PROFILE_API_VERSION, TARGET_PROVIDER_ABI, TARGET_PROVIDER_MANIFEST_API_VERSION, + TARGET_PROVIDER_PROTOCOL_VERSION, +}; +use sha2::{Digest as _, Sha256}; + +const ECHO_SOURCE: &str = "package a.b@1;\n\ + type Input = { id: String, };\n\ + type Receipt = { id: String, };\n\ + type Output = { id: String, };\n\ + intent t(input: Input) returns Output\n\ + profile p.effectful\n\ + basis none\n\ + budget <= p.tiny {\n\ + let receipt: Receipt = target.replace(input.id)\n\ + else { rejected(reason) => domain.WriteRejected };\n\ + return { id: input.id };\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 SCHEMA_ROLE: &str = "schema.echo-provider-artifacts"; +const RAW_TARGET_IR_SHA256: &str = + "41ae7a1d95e5068cb09ec581f16a90cc6e26a80f83ec073e86d5108c3a61ea41"; +const DOMAIN_TARGET_IR_SHA256: &str = + "b0d9e218f00a102d1e951c73e5063a9bbe6077e6c7468d171ec08b420e7b47da"; +const TARGET_PROFILE_SHA256: &str = + "f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0"; +const OBSERVATION_MARKER: &str = "ECHO_EDICT_HOST_OBSERVATION="; + +const SCHEMA_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl" +); +const TARGET_PROFILE_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" +); +const LAWPACK_BYTES: &[u8] = + include_bytes!("../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"); +const TARGET_AUTHORITY_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" +); +const LAWPACK_AUTHORITY_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor" +); +const FIXTURE_LOWERER_BYTES: &[u8] = + include_bytes!("../fixtures/edict-7cd8858c/lowerer.component.wasm"); +const FIXTURE_MALFORMED_BYTES: &[u8] = + include_bytes!("../fixtures/edict-7cd8858c/malformed-lowerer.component.wasm"); + +const FIXTURE_SCHEMA: &[u8] = br#" +artifact = null / { + kind: "targetIrArtifact", + domain: tstr, + intents: { * tstr => any }, + targetProfile: { * tstr => any }, + sourceCoreCoordinate: tstr, +} +"#; +const NULL_BYTES: &[u8] = &[0xf6]; +const FIXTURE_OUTPUT_DOMAIN: &str = "runtime.output/v1"; + +struct LowerHarness { + host: ProviderComponentHost, + prepared: PreparedProviderComponent<'static>, + request: ValidatedProviderLoweringRequest<'static>, + schema: &'static ProviderArtifactSchemaRegistry, +} + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("repository root resolves") +} + +fn echo_component_path() -> PathBuf { + let configured = std::env::var_os("ECHO_PROVIDER_LOWERER_COMPONENT").map(PathBuf::from); + let path = configured.unwrap_or_else(|| { + PathBuf::from("schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm") + }); + if path.is_absolute() { + path + } else { + repo_root().join(path) + } +} + +fn echo_component_bytes() -> &'static [u8] { + let path = echo_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 { + write!(&mut output, "{byte:02x}").expect("writing hexadecimal to String cannot fail"); + } + output +} + +fn raw_sha256(bytes: &[u8]) -> String { + hex(&Sha256::digest(bytes)) +} + +fn raw_resource(coordinate: &str, bytes: &[u8]) -> ResourceRef { + ResourceRef { + coordinate: coordinate.to_owned(), + digest: Some(format!("sha256:{}", raw_sha256(bytes))), + } +} + +fn locked_test_resource(coordinate: &str, digit: char) -> ResourceRef { + ResourceRef { + coordinate: coordinate.to_owned(), + digest: Some(format!("sha256:{}", digit.to_string().repeat(64))), + } +} + +fn provider_digest(domain: &str, canonical_bytes: &[u8]) -> ProviderDigest { + let value = decode_canonical_cbor(canonical_bytes).expect("artifact is canonical CBOR"); + let frame = CanonicalValue::Array(vec![ + CanonicalValue::Text("edict.digest/v1".to_owned()), + CanonicalValue::Text(domain.to_owned()), + value, + ]); + let framed = encode_canonical_cbor(&frame).expect("provider digest frame encodes"); + ProviderDigest { + algorithm: ProviderDigestAlgorithm::Sha256, + bytes: Sha256::digest(framed).to_vec(), + } +} + +fn bound_artifact(coordinate: &str, domain: &str, bytes: &[u8]) -> ProviderBoundArtifact { + ProviderBoundArtifact { + reference: ProviderResourceRef { + coordinate: coordinate.to_owned(), + digest: provider_digest(domain, bytes), + }, + artifact: ProviderArtifact { + domain: domain.to_owned(), + bytes: bytes.to_vec(), + }, + } +} + +fn artifact_binding(bound: &ProviderBoundArtifact) -> ProviderArtifactBinding { + ProviderArtifactBinding { + reference: bound.reference.clone(), + domain: bound.artifact.domain.clone(), + } +} + +fn text(value: &str) -> CanonicalValue { + CanonicalValue::Text(value.to_owned()) +} + +fn map(entries: impl IntoIterator) -> CanonicalValue { + CanonicalValue::Map( + entries + .into_iter() + .map(|(key, value)| (text(key), value)) + .collect(), + ) +} + +fn lowerability_bytes() -> Vec { + let value = map([ + ("apiVersion", text(LOWERABILITY_DOMAIN)), + ("operationProfile", text("continuum.profile.write/v1")), + ( + "semanticEffects", + CanonicalValue::Array(vec![map([ + ("coordinate", text("target.replace")), + ("writeClass", text("replace")), + ( + "guardKinds", + CanonicalValue::Array(vec![text("precommit-atomic")]), + ), + ( + "obstructionCoordinates", + CanonicalValue::Array(vec![text("rejected")]), + ), + ( + "footprintObligations", + CanonicalValue::Array(vec![text("target.replace.footprint")]), + ), + ( + "costObligations", + CanonicalValue::Array(vec![text("target.replace.cost")]), + ), + ])]), + ), + ( + "requiredWriteClasses", + CanonicalValue::Array(vec![text("replace")]), + ), + ( + "guardKinds", + CanonicalValue::Array(vec![text("precommit-atomic")]), + ), + ("atomicity", text("atomic")), + ("postconditionSupport", CanonicalValue::Bool(true)), + ( + "obstructionCoordinates", + CanonicalValue::Array(vec![text("rejected")]), + ), + ( + "footprintObligations", + CanonicalValue::Array(vec![text("target.replace.footprint")]), + ), + ( + "costObligations", + CanonicalValue::Array(vec![text("target.replace.cost")]), + ), + ("opticContract", text("replace-point")), + ]); + encode_canonical_cbor(&value).expect("lowerability facts encode canonically") +} + +fn echo_core() -> CoreModule { + let context = CompilerContext::new() + .with_operation_profile("p.effectful", "continuum.profile.write/v1") + .with_operation_profile_write_classes("p.effectful", [WriteClass::Replace]) + .with_effect_write_class("target.replace", WriteClass::Replace) + .with_budget( + "p.tiny", + CoreBudget { + max_steps: 8, + max_allocated_bytes: 1024, + max_output_bytes: 256, + }, + ); + let module = parse_module(ECHO_SOURCE).expect("Echo source parses"); + compile_to_core(&module, &context).expect("Echo source compiles to Core") +} + +fn semantic_input( + role: &str, + kind: ProviderSemanticInputKind, + coordinate: &str, + domain: &str, + bytes: &[u8], +) -> ProviderSemanticInput { + ProviderSemanticInput { + role: role.to_owned(), + kind, + artifact: bound_artifact(coordinate, domain, bytes), + } +} + +fn echo_manifest(component_bytes: &'static [u8]) -> &'static TargetProviderManifest { + let component = raw_resource("echo.dpo.lowerer/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: LOWERER_ROLE.to_owned(), + artifact_kind: ProviderArtifactKind::Lowerer, + 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: [ + (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_registry( + manifest: &'static TargetProviderManifest, +) -> &'static ProviderArtifactSchemaRegistry { + let proof = bind_target_provider_manifest(manifest).expect("Echo provider manifest validates"); + Box::leak(Box::new( + ProviderArtifactSchemaRegistry::from_manifest( + &proof, + [ResolvedProviderSchemaArtifact { + role: SCHEMA_ROLE.to_owned(), + bytes: Arc::from(SCHEMA_BYTES), + }], + [ + 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 provider schema registry constructs"), + )) +} + +fn echo_request_from_core_bytes( + core_bytes: &[u8], + output_role: &str, +) -> (ProviderLoweringInvocationContract, ProviderLoweringRequest) { + 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 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 = ProviderLoweringInvocationContract { + core: artifact_binding(&core_artifact), + target_profile: artifact_binding(&target_profile_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 = ProviderLoweringRequest { + protocol_version: TARGET_PROVIDER_PROTOCOL_VERSION, + core: core_artifact, + target_profile: target_profile_artifact, + semantic_inputs, + requested_outputs: vec![ProviderLoweringOutputRequest { + role: output_role.to_owned(), + kind: ProviderLoweringOutputKind::TargetIr, + domain: TARGET_IR_ARTIFACT_DIGEST_DOMAIN.to_owned(), + }], + limits: ProviderResponseLimits { + max_output_count: 8, + max_diagnostic_count: 8, + max_total_response_bytes: 64 * 1024, + }, + }; + (contract, request) +} + +fn echo_request( + core: &CoreModule, + output_role: &str, +) -> (ProviderLoweringInvocationContract, ProviderLoweringRequest) { + let core_bytes = encode_core_module(core).expect("Core module encodes canonically"); + echo_request_from_core_bytes(&core_bytes, output_role) +} + +fn echo_harness_with_request( + contract: ProviderLoweringInvocationContract, + request: ProviderLoweringRequest, +) -> LowerHarness { + let component = echo_component_bytes(); + let manifest = echo_manifest(component); + let manifest_proof = Box::leak(Box::new( + bind_target_provider_manifest(manifest).expect("Echo provider manifest validates"), + )); + let selected = select_provider_component( + manifest_proof, + LOWERER_ROLE, + ProviderInvocationKind::Lowering, + ) + .expect("Echo lowerer selects"); + let resolved = ResolvedProviderComponent::new(selected, Arc::from(component)); + let host = ProviderComponentHost::new().expect("host configures"); + let prepared = host.prepare(&resolved).expect("Echo lowerer prepares"); + let schema = echo_registry(manifest); + let contract = Box::leak(Box::new(contract)); + let request = Box::leak(Box::new(request)); + let request = validate_provider_lowering_request(schema, contract, request) + .expect("Echo lowering request validates"); + LowerHarness { + host, + prepared, + request, + schema, + } +} + +fn echo_harness(core: &CoreModule, output_role: &str) -> LowerHarness { + let (contract, request) = echo_request(core, output_role); + echo_harness_with_request(contract, request) +} + +const fn host_limits() -> ProviderHostLimits { + ProviderHostLimits { + max_input_bytes: 1024 * 1024, + max_output_bytes: 3 * 1024 * 1024, + max_diagnostic_bytes: 3 * 1024 * 1024, + max_wasm_memory_bytes: 16 * 1024 * 1024, + max_table_elements: 10_000, + max_instances: 100, + max_memories: 8, + max_tables: 8, + max_wasm_fuel: 50_000_000, + max_hostcall_bytes: 4 * 1024 * 1024, + max_host_diagnostic_bytes: 512, + } +} + +fn assert_echo_refusal( + harness: &LowerHarness, + expected_kind: ProviderRefusalKind, + expected_subject: Option<&str>, +) { + let outcome = harness + .host + .invoke_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("typed Echo refusal crosses the WIT transport"); + assert!(outcome.response().is_none()); + assert!(outcome.manifest().is_none()); + let refusal = outcome + .refusal() + .expect("Echo component returned a typed refusal"); + assert_eq!(refusal.kind, expected_kind); + assert_eq!(refusal.subject.as_deref(), expected_subject); + + let replay = harness + .host + .replay_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("typed Echo refusal replays identically in fresh stores"); + let ProviderReplayObservation::Completed(replayed) = replay.observation() else { + panic!("typed Echo refusal must be a completed replay observation"); + }; + assert_eq!(replayed.refusal(), Some(refusal)); + assert!(replayed.response().is_none()); + assert!(replayed.manifest().is_none()); +} + +fn fixture_manifest(component_bytes: &'static [u8]) -> &'static TargetProviderManifest { + let component = raw_resource("runtime.lowerer/component@1", component_bytes); + let schema = raw_resource("runtime.artifacts.cddl@1", FIXTURE_SCHEMA); + 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("runtime.provider@1", '4'), + artifacts: vec![ + ProviderArtifactRef { + role: "lowerer.runtime".to_owned(), + artifact_kind: ProviderArtifactKind::Lowerer, + resource: component.clone(), + source: ProviderArtifactSource::Component { component }, + }, + ProviderArtifactRef { + role: "schema.runtime".to_owned(), + artifact_kind: ProviderArtifactKind::ArtifactSchema, + resource: schema, + source: ProviderArtifactSource::Generated { + semantic_source: locked_test_resource("runtime.semantic-source@1", '5'), + generator: locked_test_resource("runtime.provider-generator@1", '6'), + }, + }, + ], + schema_bindings: [ + CORE_MODULE_DIGEST_DOMAIN, + TARGET_PROFILE_API_VERSION, + FIXTURE_OUTPUT_DOMAIN, + ] + .into_iter() + .map(|domain| ProviderSchemaBinding { + domain: domain.to_owned(), + schema_role: "schema.runtime".to_owned(), + format: ProviderSchemaFormat::SelfContainedCddlV1, + root_rule: "artifact".to_owned(), + }) + .collect(), + })) +} + +fn fixture_registry( + manifest: &'static TargetProviderManifest, +) -> &'static ProviderArtifactSchemaRegistry { + let proof = bind_target_provider_manifest(manifest).expect("fixture manifest validates"); + Box::leak(Box::new( + ProviderArtifactSchemaRegistry::from_manifest( + &proof, + [ResolvedProviderSchemaArtifact { + role: "schema.runtime".to_owned(), + bytes: Arc::from(FIXTURE_SCHEMA), + }], + [ + CORE_MODULE_DIGEST_DOMAIN, + TARGET_PROFILE_API_VERSION, + FIXTURE_OUTPUT_DOMAIN, + ], + ) + .expect("fixture schema registry constructs"), + )) +} + +fn fixture_request(role: &str) -> (ProviderLoweringInvocationContract, ProviderLoweringRequest) { + let core = bound_artifact("core@1", CORE_MODULE_DIGEST_DOMAIN, NULL_BYTES); + let target_profile = bound_artifact("profile@1", TARGET_PROFILE_API_VERSION, NULL_BYTES); + let contract = ProviderLoweringInvocationContract { + core: artifact_binding(&core), + target_profile: artifact_binding(&target_profile), + semantic_inputs: Vec::new(), + }; + let request = ProviderLoweringRequest { + protocol_version: TARGET_PROVIDER_PROTOCOL_VERSION, + core, + target_profile, + semantic_inputs: Vec::new(), + requested_outputs: vec![ProviderLoweringOutputRequest { + role: role.to_owned(), + kind: ProviderLoweringOutputKind::GeneratedArtifact, + domain: FIXTURE_OUTPUT_DOMAIN.to_owned(), + }], + limits: ProviderResponseLimits { + max_output_count: 4, + max_diagnostic_count: 4, + max_total_response_bytes: 1024 * 1024, + }, + }; + (contract, request) +} + +fn fixture_harness_with_component(role: &str, component: &'static [u8]) -> LowerHarness { + let manifest = fixture_manifest(component); + let manifest_proof = Box::leak(Box::new( + bind_target_provider_manifest(manifest).expect("fixture manifest validates"), + )); + let selected = select_provider_component( + manifest_proof, + "lowerer.runtime", + ProviderInvocationKind::Lowering, + ) + .expect("fixture lowerer selects"); + let resolved = ResolvedProviderComponent::new(selected, Arc::from(component)); + let host = ProviderComponentHost::new().expect("host configures"); + let prepared = host.prepare(&resolved).expect("fixture lowerer prepares"); + let schema = fixture_registry(manifest); + let (contract, request) = fixture_request(role); + let contract = Box::leak(Box::new(contract)); + let request = Box::leak(Box::new(request)); + let request = validate_provider_lowering_request(schema, contract, request) + .expect("fixture request validates"); + LowerHarness { + host, + prepared, + request, + schema, + } +} + +fn fixture_harness(role: &str) -> LowerHarness { + fixture_harness_with_component(role, FIXTURE_LOWERER_BYTES) +} + +fn echo_observation() -> String { + let core = echo_core(); + let harness = echo_harness(&core, TARGET_IR_ROLE); + let replay = harness + .host + .replay_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("fresh-store Echo component observations agree"); + let ProviderReplayObservation::Completed(outcome) = replay.observation() else { + panic!("valid Echo component must replay as a completed observation"); + }; + let response = outcome.response().expect("Echo component returns success"); + let manifest = outcome + .manifest() + .expect("host authors the Echo component output manifest"); + format!( + "{}:{}:{}", + raw_sha256(echo_component_bytes()), + raw_sha256(&response.outputs[0].artifact.bytes), + hex(&manifest.outputs()[0].digest.bytes) + ) +} + +fn oracle_target_ir(core: &CoreModule) -> (Vec, ProviderDigest) { + let facts = TargetIrLoweringFacts { + target_profile: ResourceRef { + coordinate: ECHO_DPO_TARGET_PROFILE.to_owned(), + digest: Some(format!("sha256:{TARGET_PROFILE_SHA256}")), + }, + target_ir_domain: ECHO_SPAN_IR_DOMAIN.to_owned(), + operation_profiles: vec!["continuum.profile.write/v1".to_owned()], + obstruction_coordinates: vec!["rejected".to_owned()], + effect_lowerings: vec![TargetEffectLowering { + effect: "target.replace".to_owned(), + target_intrinsic: "echo.dpo@1.replace".to_owned(), + }], + }; + let report = lower_with_builtin_lowerer( + BuiltinTargetLowerer::EchoDpo, + BuiltinLowererRequest { + core, + facts: &facts, + }, + ) + .expect("Edict built-in Echo lowerer accepts the exact fixture"); + let artifact = report.artifact.expect("Echo Core lowers to Target IR"); + let bytes = encode_target_ir_artifact(&artifact).expect("oracle Target IR encodes"); + let digest = digest_target_ir_artifact(&artifact).expect("oracle Target IR digests"); + assert_eq!(digest.algorithm(), "sha256"); + ( + bytes, + ProviderDigest { + algorithm: ProviderDigestAlgorithm::Sha256, + bytes: digest.bytes().to_vec(), + }, + ) +} + +#[test] +fn echo_component_matches_independent_edict_target_ir_bytes_and_digest() { + let core = echo_core(); + let core_bytes = encode_core_module(&core).expect("Core module encodes"); + assert_eq!(core_bytes.len(), 1209); + assert_eq!( + hex(&provider_digest(CORE_MODULE_DIGEST_DOMAIN, &core_bytes).bytes), + "c3dbe413c78a82f6120e64c9a04bc94e2d79505f9e4b8a65c2bc26b408d775de" + ); + assert_eq!( + hex(&provider_digest(TARGET_PROFILE_API_VERSION, TARGET_PROFILE_BYTES).bytes), + TARGET_PROFILE_SHA256 + ); + + let (oracle_bytes, oracle_digest) = oracle_target_ir(&core); + let harness = echo_harness(&core, TARGET_IR_ROLE); + let outcome = harness + .host + .invoke_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("component result crosses complete Edict host admission"); + assert!(outcome.refusal().is_none()); + let response = outcome.response().expect("component returns success"); + assert!(response.diagnostics.is_empty()); + assert_eq!(response.outputs.len(), 1); + let output = &response.outputs[0]; + assert_eq!(output.role, TARGET_IR_ROLE); + assert_eq!(output.kind, ProviderLoweringOutputKind::TargetIr); + assert_eq!(output.artifact.domain, TARGET_IR_ARTIFACT_DIGEST_DOMAIN); + assert_eq!(output.logical_path, None); + assert_eq!(output.artifact.bytes, oracle_bytes); + assert_eq!(output.artifact.bytes.len(), 848); + assert_eq!(raw_sha256(&output.artifact.bytes), RAW_TARGET_IR_SHA256); + + let manifest = outcome.manifest().expect("host authors an output manifest"); + assert_eq!(manifest.outputs().len(), 1); + let entry = &manifest.outputs()[0]; + assert_eq!(entry.role, TARGET_IR_ROLE); + assert_eq!(entry.kind, ProviderLoweringOutputKind::TargetIr); + assert_eq!(entry.domain, TARGET_IR_ARTIFACT_DIGEST_DOMAIN); + assert_eq!(entry.digest, oracle_digest); + assert_eq!(hex(&entry.digest.bytes), DOMAIN_TARGET_IR_SHA256); + + let replay = harness + .host + .replay_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("Echo component success replays identically in fresh stores"); + let ProviderReplayObservation::Completed(replayed) = replay.observation() else { + panic!("Echo component success must be a completed replay observation"); + }; + assert_eq!(replayed.response(), outcome.response()); + assert_eq!(replayed.manifest(), outcome.manifest()); + assert!(replayed.refusal().is_none()); +} + +#[test] +fn exact_merged_edict_host_fixtures_are_unchanged() { + assert_eq!(FIXTURE_LOWERER_BYTES.len(), 28_007); + assert_eq!( + raw_sha256(FIXTURE_LOWERER_BYTES), + "bec69c4fa02aa2dfb4f492d4a1c6849ae4ebe81c1725477efb6b0f2e885676aa" + ); + assert_eq!(FIXTURE_MALFORMED_BYTES.len(), 2_165); + assert_eq!( + raw_sha256(FIXTURE_MALFORMED_BYTES), + "dfcd171918373d18b9dff16778e98b7618eeb4ac85976dd7134b9e201562f41b" + ); +} + +#[test] +fn echo_component_refuses_an_unsupported_profile_through_the_actual_host() { + let core = echo_core(); + let (mut contract, mut request) = echo_request(&core, TARGET_IR_ROLE); + request.target_profile.reference.coordinate = "echo.other@1".to_owned(); + contract.target_profile = artifact_binding(&request.target_profile); + let harness = echo_harness_with_request(contract, request); + + assert_echo_refusal( + &harness, + ProviderRefusalKind::UnsupportedTargetProfile, + Some("echo.other@1"), + ); +} + +#[test] +fn echo_component_refuses_unsupported_core_semantics_through_the_actual_host() { + let mut core = echo_core(); + core.coordinate = "x.y@1".to_owned(); + let (mut contract, mut request) = echo_request(&core, TARGET_IR_ROLE); + request.core.reference.coordinate = core.coordinate; + contract.core = artifact_binding(&request.core); + let harness = echo_harness_with_request(contract, request); + + assert_echo_refusal( + &harness, + ProviderRefusalKind::UnsupportedSemantics, + Some("x.y@1"), + ); +} + +#[test] +fn echo_component_refuses_output_overclaim_through_the_actual_host() { + let core = echo_core(); + let harness = echo_harness(&core, "generated.echo-dpo"); + + assert_echo_refusal( + &harness, + ProviderRefusalKind::UnsupportedOutputRole, + Some("generated.echo-dpo"), + ); +} + +#[test] +fn wrong_target_profile_domain_rejects_before_component_invocation() { + let core = echo_core(); + let (mut contract, mut request) = echo_request(&core, TARGET_IR_ROLE); + request.target_profile = bound_artifact( + ECHO_DPO_TARGET_PROFILE, + "wrong.target-profile/v1", + TARGET_PROFILE_BYTES, + ); + contract.target_profile = artifact_binding(&request.target_profile); + let manifest = echo_manifest(echo_component_bytes()); + let schema = echo_registry(manifest); + + let report = validate_provider_lowering_request(schema, &contract, &request) + .expect_err("wrong fixed target-profile domain rejects before invocation"); + assert!(report.failures.iter().any(|failure| { + failure.kind == ProviderInvocationValidationFailureKind::ArtifactDomainMismatch + })); +} + +#[test] +fn typed_provider_refusal_is_completed_without_an_output_manifest() { + let harness = fixture_harness("fixture.refusal"); + let outcome = harness + .host + .invoke_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("typed refusal crosses the WIT transport"); + assert!(outcome.response().is_none()); + assert!(outcome.manifest().is_none()); + let refusal = outcome + .refusal() + .expect("provider returned a typed refusal"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject, None); + + let replay = harness + .host + .replay_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("typed refusal replays identically in fresh stores"); + let ProviderReplayObservation::Completed(replayed) = replay.observation() else { + panic!("typed refusal must be a completed replay observation"); + }; + assert_eq!(replayed.refusal(), Some(refusal)); + assert!(replayed.manifest().is_none()); +} + +#[test] +fn host_rejections_preserve_trap_lifting_and_envelope_identity() { + let trapped = fixture_harness("fixture.trap"); + let failure = trapped + .host + .invoke_lowerer( + &trapped.prepared, + &trapped.request, + trapped.schema, + host_limits(), + ) + .expect_err("explicit guest trap rejects"); + assert_eq!(failure.kind(), ProviderHostFailureKind::GuestTrap); + assert_eq!(failure.phase(), ProviderHostPhase::Lower); + assert!(failure.validation_report().is_none()); + + let malformed = fixture_harness_with_component("output.runtime", FIXTURE_MALFORMED_BYTES); + let failure = malformed + .host + .invoke_lowerer( + &malformed.prepared, + &malformed.request, + malformed.schema, + host_limits(), + ) + .expect_err("invalid canonical ABI discriminant rejects during lifting"); + assert_eq!(failure.kind(), ProviderHostFailureKind::MalformedResponse); + assert!(failure.validation_report().is_none()); + + let undeclared = fixture_harness("fixture.undeclared-output"); + let failure = undeclared + .host + .invoke_lowerer( + &undeclared.prepared, + &undeclared.request, + undeclared.schema, + host_limits(), + ) + .expect_err("undeclared output cannot cross host admission"); + assert_eq!( + failure.kind(), + ProviderHostFailureKind::ResponseEnvelopeInvalid + ); + assert_eq!(failure.phase(), ProviderHostPhase::ValidateResponse); + let report = failure + .validation_report() + .expect("envelope rejection retains structured validation evidence"); + assert!(report + .failures + .iter() + .any(|item| { item.kind == ProviderInvocationValidationFailureKind::UndeclaredOutput })); +} + +#[test] +fn rejected_host_invocation_replays_with_the_same_typed_failure() { + let harness = fixture_harness("fixture.trap"); + let replay = harness + .host + .replay_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("guest trap replays as equal host rejections"); + let ProviderReplayObservation::Rejected(failure) = replay.observation() else { + panic!("guest trap must remain a rejected replay observation"); + }; + assert_eq!(failure.kind(), ProviderHostFailureKind::GuestTrap); + assert_eq!(failure.phase(), ProviderHostPhase::Lower); +} + +#[test] +#[ignore = "child entrypoint exercised by the independent-process witness"] +fn emit_echo_component_host_observation() { + println!("{OBSERVATION_MARKER}{}", echo_observation()); +} + +#[test] +fn independent_processes_reproduce_the_same_echo_component_observation() { + let executable = std::env::current_exe().expect("current test executable is discoverable"); + let run_child = || { + let output = Command::new(&executable) + .arg("emit_echo_component_host_observation") + .args(["--exact", "--ignored", "--nocapture", "--test-threads=1"]) + .output() + .expect("child host process launches"); + assert!( + output.status.success(), + "child 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 observation:\n{stdout}")) + .to_owned() + }; + + let first = run_child(); + let second = run_child(); + assert_eq!(first, second); +} diff --git a/tests/hooks/test_verify_local.sh b/tests/hooks/test_verify_local.sh index c91a62cc..21f68582 100755 --- a/tests/hooks/test_verify_local.sh +++ b/tests/hooks/test_verify_local.sh @@ -28,6 +28,11 @@ if grep -q -- 'cargo clippy -p warp-math --all-targets -- -D warnings -D missing else fail "CI clippy should cover all warp-math targets" fi +if grep -q -- 'cargo +1.90.0 clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs' .github/workflows/ci.yml; then + pass "CI clippy covers the wasm-only Edict provider adapter" +else + fail "CI clippy should cover the wasm-only Edict provider adapter" +fi if awk ' /^permissions:$/ { in_permissions = 1; next } in_permissions && /^[^[:space:]]/ { in_permissions = 0 } @@ -243,6 +248,12 @@ run_fake_verify() { mkdir -p "$tmp/crates/warp-math/src/bin" cp scripts/verify-local.sh "$tmp/scripts/verify-local.sh" chmod +x "$tmp/scripts/verify-local.sh" + cat >"$tmp/scripts/verify-edict-provider-host-v1.sh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "edict-provider-host-v1" >>"$VERIFY_FAKE_HOOK_LOG" +EOF + chmod +x "$tmp/scripts/verify-edict-provider-host-v1.sh" cat >"$tmp/rust-toolchain.toml" <<'EOF' [toolchain] @@ -1822,6 +1833,68 @@ else pass "canonical ABI changes avoid the generic lib smoke lane" fi +fake_edict_component_output="$(run_fake_verify full schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm)" +if printf '%s\n' "$fake_edict_component_output" | grep -q 'critical local gate (targeted-rust)'; then + pass "checked Edict component changes select the lowerer Rust scope" +else + fail "checked Edict component changes should select the lowerer Rust scope" + printf '%s\n' "$fake_edict_component_output" +fi +if printf '%s\n' "$fake_edict_component_output" | grep -q -- 'clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs'; then + pass "checked Edict component changes run wasm-target strict Clippy" +else + fail "checked Edict component changes should run wasm-target strict Clippy" + printf '%s\n' "$fake_edict_component_output" +fi +if printf '%s\n' "$fake_edict_component_output" | grep -q '^edict-provider-host-v1$'; then + pass "checked Edict component changes run the isolated host witness" +else + fail "checked Edict component changes should run the isolated host witness" + printf '%s\n' "$fake_edict_component_output" +fi + +fake_edict_wit_output="$(run_fake_verify full crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit)" +if printf '%s\n' "$fake_edict_wit_output" | grep -q 'critical local gate (targeted-rust)'; then + pass "Edict lowerer WIT changes select the lowerer Rust scope" +else + fail "Edict lowerer WIT changes should select the lowerer Rust scope" + printf '%s\n' "$fake_edict_wit_output" +fi +if printf '%s\n' "$fake_edict_wit_output" | grep -q -- 'clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs'; then + pass "Edict lowerer WIT changes run wasm-target strict Clippy" +else + fail "Edict lowerer WIT changes should run wasm-target strict Clippy" + printf '%s\n' "$fake_edict_wit_output" +fi +if printf '%s\n' "$fake_edict_wit_output" | grep -q '^edict-provider-host-v1$'; then + pass "Edict lowerer WIT changes run the isolated host witness" +else + fail "Edict lowerer WIT changes should run the isolated host witness" + printf '%s\n' "$fake_edict_wit_output" +fi + +fake_edict_host_output="$(run_fake_verify full tests/edict-provider-host-v1/tests/provider_host.rs)" +if printf '%s\n' "$fake_edict_host_output" | grep -q -- 'clippy -p echo-edict-provider-lowerer --target wasm32-unknown-unknown --lib -- -D warnings -D missing_docs'; then + pass "isolated Edict host changes run wasm-target strict Clippy" +else + fail "isolated Edict host changes should run wasm-target strict Clippy" + printf '%s\n' "$fake_edict_host_output" +fi +if printf '%s\n' "$fake_edict_host_output" | grep -q '^edict-provider-host-v1$'; then + pass "isolated Edict host changes rerun the host witness" +else + fail "isolated Edict host changes should rerun the host witness" + printf '%s\n' "$fake_edict_host_output" +fi + +fake_edict_host_script_output="$(run_fake_verify full scripts/verify-edict-provider-host-v1.sh)" +if printf '%s\n' "$fake_edict_host_script_output" | grep -q '^edict-provider-host-v1$'; then + pass "Edict host verifier script changes rerun the host witness" +else + fail "Edict host verifier script changes should rerun the host witness" + printf '%s\n' "$fake_edict_host_script_output" +fi + 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/Cargo.toml b/xtask/Cargo.toml index d0487d85..b43384cd 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -21,8 +21,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" time = { version = "0.3.47", features = ["formatting", "parsing"] } +wasm-encoder = "=0.251.0" +wasmparser = { version = "=0.251.0", features = ["component-model", "validate"] } warp-cli = { path = "../crates/warp-cli", version = "0.1.0" } warp-core = { workspace = true, features = ["native_rule_bootstrap"] } +wit-component = "=0.251.0" +wit-parser = "=0.251.0" [lints] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index fed6bec4..84c7cf43 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -23,6 +23,7 @@ use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; mod hello_echo; +mod provider_lowerer_component; #[derive(Parser)] #[command( @@ -47,6 +48,8 @@ enum Commands { PrStatus(PrStatusArgs), /// Record a durable PR review-state snapshot under local ignored artifacts. PrSnapshot(PrSnapshotArgs), + /// Build or check the exact Echo Edict provider lowerer component. + ProviderLowererComponent(ProviderLowererComponentArgs), /// List, reply to, or resolve PR review threads via `gh`. PrThreads(PrThreadsArgs), /// Run the high-signal local gate before opening a PR. @@ -75,6 +78,19 @@ struct TestSliceArgs { dry_run: bool, } +#[derive(Args)] +struct ProviderLowererComponentArgs { + /// Explicit output path, relative to the repository root unless absolute. + #[arg(long)] + output: PathBuf, + /// Explicit Cargo target directory, relative to the repository root unless absolute. + #[arg(long)] + target_dir: PathBuf, + /// Write missing or stale output bytes; the default only reports drift. + #[arg(long)] + write: bool, +} + #[derive(Args)] struct HelloEchoArgs { /// Output the evidence capsule as JSON. @@ -370,6 +386,7 @@ fn main() -> Result<()> { Commands::HelloEcho(args) => run_hello_echo(args), Commands::PrStatus(args) => run_pr_status(args), Commands::PrSnapshot(args) => run_pr_snapshot(args), + Commands::ProviderLowererComponent(args) => run_provider_lowerer_component(args), Commands::PrThreads(args) => run_pr_threads(args), Commands::PrPreflight(args) => run_pr_preflight(args), Commands::Dind(args) => run_dind(args), @@ -381,6 +398,33 @@ fn main() -> Result<()> { } } +fn run_provider_lowerer_component(args: ProviderLowererComponentArgs) -> Result<()> { + let repository_root = find_repo_root()?; + let output_path = if args.output.is_absolute() { + args.output + } else { + repository_root.join(args.output) + }; + let component = + provider_lowerer_component::build_component(&repository_root, &args.target_dir)?; + let mode = if args.write { + provider_lowerer_component::ComponentOutputMode::Write + } else { + provider_lowerer_component::ComponentOutputMode::Check + }; + let status = provider_lowerer_component::sync_output(&output_path, component.bytes(), mode)?; + let status = match status { + provider_lowerer_component::ComponentOutputStatus::Current => "current", + provider_lowerer_component::ComponentOutputStatus::Written => "written", + }; + println!( + "provider lowerer component: {status}; sha256:{}; {}", + component.sha256_hex(), + output_path.display() + ); + Ok(()) +} + fn run_hello_echo(args: HelloEchoArgs) -> Result<()> { let report = hello_echo::run(hello_echo::HelloEchoConfig { out_dir: args.out_dir, diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs new file mode 100644 index 00000000..f5397d67 --- /dev/null +++ b/xtask/src/provider_lowerer_component.rs @@ -0,0 +1,1018 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Deterministic build and structural admission for the Echo Edict lowerer component. + +use sha2::{Digest, Sha256}; +use std::borrow::Cow; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use wasm_encoder::{ComponentSection, CustomSection}; +use wasmparser::{ + ComponentExternalKind, ComponentType, ComponentTypeRef, Encoding, InstanceTypeDeclaration, + Parser, Payload, TypeBounds, Validator, WasmFeatures, +}; +use wit_component::{ComponentEncoder, DecodedWasm, StringEncoding}; +use wit_parser::{LiveTypes, PackageName, Resolve, WorldId, WorldItem}; + +/// Name of the sole provider-contract attestation custom section. +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. +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"]; +const LOWERER_WORLD_NAME: &str = "lowerer"; +const LOWERER_WIT_SOURCE: &str = + include_str!("../../crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit"); +const LOWERER_PACKAGE: &str = "echo-edict-provider-lowerer"; +const LOWERER_CORE_WASM: &str = "echo_edict_provider_lowerer.wasm"; +const LOWER_EXPORT: &str = "lower"; + +/// Stable classification for a component build or audit failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ProviderLowererComponentErrorKind { + /// A path required by the deterministic build is not valid UTF-8. + InvalidPath, + /// Cargo could not be started. + BuildInvocationFailed, + /// Cargo completed without producing a successful lowerer build. + BuildFailed, + /// The expected core WebAssembly artifact could not be read. + CoreWasmReadFailed, + /// The core module could not be componentized. + ComponentEncodingFailed, + /// The component byte stream is malformed or fails WebAssembly validation. + ComponentInvalid, + /// The component is not a top-level WebAssembly component. + ComponentEncodingInvalid, + /// The required contract attestation is absent. + ContractAttestationMissing, + /// More than one contract attestation is present. + ContractAttestationDuplicate, + /// A contract attestation is nested rather than top-level. + ContractAttestationNested, + /// The contract attestation carries bytes other than the frozen coordinate. + ContractAttestationMismatch, + /// A core WebAssembly import is present. + CoreImportForbidden, + /// A component import other than the frozen protocol instance is present. + ComponentImportForbidden, + /// More than one frozen protocol instance import is present. + ProtocolImportDuplicate, + /// The protocol import is callable, unknown, or not backed by a type-only instance. + ProtocolImportInvalid, + /// The protocol instance and its exact type aliases do not form one closure. + 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. + WorldContractMismatch, + /// The required `lower` export is absent. + LowerExportMissing, + /// More than one `lower` export is present. + LowerExportDuplicate, + /// A top-level export is not the exact callable `lower` export. + LowerExportInvalid, + /// An explicit checked output does not exist. + OutputMissing, + /// An explicit checked output could not be read. + OutputReadFailed, + /// An explicit checked output differs from the newly built bytes. + OutputDrift, + /// An explicit output could not be written. + OutputWriteFailed, +} + +impl ProviderLowererComponentErrorKind { + fn code(self) -> &'static str { + match self { + Self::InvalidPath => "invalid-path", + Self::BuildInvocationFailed => "build-invocation-failed", + Self::BuildFailed => "build-failed", + Self::CoreWasmReadFailed => "core-wasm-read-failed", + Self::ComponentEncodingFailed => "component-encoding-failed", + Self::ComponentInvalid => "component-invalid", + Self::ComponentEncodingInvalid => "component-encoding-invalid", + Self::ContractAttestationMissing => "contract-attestation-missing", + Self::ContractAttestationDuplicate => "contract-attestation-duplicate", + Self::ContractAttestationNested => "contract-attestation-nested", + Self::ContractAttestationMismatch => "contract-attestation-mismatch", + Self::CoreImportForbidden => "core-import-forbidden", + Self::ComponentImportForbidden => "component-import-forbidden", + Self::ProtocolImportDuplicate => "protocol-import-duplicate", + Self::ProtocolImportInvalid => "protocol-import-invalid", + Self::ProtocolImportClosureInvalid => "protocol-import-closure-invalid", + Self::WorldContractInvalid => "world-contract-invalid", + Self::WorldContractMismatch => "world-contract-mismatch", + Self::LowerExportMissing => "lower-export-missing", + Self::LowerExportDuplicate => "lower-export-duplicate", + Self::LowerExportInvalid => "lower-export-invalid", + Self::OutputMissing => "output-missing", + Self::OutputReadFailed => "output-read-failed", + Self::OutputDrift => "output-drift", + Self::OutputWriteFailed => "output-write-failed", + } + } +} + +/// Stable, typed error returned by component build and audit operations. +#[derive(Debug)] +pub(crate) struct ProviderLowererComponentError { + kind: ProviderLowererComponentErrorKind, + subject: String, + reference: Option, + detail: Option, +} + +impl ProviderLowererComponentError { + fn new( + kind: ProviderLowererComponentErrorKind, + subject: impl Into, + reference: Option, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference, + detail: None, + } + } + + fn with_detail(mut self, detail: impl Into) -> Self { + self.detail = Some(detail.into()); + self + } + + /// Returns the stable failure classification. + #[cfg(test)] + pub(crate) fn kind(&self) -> ProviderLowererComponentErrorKind { + self.kind + } + + /// Returns the stable subject of the failed operation. + #[cfg(test)] + pub(crate) fn subject(&self) -> &str { + &self.subject + } + + /// Returns the optional stable reference associated with the failure. + #[cfg(test)] + pub(crate) fn reference(&self) -> Option<&str> { + self.reference.as_deref() + } +} + +impl fmt::Display for ProviderLowererComponentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider-lowerer-component:{}: {}", + self.kind.code(), + self.subject + )?; + if let Some(reference) = &self.reference { + write!(formatter, " ({reference})")?; + } + Ok(()) + } +} + +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. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct ProviderLowererComponent { + bytes: Vec, + sha256: [u8; 32], +} + +impl ProviderLowererComponent { + /// Returns the exact final component bytes. + pub(crate) fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// Returns the lowercase hexadecimal SHA-256 digest. + pub(crate) fn sha256_hex(&self) -> String { + digest_hex(&self.sha256) + } +} + +/// Structural facts established by [`audit_component`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ComponentAudit { + /// Number of exact top-level contract attestations. + pub(crate) contract_attestations: u32, + /// Number of exact type-only protocol imports (zero or one). + 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, +} + +/// Whether an explicit output should be checked or updated. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ComponentOutputMode { + /// Report missing or stale bytes without changing the output. + Check, + /// Write exact bytes when the output is missing or stale. + Write, +} + +/// Outcome of checking or writing an explicit component output. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ComponentOutputStatus { + /// The explicit output already contained the exact bytes. + Current, + /// The explicit output was written with the exact bytes. + Written, +} + +/// Builds, componentizes, attests, audits, and hashes the provider lowerer. +/// +/// Both paths are explicit inputs. This boundary performs no repository or +/// output discovery. Cargo is pinned to Rust 1.90.0, uses the lockfile, and +/// receives a caller-owned target directory. +pub(crate) fn build_component( + repository_root: &Path, + target_directory: &Path, +) -> Result { + let target_directory = absolute_target_directory(repository_root, target_directory); + let repository_root_text = path_text(repository_root)?; + let target_directory_text = path_text(&target_directory)?; + + let encoded_rustflags = [ + format!("--remap-path-prefix={repository_root_text}=/echo"), + format!("--remap-path-prefix={target_directory_text}=/target"), + ] + .join("\u{1f}"); + + let output = Command::new("cargo") + .args([ + "+1.90.0", + "build", + "-p", + LOWERER_PACKAGE, + "--target", + "wasm32-unknown-unknown", + "--release", + "--locked", + ]) + .current_dir(repository_root) + .env("CARGO_TARGET_DIR", &target_directory) + .env("CARGO_INCREMENTAL", "0") + .env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags) + .env("SOURCE_DATE_EPOCH", "1") + .env_remove("RUSTFLAGS") + .env_remove("RUSTC_WRAPPER") + .env_remove("RUSTC_WORKSPACE_WRAPPER") + .output() + .map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuildInvocationFailed, + LOWERER_PACKAGE, + Some("cargo +1.90.0".to_owned()), + ) + .with_detail(error.to_string()) + })?; + + if !output.status.success() { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuildFailed, + LOWERER_PACKAGE, + Some("wasm32-unknown-unknown/release".to_owned()), + ) + .with_detail(String::from_utf8_lossy(&output.stderr))); + } + + let core_path = target_directory + .join("wasm32-unknown-unknown") + .join("release") + .join(LOWERER_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()), + ) + .with_detail(error.to_string()) + })?; + + componentize(&core_bytes) +} + +/// Componentizes explicit core Wasm bytes and appends the exact attestation. +pub(crate) fn componentize(core_bytes: &[u8]) -> Result { + let encoder = ComponentEncoder::default() + .validate(true) + .merge_imports_based_on_semver(false) + .module(core_bytes) + .map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentEncodingFailed, + LOWERER_PACKAGE, + Some("core-module".to_owned()), + ) + .with_detail(error.to_string()) + })?; + let mut encoder = encoder; + let mut bytes = encoder.encode().map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentEncodingFailed, + LOWERER_PACKAGE, + Some("component".to_owned()), + ) + .with_detail(error.to_string()) + })?; + + append_contract_attestation(&mut bytes); + audit_component(&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 { + 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 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()))?; + match payload { + Payload::Version { encoding, .. } => { + depth = depth.checked_add(1).ok_or_else(|| { + component_invalid("component nesting depth overflow".to_owned()) + })?; + if depth == 1 { + outer_is_component = encoding == Encoding::Component; + } + } + Payload::End(_) => { + depth = depth.checked_sub(1).ok_or_else(|| { + component_invalid("component nesting depth underflow".to_owned()) + })?; + } + Payload::CustomSection(section) if section.name() == CONTRACT_SECTION_NAME => { + if depth != 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ContractAttestationNested, + CONTRACT_SECTION_NAME, + Some(format!("depth:{depth}")), + )); + } + contract_attestations += 1; + if contract_attestations > 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ContractAttestationDuplicate, + CONTRACT_SECTION_NAME, + Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + )); + } + if section.data() != LOWERER_CONTRACT_COORDINATE.as_bytes() { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ContractAttestationMismatch, + CONTRACT_SECTION_NAME, + Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + )); + } + } + Payload::ImportSection(section) => { + let import = section + .into_imports() + .next() + .transpose() + .map_err(|error| component_invalid(error.to_string()))?; + if let Some(import) = import { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CoreImportForbidden, + "core-import", + Some(format!("{}::{}", import.module, import.name)), + )); + } + } + Payload::ComponentTypeSection(section) if depth == 1 => { + for component_type in section { + let component_type = + component_type.map_err(|error| component_invalid(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()))?; + if depth != 1 || import.name.implements.is_some() { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentImportForbidden, + import.name.name, + Some(format!("{};depth:{depth}", import.ty.kind().desc())), + )); + } + + if import.name.name == PROTOCOL_INSTANCE_COORDINATE { + protocol_imports += 1; + if protocol_imports > 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportDuplicate, + PROTOCOL_INSTANCE_COORDINATE, + None, + )); + } + + let ComponentTypeRef::Instance(type_index) = import.ty else { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportInvalid, + PROTOCOL_INSTANCE_COORDINATE, + Some(import.ty.kind().desc().to_owned()), + )); + }; + if top_level_types.get(type_index as usize) != Some(&true) { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportInvalid, + PROTOCOL_INSTANCE_COORDINATE, + Some(format!("type-index:{type_index}")), + )); + } + continue; + } + + let Some(alias_index) = PROTOCOL_TYPE_IMPORTS + .iter() + .position(|name| *name == import.name.name) + else { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentImportForbidden, + import.name.name, + Some(import.ty.kind().desc().to_owned()), + )); + }; + if protocol_type_imports[alias_index] { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportDuplicate, + import.name.name, + Some("type-alias".to_owned()), + )); + } + if !matches!(import.ty, ComponentTypeRef::Type(TypeBounds::Eq(_))) { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportInvalid, + import.name.name, + Some(import.ty.kind().desc().to_owned()), + )); + } + protocol_type_imports[alias_index] = true; + } + } + 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 + || export.name.implements.is_some() + || export.kind != ComponentExternalKind::Func + { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::LowerExportInvalid, + export.name.name, + Some(export.kind.desc().to_owned()), + )); + } + lower_exports += 1; + if lower_exports > 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::LowerExportDuplicate, + LOWER_EXPORT, + None, + )); + } + } + } + _ => {} + } + } + + if !outer_is_component { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentEncodingInvalid, + LOWERER_PACKAGE, + Some("component-model".to_owned()), + )); + } + if contract_attestations != 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ContractAttestationMissing, + CONTRACT_SECTION_NAME, + Some(LOWERER_CONTRACT_COORDINATE.to_owned()), + )); + } + let protocol_type_import_count = protocol_type_imports + .iter() + .fold(0_u32, |count, present| count + u32::from(*present)); + if !matches!( + (protocol_imports, protocol_type_import_count), + (0, 0) | (1, 2) + ) { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ProtocolImportClosureInvalid, + PROTOCOL_INSTANCE_COORDINATE, + Some(format!( + "instances:{protocol_imports};type-aliases:{protocol_type_import_count}" + )), + )); + } + if lower_exports != 1 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::LowerExportMissing, + LOWER_EXPORT, + None, + )); + } + + Validator::new_with_features(WasmFeatures::all()) + .validate_all(bytes) + .map_err(|error| component_invalid(error.to_string()))?; + authenticate_component_world(bytes)?; + + Ok(ComponentAudit { + contract_attestations, + protocol_imports, + protocol_type_imports: protocol_type_import_count, + lower_exports, + }) +} + +/// Checks or writes exact component bytes at a caller-selected output path. +pub(crate) fn sync_output( + output_path: &Path, + bytes: &[u8], + mode: ComponentOutputMode, +) -> Result { + match fs::read(output_path) { + Ok(existing) if existing == bytes => Ok(ComponentOutputStatus::Current), + Ok(existing) if mode == ComponentOutputMode::Check => { + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputDrift, + output_path.display().to_string(), + Some(format!( + "expected:{};observed:{}", + digest_hex(&sha256(bytes)), + digest_hex(&sha256(&existing)) + )), + )) + } + Ok(_) => write_output(output_path, bytes), + Err(error) + if error.kind() == std::io::ErrorKind::NotFound + && mode == ComponentOutputMode::Check => + { + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputMissing, + output_path.display().to_string(), + None, + )) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + write_output(output_path, bytes) + } + Err(error) => Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputReadFailed, + output_path.display().to_string(), + None, + ) + .with_detail(error.to_string())), + } +} + +fn write_output(output_path: &Path, bytes: &[u8]) -> Result { + if let Some(parent) = output_path.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputWriteFailed, + output_path.display().to_string(), + Some("create-parent".to_owned()), + ) + .with_detail(error.to_string()) + })?; + } + } + fs::write(output_path, bytes).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputWriteFailed, + output_path.display().to_string(), + Some("write".to_owned()), + ) + .with_detail(error.to_string()) + })?; + Ok(ComponentOutputStatus::Written) +} + +fn append_contract_attestation(bytes: &mut Vec) { + CustomSection { + name: Cow::Borrowed(CONTRACT_SECTION_NAME), + data: Cow::Borrowed(LOWERER_CONTRACT_COORDINATE.as_bytes()), + } + .append_to_component(bytes); +} + +fn type_only_instance(component_type: &ComponentType<'_>) -> bool { + let ComponentType::Instance(declarations) = component_type else { + return false; + }; + let mut export_count = 0_u32; + for declaration in declarations { + if let InstanceTypeDeclaration::Export { ty, .. } = declaration { + export_count += 1; + if !matches!(ty, ComponentTypeRef::Type(_)) { + return false; + } + } + } + export_count > 0 +} + +fn authenticate_component_world(bytes: &[u8]) -> Result<()> { + let (mut expected_resolve, expected_world) = + parse_lowerer_world(LOWERER_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"))?; + canonicalize_world_type_closure(&mut expected_resolve, expected_world); + let expected = encode_world_contract(&expected_resolve, expected_world, "frozen-wit")?; + + let decoded = wit_component::decode(bytes).map_err(|error| { + world_contract_invalid("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( + "candidate-component", + "decoded bytes are a WIT package rather than a component", + )); + }; + normalize_decoded_world_identity(&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) +} + +#[cfg(test)] +fn authenticate_wit_source_for_test(source: &str) -> Result<()> { + let (mut expected_resolve, expected_world) = + parse_lowerer_world(LOWERER_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")?; + 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) +} + +fn parse_lowerer_world(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()) + })?; + let world = resolve + .select_world(&[package], Some(LOWERER_WORLD_NAME)) + .map_err(|error| { + world_contract_invalid(reference, "lowerer world selection failed") + .with_detail(error.to_string()) + })?; + Ok((resolve, world)) +} + +fn normalize_decoded_world_identity( + 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") + })?; + let previous_world_name = std::mem::replace( + &mut resolve.worlds[world].name, + LOWERER_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); + } + resolve.packages[package].name = package_name; + Ok(()) +} + +fn canonicalize_world_type_closure(resolve: &mut Resolve, world: WorldId) { + let live_types = { + let mut live = LiveTypes::default(); + for item in resolve.worlds[world] + .imports + .values() + .chain(resolve.worlds[world].exports.values()) + { + match item { + WorldItem::Function(function) => live.add_func(resolve, function), + WorldItem::Type { id, .. } => live.add_type_id(resolve, *id), + WorldItem::Interface { id, .. } => { + for function in resolve.interfaces[*id].functions.values() { + live.add_func(resolve, function); + } + } + } + } + live.iter().collect::>() + }; + + for (_, interface) in &mut resolve.interfaces { + let declared_types = std::mem::take(&mut interface.types); + for live_type in &live_types { + if let Some((name, id)) = declared_types + .iter() + .find(|(_, declared_type)| *declared_type == live_type) + { + interface.types.insert(name.clone(), *id); + } + } + } +} + +fn encode_world_contract(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") + .with_detail(error.to_string()) + }) +} + +fn compare_world_contracts(expected: &[u8], observed: &[u8]) -> Result<()> { + if expected == observed { + return Ok(()); + } + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::WorldContractMismatch, + LOWERER_CONTRACT_COORDINATE, + Some(format!( + "expected:{};observed:{}", + digest_hex(&sha256(expected)), + digest_hex(&sha256(observed)) + )), + )) +} + +fn world_contract_invalid( + reference: impl Into, + detail: impl Into, +) -> ProviderLowererComponentError { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::WorldContractInvalid, + LOWERER_CONTRACT_COORDINATE, + Some(reference.into()), + ) + .with_detail(detail) +} + +fn absolute_target_directory(repository_root: &Path, target_directory: &Path) -> PathBuf { + if target_directory.is_absolute() { + target_directory.to_owned() + } else { + repository_root.join(target_directory) + } +} + +fn path_text(path: &Path) -> Result<&str> { + path.to_str().ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::InvalidPath, + path.display().to_string(), + Some("utf-8".to_owned()), + ) + }) +} + +fn component_invalid(detail: String) -> ProviderLowererComponentError { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentInvalid, + LOWERER_PACKAGE, + Some("wasmparser".to_owned()), + ) + .with_detail(detail) +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + Sha256::digest(bytes).into() +} + +fn digest_hex(digest: &[u8; 32]) -> String { + hex::encode(digest) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + use wasm_encoder::Component; + + static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); + + #[test] + fn appends_one_exact_top_level_attestation( + ) -> std::result::Result<(), Box> { + let mut bytes = Component::new().finish(); + append_contract_attestation(&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, [LOWERER_CONTRACT_COORDINATE.as_bytes()]); + Ok(()) + } + + #[test] + fn rejects_duplicate_contract_attestations( + ) -> std::result::Result<(), Box> { + let mut bytes = Component::new().finish(); + append_contract_attestation(&mut bytes); + append_contract_attestation(&mut bytes); + + let error = match audit_component(&bytes) { + Err(error) => error, + Ok(_) => { + return Err(std::io::Error::other("duplicate attestation must fail").into()); + } + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::ContractAttestationDuplicate + ); + assert_eq!(error.subject(), CONTRACT_SECTION_NAME); + assert_eq!(error.reference(), Some(LOWERER_CONTRACT_COORDINATE)); + Ok(()) + } + + #[test] + fn rejects_mismatched_contract_attestation_bytes( + ) -> std::result::Result<(), Box> { + let mut bytes = Component::new().finish(); + CustomSection { + name: Cow::Borrowed(CONTRACT_SECTION_NAME), + data: Cow::Borrowed(b"edict:target-provider/lowerer@9.9.9"), + } + .append_to_component(&mut bytes); + + let error = match audit_component(&bytes) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("wrong attestation must fail").into()), + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::ContractAttestationMismatch + ); + Ok(()) + } + + #[test] + fn zero_import_topology_requires_only_the_lower_export( + ) -> std::result::Result<(), Box> { + let mut bytes = Component::new().finish(); + append_contract_attestation(&mut bytes); + + let error = match audit_component(&bytes) { + Err(error) => error, + Ok(_) => { + return Err( + std::io::Error::other("component without lower export must fail").into(), + ); + } + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::LowerExportMissing + ); + Ok(()) + } + + #[test] + fn wrong_lower_parameter_and_result_cannot_receive_the_frozen_attestation( + ) -> std::result::Result<(), Box> { + let source = + include_str!("../../crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit") + .replacen( + "export lower: func(request: lowering-request-v1) -> lowering-result-v1;", + "export lower: func(request: lowering-result-v1) -> lowering-request-v1;", + 1, + ); + + let error = match authenticate_wit_source_for_test(&source) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "wrong lower signature received the frozen attestation", + ) + .into()); + } + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractMismatch + ); + assert_eq!(error.subject(), LOWERER_CONTRACT_COORDINATE); + Ok(()) + } + + #[test] + fn reachable_alias_graph_change_cannot_receive_the_frozen_attestation( + ) -> std::result::Result<(), Box> { + let source = + include_str!("../../crates/echo-edict-provider-lowerer/wit/edict-target-provider.wit") + .replacen( + " core: bound-artifact,\n target-profile: bound-artifact,", + " core: string,\n target-profile: bound-artifact,", + 1, + ); + + let error = match authenticate_wit_source_for_test(&source) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "unrelated alias graph received the frozen attestation", + ) + .into()); + } + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::WorldContractMismatch + ); + assert_eq!(error.subject(), LOWERER_CONTRACT_COORDINATE); + Ok(()) + } + + #[test] + fn check_reports_drift_without_rewriting() -> std::result::Result<(), Box> + { + let output = temporary_output("check-drift"); + fs::write(&output, b"old")?; + + let error = match sync_output(&output, b"new", ComponentOutputMode::Check) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("check must report drift").into()), + }; + + assert_eq!(error.kind(), ProviderLowererComponentErrorKind::OutputDrift); + assert_eq!(fs::read(&output)?, b"old"); + fs::remove_file(output)?; + Ok(()) + } + + #[test] + fn write_is_exact_and_idempotent() -> std::result::Result<(), Box> { + let output = temporary_output("write"); + + assert_eq!( + sync_output(&output, b"component", ComponentOutputMode::Write)?, + ComponentOutputStatus::Written + ); + assert_eq!( + sync_output(&output, b"component", ComponentOutputMode::Write)?, + ComponentOutputStatus::Current + ); + assert_eq!(fs::read(&output)?, b"component"); + fs::remove_file(output)?; + Ok(()) + } + + fn temporary_output(label: &str) -> PathBuf { + let nonce = NEXT_TEMP.fetch_add(1, Ordering::Relaxed); + std::env::temp_dir().join(format!( + "echo-provider-lowerer-component-{}-{label}-{nonce}.wasm", + std::process::id() + )) + } +} From 232421f9b312a65122bc38eb8130f7367e9bf55d Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 13:33:02 -0700 Subject: [PATCH 2/6] Define the provider component release boundary --- .github/workflows/ci.yml | 6 +- .github/workflows/det-gates.yml | 14 +- CHANGELOG.md | 13 +- Cargo.lock | 1 + crates/echo-edict-provider-lowerer/Cargo.toml | 4 +- crates/echo-edict-provider-lowerer/README.md | 33 +- .../resources/authority-facts.echo-dpo.cbor | 1 + .../authority-facts.echo-lawpack.cbor | Bin 0 -> 243 bytes .../resources/lawpack.echo-dpo.cbor | Bin 0 -> 799 bytes .../resources/target-profile.echo-dpo.cbor | Bin 0 -> 1733 bytes crates/echo-edict-provider-lowerer/src/lib.rs | 16 +- .../tests/lowerer_contract.rs | 15 +- .../edict-provider/components/v1/README.md | 51 +- .../v1/lowerer.echo-dpo.component.wasm | Bin 112718 -> 112937 bytes .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- scripts/verify-edict-provider-host-v1.sh | 9 +- xtask/Cargo.toml | 1 + xtask/src/main.rs | 177 ++- xtask/src/provider_lowerer_component.rs | 1032 ++++++++++++++++- 20 files changed, 1289 insertions(+), 88 deletions(-) create mode 100644 crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor create mode 100644 crates/echo-edict-provider-lowerer/resources/authority-facts.echo-lawpack.cbor create mode 100644 crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor create mode 100644 crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e4b3a11..3f5a5f62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,7 +182,7 @@ jobs: build-edict-provider-lowerer: name: Build and check Edict provider lowerer - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: @@ -197,7 +197,7 @@ jobs: . - name: build, audit, and check exact component bytes run: | - cargo xtask provider-lowerer-component \ + cargo xtask provider-lowerer-component check \ --target-dir target/provider-lowerer-component \ --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm - name: clippy (wasm32 Edict provider adapter) @@ -205,7 +205,7 @@ jobs: edict-provider-host-v1: name: Edict provider host contract (Rust 1.94) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/det-gates.yml b/.github/workflows/det-gates.yml index 5c173506..25ac3362 100644 --- a/.github/workflows/det-gates.yml +++ b/.github/workflows/det-gates.yml @@ -224,7 +224,7 @@ jobs: name: G4 build reproducibility (wasm) needs: classify-changes if: needs.classify-changes.outputs.run_g4 == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Setup Rust (Global) @@ -244,10 +244,9 @@ jobs: cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash1.txt cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build1.wasm - cargo xtask provider-lowerer-component \ + cargo xtask provider-lowerer-component designated-build \ --target-dir target/provider-lowerer-repro \ - --output target/lowerer.echo-dpo.component.wasm \ - --write + --output target/lowerer.echo-dpo.component.wasm sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash1.txt cp target/lowerer.echo-dpo.component.wasm ../build1.lowerer.component.wasm - name: Checkout Build 2 @@ -263,10 +262,9 @@ jobs: cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash2.txt cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build2.wasm - cargo xtask provider-lowerer-component \ + cargo xtask provider-lowerer-component designated-build \ --target-dir target/provider-lowerer-repro \ - --output target/lowerer.echo-dpo.component.wasm \ - --write + --output target/lowerer.echo-dpo.component.wasm sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash2.txt cp target/lowerer.echo-dpo.component.wasm ../build2.lowerer.component.wasm - name: Compare hashes @@ -274,6 +272,8 @@ jobs: diff hash1.txt hash2.txt || (echo "Reproducibility failure: Hashes differ!" && exit 1) diff lowerer-hash1.txt lowerer-hash2.txt || (echo "Lowerer reproducibility failure: Hashes differ!" && exit 1) cmp build1.lowerer.component.wasm build2.lowerer.component.wasm + cmp build1.lowerer.component.wasm build1/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm + cmp build2.lowerer.component.wasm build2/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm echo "Hashes match: $(cat hash1.txt)" echo "Lowerer hashes match: $(cat lowerer-hash1.txt)" - name: Upload build artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c2173e..009ad41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,17 @@ intrinsics, and output roles. A deterministic build boundary pins the frozen WIT bytes, rejects ambient or callable imports, checks the exact decoded world type graph and contract attestation, and reproduces the checked component - byte-for-byte across fresh target directories. These artifacts describe and - translate provider semantics; they confer no Echo runtime authority. + byte-for-byte across fresh target directories on the designated Rust 1.90.0 + `x86_64-unknown-linux-gnu` builder. The builder resolves and authenticates the + exact Rust and Cargo executables, binds Cargo to that compiler despite ambient + overrides, and atomically promotes only distinct candidates matching a reviewed + repository digest. Other-host builds are structural and semantic witnesses + rather than cross-host compiler-identity claims. The publication-enabled, + archive-self-contained `echo-edict-provider-lowerer` source crate carries + package-local copies of its four exact admitted resources, with a workspace + 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-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 474fed16..89e06c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2725,6 +2725,7 @@ dependencies = [ "clap_mangen", "hex", "pulldown-cmark", + "same-file", "serde", "serde_json", "sha2", diff --git a/crates/echo-edict-provider-lowerer/Cargo.toml b/crates/echo-edict-provider-lowerer/Cargo.toml index fa8261d5..6da1a4fc 100644 --- a/crates/echo-edict-provider-lowerer/Cargo.toml +++ b/crates/echo-edict-provider-lowerer/Cargo.toml @@ -8,7 +8,9 @@ rust-version = "1.90.0" description = "Pure Echo provider lowerer for Edict's frozen Component Model boundary." license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" -publish = false +readme = "README.md" +keywords = ["echo", "edict", "wasm", "component-model", "compiler"] +categories = ["compilers", "wasm", "development-tools"] [lib] crate-type = ["cdylib", "rlib"] diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index 2f3646b1..5765c6a5 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -9,6 +9,17 @@ closure. Its production boundary is pure: callers supply canonical Core, the exact target profile, the complete semantic closure, requested output roles, and response limits through the frozen Edict WIT request. +The `0.1.0` Rust source crate is enabled and configured for publication, and its +source archive is self-contained. It packages the four exact admitted provider +resources needed by this first closure, while a repository-owned witness requires +those package-local bytes to remain identical to the checked generated corpus. +Registry publication exposes the pure native model and a reproducible component +source build; the frozen WIT component remains the provider ABI, and neither +distribution channel grants Echo runtime authority. Release +`echo-edict-canonical 0.1.0` first; after that dependency is visible in the +registry index, the lowerer's full package/publish gate becomes executable and +must run from the same clean revision. + The first supported operation is the mutating `a.b@1.t` compatibility fixture. The lowerer emits canonical `edict.target-ir.artifact/v1` bytes whose decoded inner domain is `echo.span-ir/v1`. It accepts only the exact Core module @@ -26,17 +37,25 @@ and from that model. The component imports only the frozen WIT's type closure; it imports no filesystem, network, environment, clock, randomness, registry, logging callback, or WASI capability. -Build and audit the checked component from the repository root with: +Build and audit local component bytes from the repository root with: ```sh -cargo +1.90.0 xtask provider-lowerer-component \ - --target-dir target/provider-lowerer-component \ - --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm +cargo +1.90.0 xtask provider-lowerer-component build \ + --target-dir target/provider-lowerer-component ``` -The command checks by default and never rewrites a stale output. Add `--write` -only for an intentional component refresh. Component construction validates the -complete module, exact import/export topology, and exact contract attestation. +Local component construction resolves and authenticates absolute Rust 1.90.0 +Cargo and compiler executables, binds the inner Cargo build to that exact +compiler, disables configured wrappers, and validates the complete module, +import/export topology, and contract attestation without claiming cross-host byte +identity. The checked artifact is built and compared exactly on the designated +`x86_64-unknown-linux-gnu` builder. `audit` authenticates an existing component +without rebuilding it. `promote` requires two distinct underlying candidate +files, byte equality, the repository-approved SHA-256 identity, complete +component admission, and `--write`; G4 separately proves those candidates came +from fresh designated builds. Successful refresh uses synchronized temporary +bytes and atomic replacement, while write failure preserves the prior artifact. +`designated-build` can emit candidates but refuses the checked repository path. Edict-host invocation evidence lives in the isolated Rust 1.94 witness under `tests/edict-provider-host-v1/` so Wasmtime and unpublished Edict host crates do not enter Echo's Rust 1.90 workspace dependency graph. diff --git a/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor b/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor new file mode 100644 index 00000000..807c2186 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/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-lowerer/resources/authority-facts.echo-lawpack.cbor b/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-lawpack.cbor new file mode 100644 index 0000000000000000000000000000000000000000..a3a7b68102a0f23667ec264b602f15548bc255a0 GIT binary patch literal 243 zcmXYpJxc^J5I|QDyv|lTTZ>ycL2dmgt`QW3^K67Mne5JOOmdlt-nI%>{slof#9p+p zwf6^D3fkED4-9Dccs$;{VV|PHscy*W%EEa7)fi0`vRoLhVRdbNxYRG2&(~-A**KcJ z9?XArK88=rZ#TQg{r89OtC!2ik@7xjav&H{)xOlhcmHRbz3L?C3We`XD1}@*txsWc zAQ-sgGVLV^Nw!--UYse$*=$+sRetF|Ia`b%&;TQocasIwq@;u-wT~oi*MJgUIqQvm azzC9?{pbx@o1`pIZQQO;Tn~W=SR3B?=fWK~aOnImgys_m?`cOROqe;Jw^r;psww3q4J< zDw>z7h4e%@Z%WK7Ni0fFEpbdqEGPlGs&R38B7_OHPZ`5LsH`4HLKhfcSgiDQV%v8A zx7Y4qX_u#yUB7-@lcB0=CoJ76mtyd4vjR#=<6nf!?k?w|kn5oYBMX8QSnT@%S zI7zYClQb4JgQNljNP;o%~s4yUyOM)?VU_fz(Li+%#8EM+RgM(@I&wZX}f6w##*ggBC z8L}3@SWpNnBVIr@fC2tQD=L%4kcr|dtBGSF4-`_&DHb~Pg%>lItZNIt`)fzi*wXPM zt+!yz^3nbcg^L0mo$+(;wI7^yrkq!3i8Uj;4rl``WJrjORz!+HqKqUU5hKt?vJ#vh zJduN~X|Jk#zUmi1Ep4Y?!zs!wjz(MJn;E5Hgw5^b^&8uNg><~kU;4pwAZq98WH z^~zXSU>SSt(2Y*-{HVYln{&roi!VhqH&FZHkND#Ut1KEWMD zh7sZh7=6Oo-L#;yW={Qq_4MJ{OQ=fUAm*DH!iuIYpVZ^IqAC!$$z|WDXjUaw0Lg9| z`U31(;BFbM_-tlG(|7m$?+T~8%ekfZy&o>>HN{o6=a)S6#+wx*`!9$rqB=k zsG}0M{!?|u2sd~f*A_2Z5Lw*jz46)WC+oXcnu` zS+O7@Pb<3e!?|CUIixi;i>KS6deriCjfjorpU&j(6dtq zA1S@vySo+UAsJxT852&pGGq_(pnLk0i8ItM?P($9{*^_?c3gq$GFz%5O6r_hm8E}G z*6!RtXU&$G3-#mO8BL@`pnb0l$eCObZBmd~Ay{Q}=~9s(m}w>#ONg>U1)2pw0geL< zQKpY=qU}gR;x^r4d%z@A$TT3sJCip1Rc+mxcDZm@a-NrU`0)9!hUQ;fndY0F^}4Af zFp4FzmK_F3EYxpFQ)L-pGG=9H2}zn=LY%}P7D{}oDgc*K6Cb5#fMU$Z@JHkv7GD{t zwXTeHv?QHaym{cyjGFqLe*IwurNba=)VIpY0s^oIB`qP4iL(Pn)lZlnVb6eQT|B0K zeVn^xC|~e}Tl1D@J9b?k$FXMyk3Dy@?^;Sz%+JxkhZppH-Z%p7kMls%^90x!Odv-y zWHMGo|8oihUSsNxo)MkrZL1UZgjoWf`r+U^&-9-?*Ks*N-@E11+G~5Ocjx``cHMu@ C+TC^l literal 0 HcmV?d00001 diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index e00a8f75..70622865 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -39,17 +39,11 @@ const SEMANTIC_EFFECT: &str = "target.replace"; const TARGET_INTRINSIC: &str = "echo.dpo@1.replace"; const FAILURE_COORDINATE: &str = "rejected"; -const TARGET_PROFILE_BYTES: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" -); -const LAWPACK_BYTES: &[u8] = - include_bytes!("../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"); -const TARGET_AUTHORITY_BYTES: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" -); -const LAWPACK_AUTHORITY_BYTES: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor" -); +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)] diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs index 77fb0ad3..94da0a41 100644 --- a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -13,17 +13,10 @@ use echo_edict_provider_lowerer::{ }; use sha2::{Digest as ShaDigest, Sha256}; -const TARGET_PROFILE: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" -); -const LAWPACK: &[u8] = - include_bytes!("../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"); -const TARGET_AUTHORITY: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" -); -const LAWPACK_AUTHORITY: &[u8] = include_bytes!( - "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor" -); +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"; diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index 13c24faf..6afd8e67 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -9,13 +9,17 @@ into the digest-locked provider package; its presence here does not claim that Echo has installed, admitted, authorized, or executed it. `lowerer.echo-dpo.component.wasm` implements -`edict:target-provider/lowerer@1.0.0`. It was built with Rust 1.90.0 from -`echo-edict-provider-lowerer` and componentized with `wit-component` 0.251.0. +`edict:target-provider/lowerer@1.0.0`. It was built from +`echo-edict-provider-lowerer` with the absolute rustup-resolved Rust 1.90.0 +compiler at commit `1159e78c4747b02ef996e55082b704c09b970588` and Cargo at +commit `840b83a10fb0e039a83f4d70ad032892c287570a`. The build binds Cargo to +that exact compiler and explicitly disables compiler wrappers. The core module +is componentized with `wit-component` 0.251.0. The source WIT is the exact 7,392-byte Edict contract with SHA-256 `2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. -The checked component is 112,718 bytes with SHA-256 -`ea068940b8ca520585c395c63f18855c243a5b2ce731d601e61e5b508a7c6bf7`. +The checked component is 112,937 bytes with SHA-256 +`03a73240dda6dce6f16c33aa55537a45e4491bb59f25ea9911bb3fbe0c4b8de4`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's @@ -23,16 +27,41 @@ 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. -Check the artifact without rewriting it: +On the designated `x86_64-unknown-linux-gnu` Rust 1.90.0 builder, rebuild and +check the artifact without rewriting it: ```sh -cargo +1.90.0 xtask provider-lowerer-component \ +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 ``` -Two independent fresh target directories must produce byte-identical component -bytes. The standalone Edict-host witness 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. +Two independent fresh target directories on that designated builder 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. + +After two designated-builder candidates compare exactly, promote either explicit +candidate through the same structural admission boundary: + +```sh +cargo +1.90.0 xtask provider-lowerer-component promote \ + --candidate-a /explicit/build-a/lowerer.echo-dpo.component.wasm \ + --candidate-b /explicit/build-b/lowerer.echo-dpo.component.wasm \ + --output schemas/edict-provider/components/v1/lowerer.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 +fresh designated checkouts and target directories—or equivalent explicit +operator evidence—establish independent build provenance. diff --git a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm index 88faa5a73f4506c680b8cee61914f15a7617d0df..8076d22830ee912773c4eca360d7c052d262875d 100644 GIT binary patch delta 21855 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 delta 21615 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 Y_g61LnT>e{?=-wO)-PJWm7Z1q53;Oxpa1{> 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 c056ce64..490f121c 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:eb725421e9dd6a1af5b54553bef12f46c611346c0dcf7874cbf02548b93470cd","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: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 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 fcde2331..d0564d71 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:eb725421e9dd6a1af5b54553bef12f46c611346c0dcf7874cbf02548b93470cd","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:37f49b466dbba2c2265e043a259e690c30b914a9d188c89c2138ebe1965385ed","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: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 diff --git a/scripts/verify-edict-provider-host-v1.sh b/scripts/verify-edict-provider-host-v1.sh index 557dbd98..5f0f7af1 100755 --- a/scripts/verify-edict-provider-host-v1.sh +++ b/scripts/verify-edict-provider-host-v1.sh @@ -8,7 +8,7 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" 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-component" +readonly COMPONENT_TARGET_DIR="$ROOT/target/provider-lowerer-local" component="${ECHO_PROVIDER_LOWERER_COMPONENT:-schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm}" if [[ "$component" != /* ]]; then @@ -18,9 +18,10 @@ readonly component cd "$ROOT" -cargo +1.90.0 xtask provider-lowerer-component \ - --target-dir "$COMPONENT_TARGET_DIR" \ - --output "$component" +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.94.0 fmt --manifest-path "$MANIFEST" --all -- --check diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index b43384cd..5612850a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -19,6 +19,7 @@ hex = "0.4" pulldown-cmark = { version = "0.13", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = "1" +same-file = "1" sha2 = "0.10" time = { version = "0.3.47", features = ["formatting", "parsing"] } wasm-encoder = "=0.251.0" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 84c7cf43..0f2b5fd2 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -80,14 +80,62 @@ struct TestSliceArgs { #[derive(Args)] struct ProviderLowererComponentArgs { + /// Exact component operation to perform. + #[command(subcommand)] + command: ProviderLowererComponentCommand, +} + +#[derive(Subcommand)] +enum ProviderLowererComponentCommand { + /// Build and audit local bytes without claiming checked cross-host identity. + Build(ProviderLowererComponentLocalBuildArgs), + /// Build and write candidate bytes on the exact designated host. + DesignatedBuild(ProviderLowererComponentBuildArgs), + /// Build on the designated host and compare exact checked bytes. + Check(ProviderLowererComponentBuildArgs), + /// Audit an explicit existing component without rebuilding it. + Audit(ProviderLowererComponentAuditArgs), + /// Audit and intentionally promote an explicit candidate to an output. + Promote(ProviderLowererComponentPromoteArgs), +} + +#[derive(Args)] +struct ProviderLowererComponentLocalBuildArgs { + /// Explicit Cargo target directory, relative to the repository root unless absolute. + #[arg(long)] + target_dir: PathBuf, +} + +#[derive(Args)] +struct ProviderLowererComponentBuildArgs { /// Explicit output path, relative to the repository root unless absolute. #[arg(long)] output: PathBuf, /// Explicit Cargo target directory, relative to the repository root unless absolute. #[arg(long)] target_dir: PathBuf, - /// Write missing or stale output bytes; the default only reports drift. +} + +#[derive(Args)] +struct ProviderLowererComponentAuditArgs { + /// Explicit component path, relative to the repository root unless absolute. + #[arg(long)] + input: PathBuf, +} + +#[derive(Args)] +struct ProviderLowererComponentPromoteArgs { + /// First explicit candidate path, relative to the repository root unless absolute. #[arg(long)] + candidate_a: PathBuf, + /// Second distinct candidate path, relative to the repository root unless absolute. + #[arg(long)] + candidate_b: PathBuf, + /// Explicit checked output path, relative to the repository root unless absolute. + #[arg(long)] + output: PathBuf, + /// Confirm the intentional checked-artifact write. + #[arg(long, required = true)] write: bool, } @@ -400,29 +448,116 @@ fn main() -> Result<()> { fn run_provider_lowerer_component(args: ProviderLowererComponentArgs) -> Result<()> { let repository_root = find_repo_root()?; - let output_path = if args.output.is_absolute() { - args.output - } else { - repository_root.join(args.output) - }; - let component = - provider_lowerer_component::build_component(&repository_root, &args.target_dir)?; - let mode = if args.write { - provider_lowerer_component::ComponentOutputMode::Write + 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); + } + 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( + &output_path, + component.bytes(), + provider_lowerer_component::ComponentOutputMode::Write, + )?; + print_provider_component( + &format!("designated-build:{}", builder.host()), + Some(&output_path), + &component, + Some(status), + ); + } + 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( + &output_path, + component.bytes(), + provider_lowerer_component::ComponentOutputMode::Check, + )?; + print_provider_component( + &format!("checked-build:{}", builder.host()), + Some(&output_path), + &component, + Some(status), + ); + } + 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); + } + ProviderLowererComponentCommand::Promote(args) => { + if !args.write { + bail!("provider lowerer promotion requires --write"); + } + 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)); + } + } + Ok(()) +} + +fn repository_path(repository_root: &Path, path: PathBuf) -> PathBuf { + if path.is_absolute() { + path } else { - provider_lowerer_component::ComponentOutputMode::Check - }; - let status = provider_lowerer_component::sync_output(&output_path, component.bytes(), mode)?; + repository_root.join(path) + } +} + +fn print_provider_component( + action: &str, + path: Option<&Path>, + component: &provider_lowerer_component::ProviderLowererComponent, + status: Option, +) { let status = match status { - provider_lowerer_component::ComponentOutputStatus::Current => "current", - provider_lowerer_component::ComponentOutputStatus::Written => "written", + Some(provider_lowerer_component::ComponentOutputStatus::Current) => "; current", + Some(provider_lowerer_component::ComponentOutputStatus::Written) => "; written", + None => "", }; - println!( - "provider lowerer component: {status}; sha256:{}; {}", - component.sha256_hex(), - output_path.display() - ); - Ok(()) + if let Some(path) = path { + println!( + "provider lowerer component: {action}{status}; sha256:{}; {}", + component.sha256_hex(), + path.display() + ); + } else { + println!( + "provider lowerer component: {action}{status}; sha256:{}", + component.sha256_hex() + ); + } } fn run_hello_echo(args: HelloEchoArgs) -> Result<()> { diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index f5397d67..b94cc3b4 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -4,8 +4,10 @@ use sha2::{Digest, Sha256}; use std::borrow::Cow; +use std::ffi::OsString; use std::fmt; -use std::fs; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; use wasm_encoder::{ComponentSection, CustomSection}; @@ -33,6 +35,18 @@ const LOWERER_PACKAGE: &str = "echo-edict-provider-lowerer"; const LOWERER_CORE_WASM: &str = "echo_edict_provider_lowerer.wasm"; const LOWER_EXPORT: &str = "lower"; +/// 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"; +const PINNED_RUSTC_COMMIT: &str = "1159e78c4747b02ef996e55082b704c09b970588"; +const PINNED_CARGO_COMMIT: &str = "840b83a10fb0e039a83f4d70ad032892c287570a"; + +/// Reviewed identity that the portable promotion command is permitted to install. +pub(crate) const APPROVED_CHECKED_COMPONENT_SHA256: &str = + "03a73240dda6dce6f16c33aa55537a45e4491bb59f25ea9911bb3fbe0c4b8de4"; +pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = + "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; + /// Stable classification for a component build or audit failure. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ProviderLowererComponentErrorKind { @@ -44,6 +58,28 @@ pub(crate) enum ProviderLowererComponentErrorKind { BuildFailed, /// The expected core WebAssembly artifact could not be read. CoreWasmReadFailed, + /// The pinned Rust compiler could not report its host identity. + BuilderHostInvocationFailed, + /// The pinned Rust compiler reported no unique host identity. + BuilderHostInvalid, + /// A pinned Rust tool reported a release other than the reviewed release. + BuilderReleaseMismatch, + /// A pinned Rust tool reported a commit other than the reviewed commit. + BuilderCommitMismatch, + /// Exact checked-byte comparison was attempted on a non-designated host. + BuilderHostMismatch, + /// An explicit component candidate could not be read. + ComponentReadFailed, + /// Two explicit reproducibility candidates differ byte-for-byte. + CandidateMismatch, + /// Two candidate paths resolve to the same underlying file. + CandidateAliased, + /// An expected candidate identity is not exact lowercase SHA-256. + ExpectedDigestInvalid, + /// Reproducible candidates do not have the explicitly expected digest. + CandidateDigestMismatch, + /// A one-build candidate command targeted the checked repository artifact. + CheckedOutputRequiresPromotion, /// The core module could not be componentized. ComponentEncodingFailed, /// The component byte stream is malformed or fails WebAssembly validation. @@ -84,6 +120,8 @@ pub(crate) enum ProviderLowererComponentErrorKind { OutputReadFailed, /// An explicit checked output differs from the newly built bytes. OutputDrift, + /// An explicit output path is a symlink or is not a regular file. + OutputTypeInvalid, /// An explicit output could not be written. OutputWriteFailed, } @@ -95,6 +133,17 @@ impl ProviderLowererComponentErrorKind { Self::BuildInvocationFailed => "build-invocation-failed", Self::BuildFailed => "build-failed", Self::CoreWasmReadFailed => "core-wasm-read-failed", + Self::BuilderHostInvocationFailed => "builder-host-invocation-failed", + Self::BuilderHostInvalid => "builder-host-invalid", + Self::BuilderReleaseMismatch => "builder-release-mismatch", + Self::BuilderCommitMismatch => "builder-commit-mismatch", + Self::BuilderHostMismatch => "builder-host-mismatch", + Self::ComponentReadFailed => "component-read-failed", + Self::CandidateMismatch => "candidate-mismatch", + Self::CandidateAliased => "candidate-aliased", + Self::ExpectedDigestInvalid => "expected-digest-invalid", + Self::CandidateDigestMismatch => "candidate-digest-mismatch", + Self::CheckedOutputRequiresPromotion => "checked-output-requires-promotion", Self::ComponentEncodingFailed => "component-encoding-failed", Self::ComponentInvalid => "component-invalid", Self::ComponentEncodingInvalid => "component-encoding-invalid", @@ -115,6 +164,7 @@ impl ProviderLowererComponentErrorKind { Self::OutputMissing => "output-missing", Self::OutputReadFailed => "output-read-failed", Self::OutputDrift => "output-drift", + Self::OutputTypeInvalid => "output-type-invalid", Self::OutputWriteFailed => "output-write-failed", } } @@ -194,6 +244,19 @@ pub(crate) struct ProviderLowererComponent { sha256: [u8; 32], } +#[derive(Debug)] +pub(crate) struct PinnedRustToolchain { + cargo: PathBuf, + rustc: PathBuf, + host: String, +} + +impl PinnedRustToolchain { + pub(crate) fn host(&self) -> &str { + &self.host + } +} + impl ProviderLowererComponent { /// Returns the exact final component bytes. pub(crate) fn bytes(&self) -> &[u8] { @@ -206,6 +269,352 @@ impl ProviderLowererComponent { } } +/// Resolves and authenticates the exact Rust toolchain used by component builds. +pub(crate) fn pinned_rust_toolchain() -> Result { + let rustc = resolve_rustup_tool("rustc")?; + let cargo = resolve_rustup_tool("cargo")?; + let rustc_identity = invoke_tool_identity(&rustc, "-vV", "rustc")?; + let cargo_identity = invoke_tool_identity(&cargo, "-Vv", "cargo")?; + let rustc_host = authenticate_tool_identity("rustc", &rustc_identity, PINNED_RUSTC_COMMIT)?; + let cargo_host = authenticate_tool_identity("cargo", &cargo_identity, PINNED_CARGO_COMMIT)?; + if rustc_host != cargo_host { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostMismatch, + cargo_host, + Some(rustc_host.to_owned()), + )); + } + Ok(PinnedRustToolchain { + cargo, + rustc, + host: rustc_host.to_owned(), + }) +} + +/// Authenticates the pinned compiler host before an exact checked-byte build. +pub(crate) fn require_checked_builder() -> Result { + let toolchain = pinned_rust_toolchain()?; + ensure_checked_builder_host(toolchain.host())?; + Ok(toolchain) +} + +fn resolve_rustup_tool(tool: &str) -> Result { + let output = Command::new("rustup") + .args(["which", "--toolchain", PINNED_RUST_TOOLCHAIN, tool]) + .output() + .map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvocationFailed, + format!("rustup which {tool}"), + Some(PINNED_RUST_TOOLCHAIN.to_owned()), + ) + .with_detail(error.to_string()) + })?; + if !output.status.success() { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvocationFailed, + format!("rustup which {tool}"), + Some(PINNED_RUST_TOOLCHAIN.to_owned()), + ) + .with_detail(String::from_utf8_lossy(&output.stderr))); + } + + let resolved = String::from_utf8(output.stdout).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + format!("rustup which {tool}"), + Some("utf-8-absolute-path".to_owned()), + ) + .with_detail(error.to_string()) + })?; + let path = parse_resolved_tool_path(&resolved).ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + format!("rustup which {tool}"), + Some("one-absolute-path".to_owned()), + ) + })?; + fs::canonicalize(&path).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + path.display().to_string(), + Some(tool.to_owned()), + ) + .with_detail(error.to_string()) + }) +} + +fn parse_resolved_tool_path(output: &str) -> Option { + let mut lines = output.lines(); + let path = lines.next()?; + if path.is_empty() || lines.next().is_some() { + return None; + } + let path = PathBuf::from(path); + path.is_absolute().then_some(path) +} + +fn invoke_tool_identity(path: &Path, version_arg: &str, tool: &str) -> Result { + let output = Command::new(path) + .arg(version_arg) + .output() + .map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvocationFailed, + path.display().to_string(), + Some(tool.to_owned()), + ) + .with_detail(error.to_string()) + })?; + if !output.status.success() { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvocationFailed, + path.display().to_string(), + Some(tool.to_owned()), + ) + .with_detail(String::from_utf8_lossy(&output.stderr))); + } + String::from_utf8(output.stdout).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + path.display().to_string(), + Some(tool.to_owned()), + ) + .with_detail(error.to_string()) + }) +} + +fn authenticate_tool_identity<'a>( + tool: &str, + identity: &'a str, + expected_commit: &str, +) -> Result<&'a str> { + let release = parse_identity_field(identity, "release").ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + tool, + Some("release".to_owned()), + ) + })?; + if release != PINNED_RUST_TOOLCHAIN { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderReleaseMismatch, + release, + Some(PINNED_RUST_TOOLCHAIN.to_owned()), + )); + } + let commit = parse_identity_field(identity, "commit-hash").ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + tool, + Some("commit-hash".to_owned()), + ) + })?; + if commit != expected_commit { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderCommitMismatch, + commit, + Some(expected_commit.to_owned()), + )); + } + parse_identity_field(identity, "host").ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostInvalid, + tool, + Some("host".to_owned()), + ) + }) +} + +#[cfg(test)] +fn parse_rustc_host(version: &str) -> Option<&str> { + parse_identity_field(version, "host") +} + +fn parse_identity_field<'a>(identity: &'a str, field: &str) -> Option<&'a str> { + let prefix = format!("{field}: "); + let mut values = identity + .lines() + .filter_map(|line| line.strip_prefix(&prefix)); + let value = values.next()?; + if value.is_empty() || values.next().is_some() { + return None; + } + Some(value) +} + +fn ensure_checked_builder_host(host: &str) -> Result<()> { + if host == CHECKED_COMPONENT_BUILDER_HOST { + return Ok(()); + } + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::BuilderHostMismatch, + host, + Some(CHECKED_COMPONENT_BUILDER_HOST.to_owned()), + )) +} + +/// Reads and fully audits an explicit component candidate. +pub(crate) fn read_component(input_path: &Path) -> Result { + authenticated_component(read_component_bytes(input_path)?) +} + +fn read_component_bytes(input_path: &Path) -> Result> { + fs::read(input_path).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentReadFailed, + input_path.display().to_string(), + None, + ) + .with_detail(error.to_string()) + }) +} + +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) { + Ok(aliases) => aliases, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + normalized_destination(output_path)? == normalized_destination(checked_path)? + } + Err(error) => { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputReadFailed, + output_path.display().to_string(), + Some(checked_path.display().to_string()), + ) + .with_detail(error.to_string())); + } + }; + if aliases_checked { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CheckedOutputRequiresPromotion, + output_path.display().to_string(), + Some(checked_path.display().to_string()), + )); + } + Ok(()) +} + +fn normalized_destination(path: &Path) -> Result { + match fs::canonicalize(path) { + Ok(path) => Ok(path), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let parent = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + let file_name = path.file_name().ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::InvalidPath, + path.display().to_string(), + Some("file-name".to_owned()), + ) + })?; + fs::canonicalize(parent) + .map(|parent| parent.join(file_name)) + .map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputReadFailed, + path.display().to_string(), + Some("canonical-parent".to_owned()), + ) + .with_detail(error.to_string()) + }) + } + Err(error) => Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputReadFailed, + path.display().to_string(), + Some("canonical-path".to_owned()), + ) + .with_detail(error.to_string())), + } +} + +/// Audits two explicit candidates and admits only one exact expected identity. +pub(crate) fn read_reproducible_candidates( + first_path: &Path, + second_path: &Path, +) -> Result { + let first = read_component_bytes(first_path)?; + let second = read_component_bytes(second_path)?; + let aliased = same_file::is_same_file(first_path, second_path).map_err(|error| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ComponentReadFailed, + first_path.display().to_string(), + Some(second_path.display().to_string()), + ) + .with_detail(error.to_string()) + })?; + if aliased { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CandidateAliased, + first_path.display().to_string(), + Some(second_path.display().to_string()), + )); + } + ensure_reproducible_candidates(&first, &second, APPROVED_CHECKED_COMPONENT_SHA256)?; + authenticated_component(first) +} + +/// Audits, authenticates, and intentionally writes two reproducible candidates. +pub(crate) fn promote_reproducible_candidates( + first_path: &Path, + second_path: &Path, + output_path: &Path, +) -> Result<(ProviderLowererComponent, ComponentOutputStatus)> { + let component = read_reproducible_candidates(first_path, second_path)?; + let status = sync_output(output_path, component.bytes(), ComponentOutputMode::Write)?; + Ok((component, status)) +} + +fn ensure_reproducible_candidates( + first: &[u8], + second: &[u8], + expected_sha256: &str, +) -> Result<()> { + if first != second { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CandidateMismatch, + LOWERER_CONTRACT_COORDINATE, + Some(format!( + "first:{};second:{}", + digest_hex(&sha256(first)), + digest_hex(&sha256(second)) + )), + )); + } + if expected_sha256.len() != 64 + || !expected_sha256 + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::ExpectedDigestInvalid, + expected_sha256, + Some("lowercase-sha256".to_owned()), + )); + } + if expected_sha256 != APPROVED_CHECKED_COMPONENT_SHA256 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CandidateDigestMismatch, + APPROVED_CHECKED_COMPONENT_SHA256, + Some(expected_sha256.to_owned()), + )); + } + let observed = digest_hex(&sha256(first)); + if observed != expected_sha256 { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::CandidateDigestMismatch, + expected_sha256, + Some(observed), + )); + } + Ok(()) +} + /// Structural facts established by [`audit_component`]. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct ComponentAudit { @@ -245,6 +654,7 @@ pub(crate) enum ComponentOutputStatus { pub(crate) fn build_component( repository_root: &Path, target_directory: &Path, + toolchain: &PinnedRustToolchain, ) -> Result { let target_directory = absolute_target_directory(repository_root, target_directory); let repository_root_text = path_text(repository_root)?; @@ -256,9 +666,10 @@ pub(crate) fn build_component( ] .join("\u{1f}"); - let output = Command::new("cargo") + let mut command = Command::new(&toolchain.cargo); + bind_pinned_toolchain(&mut command, toolchain); + let output = command .args([ - "+1.90.0", "build", "-p", LOWERER_PACKAGE, @@ -273,14 +684,13 @@ pub(crate) fn build_component( .env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags) .env("SOURCE_DATE_EPOCH", "1") .env_remove("RUSTFLAGS") - .env_remove("RUSTC_WRAPPER") - .env_remove("RUSTC_WORKSPACE_WRAPPER") + .env_remove("RUSTC_BOOTSTRAP") .output() .map_err(|error| { ProviderLowererComponentError::new( ProviderLowererComponentErrorKind::BuildInvocationFailed, LOWERER_PACKAGE, - Some("cargo +1.90.0".to_owned()), + Some(toolchain.cargo.display().to_string()), ) .with_detail(error.to_string()) })?; @@ -310,6 +720,16 @@ pub(crate) fn build_component( componentize(&core_bytes) } +fn bind_pinned_toolchain(command: &mut Command, toolchain: &PinnedRustToolchain) { + command + .env("RUSTC", &toolchain.rustc) + .env("CARGO_BUILD_RUSTC", &toolchain.rustc) + .env("RUSTC_WRAPPER", "") + .env("RUSTC_WORKSPACE_WRAPPER", "") + .env_remove("CARGO_BUILD_RUSTC_WRAPPER") + .env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER"); +} + /// Componentizes explicit core Wasm bytes and appends the exact attestation. pub(crate) fn componentize(core_bytes: &[u8]) -> Result { let encoder = ComponentEncoder::default() @@ -335,6 +755,10 @@ pub(crate) fn componentize(core_bytes: &[u8]) -> Result) -> Result { audit_component(&bytes)?; let sha256 = sha256(&bytes); Ok(ProviderLowererComponent { bytes, sha256 }) @@ -559,6 +983,7 @@ pub(crate) fn sync_output( bytes: &[u8], mode: ComponentOutputMode, ) -> Result { + validate_output_type(output_path)?; match fs::read(output_path) { Ok(existing) if existing == bytes => Ok(ComponentOutputStatus::Current), Ok(existing) if mode == ComponentOutputMode::Check => { @@ -596,6 +1021,13 @@ pub(crate) fn sync_output( } fn write_output(output_path: &Path, bytes: &[u8]) -> Result { + write_output_with(output_path, |temporary| temporary.write_all(bytes)) +} + +fn write_output_with( + output_path: &Path, + writer: impl FnOnce(&mut File) -> std::io::Result<()>, +) -> Result { if let Some(parent) = output_path.parent() { if !parent.as_os_str().is_empty() { fs::create_dir_all(parent).map_err(|error| { @@ -608,17 +1040,102 @@ fn write_output(output_path: &Path, bytes: &[u8]) -> Result Result<()> { + match fs::symlink_metadata(output_path) { + Ok(metadata) if metadata.file_type().is_file() => Ok(()), + Ok(metadata) => { + let kind = if metadata.file_type().is_symlink() { + "symlink" + } else if metadata.file_type().is_dir() { + "directory" + } else { + "non-regular" + }; + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputTypeInvalid, + output_path.display().to_string(), + Some(kind.to_owned()), + )) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputReadFailed, + output_path.display().to_string(), + Some("metadata".to_owned()), + ) + .with_detail(error.to_string())), + } +} + +fn create_temporary_sibling(output_path: &Path) -> Result<(PathBuf, File)> { + let parent = output_path + .parent() + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + let file_name = output_path.file_name().ok_or_else(|| { + ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::InvalidPath, + output_path.display().to_string(), + Some("file-name".to_owned()), + ) + })?; + + for nonce in 0_u16..128 { + let mut temporary_name = OsString::from("."); + temporary_name.push(file_name); + temporary_name.push(format!(".tmp-{}-{nonce}", std::process::id())); + let temporary_path = parent.join(temporary_name); + match OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary_path) + { + Ok(file) => return Ok((temporary_path, file)), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputWriteFailed, + output_path.display().to_string(), + Some("create-temporary".to_owned()), + ) + .with_detail(error.to_string())); + } + } + } + + Err(ProviderLowererComponentError::new( + ProviderLowererComponentErrorKind::OutputWriteFailed, + output_path.display().to_string(), + Some("temporary-name-exhausted".to_owned()), + )) +} + fn append_contract_attestation(bytes: &mut Vec) { CustomSection { name: Cow::Borrowed(CONTRACT_SECTION_NAME), @@ -1008,6 +1525,505 @@ mod tests { Ok(()) } + #[test] + fn failed_atomic_replacement_preserves_the_previous_output( + ) -> std::result::Result<(), Box> { + let output = temporary_output("atomic-write-failure"); + fs::write(&output, b"preserve-me")?; + + let error = match write_output_with(&output, |temporary| { + temporary.write_all(b"partial")?; + Err(std::io::Error::other("injected write failure")) + }) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("partial output was installed").into()), + }; + + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::OutputWriteFailed + ); + assert_eq!(fs::read(&output)?, b"preserve-me"); + fs::remove_file(output)?; + Ok(()) + } + + #[test] + fn non_regular_output_paths_fail_closed() -> std::result::Result<(), Box> + { + let output = temporary_output("non-regular"); + fs::create_dir(&output)?; + + let error = match sync_output(&output, b"component", ComponentOutputMode::Write) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("directory output was accepted").into()), + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::OutputTypeInvalid + ); + fs::remove_dir(output)?; + Ok(()) + } + + #[cfg(unix)] + #[test] + fn symlink_outputs_fail_without_mutating_the_link_target( + ) -> std::result::Result<(), Box> { + use std::os::unix::fs::symlink; + + let target = temporary_output("symlink-target"); + let output = temporary_output("symlink-output"); + fs::write(&target, b"preserve-target")?; + symlink(&target, &output)?; + + let error = match sync_output(&output, b"component", ComponentOutputMode::Write) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("symlink output was accepted").into()), + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::OutputTypeInvalid + ); + assert_eq!(error.reference(), Some("symlink")); + assert_eq!(fs::read(&target)?, b"preserve-target"); + assert!(fs::symlink_metadata(&output)?.file_type().is_symlink()); + fs::remove_file(output)?; + fs::remove_file(target)?; + Ok(()) + } + + #[test] + fn checked_builder_policy_is_explicit_and_fails_closed( + ) -> std::result::Result<(), Box> { + assert_eq!( + parse_rustc_host( + "rustc 1.90.0 (1159e78c4 2025-09-14)\nbinary: rustc\nhost: x86_64-unknown-linux-gnu\n" + ), + Some(CHECKED_COMPONENT_BUILDER_HOST) + ); + assert_eq!( + parse_rustc_host("host: x86_64-unknown-linux-gnu\nhost: aarch64-apple-darwin\n"), + None + ); + + assert!(ensure_checked_builder_host(CHECKED_COMPONENT_BUILDER_HOST).is_ok()); + let error = match ensure_checked_builder_host("aarch64-apple-darwin") { + Err(error) => error, + Ok(()) => { + return Err( + std::io::Error::other("a non-designated builder must fail closed").into(), + ); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::BuilderHostMismatch + ); + assert_eq!(error.subject(), "aarch64-apple-darwin"); + assert_eq!(error.reference(), Some(CHECKED_COMPONENT_BUILDER_HOST)); + Ok(()) + } + + #[test] + fn pinned_tool_paths_and_identities_fail_closed( + ) -> std::result::Result<(), Box> { + assert_eq!( + parse_resolved_tool_path("/toolchains/1.90.0/bin/rustc\n"), + Some(PathBuf::from("/toolchains/1.90.0/bin/rustc")) + ); + for malformed in ["", "relative/rustc\n", "/first/rustc\n/second/rustc\n"] { + assert_eq!(parse_resolved_tool_path(malformed), None); + } + + let valid = format!( + "release: {PINNED_RUST_TOOLCHAIN}\ncommit-hash: {PINNED_RUSTC_COMMIT}\nhost: {CHECKED_COMPONENT_BUILDER_HOST}\n" + ); + assert_eq!( + authenticate_tool_identity("rustc", &valid, PINNED_RUSTC_COMMIT)?, + CHECKED_COMPONENT_BUILDER_HOST + ); + + let duplicate_release = format!( + "release: {PINNED_RUST_TOOLCHAIN}\nrelease: {PINNED_RUST_TOOLCHAIN}\ncommit-hash: {PINNED_RUSTC_COMMIT}\nhost: {CHECKED_COMPONENT_BUILDER_HOST}\n" + ); + let error = + match authenticate_tool_identity("rustc", &duplicate_release, PINNED_RUSTC_COMMIT) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("duplicate release was accepted").into()), + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::BuilderHostInvalid + ); + + let wrong_release = format!( + "release: 1.91.0\ncommit-hash: {PINNED_RUSTC_COMMIT}\nhost: {CHECKED_COMPONENT_BUILDER_HOST}\n" + ); + let error = match authenticate_tool_identity("rustc", &wrong_release, PINNED_RUSTC_COMMIT) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("wrong release was accepted").into()), + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::BuilderReleaseMismatch + ); + + let wrong_commit = format!( + "release: {PINNED_RUST_TOOLCHAIN}\ncommit-hash: deadbeef\nhost: {CHECKED_COMPONENT_BUILDER_HOST}\n" + ); + let error = match authenticate_tool_identity("rustc", &wrong_commit, PINNED_RUSTC_COMMIT) { + Err(error) => error, + Ok(_) => return Err(std::io::Error::other("wrong commit was accepted").into()), + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::BuilderCommitMismatch + ); + Ok(()) + } + + #[test] + fn installed_pinned_toolchain_has_one_authenticated_identity( + ) -> std::result::Result<(), Box> { + let toolchain = pinned_rust_toolchain()?; + assert!(toolchain.cargo.is_absolute()); + assert!(toolchain.rustc.is_absolute()); + assert!(!toolchain.host().is_empty()); + Ok(()) + } + + #[test] + fn cargo_build_is_bound_to_the_authenticated_toolchain() { + let toolchain = PinnedRustToolchain { + cargo: PathBuf::from("/approved/cargo"), + rustc: PathBuf::from("/approved/rustc"), + host: CHECKED_COMPONENT_BUILDER_HOST.to_owned(), + }; + let mut command = Command::new(&toolchain.cargo); + for name in [ + "RUSTC", + "CARGO_BUILD_RUSTC", + "RUSTC_WRAPPER", + "CARGO_BUILD_RUSTC_WRAPPER", + "RUSTC_WORKSPACE_WRAPPER", + "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", + ] { + command.env(name, "/ambient/override"); + } + + bind_pinned_toolchain(&mut command, &toolchain); + + assert_eq!(command.get_program(), toolchain.cargo.as_os_str()); + assert_eq!( + command_environment_value(&command, "RUSTC"), + Some(toolchain.rustc.as_os_str()) + ); + assert_eq!( + command_environment_value(&command, "CARGO_BUILD_RUSTC"), + Some(toolchain.rustc.as_os_str()) + ); + for name in ["RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER"] { + assert_eq!( + command_environment_value(&command, name), + Some(std::ffi::OsStr::new("")) + ); + } + for name in [ + "CARGO_BUILD_RUSTC_WRAPPER", + "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", + ] { + assert!(command_environment_is_removed(&command, name)); + } + } + + fn command_environment_value<'a>( + command: &'a Command, + name: &str, + ) -> Option<&'a std::ffi::OsStr> { + command + .get_envs() + .find(|(key, _)| *key == std::ffi::OsStr::new(name)) + .and_then(|(_, value)| value) + } + + fn command_environment_is_removed(command: &Command, name: &str) -> bool { + command + .get_envs() + .any(|(key, value)| key == std::ffi::OsStr::new(name) && value.is_none()) + } + + #[test] + fn explicit_component_candidates_are_audited_before_use( + ) -> std::result::Result<(), Box> { + let candidate = temporary_output("candidate-invalid"); + fs::write(&candidate, b"not-a-component")?; + + let error = match read_component(&candidate) { + Err(error) => error, + Ok(_) => { + return Err( + std::io::Error::other("an unaudited candidate became a component").into(), + ); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::ComponentInvalid + ); + fs::remove_file(candidate)?; + Ok(()) + } + + #[test] + fn failed_promotion_never_touches_the_output( + ) -> std::result::Result<(), Box> { + let first = temporary_output("promotion-invalid-a"); + let second = temporary_output("promotion-invalid-b"); + let output = temporary_output("promotion-preserved"); + fs::write(&first, b"not-a-component")?; + fs::write(&second, b"not-a-component")?; + fs::write(&output, b"preserve-me")?; + let error = match promote_reproducible_candidates(&first, &second, &output) { + Err(error) => error, + Ok(_) => { + return Err(std::io::Error::other("an invalid candidate was promoted").into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateDigestMismatch + ); + assert_eq!(fs::read(&output)?, b"preserve-me"); + fs::remove_file(first)?; + fs::remove_file(second)?; + fs::remove_file(output)?; + Ok(()) + } + + #[test] + fn promotion_rejects_one_candidate_supplied_twice( + ) -> std::result::Result<(), Box> { + let candidate = temporary_output("promotion-aliased"); + fs::write(&candidate, b"one-candidate")?; + let error = match read_reproducible_candidates(&candidate, &candidate) { + Err(error) => error, + Ok(_) => { + return Err(std::io::Error::other( + "one candidate path satisfied the two-candidate boundary", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateAliased + ); + fs::remove_file(candidate)?; + Ok(()) + } + + #[test] + fn promotion_rejects_two_paths_to_one_underlying_file( + ) -> std::result::Result<(), Box> { + let first = temporary_output("promotion-hardlink-a"); + let second = temporary_output("promotion-hardlink-b"); + fs::write(&first, b"one-underlying-file")?; + fs::hard_link(&first, &second)?; + + let error = match read_reproducible_candidates(&first, &second) { + Err(error) => error, + Ok(_) => { + return Err(std::io::Error::other( + "hard-linked paths satisfied the two-candidate boundary", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateAliased + ); + fs::remove_file(first)?; + fs::remove_file(second)?; + Ok(()) + } + + #[test] + fn designated_candidate_build_cannot_target_the_checked_output( + ) -> std::result::Result<(), Box> { + let checked = temporary_output("checked-route"); + fs::write(&checked, b"checked")?; + + let error = match ensure_designated_candidate_output(&checked, &checked) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "one designated build could write the checked artifact", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CheckedOutputRequiresPromotion + ); + let alias = temporary_output("checked-route-hardlink"); + fs::hard_link(&checked, &alias)?; + let error = match ensure_designated_candidate_output(&alias, &checked) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "hard-linked candidate output bypassed promotion", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CheckedOutputRequiresPromotion + ); + fs::remove_file(alias)?; + fs::remove_file(checked)?; + Ok(()) + } + + #[test] + fn promotion_requires_two_equal_candidates_and_the_expected_digest( + ) -> std::result::Result<(), Box> { + let first = b"first"; + let second = b"second"; + let first_digest = digest_hex(&sha256(first)); + let second_digest = digest_hex(&sha256(second)); + + let error = match ensure_reproducible_candidates(first, second, &first_digest) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other("different candidates were promotable").into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateMismatch + ); + + let error = match ensure_reproducible_candidates(first, first, "not-a-sha256") { + Err(error) => error, + Ok(()) => { + return Err( + std::io::Error::other("a malformed expected digest was promotable").into(), + ); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::ExpectedDigestInvalid + ); + + let error = match ensure_reproducible_candidates(first, first, &second_digest) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "an unexpected candidate identity was promotable", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateDigestMismatch + ); + let error = match ensure_reproducible_candidates(first, first, &first_digest) { + Err(error) => error, + Ok(()) => { + return Err(std::io::Error::other( + "a caller-selected digest became release authority", + ) + .into()); + } + }; + assert_eq!( + error.kind(), + ProviderLowererComponentErrorKind::CandidateDigestMismatch + ); + Ok(()) + } + + #[test] + fn packaged_lowerer_resources_match_the_checked_provider_corpus() { + let pairs: [(&[u8], &[u8]); 4] = [ + ( + include_bytes!("../../crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor"), + include_bytes!("../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor"), + ), + ( + include_bytes!("../../crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor"), + include_bytes!("../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor"), + ), + ( + include_bytes!("../../crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor"), + include_bytes!("../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor"), + ), + ( + include_bytes!("../../crates/echo-edict-provider-lowerer/resources/authority-facts.echo-lawpack.cbor"), + include_bytes!("../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor"), + ), + ]; + + for (packaged, checked) in pairs { + assert_eq!(packaged, checked); + } + } + + #[test] + fn checked_component_promotion_is_exact_and_idempotent( + ) -> std::result::Result<(), Box> { + let bytes = include_bytes!( + "../../schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + ); + assert_eq!( + digest_hex(&sha256(bytes)), + APPROVED_CHECKED_COMPONENT_SHA256 + ); + let first = temporary_output("promotion-valid-a"); + let second = temporary_output("promotion-valid-b"); + let output = temporary_output("promotion-output"); + fs::write(&first, bytes)?; + fs::write(&second, bytes)?; + + let (component, status) = promote_reproducible_candidates(&first, &second, &output)?; + assert_eq!(status, ComponentOutputStatus::Written); + assert_eq!(component.bytes(), bytes); + let (_, status) = promote_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 repository_routes_preserve_separate_build_propositions() { + let ci = include_str!("../../.github/workflows/ci.yml"); + assert!(ci.contains("cargo xtask provider-lowerer-component check")); + assert!(ci.contains("runs-on: ubuntu-24.04")); + + let determinism = include_str!("../../.github/workflows/det-gates.yml"); + assert!(determinism.contains("cargo xtask provider-lowerer-component designated-build")); + assert!(determinism.contains( + "cmp build1.lowerer.component.wasm build1/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + )); + assert!(determinism.contains( + "cmp build2.lowerer.component.wasm build2/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + )); + + let host = include_str!("../../scripts/verify-edict-provider-host-v1.sh"); + assert!(host.contains("provider-lowerer-component build")); + assert!(host.contains("provider-lowerer-component audit")); + assert!(!host.contains("provider-lowerer-component promote")); + } + fn temporary_output(label: &str) -> PathBuf { let nonce = NEXT_TEMP.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!( From 7937293224d46a4d1151c696dd47a90883f2207f Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 14:26:56 -0700 Subject: [PATCH 3/6] Authenticate the reviewed Core closure --- CHANGELOG.md | 9 +- crates/echo-edict-provider-lowerer/README.md | 13 +- crates/echo-edict-provider-lowerer/src/lib.rs | 279 ++++++++++++--- .../tests/lowerer_contract.rs | 336 +++++++++++++++++- .../application-contract-hosting.md | 13 +- .../edict-provider/components/v1/README.md | 4 +- .../v1/lowerer.echo-dpo.component.wasm | Bin 112937 -> 130534 bytes .../tests/host_contract.rs | 107 +++++- xtask/src/provider_lowerer_component.rs | 2 +- 9 files changed, 680 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 009ad41b..c3cc8a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,14 @@ `echo.span-ir/v1` Target IR with byte-for-byte parity to Edict's built-in Echo wrapper; and returns typed refusals for unsupported ABI, profiles, semantics, reads, rebound operations, unresolved authored optics, changed type bindings, - intrinsics, and output roles. A deterministic build boundary pins the frozen + Core type definitions, evaluation budgets, out-of-scope locals, intrinsics, + and output roles. Local admission distinguishes pre-effect, obstruction-arm, + and post-effect scope from the exact input, effect-result, and obstruction + declarations before cloning any expression into Target IR. The first closure + also requires an empty input-constraint set and the exact zero-argument + `domain.WriteRejected` obstruction constructor, refusing those semantics until + their own lowering laws exist. A + deterministic build boundary pins the frozen WIT bytes, rejects ambient or callable imports, checks the exact decoded world type graph and contract attestation, and reproduces the checked component byte-for-byte across fresh target directories on the designated Rust 1.90.0 diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index 5765c6a5..02c1f4a2 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -26,9 +26,16 @@ inner domain is `echo.span-ir/v1`. It accepts only the exact Core module coordinate, local intent key, input/output type bindings, Echo DPO target profile, and #652 lawpack, authority, and lowerability identities. Rebound operations, authored optics that this crossing cannot yet discharge, changed -type bindings, unsupported Core ABI, target profiles, semantics, and output -roles produce typed provider refusals. Reads remain unsupported and fail -closed; the lowerer never represents a read as a synthetic mutation. +type bindings or definitions, evaluation budgets, unsupported Core ABI, target +profiles, semantics, and output roles produce typed provider refusals. The +one-effect closure requires exactly the compiler-owned input, effect-result, and +obstruction declarations, then validates pre-effect, obstruction-arm, and +post-effect scope by their complete identities before cloning expressions into +Target IR. It accepts only an empty input-constraint set and the reviewed +zero-argument `domain.WriteRejected` obstruction constructor; later constraint +or constructor semantics require explicit lowering laws. Reads remain +unsupported and fail closed; the lowerer never represents a read as a synthetic +mutation. The native Rust model is also the narrow unit-test boundary. A `wasm32` adapter generated from [`wit/edict-target-provider.wit`](wit/edict-target-provider.wit) diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index 70622865..9353646b 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -34,10 +34,13 @@ const TARGET_IR_ROLE: &str = "target-ir.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 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 TARGET_PROFILE_BYTES: &[u8] = include_bytes!("../resources/target-profile.echo-dpo.cbor"); const LAWPACK_BYTES: &[u8] = include_bytes!("../resources/lawpack.echo-dpo.cbor"); @@ -437,6 +440,43 @@ fn expected_lowerability() -> Result { ]) } +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_evaluation_budget() -> Result { + canonical_sorted_map([ + ("maxSteps", CanonicalValueV1::Integer(8)), + ("maxAllocatedBytes", CanonicalValueV1::Integer(1024)), + ("maxOutputBytes", CanonicalValueV1::Integer(256)), + ]) +} + fn canonical_sorted_map<'a>( entries: impl IntoIterator, ) -> Result { @@ -520,8 +560,14 @@ fn lower_core( if coordinate != CORE_COORDINATE { return Err(unsupported_semantics(coordinate)); } + let expected_types = expected_core_types().map_err(|()| { + invalid_artifact( + "core.echo-provider", + "reviewed Core type definitions are invalid", + ) + })?; if !matches!(array_field(&value, "imports"), Some(imports) if imports.is_empty()) - || !matches!(map_field(&value, "types"), Some(CanonicalValueV1::Map(_))) + || map_field(&value, "types") != Some(&expected_types) || !matches!(array_field(&value, "requiredCoreCapabilities"), Some(capabilities) if capabilities.is_empty()) { return Err(unsupported_semantics(coordinate)); @@ -573,17 +619,29 @@ fn lower_intent( } let input_constraints = array_field(intent, "inputConstraints") .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "input constraints are invalid"))?; + if !input_constraints.is_empty() { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } let budget = map_field(intent, "coreEvaluationBudget") .filter(|budget| validate_budget(budget)) .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core budget is invalid"))?; + let expected_budget = expected_core_evaluation_budget().map_err(|()| { + invalid_artifact( + OPERATION_COORDINATE, + "reviewed Core evaluation budget is invalid", + ) + })?; + if budget != &expected_budget { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } let body = map_field(intent, "body") .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core body is absent"))?; let locals = array_field(body, "locals") .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core locals are invalid"))?; - if !locals.iter().all(validate_local) { + if !validate_local_inventory(locals) { return Err(invalid_artifact( OPERATION_COORDINATE, - "Core local reference is invalid", + "Core local declarations are invalid", )); } let nodes = array_field(body, "nodes") @@ -591,10 +649,12 @@ fn lower_intent( let [node] = nodes.as_slice() else { return Err(unsupported_semantics(OPERATION_COORDINATE)); }; - let step = lower_effect_node(intent_name, node)?; + let lowered_effect = lower_effect_node(intent_name, node, locals)?; + let result_scope = [lowered_effect.input_local, lowered_effect.binding]; let result = map_field(body, "result") - .filter(|result| validate_expr(result)) .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core result is invalid"))?; + validate_expr(result, &result_scope) + .map_err(|error| expression_refusal(error, "Core result is invalid"))?; Ok(canonical_map([ ("operationProfile", canonical_text(OPERATION_PROFILE)), @@ -604,15 +664,22 @@ fn lower_intent( ), ("coreEvaluationBudget", budget.clone()), ("requirements", CanonicalValueV1::Array(Vec::new())), - ("steps", CanonicalValueV1::Array(vec![step])), + ("steps", CanonicalValueV1::Array(vec![lowered_effect.value])), ("result", result.clone()), ])) } -fn lower_effect_node( +struct LoweredEffect<'a> { + value: CanonicalValueV1, + input_local: &'a CanonicalValueV1, + binding: &'a CanonicalValueV1, +} + +fn lower_effect_node<'a>( intent_name: &str, - node: &CanonicalValueV1, -) -> Result { + node: &'a CanonicalValueV1, + locals: &'a [CanonicalValueV1], +) -> Result, ProviderRefusalV1> { if text_field(node, "kind") != Some("effect") || text_field(node, "effect") != Some(SEMANTIC_EFFECT) { @@ -621,34 +688,61 @@ fn lower_effect_node( let binding = map_field(node, "binding") .filter(|binding| validate_local(binding)) .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect binding is invalid"))?; - let input = map_field(node, "input") - .filter(|input| validate_expr(input)) - .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect input is invalid"))?; 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) || !validate_obstruction_arm(arm) { + if as_text(failure) != Some(FAILURE_COORDINATE) { return Err(unsupported_semantics(OPERATION_COORDINATE)); } + let obstruction_binder = map_field(arm, "binder") + .filter(|binder| validate_local(binder)) + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "obstruction binder is invalid"))?; + let input_local = + reviewed_input_local(locals, binding, obstruction_binder).ok_or_else(|| { + invalid_artifact(OPERATION_COORDINATE, "Core local declarations are invalid") + })?; - Ok(canonical_map([ - ("id", canonical_text(&format!("{intent_name}.step.0"))), - ("binding", binding.clone()), - ("effect", canonical_text(SEMANTIC_EFFECT)), - ("targetIntrinsic", canonical_text(TARGET_INTRINSIC)), - ("input", input.clone()), - ( - "obstructionFailures", - CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]), - ), - ( - "obstructionArms", - canonical_map([(FAILURE_COORDINATE, arm.clone())]), - ), - ])) + let pre_effect_scope = [input_local]; + let input = map_field(node, "input") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect input is invalid"))?; + validate_expr(input, &pre_effect_scope) + .map_err(|error| expression_refusal(error, "effect input is invalid"))?; + + let obstruction_scope = [input_local, obstruction_binder]; + let obstruction_value = map_field(arm, "value") + .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "obstruction value is invalid"))?; + if text_field(obstruction_value, "kind") != Some("call") + || text_field(obstruction_value, "callee") != Some(DOMAIN_OBSTRUCTION) + || !matches!(array_field(obstruction_value, "typeArgs"), Some(arguments) if arguments.is_empty()) + || !matches!(array_field(obstruction_value, "args"), Some(arguments) if arguments.is_empty()) + { + return Err(unsupported_semantics(OPERATION_COORDINATE)); + } + validate_expr(obstruction_value, &obstruction_scope) + .map_err(|error| expression_refusal(error, "obstruction value is invalid"))?; + + Ok(LoweredEffect { + value: canonical_map([ + ("id", canonical_text(&format!("{intent_name}.step.0"))), + ("binding", binding.clone()), + ("effect", canonical_text(SEMANTIC_EFFECT)), + ("targetIntrinsic", canonical_text(TARGET_INTRINSIC)), + ("input", input.clone()), + ( + "obstructionFailures", + CanonicalValueV1::Array(vec![canonical_text(FAILURE_COORDINATE)]), + ), + ( + "obstructionArms", + canonical_map([(FAILURE_COORDINATE, arm.clone())]), + ), + ]), + input_local, + binding, + }) } fn validate_binding(bound: &BoundArtifact) -> Result { @@ -699,33 +793,115 @@ fn validate_local(value: &CanonicalValueV1) -> bool { .all(|field| text_field(value, field).is_some_and(|value| !value.is_empty())) } -fn validate_obstruction_arm(value: &CanonicalValueV1) -> bool { - map_field(value, "binder").is_some_and(validate_local) - && map_field(value, "value").is_some_and(validate_expr) +fn validate_local_inventory(locals: &[CanonicalValueV1]) -> bool { + locals.iter().enumerate().all(|(index, local)| { + validate_local(local) + && locals[index + 1..] + .iter() + .all(|other| !same_local_id(local, other)) + }) +} + +fn reviewed_input_local<'a>( + locals: &'a [CanonicalValueV1], + binding: &CanonicalValueV1, + obstruction_binder: &CanonicalValueV1, +) -> Option<&'a CanonicalValueV1> { + if locals.len() != 3 + || text_field(binding, "type") != Some(OPERATION_RECEIPT_TYPE) + || text_field(obstruction_binder, "type") != Some(FAILURE_PAYLOAD_TYPE) + || !locals.contains(binding) + || !locals.contains(obstruction_binder) + || same_local_id(binding, obstruction_binder) + { + return None; + } + + let mut inputs = locals.iter().filter(|local| { + !same_local_id(local, binding) && !same_local_id(local, obstruction_binder) + }); + let input = inputs.next()?; + if inputs.next().is_some() || text_field(input, "type") != Some(OPERATION_INPUT_TYPE) { + return None; + } + Some(input) +} + +fn same_local_id(left: &CanonicalValueV1, right: &CanonicalValueV1) -> bool { + text_field(left, "id").is_some_and(|id| text_field(right, "id") == Some(id)) +} + +#[derive(Clone, Copy)] +enum ExpressionValidationError { + Invalid, + LocalOutOfScope, } -fn validate_expr(value: &CanonicalValueV1) -> bool { +fn validate_expr( + value: &CanonicalValueV1, + scope: &[&CanonicalValueV1], +) -> Result<(), ExpressionValidationError> { match text_field(value, "kind") { - Some("local") => map_field(value, "ref").is_some_and(validate_local), - Some("const") => map_field(value, "value").is_some_and(validate_core_value), - Some("record") => map_field(value, "fields") - .and_then(as_map) - .is_some_and(|fields| { - fields.iter().all(|(key, value)| { - as_text(key).is_some_and(|key| !key.is_empty()) && validate_expr(value) - }) - }), + Some("local") => { + let reference = map_field(value, "ref") + .filter(|reference| validate_local(reference)) + .ok_or(ExpressionValidationError::Invalid)?; + if scope.contains(&reference) { + Ok(()) + } else { + Err(ExpressionValidationError::LocalOutOfScope) + } + } + Some("const") => map_field(value, "value") + .filter(|value| validate_core_value(value)) + .map(|_| ()) + .ok_or(ExpressionValidationError::Invalid), + Some("record") => { + let fields = map_field(value, "fields") + .and_then(as_map) + .ok_or(ExpressionValidationError::Invalid)?; + for (key, value) in fields { + if as_text(key).is_none_or(str::is_empty) { + return Err(ExpressionValidationError::Invalid); + } + validate_expr(value, scope)?; + } + Ok(()) + } Some("field") => { - text_field(value, "field").is_some_and(|field| !field.is_empty()) - && map_field(value, "base").is_some_and(validate_expr) + if text_field(value, "field").is_none_or(str::is_empty) { + return Err(ExpressionValidationError::Invalid); + } + validate_expr( + map_field(value, "base").ok_or(ExpressionValidationError::Invalid)?, + scope, + ) } Some("call") => { - text_field(value, "callee").is_some_and(|callee| !callee.is_empty()) - && array_field(value, "typeArgs") + if text_field(value, "callee").is_none_or(str::is_empty) + || !array_field(value, "typeArgs") .is_some_and(|values| values.iter().all(|value| as_text(value).is_some())) - && array_field(value, "args").is_some_and(|values| values.iter().all(validate_expr)) + { + return Err(ExpressionValidationError::Invalid); + } + for argument in array_field(value, "args").ok_or(ExpressionValidationError::Invalid)? { + validate_expr(argument, scope)?; + } + Ok(()) } - _ => false, + _ => Err(ExpressionValidationError::Invalid), + } +} + +fn expression_refusal( + error: ExpressionValidationError, + invalid_message: &str, +) -> ProviderRefusalV1 { + match error { + ExpressionValidationError::Invalid => { + invalid_artifact(OPERATION_COORDINATE, invalid_message) + } + ExpressionValidationError::LocalOutOfScope => local_scope_refusal(), } } @@ -819,6 +995,15 @@ fn invalid_artifact(subject: &str, message: &str) -> ProviderRefusalV1 { ) } +fn local_scope_refusal() -> ProviderRefusalV1 { + refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + OPERATION_COORDINATE, + "echo.provider.local-reference-out-of-scope", + "Core local reference is not in scope", + ) +} + fn unsupported_semantics(subject: &str) -> ProviderRefusalV1 { refusal( ProviderRefusalKind::UnsupportedSemantics, diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs index 94da0a41..d3c2cb78 100644 --- a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -127,12 +127,12 @@ fn record_expr(id: CanonicalValueV1) -> CanonicalValueV1 { ]) } -fn call_expr(callee: &str, argument: CanonicalValueV1) -> CanonicalValueV1 { +fn call_expr(callee: &str) -> CanonicalValueV1 { map([ ("kind", text("call")), ("callee", text(callee)), ("typeArgs", CanonicalValueV1::Array(Vec::new())), - ("args", CanonicalValueV1::Array(vec![argument])), + ("args", CanonicalValueV1::Array(Vec::new())), ]) } @@ -166,6 +166,56 @@ fn bound(coordinate: &str, domain: &str, bytes: impl Into>) -> BoundArti } } +fn core_types() -> CanonicalValueV1 { + string_map([ + ( + "Input", + map([ + ("kind", text("Record")), + ("fields", string_map([("id", text("a.b@1.Input.id"))])), + ]), + ), + ( + "Output", + map([ + ("kind", text("Record")), + ("fields", string_map([("id", text("a.b@1.Output.id"))])), + ]), + ), + ( + "Receipt", + map([ + ("kind", text("Record")), + ("fields", string_map([("id", text("a.b@1.Receipt.id"))])), + ]), + ), + ( + "Input.id", + map([ + ("kind", text("String")), + ("max", integer(16)), + ("canonical", text("raw-utf8")), + ]), + ), + ( + "Output.id", + map([ + ("kind", text("String")), + ("max", integer(16)), + ("canonical", text("raw-utf8")), + ]), + ), + ( + "Receipt.id", + map([ + ("kind", text("String")), + ("max", integer(16)), + ("canonical", text("raw-utf8")), + ]), + ), + ]) +} + fn core_value(result: CanonicalValueV1, effect: Option<&str>) -> CanonicalValueV1 { let input = local("local:0", "input", "a.b@1.Input"); let receipt = local("local:1", "receipt", "a.b@1.Receipt"); @@ -182,10 +232,7 @@ fn core_value(result: CanonicalValueV1, effect: Option<&str>) -> CanonicalValueV "rejected", map([ ("binder", reason.clone()), - ( - "value", - call_expr("domain.WriteRejected", local_expr(reason)), - ), + ("value", call_expr("domain.WriteRejected")), ]), )]), ), @@ -210,7 +257,10 @@ fn core_value(result: CanonicalValueV1, effect: Option<&str>) -> CanonicalValueV ( "body", map([ - ("locals", CanonicalValueV1::Array(vec![input, receipt])), + ( + "locals", + CanonicalValueV1::Array(vec![input, receipt, reason]), + ), ("nodes", CanonicalValueV1::Array(nodes)), ("result", result), ]), @@ -220,7 +270,7 @@ fn core_value(result: CanonicalValueV1, effect: Option<&str>) -> CanonicalValueV ("apiVersion", text("edict.core/v1")), ("coordinate", text("a.b@1")), ("imports", CanonicalValueV1::Array(Vec::new())), - ("types", CanonicalValueV1::Map(Vec::new())), + ("types", core_types()), ("intents", string_map([("t", intent)])), ( "requiredCoreCapabilities", @@ -446,13 +496,7 @@ fn minimal_echo_mutation_lowers_from_explicit_semantics() { "binder", local("local:2", "reason", "target.replace.rejected") ), - ( - "value", - call_expr( - "domain.WriteRejected", - local_expr(local("local:2", "reason", "target.replace.rejected")) - ) - ), + ("value", call_expr("domain.WriteRejected")), ]) ); } @@ -461,7 +505,6 @@ fn minimal_echo_mutation_lowers_from_explicit_semantics() { fn reviewed_edict_fixture_has_exact_builtin_wrapper_parity() { let core_bytes = hex::decode(EDICT_ORACLE_CORE_HEX).expect("oracle Core hex is valid"); assert_eq!(core_bytes.len(), 1209); - let mut request = request(); request.core = bound("a.b@1", CORE_DOMAIN, core_bytes); assert_eq!( @@ -702,6 +745,195 @@ fn unsupported_intent_type_bindings_refuse_instead_of_being_ignored() { } } +#[test] +fn altered_core_type_definitions_refuse_instead_of_disappearing() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let input_id = map_field_mut(map_field_mut(&mut core, "types"), "Input.id"); + *map_field_mut(input_id, "max") = integer(17); + + let refusal = lower(request_with_core(core)) + .expect_err("changed Core type semantics cannot disappear across lowering"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1")); + assert_eq!(refusal.diagnostics.len(), 1); + assert_eq!( + refusal.diagnostics[0].code, + "echo.provider.unsupported-semantics" + ); +} + +#[test] +fn changed_evaluation_budget_refuses_instead_of_broadening_the_closure() { + for (field, value) in [ + ("maxSteps", 0), + ("maxAllocatedBytes", 2048), + ("maxOutputBytes", 512), + ] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let intent = map_field_mut(map_field_mut(&mut core, "intents"), "t"); + *map_field_mut(map_field_mut(intent, "coreEvaluationBudget"), field) = integer(value); + + let refusal = lower(request_with_core(core)) + .expect_err("a different evaluation budget is outside the reviewed closure"); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); + assert_eq!(refusal.diagnostics.len(), 1); + assert_eq!( + refusal.diagnostics[0].code, + "echo.provider.unsupported-semantics" + ); + } +} + +#[test] +fn nonempty_input_constraints_refuse_instead_of_crossing_unchecked() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let CanonicalValueV1::Array(constraints) = + map_field_mut(operation_intent_mut(&mut core), "inputConstraints") + else { + panic!("Core input constraints is not an array"); + }; + constraints.push(map([ + ("coordinate", text("a.b@1.t.where.0")), + ("source", text("where")), + ( + "predicate", + map([ + ("kind", text("call")), + ("predicate", text("domain.Unreviewed")), + ( + "args", + CanonicalValueV1::Array(vec![local_expr(local( + "local:99", + "ghost", + "a.b@1.Input", + ))]), + ), + ]), + ), + ])); + + assert_unsupported_semantics( + core, + "input constraints need explicit lowering and scope validation", + ); +} + +#[test] +fn nested_undeclared_local_reference_refuses_with_stable_details() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_body_mut(&mut core), "result") = record_expr(field_expr( + local_expr(local("local:99", "ghost", "a.b@1.Input")), + "id", + )); + assert_out_of_scope(core, "a nested undeclared result local cannot lower"); +} + +#[test] +fn effect_result_binding_is_not_visible_in_its_own_input() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_node_mut(&mut core), "input") = + local_expr(local("local:1", "receipt", "a.b@1.Receipt")); + assert_out_of_scope(core, "an effect cannot consume its own result binding"); +} + +#[test] +fn obstruction_binder_does_not_escape_its_arm() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_body_mut(&mut core), "result") = + local_expr(local("local:2", "reason", "target.replace.rejected")); + assert_out_of_scope(core, "an obstruction binder cannot escape its arm"); +} + +#[test] +fn local_reference_must_match_the_declared_identity_triple() { + for changed_reference in [ + local("local:0", "other", "a.b@1.Input"), + local("local:0", "input", "a.b@1.Output"), + ] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_body_mut(&mut core), "result") = + record_expr(field_expr(local_expr(changed_reference), "id")); + assert_out_of_scope(core, "a local reference cannot alter its declaration"); + } +} + +#[test] +fn duplicate_or_conflicting_local_ids_refuse() { + for duplicate in [ + local("local:0", "input", "a.b@1.Input"), + local("local:0", "other", "a.b@1.Output"), + ] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + let CanonicalValueV1::Array(locals) = + map_field_mut(operation_body_mut(&mut core), "locals") + else { + panic!("Core locals is not an array"); + }; + locals.push(duplicate); + assert_invalid_local_declarations(core); + } +} + +#[test] +fn local_inventory_is_exactly_the_reviewed_binding_closure() { + for missing_id in ["local:1", "local:2"] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + operation_locals_mut(&mut core) + .retain(|local| text_value(map_field(local, "id")) != missing_id); + assert_invalid_local_declarations(core); + } + + let mut core = core_value(ordinary_result(), Some("target.replace")); + operation_locals_mut(&mut core).push(local("local:99", "ghost", "a.b@1.Input")); + assert_invalid_local_declarations(core); +} + +#[test] +fn local_binding_roles_authenticate_their_reviewed_types() { + let mut input = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(local_by_id_mut(&mut input, "local:0"), "type") = text("a.b@1.Output"); + assert_invalid_local_declarations(input); + + let mut receipt = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(local_by_id_mut(&mut receipt, "local:1"), "type") = text("a.b@1.Output"); + *map_field_mut( + map_field_mut(operation_node_mut(&mut receipt), "binding"), + "type", + ) = text("a.b@1.Output"); + assert_invalid_local_declarations(receipt); + + let mut reason = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(local_by_id_mut(&mut reason, "local:2"), "type") = text("a.b@1.Input"); + *map_field_mut( + map_field_mut(obstruction_arm_mut(&mut reason), "binder"), + "type", + ) = text("a.b@1.Input"); + assert_invalid_local_declarations(reason); +} + +#[test] +fn obstruction_constructor_is_exactly_the_reviewed_mapping() { + for (field, changed) in [ + ("callee", text("domain.Unreviewed")), + ("typeArgs", CanonicalValueV1::Array(vec![text("T")])), + ( + "args", + CanonicalValueV1::Array(vec![local_expr(local("local:0", "input", "a.b@1.Input"))]), + ), + ] { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut( + map_field_mut(obstruction_arm_mut(&mut core), "value"), + field, + ) = changed; + assert_unsupported_semantics( + core, + "an unreviewed obstruction constructor cannot cross lowering", + ); + } +} + #[test] fn renamed_lowerability_artifact_refuses_as_an_invalid_closure_member() { let mut request = request(); @@ -745,3 +977,75 @@ fn map_field_mut<'a>(value: &'a mut CanonicalValueV1, field: &str) -> &'a mut Ca .find_map(|(key, value)| (key == &text(field)).then_some(value)) .unwrap_or_else(|| panic!("map field `{field}` is absent")) } + +fn operation_body_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + map_field_mut(operation_intent_mut(core), "body") +} + +fn operation_intent_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + map_field_mut(map_field_mut(core, "intents"), "t") +} + +fn operation_locals_mut(core: &mut CanonicalValueV1) -> &mut Vec { + let CanonicalValueV1::Array(locals) = map_field_mut(operation_body_mut(core), "locals") else { + panic!("Core locals is not an array"); + }; + locals +} + +fn local_by_id_mut<'a>( + core: &'a mut CanonicalValueV1, + expected_id: &str, +) -> &'a mut CanonicalValueV1 { + operation_locals_mut(core) + .iter_mut() + .find(|local| text_value(map_field(local, "id")) == expected_id) + .unwrap_or_else(|| panic!("Core local `{expected_id}` is absent")) +} + +fn operation_node_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + let CanonicalValueV1::Array(nodes) = map_field_mut(operation_body_mut(core), "nodes") else { + panic!("Core nodes is not an array"); + }; + nodes.first_mut().expect("reviewed closure has one node") +} + +fn obstruction_arm_mut(core: &mut CanonicalValueV1) -> &mut CanonicalValueV1 { + map_field_mut( + map_field_mut(operation_node_mut(core), "obstructionMap"), + "rejected", + ) +} + +fn assert_out_of_scope(core: CanonicalValueV1, message: &str) { + let refusal = lower(request_with_core(core)).expect_err(message); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); + assert_eq!(refusal.diagnostics.len(), 1); + assert_eq!( + refusal.diagnostics[0].code, + "echo.provider.local-reference-out-of-scope" + ); +} + +fn assert_invalid_local_declarations(core: CanonicalValueV1) { + let refusal = lower(request_with_core(core)).expect_err("ambiguous Core locals cannot lower"); + assert_eq!(refusal.kind, ProviderRefusalKind::InvalidSemanticArtifact); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); + assert_eq!(refusal.diagnostics.len(), 1); + assert_eq!( + refusal.diagnostics[0].code, + "echo.provider.invalid-semantic-artifact" + ); +} + +fn assert_unsupported_semantics(core: CanonicalValueV1, message: &str) { + let refusal = lower(request_with_core(core)).expect_err(message); + assert_eq!(refusal.kind, ProviderRefusalKind::UnsupportedSemantics); + assert_eq!(refusal.subject.as_deref(), Some("a.b@1.t")); + assert_eq!(refusal.diagnostics.len(), 1); + assert_eq!( + refusal.diagnostics[0].code, + "echo.provider.unsupported-semantics" + ); +} diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index 9503b7ab..e914c2b5 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -200,8 +200,17 @@ Target IR bytes produced by Edict's built-in Echo compatibility wrapper. An effect-free operation is refused as unsupported semantics rather than encoded as a synthetic mutation. The same fail-closed rule applies to a rebound Core module, changed operation type binding, or authored Core optic that this first -crossing cannot faithfully discharge. The complete semantic closure includes -the exact lowerability coordinate as well as its canonical bytes and digest. +crossing cannot faithfully discharge. The lowerer authenticates the complete +reviewed Core type-definition map and exact evaluation budget because neither +may disappear or broaden across Target IR projection. It also derives bounded +pre-effect, obstruction-arm, and post-effect local scopes from exactly one input, +one effect-result, and one obstruction declaration. Every local expression must +resolve to one complete compiler-owned identity before the expression is copied. +This first closure admits no input constraints and only the reviewed +zero-argument `domain.WriteRejected` obstruction constructor; non-empty +constraints or a different construction fail closed until their own semantic +crossings are implemented. The complete semantic closure includes the exact +lowerability coordinate as well as its canonical bytes and digest. The component's protocol instance and request/result aliases are type-only imports; filesystem, network, environment, clock, randomness, registry, logging callback, WASI, and every other callable import are forbidden. This diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index 6afd8e67..868fe208 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -18,8 +18,8 @@ is componentized with `wit-component` 0.251.0. The source WIT is the exact 7,392-byte Edict contract with SHA-256 `2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. -The checked component is 112,937 bytes with SHA-256 -`03a73240dda6dce6f16c33aa55537a45e4491bb59f25ea9911bb3fbe0c4b8de4`. +The checked component is 130,534 bytes with SHA-256 +`4fc9cd57b75ec3c5c71bf4a2a08ecaa7d3705234312bba5bea525005fa518f39`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's diff --git a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm index 8076d22830ee912773c4eca360d7c052d262875d..f91bc23fc6aec63a48ac05db4bd4f12daa6f8636 100644 GIT binary patch delta 39221 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 delta 30642 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 diff --git a/tests/edict-provider-host-v1/tests/host_contract.rs b/tests/edict-provider-host-v1/tests/host_contract.rs index 8697c75e..732b90cb 100644 --- a/tests/edict-provider-host-v1/tests/host_contract.rs +++ b/tests/edict-provider-host-v1/tests/host_contract.rs @@ -32,10 +32,10 @@ use edict_syntax::{ ProviderSchemaFormat, ProviderSemanticInput, ProviderSemanticInputBinding, ProviderSemanticInputKind, ResourceRef, TargetEffectLowering, TargetIrLoweringFacts, TargetProviderManifest, ValidatedProviderLoweringRequest, WriteClass, - AUTHORITY_FACTS_API_VERSION, CORE_MODULE_DIGEST_DOMAIN, ECHO_DPO_TARGET_PROFILE, - ECHO_SPAN_IR_DOMAIN, PROVIDER_LAWPACK_ARTIFACT_DOMAIN, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, - TARGET_PROFILE_API_VERSION, TARGET_PROVIDER_ABI, TARGET_PROVIDER_MANIFEST_API_VERSION, - TARGET_PROVIDER_PROTOCOL_VERSION, + 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, + TARGET_PROVIDER_ABI, TARGET_PROVIDER_MANIFEST_API_VERSION, TARGET_PROVIDER_PROTOCOL_VERSION, }; use sha2::{Digest as _, Sha256}; @@ -155,13 +155,12 @@ fn locked_test_resource(coordinate: &str, digit: char) -> ResourceRef { } fn provider_digest(domain: &str, canonical_bytes: &[u8]) -> ProviderDigest { - let value = decode_canonical_cbor(canonical_bytes).expect("artifact is canonical CBOR"); - let frame = CanonicalValue::Array(vec![ - CanonicalValue::Text("edict.digest/v1".to_owned()), - CanonicalValue::Text(domain.to_owned()), - value, - ]); - let framed = encode_canonical_cbor(&frame).expect("provider digest frame encodes"); + decode_canonical_cbor(canonical_bytes).expect("artifact is canonical CBOR"); + let mut framed = vec![0x83]; + framed + .extend(encode_canonical_cbor(&text(CORE_DIGEST_FRAME)).expect("digest frame tag encodes")); + framed.extend(encode_canonical_cbor(&text(domain)).expect("digest domain encodes")); + framed.extend_from_slice(canonical_bytes); ProviderDigest { algorithm: ProviderDigestAlgorithm::Sha256, bytes: Sha256::digest(framed).to_vec(), @@ -439,6 +438,23 @@ fn echo_request( echo_request_from_core_bytes(&core_bytes, output_role) } +fn canonical_map_field_mut<'a>( + value: &'a mut CanonicalValue, + field: &str, +) -> &'a mut CanonicalValue { + let CanonicalValue::Map(entries) = value else { + panic!("canonical value is not a map"); + }; + entries + .iter_mut() + .find_map(|(key, value)| (key == &text(field)).then_some(value)) + .unwrap_or_else(|| panic!("canonical map field `{field}` is absent")) +} + +fn canonical_core_intent_mut(core: &mut CanonicalValue) -> &mut CanonicalValue { + canonical_map_field_mut(canonical_map_field_mut(core, "intents"), "t") +} + fn echo_harness_with_request( contract: ProviderLoweringInvocationContract, request: ProviderLoweringRequest, @@ -790,6 +806,20 @@ fn exact_merged_edict_host_fixtures_are_unchanged() { ); } +#[test] +fn provider_digest_accepts_the_maximum_canonical_value_depth() { + let mut value = text("leaf"); + for _ in 0..MAX_CANONICAL_NESTING_DEPTH { + value = CanonicalValue::Array(vec![value]); + } + let bytes = encode_canonical_cbor(&value).expect("maximum-depth value encodes"); + + let digest = provider_digest("test.maximum-depth/v1", &bytes); + + assert_eq!(digest.algorithm, ProviderDigestAlgorithm::Sha256); + assert_eq!(digest.bytes.len(), 32); +} + #[test] fn echo_component_refuses_an_unsupported_profile_through_the_actual_host() { let core = echo_core(); @@ -821,6 +851,61 @@ fn echo_component_refuses_unsupported_core_semantics_through_the_actual_host() { ); } +#[test] +fn echo_component_refuses_nonempty_input_constraints_through_the_actual_host() { + let core_bytes = encode_core_module(&echo_core()).expect("Core module encodes canonically"); + let mut core = decode_canonical_cbor(&core_bytes).expect("Core module decodes canonically"); + *canonical_map_field_mut(canonical_core_intent_mut(&mut core), "inputConstraints") = + CanonicalValue::Array(vec![map([ + ("coordinate", text("a.b@1.t.where.0")), + ("source", text("where")), + ( + "predicate", + map([ + ("kind", text("call")), + ("predicate", text("domain.Unreviewed")), + ("args", CanonicalValue::Array(Vec::new())), + ]), + ), + ])]); + let core_bytes = encode_canonical_cbor(&core).expect("mutated Core encodes canonically"); + let (contract, request) = echo_request_from_core_bytes(&core_bytes, TARGET_IR_ROLE); + let harness = echo_harness_with_request(contract, request); + + assert_echo_refusal( + &harness, + ProviderRefusalKind::UnsupportedSemantics, + Some("a.b@1.t"), + ); +} + +#[test] +fn echo_component_refuses_incomplete_local_inventory_through_the_actual_host() { + let core_bytes = encode_core_module(&echo_core()).expect("Core module encodes canonically"); + let mut core = decode_canonical_cbor(&core_bytes).expect("Core module decodes canonically"); + let body = canonical_map_field_mut(canonical_core_intent_mut(&mut core), "body"); + let CanonicalValue::Array(locals) = canonical_map_field_mut(body, "locals") else { + panic!("Core locals is not an array"); + }; + locals.retain(|local| { + let CanonicalValue::Map(entries) = local else { + return true; + }; + !entries + .iter() + .any(|(key, value)| key == &text("type") && value == &text("target.replace.rejected")) + }); + let core_bytes = encode_canonical_cbor(&core).expect("mutated Core encodes canonically"); + let (contract, request) = echo_request_from_core_bytes(&core_bytes, TARGET_IR_ROLE); + let harness = echo_harness_with_request(contract, request); + + assert_echo_refusal( + &harness, + ProviderRefusalKind::InvalidSemanticArtifact, + Some("a.b@1.t"), + ); +} + #[test] fn echo_component_refuses_output_overclaim_through_the_actual_host() { let core = echo_core(); diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index b94cc3b4..fc6abab1 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -43,7 +43,7 @@ const PINNED_CARGO_COMMIT: &str = "840b83a10fb0e039a83f4d70ad032892c287570a"; /// Reviewed identity that the portable promotion command is permitted to install. pub(crate) const APPROVED_CHECKED_COMPONENT_SHA256: &str = - "03a73240dda6dce6f16c33aa55537a45e4491bb59f25ea9911bb3fbe0c4b8de4"; + "4fc9cd57b75ec3c5c71bf4a2a08ecaa7d3705234312bba5bea525005fa518f39"; pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; From 40da2182d969f806ba6bdce5ce53bd2a9daca572 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 15:12:24 -0700 Subject: [PATCH 4/6] Close the provider builder environment --- .github/workflows/ci.yml | 7 +- .github/workflows/det-gates.yml | 107 +++++++++---- CHANGELOG.md | 12 +- crates/echo-edict-provider-lowerer/README.md | 14 +- .../edict-provider/components/v1/README.md | 33 ++-- .../v1/lowerer.echo-dpo.component.wasm | Bin 130534 -> 130526 bytes xtask/src/provider_lowerer_component.rs | 145 ++++++++++++++++-- 7 files changed, 246 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f5a5f62..9f30f180 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,9 @@ jobs: build-edict-provider-lowerer: name: Build and check Edict provider lowerer runs-on: ubuntu-24.04 + container: + image: docker.io/library/rust@sha256:3914072ca0c3b8aad871db9169a651ccfce30cf58303e5d6f2db16d1d8a7e58f + options: --platform linux/amd64 steps: - uses: actions/checkout@v4 with: @@ -191,10 +194,6 @@ jobs: with: components: clippy targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - . - name: build, audit, and check exact component bytes run: | cargo xtask provider-lowerer-component check \ diff --git a/.github/workflows/det-gates.yml b/.github/workflows/det-gates.yml index 25ac3362..7c1efc66 100644 --- a/.github/workflows/det-gates.yml +++ b/.github/workflows/det-gates.yml @@ -220,60 +220,100 @@ jobs: perf.log perf-report.json - build-repro: - name: G4 build reproducibility (wasm) + build-repro-candidate: + name: G4 build candidate ${{ matrix.candidate }} needs: classify-changes if: needs.classify-changes.outputs.run_g4 == 'true' runs-on: ubuntu-24.04 + container: + image: docker.io/library/rust@sha256:3914072ca0c3b8aad871db9169a651ccfce30cf58303e5d6f2db16d1d8a7e58f + options: --platform linux/amd64 timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + candidate: [1, 2] steps: - - name: Setup Rust (Global) - uses: dtolnay/rust-toolchain@1.90.0 - with: - targets: wasm32-unknown-unknown - - name: Checkout Build 1 + - name: Checkout exact candidate source uses: actions/checkout@v4 with: - path: build1 - - name: Build 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Install wasm target + run: rustup target add wasm32-unknown-unknown + - name: Build isolated candidate + env: + CANDIDATE: ${{ matrix.candidate }} run: | - cd build1 - rustup target add wasm32-unknown-unknown repo_root="$(pwd -P)" export RUSTFLAGS="--remap-path-prefix=${repo_root}=/workspace" cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine - sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash1.txt - cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build1.wasm + sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > "hash${CANDIDATE}.txt" + cp target/wasm32-unknown-unknown/release/warp_wasm.wasm "build${CANDIDATE}.wasm" cargo xtask provider-lowerer-component designated-build \ --target-dir target/provider-lowerer-repro \ --output target/lowerer.echo-dpo.component.wasm - sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash1.txt - cp target/lowerer.echo-dpo.component.wasm ../build1.lowerer.component.wasm - - name: Checkout Build 2 + sha256sum target/lowerer.echo-dpo.component.wasm > "lowerer-hash${CANDIDATE}.txt" + cp target/lowerer.echo-dpo.component.wasm "build${CANDIDATE}.lowerer.component.wasm" + - name: Upload isolated candidate + uses: actions/upload-artifact@v4 + with: + name: build-repro-candidate-${{ matrix.candidate }} + overwrite: true + path: | + hash${{ matrix.candidate }}.txt + build${{ matrix.candidate }}.wasm + lowerer-hash${{ matrix.candidate }}.txt + build${{ matrix.candidate }}.lowerer.component.wasm + + build-repro: + name: G4 independent build comparison + needs: + - classify-changes + - build-repro-candidate + if: needs.classify-changes.outputs.run_g4 == 'true' && needs.build-repro-candidate.result == 'success' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout exact candidate source uses: actions/checkout@v4 with: - path: build2 - - name: Build 2 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Setup Rust for portable promotion + uses: dtolnay/rust-toolchain@1.90.0 + - name: Download candidate 1 + uses: actions/download-artifact@v4 + with: + name: build-repro-candidate-1 + path: candidate1 + - name: Download candidate 2 + uses: actions/download-artifact@v4 + with: + name: build-repro-candidate-2 + path: candidate2 + - name: Assemble exact evidence names run: | - cd build2 - rustup target add wasm32-unknown-unknown - repo_root="$(pwd -P)" - export RUSTFLAGS="--remap-path-prefix=${repo_root}=/workspace" - cargo build --release --target wasm32-unknown-unknown -p warp-wasm --features engine - sha256sum target/wasm32-unknown-unknown/release/warp_wasm.wasm > ../hash2.txt - cp target/wasm32-unknown-unknown/release/warp_wasm.wasm ../build2.wasm - cargo xtask provider-lowerer-component designated-build \ - --target-dir target/provider-lowerer-repro \ - --output target/lowerer.echo-dpo.component.wasm - sha256sum target/lowerer.echo-dpo.component.wasm > ../lowerer-hash2.txt - cp target/lowerer.echo-dpo.component.wasm ../build2.lowerer.component.wasm - - name: Compare hashes + cp candidate1/hash1.txt hash1.txt + cp candidate2/hash2.txt hash2.txt + cp candidate1/build1.wasm build1.wasm + cp candidate2/build2.wasm build2.wasm + cp candidate1/lowerer-hash1.txt lowerer-hash1.txt + 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 + - name: Compare, promote, and check exact bytes run: | diff hash1.txt hash2.txt || (echo "Reproducibility failure: Hashes differ!" && exit 1) diff lowerer-hash1.txt lowerer-hash2.txt || (echo "Lowerer reproducibility failure: Hashes differ!" && exit 1) + cmp build1.wasm build2.wasm cmp build1.lowerer.component.wasm build2.lowerer.component.wasm - cmp build1.lowerer.component.wasm build1/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm - cmp build2.lowerer.component.wasm build2/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm + cargo xtask provider-lowerer-component promote \ + --candidate-a build1.lowerer.component.wasm \ + --candidate-b build2.lowerer.component.wasm \ + --output target/lowerer.echo-dpo.promoted.component.wasm \ + --write + 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 echo "Hashes match: $(cat hash1.txt)" echo "Lowerer hashes match: $(cat lowerer-hash1.txt)" - name: Upload build artifacts @@ -281,6 +321,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: build-repro-artifacts + overwrite: true path: | hash1.txt hash2.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cc8a3d..b06036ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,13 @@ deterministic build boundary pins the frozen WIT bytes, rejects ambient or callable imports, checks the exact decoded world type graph and contract attestation, and reproduces the checked component - byte-for-byte across fresh target directories on the designated Rust 1.90.0 - `x86_64-unknown-linux-gnu` builder. The builder resolves and authenticates the - exact Rust and Cargo executables, binds Cargo to that compiler despite ambient - overrides, and atomically promotes only distinct candidates matching a reviewed - repository digest. Other-host builds are structural and semantic witnesses + byte-for-byte across independently provisioned `linux/amd64` containers from + the immutable Rust image used by CI. The builder resolves and authenticates the + exact Rust and Cargo executables, binds Cargo to that compiler, owns the inner + Cargo home, removes ambient Cargo profile/build/target overrides, remaps its + dependency source paths to `/cargo`, and atomically promotes only distinct + candidates matching a reviewed repository digest. + Other-host builds are structural and semantic witnesses rather than cross-host compiler-identity claims. The publication-enabled, archive-self-contained `echo-edict-provider-lowerer` source crate carries package-local copies of its four exact admitted resources, with a workspace diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index 02c1f4a2..42b85d84 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -56,11 +56,15 @@ Cargo and compiler executables, binds the inner Cargo build to that exact compiler, disables configured wrappers, and validates the complete module, import/export topology, and contract attestation without claiming cross-host byte identity. The checked artifact is built and compared exactly on the designated -`x86_64-unknown-linux-gnu` builder. `audit` authenticates an existing component -without rebuilding it. `promote` requires two distinct underlying candidate -files, byte equality, the repository-approved SHA-256 identity, complete -component admission, and `--write`; G4 separately proves those candidates came -from fresh designated builds. Successful refresh uses synchronized temporary +`linux/amd64` Rust image pinned by OCI digest. The inner build uses a controlled +Cargo home beneath its target directory, removes ambient Cargo +profile/build/target overrides, and remaps dependency source paths to `/cargo`; +physical cache locations therefore cannot alter the component bytes. +`audit` authenticates an existing component without rebuilding it. `promote` +requires two distinct underlying candidate files, byte equality, the +repository-approved SHA-256 identity, complete component admission, and +`--write`; G4 separately proves those candidates came from independently +provisioned designated builds. Successful refresh uses synchronized temporary bytes and atomic replacement, while write failure preserves the prior artifact. `designated-build` can emit candidates but refuses the checked repository path. Edict-host invocation evidence lives in the isolated Rust 1.94 witness under diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index 868fe208..ed6cfcc5 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -10,16 +10,20 @@ Echo has installed, admitted, authorized, or executed it. `lowerer.echo-dpo.component.wasm` implements `edict:target-provider/lowerer@1.0.0`. It was built from -`echo-edict-provider-lowerer` with the absolute rustup-resolved Rust 1.90.0 -compiler at commit `1159e78c4747b02ef996e55082b704c09b970588` and Cargo at -commit `840b83a10fb0e039a83f4d70ad032892c287570a`. The build binds Cargo to -that exact compiler and explicitly disables compiler wrappers. The core module +`echo-edict-provider-lowerer` in the `linux/amd64` image +`docker.io/library/rust@sha256:3914072ca0c3b8aad871db9169a651ccfce30cf58303e5d6f2db16d1d8a7e58f`. +Inside that immutable builder, the absolute rustup-resolved Rust 1.90.0 compiler +is commit `1159e78c4747b02ef996e55082b704c09b970588` and Cargo is commit +`840b83a10fb0e039a83f4d70ad032892c287570a`. The build binds Cargo to that +exact compiler, explicitly disables compiler wrappers, creates a controlled +Cargo home beneath the build target, removes ambient Cargo profile/build/target +overrides, and remaps that dependency source root to `/cargo`. The core module is componentized with `wit-component` 0.251.0. The source WIT is the exact 7,392-byte Edict contract with SHA-256 `2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. -The checked component is 130,534 bytes with SHA-256 -`4fc9cd57b75ec3c5c71bf4a2a08ecaa7d3705234312bba5bea525005fa518f39`. +The checked component is 130,526 bytes with SHA-256 +`14b6578e469ac8b2bab754ff13e1aa97cec8d9178235aa2993e1eabec4785a28`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's @@ -27,7 +31,7 @@ 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. -On the designated `x86_64-unknown-linux-gnu` Rust 1.90.0 builder, rebuild and +In the designated immutable `linux/amd64` Rust 1.90.0 builder above, rebuild and check the artifact without rewriting it: ```sh @@ -36,10 +40,11 @@ cargo +1.90.0 xtask provider-lowerer-component check \ --output schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm ``` -Two independent fresh target directories on that designated builder 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 +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 @@ -62,6 +67,6 @@ 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 -fresh designated checkouts and target directories—or equivalent explicit -operator evidence—establish independent build provenance. +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/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm index f91bc23fc6aec63a48ac05db4bd4f12daa6f8636..8cdfdb639482973e0472fd749e3d827ace38f9f9 100644 GIT binary patch delta 8709 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 mOk9d delta 8767 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 oIC;{0 Result { let target_directory = absolute_target_directory(repository_root, target_directory); - let repository_root_text = path_text(repository_root)?; - let target_directory_text = path_text(&target_directory)?; - - let encoded_rustflags = [ - format!("--remap-path-prefix={repository_root_text}=/echo"), - format!("--remap-path-prefix={target_directory_text}=/target"), - ] - .join("\u{1f}"); + let cargo_home = target_directory.join("cargo-home"); + let encoded_rustflags = + encoded_build_rustflags(repository_root, &target_directory, &cargo_home)?; let mut command = Command::new(&toolchain.cargo); + remove_ambient_cargo_build_overrides(&mut command, std::env::vars_os().map(|(name, _)| name)); bind_pinned_toolchain(&mut command, toolchain); let output = command .args([ @@ -679,6 +675,7 @@ pub(crate) fn build_component( "--locked", ]) .current_dir(repository_root) + .env("CARGO_HOME", &cargo_home) .env("CARGO_TARGET_DIR", &target_directory) .env("CARGO_INCREMENTAL", "0") .env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags) @@ -720,6 +717,41 @@ pub(crate) fn build_component( componentize(&core_bytes) } +fn encoded_build_rustflags( + repository_root: &Path, + target_directory: &Path, + cargo_home: &Path, +) -> Result { + let repository_root_text = path_text(repository_root)?; + let target_directory_text = path_text(target_directory)?; + let cargo_home_text = path_text(cargo_home)?; + + // rustc uses the last matching remap, so broad roots precede nested roots. + Ok([ + format!("--remap-path-prefix={repository_root_text}=/echo"), + format!("--remap-path-prefix={target_directory_text}=/target"), + format!("--remap-path-prefix={cargo_home_text}=/cargo"), + ] + .join("\u{1f}")) +} + +fn remove_ambient_cargo_build_overrides( + command: &mut Command, + names: impl IntoIterator, +) { + for name in names { + let Some(name_text) = name.to_str() else { + continue; + }; + if ["CARGO_PROFILE_", "CARGO_BUILD_", "CARGO_TARGET_"] + .iter() + .any(|prefix| name_text.starts_with(prefix)) + { + command.env_remove(name); + } + } +} + fn bind_pinned_toolchain(command: &mut Command, toolchain: &PinnedRustToolchain) { command .env("RUSTC", &toolchain.rustc) @@ -1593,6 +1625,38 @@ mod tests { Ok(()) } + #[test] + fn provider_component_build_flags_remap_cargo_home_to_one_stable_prefix( + ) -> std::result::Result<(), Box> { + assert_eq!( + encoded_build_rustflags( + Path::new("/workspace/echo"), + Path::new("/workspace/echo/target/provider-a"), + Path::new("/workspace/echo/target/provider-a/cargo-home"), + )?, + [ + "--remap-path-prefix=/workspace/echo=/echo", + "--remap-path-prefix=/workspace/echo/target/provider-a=/target", + "--remap-path-prefix=/workspace/echo/target/provider-a/cargo-home=/cargo", + ] + .join("\u{1f}") + ); + assert_eq!( + encoded_build_rustflags( + Path::new("/checkout-b/echo"), + Path::new("/var/tmp/provider-b"), + Path::new("/var/tmp/provider-b/cargo-home"), + )?, + [ + "--remap-path-prefix=/checkout-b/echo=/echo", + "--remap-path-prefix=/var/tmp/provider-b=/target", + "--remap-path-prefix=/var/tmp/provider-b/cargo-home=/cargo", + ] + .join("\u{1f}") + ); + Ok(()) + } + #[test] fn checked_builder_policy_is_explicit_and_fails_closed( ) -> std::result::Result<(), Box> { @@ -1693,6 +1757,39 @@ mod tests { Ok(()) } + #[test] + fn ambient_cargo_build_overrides_are_removed_by_prefix_family() { + let mut command = Command::new("cargo"); + let overridden = [ + "CARGO_PROFILE_RELEASE_OPT_LEVEL", + "CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_DEBUG", + "CARGO_BUILD_TARGET", + "CARGO_BUILD_JOBS", + "CARGO_TARGET_DIR", + "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS", + ]; + for name in overridden { + command.env(name, "ambient"); + } + command.env("CARGO_NET_OFFLINE", "true"); + + remove_ambient_cargo_build_overrides( + &mut command, + overridden + .into_iter() + .chain(["CARGO_NET_OFFLINE"]) + .map(OsString::from), + ); + + for name in overridden { + assert!(command_environment_is_removed(&command, name)); + } + assert_eq!( + command_environment_value(&command, "CARGO_NET_OFFLINE"), + Some(std::ffi::OsStr::new("true")) + ); + } + #[test] fn cargo_build_is_bound_to_the_authenticated_toolchain() { let toolchain = PinnedRustToolchain { @@ -2003,19 +2100,45 @@ mod tests { Ok(()) } + #[test] + fn checked_component_contains_no_physical_builder_paths() { + let bytes = include_bytes!( + "../../schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + ); + for forbidden in [ + b"/usr/local/cargo".as_slice(), + b"/home/runner/.cargo".as_slice(), + b"/target/cargo-home".as_slice(), + ] { + assert!(!bytes.windows(forbidden.len()).any(|part| part == forbidden)); + } + let canonical = b"/cargo/registry/src/"; + assert!(bytes.windows(canonical.len()).any(|part| part == canonical)); + } + #[test] fn repository_routes_preserve_separate_build_propositions() { + let designated_image = "docker.io/library/rust@sha256:3914072ca0c3b8aad871db9169a651ccfce30cf58303e5d6f2db16d1d8a7e58f"; let ci = include_str!("../../.github/workflows/ci.yml"); assert!(ci.contains("cargo xtask provider-lowerer-component check")); assert!(ci.contains("runs-on: ubuntu-24.04")); + assert!(ci.contains(designated_image)); + assert!(ci.contains("options: --platform linux/amd64")); let determinism = include_str!("../../.github/workflows/det-gates.yml"); + assert!(determinism.contains(designated_image)); + assert!(determinism.contains("options: --platform linux/amd64")); + assert!(determinism.contains("candidate: [1, 2]")); + assert!(determinism.contains("build-repro-candidate-${{ matrix.candidate }}")); + assert!(determinism.matches("overwrite: true").count() >= 2); assert!(determinism.contains("cargo xtask provider-lowerer-component designated-build")); + assert!(determinism.contains("cargo xtask provider-lowerer-component promote")); + assert!(determinism.contains("--write")); assert!(determinism.contains( - "cmp build1.lowerer.component.wasm build1/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" + "cmp build1.lowerer.component.wasm schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm" )); assert!(determinism.contains( - "cmp build2.lowerer.component.wasm build2/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" )); let host = include_str!("../../scripts/verify-edict-provider-host-v1.sh"); From bb6d25e834b8314432e8b1cd222c3eb21b6fbac6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 15:23:44 -0700 Subject: [PATCH 5/6] Bind provider container repository discovery --- .github/workflows/ci.yml | 4 ++++ .github/workflows/det-gates.yml | 3 +++ xtask/src/provider_lowerer_component.rs | 15 +++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f30f180..5d8d69ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -195,6 +195,10 @@ jobs: components: clippy targets: wasm32-unknown-unknown - name: build, audit, and check exact component bytes + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: ${{ github.workspace }} run: | cargo xtask provider-lowerer-component check \ --target-dir target/provider-lowerer-component \ diff --git a/.github/workflows/det-gates.yml b/.github/workflows/det-gates.yml index 7c1efc66..3bf04c51 100644 --- a/.github/workflows/det-gates.yml +++ b/.github/workflows/det-gates.yml @@ -243,6 +243,9 @@ jobs: - name: Build isolated candidate env: CANDIDATE: ${{ matrix.candidate }} + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: ${{ github.workspace }} run: | repo_root="$(pwd -P)" export RUSTFLAGS="--remap-path-prefix=${repo_root}=/workspace" diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index 966dfb6d..9f84639b 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -2124,10 +2124,25 @@ mod tests { assert!(ci.contains("runs-on: ubuntu-24.04")); assert!(ci.contains(designated_image)); assert!(ci.contains("options: --platform linux/amd64")); + assert!(ci.contains(concat!( + "- name: build, audit, and check exact component bytes\n", + " env:\n", + " GIT_CONFIG_COUNT: 1\n", + " GIT_CONFIG_KEY_0: safe.directory\n", + " GIT_CONFIG_VALUE_0: ${{ github.workspace }}", + ))); let determinism = include_str!("../../.github/workflows/det-gates.yml"); assert!(determinism.contains(designated_image)); assert!(determinism.contains("options: --platform linux/amd64")); + assert!(determinism.contains(concat!( + "- name: Build isolated candidate\n", + " env:\n", + " CANDIDATE: ${{ matrix.candidate }}\n", + " GIT_CONFIG_COUNT: 1\n", + " GIT_CONFIG_KEY_0: safe.directory\n", + " GIT_CONFIG_VALUE_0: ${{ github.workspace }}", + ))); assert!(determinism.contains("candidate: [1, 2]")); assert!(determinism.contains("build-repro-candidate-${{ matrix.candidate }}")); assert!(determinism.matches("overwrite: true").count() >= 2); From 9535b9ac5a2d801343e33d37c235924557bd5cdc Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 15 Jul 2026 15:44:42 -0700 Subject: [PATCH 6/6] Refuse unreviewed provider expression calls --- CHANGELOG.md | 5 ++-- crates/echo-edict-provider-lowerer/README.md | 5 ++-- crates/echo-edict-provider-lowerer/src/lib.rs | 25 ++++++++++++------ .../tests/lowerer_contract.rs | 23 ++++++++++++++++ .../edict-provider/components/v1/README.md | 4 +-- .../v1/lowerer.echo-dpo.component.wasm | Bin 130526 -> 130679 bytes xtask/src/provider_lowerer_component.rs | 2 +- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b06036ba..02230015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ and post-effect scope from the exact input, effect-result, and obstruction declarations before cloning any expression into Target IR. The first closure also requires an empty input-constraint set and the exact zero-argument - `domain.WriteRejected` obstruction constructor, refusing those semantics until - their own lowering laws exist. A + `domain.WriteRejected` obstruction constructor. Effect inputs and intent + results admit no call-expression callee, refusing unreviewed calls until their + own lowering laws exist. A deterministic build boundary pins the frozen WIT bytes, rejects ambient or callable imports, checks the exact decoded world type graph and contract attestation, and reproduces the checked component diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index 42b85d84..e759f4e6 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -32,8 +32,9 @@ one-effect closure requires exactly the compiler-owned input, effect-result, and obstruction declarations, then validates pre-effect, obstruction-arm, and post-effect scope by their complete identities before cloning expressions into Target IR. It accepts only an empty input-constraint set and the reviewed -zero-argument `domain.WriteRejected` obstruction constructor; later constraint -or constructor semantics require explicit lowering laws. Reads remain +zero-argument `domain.WriteRejected` obstruction constructor. Effect inputs and +intent results admit no call-expression callee in this closure; later +constraint, constructor, or call semantics require explicit lowering laws. Reads remain unsupported and fail closed; the lowerer never represents a read as a synthetic mutation. diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index 9353646b..b8cf799d 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -653,7 +653,7 @@ fn lower_intent( let result_scope = [lowered_effect.input_local, lowered_effect.binding]; let result = map_field(body, "result") .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "Core result is invalid"))?; - validate_expr(result, &result_scope) + validate_expr(result, &result_scope, &[]) .map_err(|error| expression_refusal(error, "Core result is invalid"))?; Ok(canonical_map([ @@ -708,7 +708,7 @@ fn lower_effect_node<'a>( let pre_effect_scope = [input_local]; let input = map_field(node, "input") .ok_or_else(|| invalid_artifact(OPERATION_COORDINATE, "effect input is invalid"))?; - validate_expr(input, &pre_effect_scope) + validate_expr(input, &pre_effect_scope, &[]) .map_err(|error| expression_refusal(error, "effect input is invalid"))?; let obstruction_scope = [input_local, obstruction_binder]; @@ -721,7 +721,7 @@ fn lower_effect_node<'a>( { return Err(unsupported_semantics(OPERATION_COORDINATE)); } - validate_expr(obstruction_value, &obstruction_scope) + validate_expr(obstruction_value, &obstruction_scope, &[DOMAIN_OBSTRUCTION]) .map_err(|error| expression_refusal(error, "obstruction value is invalid"))?; Ok(LoweredEffect { @@ -835,11 +835,13 @@ fn same_local_id(left: &CanonicalValueV1, right: &CanonicalValueV1) -> bool { enum ExpressionValidationError { Invalid, LocalOutOfScope, + UnsupportedCall, } fn validate_expr( value: &CanonicalValueV1, scope: &[&CanonicalValueV1], + allowed_callees: &[&str], ) -> Result<(), ExpressionValidationError> { match text_field(value, "kind") { Some("local") => { @@ -864,7 +866,7 @@ fn validate_expr( if as_text(key).is_none_or(str::is_empty) { return Err(ExpressionValidationError::Invalid); } - validate_expr(value, scope)?; + validate_expr(value, scope, allowed_callees)?; } Ok(()) } @@ -875,17 +877,23 @@ fn validate_expr( validate_expr( map_field(value, "base").ok_or(ExpressionValidationError::Invalid)?, scope, + allowed_callees, ) } Some("call") => { - if text_field(value, "callee").is_none_or(str::is_empty) - || !array_field(value, "typeArgs") - .is_some_and(|values| values.iter().all(|value| as_text(value).is_some())) + let callee = text_field(value, "callee") + .filter(|callee| !callee.is_empty()) + .ok_or(ExpressionValidationError::Invalid)?; + if !allowed_callees.contains(&callee) { + return Err(ExpressionValidationError::UnsupportedCall); + } + if !array_field(value, "typeArgs") + .is_some_and(|values| values.iter().all(|value| as_text(value).is_some())) { return Err(ExpressionValidationError::Invalid); } for argument in array_field(value, "args").ok_or(ExpressionValidationError::Invalid)? { - validate_expr(argument, scope)?; + validate_expr(argument, scope, allowed_callees)?; } Ok(()) } @@ -902,6 +910,7 @@ fn expression_refusal( invalid_artifact(OPERATION_COORDINATE, invalid_message) } ExpressionValidationError::LocalOutOfScope => local_scope_refusal(), + ExpressionValidationError::UnsupportedCall => unsupported_semantics(OPERATION_COORDINATE), } } diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs index d3c2cb78..5c39826d 100644 --- a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -819,6 +819,29 @@ fn nonempty_input_constraints_refuse_instead_of_crossing_unchecked() { ); } +#[test] +fn unreviewed_effect_input_call_refuses_as_unsupported_semantics() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_node_mut(&mut core), "input") = call_expr("domain.Unreviewed"); + + assert_unsupported_semantics( + core, + "an unreviewed effect-input call cannot cross lowering", + ); +} + +#[test] +fn unreviewed_intent_result_call_refuses_as_unsupported_semantics() { + let mut core = core_value(ordinary_result(), Some("target.replace")); + *map_field_mut(operation_body_mut(&mut core), "result") = + record_expr(call_expr("domain.Unreviewed")); + + assert_unsupported_semantics( + core, + "an unreviewed intent-result call cannot cross lowering", + ); +} + #[test] fn nested_undeclared_local_reference_refuses_with_stable_details() { let mut core = core_value(ordinary_result(), Some("target.replace")); diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index ed6cfcc5..622e1980 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -22,8 +22,8 @@ is componentized with `wit-component` 0.251.0. The source WIT is the exact 7,392-byte Edict contract with SHA-256 `2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. -The checked component is 130,526 bytes with SHA-256 -`14b6578e469ac8b2bab754ff13e1aa97cec8d9178235aa2993e1eabec4785a28`. +The checked component is 130,679 bytes with SHA-256 +`03edee44c6bc70eb998c0c17662a214809746af3bba0740f3407c18a4016309e`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's diff --git a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm index 8cdfdb639482973e0472fd749e3d827ace38f9f9..a85ae30a7ae2c25c4fa087952b53072333f352c5 100644 GIT binary patch delta 1387 zcmZWpziSjx5PrXQ-@ELtZtw@eAMkcfafpE+DS`;z6nCloA8c$^P?Qu3!bU?H8^IH? zun7oip#&C`2*OziS_oDm1S48VCm3$XM{SK7os zNWIh$DKo~f`zborMF`$BzKSQ47|}o_Q7ev!rwEZ!lbotrsv3a8aU%^vep5ADBI8@y z>J4NM>KTHlEbh@HsLvw^fR8#gMUA#15{+A7rB$T^5>$XHX@n9wuI^Dr`)ul5SfW{G`zSP#MvRM5t)khQ#To4s5fDcqqw$-(6dB)7ruT znfO6Y6K&#V3lQjTS|Y1)hdAyKqHDNJW8<_u%yM*T;)IHS(~>kJ76c$Ml?Mb!Y;exy zH^=(HWme6AlSg+*#)aE71xp2Ks^NP3#96=nwL8>)+uhlI-rd(;{%mJgyH8fTd}a6_ D3I{$iJ&F>r5@FVlG--ltV__o* zqM$4cC=rBh0;Uiw1yRu|#af7h5fkIRciv`qcZ(VBoVoX$bI+UKdyUV38@+Q{><_91 zeV%hsT{oLtY~8O;2iY5bue!8#JeW8j9V*f;S z^W#x9j#M?$xSlKk56EX<9`SJmA4hORT>Uz2$|~ThJlTOCbA5ItlXG-0mg~P;w%8dK4iC~><-+{iMKwINJP1H2izLiuo9w@1} zGXWG-d+@TJP7RxJ16a)?tH)F~H7lGxPY-jkfLG?;((@qsi2^f&MRS$`wKm@6qKurr zkpjJ31gQbFpa&+p%LNtz2|A6lMQXrR)sBORg(39)K^)~+Uq{g7DcXZ$V0X}1f{#|m z%<)u3M?&a)3p?{%u#{QuN>!xXGHa4Q$gml+$k2fIUT{quSZNxdJ$VBLB7vEFt7mwB|L!g(0W=j_Oi{G7Ci@7BEZ~t zlCHdT7V}6G2`(zYC5Tte z8p-z=u^&k~)B!RSvFMU-dYpxH3C^AfFltLO)+~V=d3Ov_`^ziau2Wmb8kggCC!bJl z&qts}(-PeQ)rN7#*2b;9Mqm6{fIR}05y=u(hi7PPyRsEauyg|EQS}PQBi@PyNVe$C zx}R{kHSgMJjDKH_G6WwM9?7r;n+edJA$H<;2XGidCm^~!#8)vVu!A}JL|7HBFtKbo egtmKeaH_j9nCf1**61z`rtWSGUTqBc&h&piJ}WT* diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index 9f84639b..8c901f83 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -43,7 +43,7 @@ const PINNED_CARGO_COMMIT: &str = "840b83a10fb0e039a83f4d70ad032892c287570a"; /// Reviewed identity that the portable promotion command is permitted to install. pub(crate) const APPROVED_CHECKED_COMPONENT_SHA256: &str = - "14b6578e469ac8b2bab754ff13e1aa97cec8d9178235aa2993e1eabec4785a28"; + "03edee44c6bc70eb998c0c17662a214809746af3bba0740f3407c18a4016309e"; pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm";