diff --git a/.ban-nondeterminism-allowlist b/.ban-nondeterminism-allowlist index 6b027bb4..f4685aa1 100644 --- a/.ban-nondeterminism-allowlist +++ b/.ban-nondeterminism-allowlist @@ -18,3 +18,4 @@ std-env crates/warp-core/tests/wsc_store_tests.rs WSC filesystem fixture temp di std-fs crates/warp-core/tests/wsc_store_tests.rs WSC filesystem fixture I/O only. std-process crates/warp-core/tests/wsc_store_tests.rs WSC filesystem fixture temp directory disambiguation only. std-fs crates/warp-math/tests/trig_golden_vectors.rs golden-vector fixture read/write only. +std-fs crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs explicit ignored checked generated-helper fixture regeneration only. diff --git a/AGENTS.md b/AGENTS.md index 674058bc..e0892b8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,24 @@ changed, update `CHANGELOG.md` when shipped behavior changed, run the narrow witness and directly relevant checks, and commit the result as a focused new commit. Record follow-on work in GitHub rather than in the repository. +## Pre-PR Documentation Accuracy Gate + +Immediately before opening a pull request, and again after material review +changes, compare the branch's actual behavior and artifacts with the current +documentation. Revisit the relevant entrances and canonical owners: +`README.md`, `GUIDE.md`, `docs/README.md`, `docs/architecture/`, `docs/spec/`, +`docs/invariants/`, `docs/topics/`, and the evidence anchors of any applicable +accepted ADRs. + +Search specifically for stale current-state claims such as “not implemented,” +“fixture-only,” old ownership or authority boundaries, obsolete version or +compatibility statements, and examples that still prescribe a superseded path. +Correct inaccuracies in their owning documents before opening the PR. Do not +create a status ledger or duplicate change-local implementation detail in +multiple documents; keep durable boundaries in their canonical owners and link +to them. If the review finds no documentation impact, say so explicitly in the +pull-request body. + --- **The goal is inevitability. Every feature is defined by its tests.** diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d97488..f2d8ab2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,13 +36,34 @@ field swaps, authority-source disagreement, malformed schemas/components, and invalid requests all fail before guest execution. This proves package readiness only, never Echo installation, execution, or runtime authority. -- `echo-wesley-gen` now carries a fixed 35-file package-local source and provider +- `echo-wesley-gen` now carries a fixed 38-file package-local source and provider asset boundary, preserving original logical source labels while making its `.crate` archive independent of workspace-parent files. An explicit sync tool distinguishes authoritative generated/component owners from their checked package corroboration, supports staged regeneration without circularity, and - checks exact Cargo archive selection. The extracted archive compiles when its + checks exact Cargo archive selection. Fixed owner leaves are opened without + following final symbolic links and read twice through one retained descriptor; + file-type, length, or byte disagreement refuses a moving owner. The generator + source identity now enumerates 20 files and includes the exact manifest and + implementation bytes of its canonicalization, operation-id-law, and + provider-registry dependencies. The extracted archive compiles when its still-unpublished Echo dependencies are supplied through local patches. +- Echo now owns the versioned semantic operation-id law + `echo.semantic-operation-id.fnv1-32/v1`. It derives a persisted `u32` from the + exact semantic coordinate and generic query/mutation kind, remains + domain-separated from Wesley's GraphQL-field-name law, and reserves the top + two ids for Echo protocol envelopes: `u32::MAX` for scheduler control and + `u32::MAX - 1` for witnessed suffix import. The canonical generated-artifact + profile carries both the law coordinate and each derived id; generation + refuses either reserved value and package-local collisions without salting, + probing, or renaming. Its CDDL bounds `operationId` to the remaining numeric + application range, but schema admission alone does not prove derivation or + collision freedom: semantic generation recomputes the law and checks the + complete operation set. This packages an exact operation-identity proposition + but does not register, install, authorize, or execute the operation. Generated + source now carries public expected constants for the profile-owned law and id, + requires both as untrusted bundle claims, refuses disagreement, and exposes + the matched claim through the resulting private-state registration descriptor. - 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, @@ -51,7 +72,7 @@ wrapper; and returns typed refusals for unsupported ABI, profiles, semantics, reads, rebound operations, unresolved authored optics, changed type bindings, Core type definitions, evaluation budgets, out-of-scope locals, intrinsics, - and output roles. Local admission distinguishes pre-effect, obstruction-arm, + and undeclared or malformed output-role claims. 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 @@ -66,7 +87,9 @@ 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. + candidates matching a reviewed repository digest. The promoted 189,668-byte + component has SHA-256 + `9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af`. 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 @@ -74,6 +97,41 @@ 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. +- The native Echo Edict lowerer model now accepts any exact, lexicographically + sorted subset of the declared `generated.echo-dpo` generated artifact, + `review.echo-dpo` review payload, and `target-ir.echo-dpo` Target IR roles. + It emits canonical-CBOR generated and review envelopes at + `generated/echo_dpo.rs` and `review/echo_dpo.json`, and refuses unknown, + mismatched, duplicate, or out-of-order role claims with typed + `UnsupportedOutputRole`. The generated Rust binds the semantic operation, + Target IR, Echo ABI and helper API, provider and operation schemas, target and + generated profiles, and abstract footprint + obligation/algebra. It then performs only an + explicit post-assembly equality and consistency comparison between an + independent expected pin and untrusted Edict semantic/release bundle claims, + with typed refusal for every identity class. Every domain-framed resource is + compared as a complete coordinate/domain/digest proposition. + The generated-artifact profile now owns `le-binary-v1`, and generated Rust + implements distinct bounded `Id`, `Input`, and `Output` types with fail-closed + decoding for malformed, over-bound, truncated, or trailing bytes. Descriptor + methods round-trip the exact input/output types and pack typed input into + canonical EINT v1; EINT `vars` remain codec-owned opaque bytes rather than a + universal canonical-CBOR value. The matched descriptor exposes a borrowed, + provider-generic registry and can bind its generated matcher to one explicitly + identified host mutation implementation. It returns only an opaque, + non-installing package proposal after checking the complete Target IR, + semantic/release bundle, target/generated/operation profile, provider/value + schema, codec, obstruction, operation-id, ABI, helper-API, rule-name, and + footprint identities. Echo adds the mandatory ingress matcher reads to the + host's effect footprint. Identity equality detects cross-binding but does not + prove arbitrary callback semantics. The mutation proposal fails closed for a + `Query`; authored reads remain a separate bounded observer/optic path. The + isolated actual-host fixture is green for binding, typed codec refusal and + round trips, EINT packing, the borrowed registry, and proposal preflight. + The permanently non-authoritative review is bound to the exact generated + artifact. Neither projection authenticates a pin, admits or installs a + package, or grants Echo runtime authority; checked-component promotion and + host-side CDDL admission remain separate crossings. - Echo now provides the exact `edict:target-provider/verifier@1.0.0` Component Model implementation for the checked provider closure. The pure verifier independently compares explicit digest-bound Core and Target IR @@ -86,8 +144,8 @@ predicate, input-constraint, require-failure, and Core-value shapes before separating malformed artifacts from well-formed unsupported semantics, and one admitted diagnostic-ABI identity now binds both the target profile and - every emitted report. The 189,515-byte checked component has SHA-256 - `11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952` + every emitted report. The 189,922-byte checked component has SHA-256 + `4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a` and reproduces byte-for-byte across independently provisioned designated `linux/amd64` builders. The isolated pinned Edict host preflights the exact request artifacts and declared output schema, invokes that checked component, diff --git a/Cargo.lock b/Cargo.lock index 87729c4c..dee1e866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,7 @@ version = "0.1.0" dependencies = [ "blake3", "ciborium", + "echo-registry-api", "half", "proptest", "serde", @@ -757,6 +758,7 @@ dependencies = [ "ciborium", "clap", "echo-edict-canonical", + "echo-registry-api", "hex", "prettyplease", "proc-macro2", diff --git a/README.md b/README.md index 2984b127..03e07108 100644 --- a/README.md +++ b/README.md @@ -103,16 +103,40 @@ model. ## How It Works -Echo currently implements two separate halves of the generated-contract path. -Wesley currently emits raw `RewriteRule` builders and generated helpers. Its -integration fixture enables the policy-gated `native_rule_bootstrap` feature -and registers those rules directly. It does not emit an `InstalledContractPackage` -or exercise package verification. - -Echo separately verifies and registers `InstalledContractPackage` values, -dispatches registered handlers through the scheduler, and stamps rule-pack -identity into runtime evidence. No current Wesley or Edict generator connects -compiler output to that package path. +Echo currently implements three adjacent generated-contract boundaries. They +do not yet form one end-to-end application path. + +The Wesley compatibility path emits raw `RewriteRule` builders and generated +helpers. Its integration fixture enables the policy-gated +`native_rule_bootstrap` feature and registers those rules directly. It does +not emit an `InstalledContractPackage` or exercise package verification. + +The Edict provider path admits exact semantic-source, contract-pack, and +settings bytes; emits canonical semantic artifacts; runs a deterministic +lowerer and an independent verifier; and publishes a digest-locked provider +package plus a generated Rust helper projection. +The helper performs pure, fail-closed preflight across exact package, Target +IR, bundle, profile, schema, +codec, obstruction, ABI, helper API, operation, and footprint claims. It +exposes typed `Id`, `Input`, and `Output` codecs, packs typed input into +canonical EINT v1, and builds a provider-generic borrowed registry plus an +opaque, non-installing provider package proposal when explicitly bound to +matching host callbacks. Matching callback claims are cross-binding evidence, +not proof of callback semantics. +The helper does not construct an `InstalledContractPackage`, register or +install anything, invoke callbacks during proposal construction, schedule +execution, or mint runtime authority. + +Echo separately verifies and registers host-constructed +`InstalledContractPackage` values, dispatches registered handlers through the +scheduler, and stamps rule-pack identity into runtime evidence. +No generated bridge yet carries either compiler path across Echo's trusted-host +installation boundary into native scheduler execution. The Edict mutation +helper can encode typed input and pack a canonical intent for submission through +a caller-owned Echo ingress; producing those bytes does not confer installation +or execution authority. + +The following sequence is the existing Wesley bootstrap fixture: ```mermaid sequenceDiagram @@ -132,13 +156,15 @@ sequenceDiagram Echo-->>Fixture: OpticReading or typed obstruction ``` -The package-shaped flow below is the target corridor, not a current end-to-end application path: +The package-shaped flow below is partially implemented, but is not yet a +current end-to-end application path: ```text -authored Wesley or Edict source --> verified compiler IR --> generated handlers, observers, footprints, and package metadata --> InstalledContractPackage verification +admitted Wesley or Edict source +-> verified mutation Target IR or lawful read semantics +-> generated handlers, observers, codecs, footprints, and package metadata +-> opaque provider proposal with explicit host binding +-> trusted-host InstalledContractPackage admission and registration -> scheduler-owned execution -> receipts and readings bound to package identity ``` @@ -148,19 +174,25 @@ authored Wesley or Edict source Echo core is intentionally generic. Application nouns belong in authored contracts and generated adapters, not in the runtime kernel. -- Wesley contract fixtures define nouns, operations, and queries in GraphQL and - use directives such as `@wes_op` and `@wes_footprint` for operation and - footprint claims. -- The current generator emits Rust rule builders and helper code, not a - verified installable package or a supported external application SDK. +- Wesley compatibility fixtures define nouns, operations, and queries in + GraphQL and use directives such as `@wes_op` and `@wes_footprint` for + operation and footprint claims. +- Edict semantic sources define admitted operations, capabilities, lawpacks, + target profiles, and schemas. The Echo provider path deterministically + lowers and verifies that meaning into a digest-locked publication package + and generated helper projection. +- The current Wesley compatibility generator emits Rust rule builders and + helper code, not a verified installable package or a supported external + application SDK. - Echo's package registry and scheduler path is implemented independently of - that fixture generator. -- A package-qualified compiler must join those halves through the generic - package boundary; it must not create a second execution engine. + both compiler publication paths. +- A trusted Echo host must verify compiler-emitted material, bind the + host-supplied executor or observer, and install it through the generic + package boundary. A compiler must not create a second execution engine. See [Generated Rule Authorship](docs/topics/GeneratedRules.md) for the exact -current/target boundary, including the fixture-only Edict bridge and the absent -release footprint-qualification lane. +current/target boundary, including the separate Wesley compatibility and Edict +provider paths and the absent release footprint-qualification lane. ```graphql type Mutation { @@ -191,8 +223,8 @@ Echo enforces determinism by narrowing every application action into explicit, canonical evidence before the scheduler can act on it: - application input enters as canonical EINT bytes, not ad hoc callbacks; -- Wesley-generated contract metadata names operation ids, codecs, and - footprint claims; +- admitted compiler-generated metadata names operation ids, exact codecs where + declared, and footprint or requirements claims; - Echo-owned admission decides whether submitted work can become scheduler work; - the scheduler drains eligible work in deterministic order under explicit @@ -224,7 +256,8 @@ You own: - domain semantics; - product policy and UI; -- authored GraphQL contracts; +- authored application contracts, including Edict semantic sources and + supported Wesley GraphQL contracts; - generated contract helpers and host integrations. ## FAQ @@ -400,11 +433,11 @@ for coincidental text. ### How Does Schema Evolution Work? -Contract identity, schema identity, operation ids, and generated helper -identity are explicit in Wesley output. A schema change produces new contract -artifact identity. Old readings name the schema and operation identity they -used, so multiple contract versions can coexist without silently invalidating -old receipts. +Contract identity, schema identity, operation ids, and generated helper or +package identity are explicit in admitted compiler output. A schema change +produces new contract artifact identity. Old readings name the schema and +operation identity they used, so multiple contract versions can coexist +without silently invalidating old receipts. ### Where Should Contributors Start? @@ -412,7 +445,8 @@ Start with `warp-core` and read [There Is No Graph](docs/architecture/there-is-no-graph.md) before changing runtime boundaries. Echo core must not grow application nouns such as `increment_counter`, `save_buffer`, or product-specific APIs. Those belong in -authored Wesley contracts and generated adapters above the runtime boundary. +authored application contracts and generated adapters above the runtime +boundary. ## Quick Start For Contributors diff --git a/crates/echo-edict-canonical/src/lib.rs b/crates/echo-edict-canonical/src/lib.rs index 53815795..891f42b8 100644 --- a/crates/echo-edict-canonical/src/lib.rs +++ b/crates/echo-edict-canonical/src/lib.rs @@ -187,6 +187,25 @@ pub fn digest_canonical_value_v1( domain: &str, value: &CanonicalValueV1, ) -> Result { + Ok(format!( + "sha256:{}", + hex::encode(digest_canonical_value_bytes_v1(domain, value)?) + )) +} + +/// Computes the typed bytes of an Edict v1 domain-framed SHA-256 digest. +/// +/// This is the wire-form counterpart of [`digest_canonical_value_v1`]. The +/// preimage and authority boundary are identical; only the returned rendering +/// differs. +/// +/// # Errors +/// +/// Returns a stable failure for an empty domain or an unencodable value. +pub fn digest_canonical_value_bytes_v1( + domain: &str, + value: &CanonicalValueV1, +) -> Result<[u8; 32], CanonicalValueError> { if domain.is_empty() { return Err(CanonicalValueError::new( CanonicalValueErrorKind::UnsupportedValue, @@ -203,7 +222,7 @@ pub fn digest_canonical_value_v1( ))?); preimage.extend(encode_canonical_cbor_v1(value)?); - Ok(format!("sha256:{}", hex::encode(Sha256::digest(preimage)))) + Ok(Sha256::digest(preimage).into()) } fn encode_value( diff --git a/crates/echo-edict-canonical/tests/canonical_contract.rs b/crates/echo-edict-canonical/tests/canonical_contract.rs index a3ff2cbb..80c4d252 100644 --- a/crates/echo-edict-canonical/tests/canonical_contract.rs +++ b/crates/echo-edict-canonical/tests/canonical_contract.rs @@ -4,9 +4,9 @@ //! 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_DECODE_NODES_V1, - MAX_CANONICAL_NESTING_DEPTH_V1, + decode_canonical_cbor_v1, digest_canonical_value_bytes_v1, digest_canonical_value_v1, + encode_canonical_cbor_v1, CanonicalValueErrorKind, CanonicalValueV1, + MAX_CANONICAL_DECODE_NODES_V1, MAX_CANONICAL_NESTING_DEPTH_V1, }; fn text(value: &str) -> CanonicalValueV1 { @@ -68,6 +68,52 @@ fn nested_empty_map_bytes(container_count: usize) -> Vec { bytes } +#[test] +fn typed_digest_bytes_and_review_rendering_are_the_same_proposition() { + let ordinary = string_map([ + ("apiVersion", text("fixture/v1")), + ("payload", CanonicalValueV1::Bytes(vec![0, 1, 2, 255])), + ]); + let maximum_depth = nested_empty_array(MAX_CANONICAL_NESTING_DEPTH_V1); + + for value in [&ordinary, &maximum_depth] { + let bytes = digest_canonical_value_bytes_v1("fixture.digest/v1", value) + .expect("typed digest bytes are computed"); + let review = digest_canonical_value_v1("fixture.digest/v1", value) + .expect("digest review rendering is computed"); + assert_eq!(review, format!("sha256:{}", hex::encode(bytes))); + } + + for value in [&ordinary, &maximum_depth] { + assert_eq!( + digest_canonical_value_bytes_v1("", value) + .expect_err("empty byte-digest domain refuses") + .kind(), + CanonicalValueErrorKind::UnsupportedValue + ); + assert_eq!( + digest_canonical_value_v1("", value) + .expect_err("empty review-digest domain refuses") + .kind(), + CanonicalValueErrorKind::UnsupportedValue + ); + } + + let too_deep = nested_empty_array(MAX_CANONICAL_NESTING_DEPTH_V1 + 1); + assert_eq!( + digest_canonical_value_bytes_v1("fixture.digest/v1", &too_deep) + .expect_err("unencodable byte digest refuses") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + assert_eq!( + digest_canonical_value_v1("fixture.digest/v1", &too_deep) + .expect_err("unencodable review digest refuses") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); +} + #[test] fn canonical_maps_are_order_independent_and_fail_closed() { let forward = string_map([("z", text("last")), ("a", text("first"))]); diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index e759f4e6..f9a52a1d 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -27,7 +27,8 @@ 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 or definitions, evaluation budgets, unsupported Core ABI, target -profiles, semantics, and output roles produce typed provider refusals. The +profiles, semantics, and undeclared, mismatched, duplicate, or unsorted +output-role claims 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 @@ -38,6 +39,75 @@ constraint, constructor, or call semantics require explicit lowering laws. Reads unsupported and fail closed; the lowerer never represents a read as a synthetic mutation. +The native model accepts exact sorted subsets of three output declarations: +`generated.echo-dpo` / `GeneratedArtifact` / `echo.generated-artifact/v1`, +`review.echo-dpo` / `ReviewPayload` / `echo.review-payload/v1`, and +`target-ir.echo-dpo` / `TargetIr` / `edict.target-ir.artifact/v1`. The generated +and review envelopes use `generated/echo_dpo.rs` and `review/echo_dpo.json`. +The generated envelope binds profile `echo.dpo.registration/v1`, operation +`a.b@1.t`, and the exact Rust source; the permanently non-authoritative review +names the exact generated artifact as its subject. `echo.span-ir/v1` is the +semantic Target IR coordinate, while `edict.target-ir.artifact/v1` is the +artifact output and digest-framing domain. Likewise, `echo.dpo.bundle/v1` is a +target-bundle profile, not an Edict contract-bundle occurrence. + +Final `edict.bundle.semantic/v1` and `edict.bundle.release/v1` identities are +supplied only after assembly. Generated `bind_contract_bundle` checks their +typed SHA-256 form and domains, expected-versus-actual bundle digests, and the +operation coordinate, Target IR, Echo ABI, helper API, +provider/input/output/effect-failure/obstruction schemas, +target/generated/operation profiles, and abstract footprint obligation and +algebra identities. Every framed identity is compared as a complete +coordinate/domain/digest proposition. The footprint binding does not invent a concrete static +read/write set. `bind_contract_bundle` remains a pure equality and consistency +preflight: it does not authenticate the expected pin, admit or install a +package, or confer registration or runtime authority. This behavior currently +belongs to the native model. The canonical generated profile/package now carry +the Echo-owned `echo.semantic-operation-id.fnv1-32/v1` law and exact persisted +id for `a.b@1.t`; generation refuses both Echo protocol-reserved ids +(`u32::MAX` scheduler control and `u32::MAX - 1` witnessed suffix import) and +package-local collisions without salting or probing. The generated CDDL bounds +the numeric application-id domain, while semantic generation independently +recomputes the law and checks collision freedom; schema admission alone proves +neither proposition. Emitted source carries public expected constants for that +exact law and id without re-deriving them, requires both as untrusted bundle +claims, and refuses a disagreement before its private-state registration +descriptor exposes the matched id claim. + +The generated-artifact profile now owns the exact `le-binary-v1` value-codec +claim, and the generated source implements that claim with distinct `Id`, +`Input`, and `Output` types rather than treating equal wire shapes as one +semantic type. The `Id` codec preserves raw UTF-8, enforces the authored +16-Unicode-scalar bound, and performs no normalization. Generated decoders fail +closed on malformed UTF-8, over-bound lengths or values, truncation, and +trailing bytes. Descriptor methods encode and decode the exact input/output +types and `pack_intent(...)` places the encoded input into Echo's canonical EINT +v1 envelope under the matched operation id. The EINT `vars` field is opaque to +Echo; its bytes are owned by the selected profile codec, not universally by +canonical CBOR. + +After bundle binding, the descriptor can also expose a borrowed, +provider-generic `ProviderRegistryV1` and can preflight one explicit host +mutation implementation into an opaque `ProviderContractPackageProposalV1`. +That crossing compares the complete Target IR, bundle, target/generated/ +operation profiles, provider and value schemas, codec, obstruction, operation +id, ABI, helper API, and footprint identities. It also binds the generated +matcher and canonical rule name. Equality of those identities detects +cross-binding; it does not prove that arbitrary host callbacks semantically +implement the operation. The proposal neither installs nor registers itself: +only a trusted Echo host may perform the later installation crossing. This +proposal constructor supports mutations and refuses a `Query`; authored reads +remain a separate bounded observer/optic path and must never be represented as +synthetic mutations. + +The refreshed 189,668-byte checked lowerer component has SHA-256 +`9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af` and has +crossed the reproducible promotion boundary. The pinned Edict host admits its +generated envelope under the owning `generated-artifact` CDDL root, and the +isolated host fixture exercises the exact helper binding, typed codecs, EINT +packing, borrowed registry, and non-installing proposal contract. Host-side +admission of the review envelope remains an independent crossing. + 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 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 index 807c2186..ef4787da 100644 --- a/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor +++ b/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-dpo.cbor @@ -1 +1 @@ -fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file +fsourcedkindmtargetProfilefdigestfsha256X ?K=,i? s&6LqAjcoordinatejecho.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 index a3a7b681..bba8cf44 100644 Binary files a/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-lawpack.cbor and b/crates/echo-edict-provider-lowerer/resources/authority-facts.echo-lawpack.cbor differ diff --git a/crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor b/crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor index 354071e7..af0722fb 100644 Binary files a/crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor and b/crates/echo-edict-provider-lowerer/resources/lawpack.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor b/crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor index b3ca7861..28493267 100644 Binary files a/crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor and b/crates/echo-edict-provider-lowerer/resources/target-profile.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index b8cf799d..b64699c2 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -8,7 +8,8 @@ #![deny(unsafe_code)] use echo_edict_canonical::{ - decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueV1, + decode_canonical_cbor_v1, digest_canonical_value_bytes_v1, digest_canonical_value_v1, + encode_canonical_cbor_v1, CanonicalValueV1, }; #[cfg(target_arch = "wasm32")] @@ -28,9 +29,25 @@ 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_ARTIFACT_DOMAIN: &str = "edict.target-ir.artifact/v1"; +const TARGET_IR_COORDINATE: &str = "echo.span-ir/v1"; +const GENERATED_ARTIFACT_DOMAIN: &str = "echo.generated-artifact/v1"; +const REVIEW_PAYLOAD_DOMAIN: &str = "echo.review-payload/v1"; +const GENERATED_ARTIFACT_ROLE: &str = "generated.echo-dpo"; +const REVIEW_PAYLOAD_ROLE: &str = "review.echo-dpo"; const TARGET_IR_ROLE: &str = "target-ir.echo-dpo"; +const GENERATED_ARTIFACT_PROFILE: &str = "echo.dpo.registration/v1"; +const GENERATED_ARTIFACT_PROFILE_DIGEST: &str = + "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d"; +const TARGET_BUNDLE_PROFILE: &str = "echo.dpo.bundle/v1"; +const TARGET_BUNDLE_PROFILE_DIGEST: &str = + "sha256:aa0438bcc6ef14ee6cb6d4976622f6080381d731459dcb7b9102595c9bed92c0"; +const SEMANTIC_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.semantic/v1"; +const RELEASE_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.release/v1"; +const GENERATED_SOURCE_MEDIA_TYPE: &str = "text/rust; charset=utf-8"; +const REVIEW_MEDIA_TYPE: &str = "application/json"; +const GENERATED_SOURCE_PATH: &str = "generated/echo_dpo.rs"; +const REVIEW_PATH: &str = "review/echo_dpo.json"; 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"; @@ -253,9 +270,11 @@ 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. +/// The function may embed deterministic, non-authoritative byte identities in +/// generated and review projections. The Edict host remains responsible for +/// validating every returned artifact against its owning schema, recomputing +/// host-authored output identities, and admitting any later bundle occurrence. +/// No output grants Echo runtime authority. /// /// # Errors /// @@ -281,27 +300,11 @@ pub fn lower(request: LoweringRequestV1) -> LoweringResultV1 { 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, - }] - }; + let outputs = build_requested_outputs( + &request.requested_outputs, + &target_ir, + &request.target_profile.reference.digest, + )?; // Response limits are deliberately host-owned. Canonical provider output is // invariant under limit changes and the host decides whether it fits. @@ -498,32 +501,1050 @@ fn canonical_sorted_map<'a>( } 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 => + let mut previous_role: Option<&str> = None; + for request in requests { + if previous_role.is_some_and(|previous| previous.as_bytes() >= request.role.as_bytes()) + || !is_declared_output(request) { - 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( + return Err(refusal( ProviderRefusalKind::UnsupportedOutputRole, - &unsupported.role, + &request.role, "echo.provider.unsupported-output-role", - "the first lowerer serves only target-ir.echo-dpo", - )) + "the lowerer serves only the exact sorted declared output roles", + )); } + previous_role = Some(&request.role); + } + Ok(()) +} + +fn is_declared_output(request: &LoweringOutputRequest) -> bool { + matches!( + (request.role.as_str(), request.kind, request.domain.as_str()), + ( + GENERATED_ARTIFACT_ROLE, + LoweringOutputKind::GeneratedArtifact, + GENERATED_ARTIFACT_DOMAIN + ) | ( + REVIEW_PAYLOAD_ROLE, + LoweringOutputKind::ReviewPayload, + REVIEW_PAYLOAD_DOMAIN + ) | ( + TARGET_IR_ROLE, + LoweringOutputKind::TargetIr, + TARGET_IR_ARTIFACT_DOMAIN + ) + ) +} + +fn build_requested_outputs( + requests: &[LoweringOutputRequest], + target_ir: &CanonicalValueV1, + target_profile_digest: &Digest, +) -> Result, ProviderRefusalV1> { + if requests.is_empty() { + return Ok(Vec::new()); + } + + let target_ir_bytes = encode_canonical_cbor_v1(target_ir).map_err(|_| { + invalid_output_artifact( + TARGET_IR_ROLE, + "echo.provider.target-ir-encoding", + "the lowered Target IR could not be canonically encoded", + ) + })?; + let target_ir_digest_bytes = + digest_canonical_value_bytes_v1(TARGET_IR_ARTIFACT_DOMAIN, target_ir).map_err(|_| { + invalid_output_artifact( + TARGET_IR_ROLE, + "echo.provider.target-ir-digest", + "the lowered Target IR identity could not be computed", + ) + })?; + let target_ir_digest = Digest { + algorithm: DigestAlgorithm::Sha256, + bytes: target_ir_digest_bytes.to_vec(), + }; + let generated = requests + .iter() + .any(|request| request.kind != LoweringOutputKind::TargetIr) + .then(|| render_generated_artifact(&target_ir_digest, target_profile_digest)) + .transpose()?; + + requests + .iter() + .map(|request| match request.kind { + LoweringOutputKind::TargetIr => Ok(LoweringOutputArtifact { + role: request.role.clone(), + kind: request.kind, + artifact: Artifact { + domain: request.domain.clone(), + bytes: target_ir_bytes.clone(), + }, + logical_path: None, + }), + LoweringOutputKind::GeneratedArtifact => { + let generated = generated.as_ref().ok_or_else(|| { + invalid_output_artifact( + GENERATED_ARTIFACT_ROLE, + "echo.provider.generated-artifact-absent", + "the generated artifact was not rendered", + ) + })?; + Ok(LoweringOutputArtifact { + role: request.role.clone(), + kind: request.kind, + artifact: Artifact { + domain: request.domain.clone(), + bytes: generated.bytes.clone(), + }, + logical_path: Some(GENERATED_SOURCE_PATH.to_owned()), + }) + } + LoweringOutputKind::ReviewPayload => { + let generated = generated.as_ref().ok_or_else(|| { + invalid_output_artifact( + REVIEW_PAYLOAD_ROLE, + "echo.provider.review-subject-absent", + "the generated artifact review subject was not rendered", + ) + })?; + let review = render_review_payload( + &target_ir_digest, + target_profile_digest, + &generated.digest, + ); + let envelope = review_payload_envelope(&generated.digest, review.into_bytes())?; + Ok(LoweringOutputArtifact { + role: request.role.clone(), + kind: request.kind, + artifact: Artifact { + domain: request.domain.clone(), + bytes: encode_output_envelope(request.role.as_str(), &envelope)?, + }, + logical_path: Some(REVIEW_PATH.to_owned()), + }) + } + }) + .collect() +} + +struct RenderedGeneratedArtifact { + bytes: Vec, + digest: Digest, +} + +fn render_generated_artifact( + target_ir_digest: &Digest, + target_profile_digest: &Digest, +) -> Result { + let source = render_generated_source(target_ir_digest, target_profile_digest); + let envelope = generated_artifact_envelope(source.into_bytes())?; + let bytes = encode_output_envelope(GENERATED_ARTIFACT_ROLE, &envelope)?; + let digest = + digest_canonical_value_bytes_v1(GENERATED_ARTIFACT_DOMAIN, &envelope).map_err(|_| { + invalid_output_artifact( + GENERATED_ARTIFACT_ROLE, + "echo.provider.generated-artifact-digest", + "the generated-artifact identity could not be computed", + ) + })?; + Ok(RenderedGeneratedArtifact { + bytes, + digest: Digest { + algorithm: DigestAlgorithm::Sha256, + bytes: digest.to_vec(), + }, + }) +} + +fn generated_artifact_envelope( + source_bytes: Vec, +) -> Result { + let profile_digest = + decode_sha256_review(GENERATED_ARTIFACT_PROFILE_DIGEST).ok_or_else(|| { + invalid_output_artifact( + GENERATED_ARTIFACT_ROLE, + "echo.provider.generated-profile-digest", + "the checked generated-artifact profile digest is invalid", + ) + })?; + canonical_sorted_map([ + ("apiVersion", canonical_text(GENERATED_ARTIFACT_DOMAIN)), + ( + "profile", + resource_ref_value(GENERATED_ARTIFACT_PROFILE, profile_digest)?, + ), + ("operation", canonical_text(OPERATION_COORDINATE)), + ("mediaType", canonical_text(GENERATED_SOURCE_MEDIA_TYPE)), + ("bytes", CanonicalValueV1::Bytes(source_bytes)), + ]) + .map_err(|()| { + invalid_output_artifact( + GENERATED_ARTIFACT_ROLE, + "echo.provider.generated-envelope", + "the generated-artifact envelope could not be constructed", + ) + }) +} + +fn review_payload_envelope( + generated_artifact_digest: &Digest, + review_bytes: Vec, +) -> Result { + canonical_sorted_map([ + ("apiVersion", canonical_text(REVIEW_PAYLOAD_DOMAIN)), + ("authoritative", CanonicalValueV1::Bool(false)), + ( + "subject", + resource_ref_value( + GENERATED_ARTIFACT_ROLE, + generated_artifact_digest.bytes.clone(), + )?, + ), + ("mediaType", canonical_text(REVIEW_MEDIA_TYPE)), + ("bytes", CanonicalValueV1::Bytes(review_bytes)), + ]) + .map_err(|()| { + invalid_output_artifact( + REVIEW_PAYLOAD_ROLE, + "echo.provider.review-envelope", + "the review-payload envelope could not be constructed", + ) + }) +} + +fn resource_ref_value( + coordinate: &str, + digest_bytes: Vec, +) -> Result { + canonical_sorted_map([ + ("id", canonical_text(coordinate)), + ( + "digest", + CanonicalValueV1::Array(vec![ + canonical_text("sha256"), + CanonicalValueV1::Bytes(digest_bytes), + ]), + ), + ]) + .map_err(|()| { + invalid_output_artifact( + coordinate, + "echo.provider.resource-reference", + "the output resource reference could not be constructed", + ) + }) +} + +fn encode_output_envelope( + role: &str, + envelope: &CanonicalValueV1, +) -> Result, ProviderRefusalV1> { + encode_canonical_cbor_v1(envelope).map_err(|_| { + invalid_output_artifact( + role, + "echo.provider.output-envelope-encoding", + "the output envelope could not be canonically encoded", + ) + }) +} + +fn render_generated_source(target_ir_digest: &Digest, target_profile_digest: &Digest) -> String { + const TEMPLATE: &str = r#"// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Generated Echo helper projection for one admitted Edict operation. +//! Final Edict contract-bundle identity is bound explicitly after assembly. + +/// Namespaced generated contract and invocation surface for `a.b@1.t`. +pub mod echo_dpo { + use echo_registry_api::{ + OpKind, ProviderBundleIdentityV1, ProviderDigestIdentityV1, + ProviderFootprintIdentityV1, ProviderOperationV1, ProviderRegistryV1, + ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, ProviderValueContractV1, + }; + use echo_wasm_abi::codec::{ + decode_from_bytes, encode_to_vec, CodecError, Decode, Encode, Reader, Writer, + }; + use echo_wasm_abi::{pack_intent_v1, EnvelopeError}; + use warp_core::{ + matches_eint_op, propose_provider_contract_package_v1, ContractPackageIdentity, + GeneratedProviderMutationDispatchV1, GraphView, NodeId, + ProviderContractPackageProposalV1, ProviderMutationHooksV1, + ProviderMutationImplementationIdentityV1, ProviderPackageProposalError, + }; + + /// Exact Edict-authored semantic operation coordinate. + pub const OPERATION_COORDINATE: &str = "a.b@1.t"; + /// Semantic domain owning the operation coordinate. + pub const OPERATION_DOMAIN: &str = "echo.edict-provider/operation/v1"; + /// Echo-owned law that derives the persisted operation id. + pub const OPERATION_ID_LAW: &str = "echo.semantic-operation-id.fnv1-32/v1"; + /// Exact persisted operation id carried by the generated-artifact profile. + pub const OPERATION_ID: u32 = 3_389_142_194; + /// Exact Echo value codec carried by the generated-artifact profile. + pub const VALUE_CODEC_ID: &str = "le-binary-v1"; + /// Exact input schema coordinate owned by the generated-artifact profile. + pub const INPUT_SCHEMA: &str = "a.b@1.Input"; + /// Exact output schema coordinate owned by the generated-artifact profile. + pub const OUTPUT_SCHEMA: &str = "a.b@1.Output"; + /// Shared semantic domain owning the exact operation type schemas. + pub const TYPE_SCHEMA_DOMAIN: &str = "echo.edict-provider/value/v1"; + /// Exact typed obstruction coordinate for the reviewed failure mapping. + pub const OBSTRUCTION_COORDINATE: &str = "domain.WriteRejected"; + /// Semantic domain owning the typed obstruction coordinate. + pub const OBSTRUCTION_DOMAIN: &str = "echo.edict-provider/obstruction/v1"; + /// Exact target failure payload schema before obstruction mapping. + pub const EFFECT_FAILURE_SCHEMA: &str = "target.replace.rejected"; + /// Exact domain obstruction payload schema after obstruction mapping. + pub const OBSTRUCTION_PAYLOAD_SCHEMA: &str = "domain.WriteRejected.Payload"; + /// Semantic coordinate carried by the emitted Target IR artifact. + pub const TARGET_IR_COORDINATE: &str = "echo.span-ir/v1"; + /// Digest-framing domain for the complete Target IR artifact envelope. + pub const TARGET_IR_DIGEST_DOMAIN: &str = "edict.target-ir.artifact/v1"; + /// Exact domain-framed identity of the emitted Target IR artifact. + pub const TARGET_IR_DIGEST: &str = "__TARGET_IR_DIGEST__"; + /// Exact target-profile coordinate. + pub const TARGET_PROFILE_COORDINATE: &str = "echo.dpo@1"; + /// Digest-framing domain for the target-profile artifact. + pub const TARGET_PROFILE_DIGEST_DOMAIN: &str = "edict.target-profile/v1"; + /// Exact domain-framed identity of the target profile. + pub const TARGET_PROFILE_DIGEST: &str = "__TARGET_PROFILE_DIGEST__"; + /// Semantic profile for Echo contract bundles; not a bundle occurrence. + pub const TARGET_BUNDLE_PROFILE_COORDINATE: &str = "echo.dpo.bundle/v1"; + /// Digest-framing domain for the target-bundle profile artifact. + pub const TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN: &str = "echo.dpo.bundle/v1"; + /// Exact domain-framed identity of the target-bundle profile. + pub const TARGET_BUNDLE_PROFILE_DIGEST: &str = "__TARGET_BUNDLE_PROFILE_DIGEST__"; + /// Echo contract ABI targeted by this generated helper. + pub const ECHO_CONTRACT_ABI_VERSION: u32 = 1; + /// Contract-host helper API targeted by this generated helper. + pub const CONTRACT_HOST_HELPER_API_VERSION: u32 = 1; + /// Exact self-contained provider CDDL coordinate. + pub const PROVIDER_SCHEMA_COORDINATE: &str = "echo.provider-artifacts.cddl@1"; + /// Raw SHA-256 of the exact self-contained provider CDDL bytes. + pub const PROVIDER_SCHEMA_SHA256_HEX: &str = + "e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"; + /// Exact generated-artifact profile coordinate owning operation schemas. + pub const GENERATED_ARTIFACT_PROFILE: &str = "echo.dpo.registration/v1"; + /// Digest-framing domain for the generated-artifact profile. + pub const GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN: &str = + "echo.generated-artifact-profile/v1"; + /// Domain-framed identity of the generated-artifact profile. + pub const GENERATED_ARTIFACT_PROFILE_DIGEST: &str = + "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d"; + /// Exact semantic operation profile selected by the authored operation. + pub const OPERATION_PROFILE: &str = "continuum.profile.write/v1"; + /// Semantic domain owning the selected operation profile. + pub const OPERATION_PROFILE_DOMAIN: &str = + "echo.edict-provider/operation-profile/v1"; + /// Exact operation-profiles document coordinate. + pub const OPERATION_PROFILES_COORDINATE: &str = "echo.dpo.operation-profiles/v1"; + /// Digest-framing domain for the operation-profiles document. + pub const OPERATION_PROFILES_DIGEST_DOMAIN: &str = "echo.dpo.operation-profiles/v1"; + /// Domain-framed identity of the operation-profiles document. + pub const OPERATION_PROFILES_DIGEST: &str = + "sha256:53256c51f6c817a77cc8694458bf9d3891abd15b9c94f79ca97d920d3c5f0416"; + /// Abstract footprint obligation carried across lowering. + pub const FOOTPRINT_OBLIGATION: &str = "target.replace.footprint"; + /// Exact target footprint-algebra coordinate. + pub const FOOTPRINT_ALGEBRA: &str = "echo.dpo.footprint/v1"; + /// Digest-framing domain for the target footprint algebra. + pub const FOOTPRINT_ALGEBRA_DIGEST_DOMAIN: &str = "echo.dpo.footprint/v1"; + /// Domain-framed identity of the target footprint algebra. + pub const FOOTPRINT_ALGEBRA_DIGEST: &str = + "sha256:f47bb65867e78099ddcfd6ae7af83870df8823f974a496a111ed94e5d785c769"; + /// Edict domain for the semantic contract-bundle digest proposition. + pub const SEMANTIC_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.semantic/v1"; + /// Edict domain for the release contract-bundle digest proposition. + pub const RELEASE_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.release/v1"; + + const MUTATION_RULE_NAME: &str = concat!( + "cmd/contract/", + "e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c", + "/3389142194/a.b@1.t" + ); + const PROVIDER_OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { + coordinate: OPERATION_COORDINATE, + semantic_domain: OPERATION_DOMAIN, + kind: OpKind::Mutation, + operation_id_law: OPERATION_ID_LAW, + operation_id: OPERATION_ID, + input: ProviderValueContractV1 { + schema_coordinate: INPUT_SCHEMA, + schema_domain: TYPE_SCHEMA_DOMAIN, + codec_id: VALUE_CODEC_ID, + }, + output: ProviderValueContractV1 { + schema_coordinate: OUTPUT_SCHEMA, + schema_domain: TYPE_SCHEMA_DOMAIN, + codec_id: VALUE_CODEC_ID, + }, + target_failure_schema: EFFECT_FAILURE_SCHEMA, + obstruction: ProviderSemanticIdentityV1 { + coordinate: OBSTRUCTION_COORDINATE, + semantic_domain: OBSTRUCTION_DOMAIN, + }, + obstruction_payload_schema: OBSTRUCTION_PAYLOAD_SCHEMA, + target_ir: ProviderDigestIdentityV1 { + coordinate: TARGET_IR_COORDINATE, + digest_domain: TARGET_IR_DIGEST_DOMAIN, + digest: TARGET_IR_DIGEST, + }, + target_profile: ProviderDigestIdentityV1 { + coordinate: TARGET_PROFILE_COORDINATE, + digest_domain: TARGET_PROFILE_DIGEST_DOMAIN, + digest: TARGET_PROFILE_DIGEST, + }, + generated_artifact_profile: ProviderDigestIdentityV1 { + coordinate: GENERATED_ARTIFACT_PROFILE, + digest_domain: GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN, + digest: GENERATED_ARTIFACT_PROFILE_DIGEST, + }, + operation_profile: ProviderSemanticIdentityV1 { + coordinate: OPERATION_PROFILE, + semantic_domain: OPERATION_PROFILE_DOMAIN, + }, + operation_profiles: ProviderDigestIdentityV1 { + coordinate: OPERATION_PROFILES_COORDINATE, + digest_domain: OPERATION_PROFILES_DIGEST_DOMAIN, + digest: OPERATION_PROFILES_DIGEST, + }, + footprint: ProviderFootprintIdentityV1 { + obligation: FOOTPRINT_OBLIGATION, + algebra_coordinate: FOOTPRINT_ALGEBRA, + algebra_digest_domain: FOOTPRINT_ALGEBRA_DIGEST_DOMAIN, + algebra_digest: FOOTPRINT_ALGEBRA_DIGEST, + }, + }]; + + const ID_MAX_SCALAR_VALUES: usize = 16; + const ID_MAX_UTF8_BYTES: usize = ID_MAX_SCALAR_VALUES * 4; + + /// Exact bounded value of semantic type `a.b@1.Id`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Id(String); + + impl Id { + /// Construct an id after enforcing the authored Unicode-scalar bound. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `value` contains more + /// than sixteen Unicode scalar values. + pub fn new(value: impl Into) -> Result { + let value = value.into(); + if value.chars().count() > ID_MAX_SCALAR_VALUES { + return Err(CodecError::StringTooLong); + } + Ok(Self(value)) + } + + /// Borrow the exact raw UTF-8 value without normalization. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume this value and return its exact raw UTF-8 string. + pub fn into_string(self) -> String { + self.0 + } + } + + impl Encode for Id { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + if self.0.chars().count() > ID_MAX_SCALAR_VALUES { + return Err(CodecError::StringTooLong); + } + writer.write_len_prefixed_bytes(self.0.as_bytes()) + } + } + + impl Decode for Id { + fn decode(reader: &mut Reader<'_>) -> Result { + let bytes = reader.read_len_prefixed_bytes(ID_MAX_UTF8_BYTES)?; + let value = core::str::from_utf8(bytes).map_err(|_| CodecError::InvalidUtf8)?; + Self::new(String::from(value)) + } + } + + /// Exact typed input for semantic operation `a.b@1.t`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Input { + id: Id, + } + + impl Input { + /// Construct a validated operation input. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `id` exceeds its + /// authored Unicode-scalar bound. + pub fn new(id: impl Into) -> Result { + Ok(Self { id: Id::new(id)? }) + } + + /// Borrow the exact raw UTF-8 id. + pub fn id(&self) -> &str { + self.id.as_str() + } + } + + impl Encode for Input { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + self.id.encode(writer) + } + } + + impl Decode for Input { + fn decode(reader: &mut Reader<'_>) -> Result { + Ok(Self { + id: Id::decode(reader)?, + }) + } + } + + /// Exact typed output for semantic operation `a.b@1.t`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Output { + id: Id, } + + impl Output { + /// Construct a validated operation output. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `id` exceeds its + /// authored Unicode-scalar bound. + pub fn new(id: impl Into) -> Result { + Ok(Self { id: Id::new(id)? }) + } + + /// Borrow the exact raw UTF-8 id. + pub fn id(&self) -> &str { + self.id.as_str() + } + } + + impl Encode for Output { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + self.id.encode(writer) + } + } + + impl Decode for Output { + fn decode(reader: &mut Reader<'_>) -> Result { + Ok(Self { + id: Id::decode(reader)?, + }) + } + } + + /// Stable failure produced while constructing one canonical invocation. + #[derive(Debug, Eq, PartialEq)] + pub enum GeneratedInvocationError { + /// The typed input violates its generated codec contract. + Codec(CodecError), + /// The canonical Echo intent envelope could not be constructed. + Envelope(EnvelopeError), + } + + /// Independent host pin for the final assembled bundle identity. + /// + /// This value is explicit expected evidence, not an admission token. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ExpectedContractBundleIdentityV1<'a> { + /// Digest proposition domain for `semantic_digest`. + pub semantic_digest_domain: &'a str, + /// Exact semantic-layer bundle digest expected by the host. + pub semantic_digest: &'a str, + /// Digest proposition domain for `release_digest`. + pub release_digest_domain: &'a str, + /// Exact release-layer bundle digest expected by the host. + pub release_digest: &'a str, + } + + /// Untrusted identity and semantic claims read from one assembled bundle. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ContractBundleIdentityV1<'a> { + /// Digest proposition domain for `semantic_digest`. + pub semantic_digest_domain: &'a str, + /// Claimed semantic-layer bundle digest computed by Edict. + pub semantic_digest: &'a str, + /// Digest proposition domain for `release_digest`. + pub release_digest_domain: &'a str, + /// Claimed release-layer bundle digest computed by Edict. + pub release_digest: &'a str, + /// Semantic operation coordinate carried by the bundle. + pub operation_coordinate: &'a str, + /// Semantic domain owning the operation coordinate. + pub operation_domain: &'a str, + /// Operation-id law claimed by the generated-artifact profile. + pub operation_id_law: &'a str, + /// Persisted operation id claimed by the generated-artifact profile. + pub operation_id: u32, + /// Echo value codec claimed by the generated-artifact profile. + pub value_codec: &'a str, + /// Semantic coordinate carried by the Target IR artifact. + pub target_ir_coordinate: &'a str, + /// Digest-framing domain for the Target IR artifact. + pub target_ir_digest_domain: &'a str, + /// Target IR digest carried by the bundle. + pub target_ir_digest: &'a str, + /// Target-profile coordinate carried by the bundle. + pub target_profile_coordinate: &'a str, + /// Digest-framing domain for the target-profile artifact. + pub target_profile_digest_domain: &'a str, + /// Target-profile digest carried by the bundle. + pub target_profile_digest: &'a str, + /// Target-bundle-profile coordinate carried by the bundle. + pub target_bundle_profile_coordinate: &'a str, + /// Digest-framing domain for the target-bundle-profile artifact. + pub target_bundle_profile_digest_domain: &'a str, + /// Target-bundle-profile digest carried by the bundle. + pub target_bundle_profile_digest: &'a str, + /// Echo contract ABI claimed by the generated registry. + pub echo_contract_abi_version: u32, + /// Contract-host helper API claimed by the generated registry. + pub helper_api_version: u32, + /// Provider CDDL coordinate claimed by the assembled bundle. + pub provider_schema_coordinate: &'a str, + /// Raw provider CDDL SHA-256 claimed by the assembled bundle. + pub provider_schema_sha256_hex: &'a str, + /// Input schema coordinate claimed for this operation. + pub input_schema: &'a str, + /// Output schema coordinate claimed for this operation. + pub output_schema: &'a str, + /// Semantic domain owning all claimed operation type schemas. + pub type_schema_domain: &'a str, + /// Typed obstruction coordinate claimed for the reviewed failure. + pub obstruction_coordinate: &'a str, + /// Semantic domain owning the typed obstruction coordinate. + pub obstruction_domain: &'a str, + /// Target failure payload schema claimed before obstruction mapping. + pub effect_failure_schema: &'a str, + /// Domain obstruction payload schema claimed after obstruction mapping. + pub obstruction_payload_schema: &'a str, + /// Generated-artifact profile coordinate claimed by the bundle. + pub generated_artifact_profile: &'a str, + /// Digest-framing domain for the generated-artifact profile. + pub generated_artifact_profile_digest_domain: &'a str, + /// Generated-artifact profile digest claimed by the bundle. + pub generated_artifact_profile_digest: &'a str, + /// Semantic operation profile claimed by the bundle. + pub operation_profile: &'a str, + /// Semantic domain owning the operation profile. + pub operation_profile_domain: &'a str, + /// Operation-profiles document coordinate claimed by the bundle. + pub operation_profiles_coordinate: &'a str, + /// Digest-framing domain for the operation-profiles document. + pub operation_profiles_digest_domain: &'a str, + /// Operation-profiles document digest claimed by the bundle. + pub operation_profiles_digest: &'a str, + /// Abstract footprint obligation claimed for this operation. + pub footprint_obligation: &'a str, + /// Footprint-algebra coordinate claimed by the bundle. + pub footprint_algebra: &'a str, + /// Digest-framing domain for the footprint algebra. + pub footprint_algebra_digest_domain: &'a str, + /// Footprint-algebra digest claimed by the bundle. + pub footprint_algebra_digest: &'a str, + } + + /// Stable reason an assembled bundle cannot bind this generated helper. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum BindingMismatchKind { + /// A bundle digest is framed under the wrong proposition domain. + BundleDigestDomain, + /// A semantic or release bundle digest is not a typed SHA-256 review value. + BundleDigest, + /// The assembled semantic-bundle digest differs from the host pin. + SemanticBundleDigest, + /// The assembled release-bundle digest differs from the host pin. + ReleaseBundleDigest, + /// The bundle names a different semantic operation. + Operation, + /// The bundle names a different persisted operation-id proposition. + OperationId, + /// The bundle names a different Echo value codec. + Codec, + /// The bundle names a different Target IR artifact. + TargetIr, + /// The bundle names a different target profile. + TargetProfile, + /// The bundle names a different target-bundle profile. + TargetBundleProfile, + /// The generated registry targets a different Echo contract ABI. + EchoAbi, + /// The generated registry targets a different contract-host helper API. + HelperApi, + /// The bundle names different provider or operation schema identities. + Schema, + /// The bundle names a different generated-artifact profile. + GeneratedArtifactProfile, + /// The bundle names a different semantic operation profile. + OperationProfile, + /// The bundle names a different footprint obligation or algebra. + Footprint, + } + + /// Exact generated registration binding, still without runtime authority. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct RegistrationDescriptorV1<'a> { + contract_bundle: ContractBundleIdentityV1<'a>, + } + + impl<'a> RegistrationDescriptorV1<'a> { + /// Return the exact bundle claims that matched the explicit host pin. + pub const fn contract_bundle(&self) -> &ContractBundleIdentityV1<'a> { + &self.contract_bundle + } + + /// Return the exact persisted operation id matched by this descriptor. + pub const fn operation_id(&self) -> u32 { + self.contract_bundle.operation_id + } + + /// Return the exact provider-generic registry claims retained by this + /// already matched descriptor. + /// + /// This constructs descriptive evidence only. It does not admit or + /// install a registry. + pub const fn provider_registry(&self) -> ProviderRegistryV1<'a> { + ProviderRegistryV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: ProviderSchemaIdentityV1 { + coordinate: PROVIDER_SCHEMA_COORDINATE, + raw_sha256_hex: PROVIDER_SCHEMA_SHA256_HEX, + }, + target_bundle_profile: ProviderDigestIdentityV1 { + coordinate: TARGET_BUNDLE_PROFILE_COORDINATE, + digest_domain: TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN, + digest: TARGET_BUNDLE_PROFILE_DIGEST, + }, + bundle: ProviderBundleIdentityV1 { + semantic_digest_domain: self.contract_bundle.semantic_digest_domain, + semantic_digest: self.contract_bundle.semantic_digest, + release_digest_domain: self.contract_bundle.release_digest_domain, + release_digest: self.contract_bundle.release_digest, + }, + operations: &PROVIDER_OPERATIONS, + } + } + + /// Return the exact identity a host implementation must independently + /// claim before its callbacks can be proposed for this operation. + pub const fn mutation_implementation_identity( + &self, + ) -> ProviderMutationImplementationIdentityV1<'a> { + let registry = self.provider_registry(); + ProviderMutationImplementationIdentityV1 { + echo_contract_abi_version: registry.echo_contract_abi_version, + helper_api_version: registry.helper_api_version, + provider_schema: registry.provider_schema, + target_bundle_profile: registry.target_bundle_profile, + bundle: registry.bundle, + operation: PROVIDER_OPERATIONS[0], + } + } + + /// Construct one opaque package proposal from exact matched claims and + /// an explicit host executor/footprint binding. + /// + /// The result grants no runtime admission, registration, installation, + /// scheduling, execution, durability, or receipt authority. + /// + /// # Errors + /// + /// Returns [`ProviderPackageProposalError`] when occurrence metadata, + /// generated dispatch identity, or any host implementation claim does + /// not exactly match this descriptor. + pub fn propose_contract_package<'proposal>( + &'proposal self, + occurrence: ContractPackageIdentity<'proposal>, + hooks: ProviderMutationHooksV1<'proposal>, + ) -> Result, ProviderPackageProposalError> + where + 'a: 'proposal, + { + let registry: ProviderRegistryV1<'proposal> = self.provider_registry(); + propose_provider_contract_package_v1( + occurrence, + registry, + GeneratedProviderMutationDispatchV1::new( + OPERATION_ID, + MUTATION_RULE_NAME, + matches_operation, + ), + hooks, + ) + } + + /// Encode one exact typed input under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] when the typed value violates its generated + /// bound or cannot be represented by the selected codec. + #[allow(clippy::unused_self)] + pub fn encode_input(&self, input: &Input) -> Result, CodecError> { + encode_to_vec(input) + } + + /// Decode one exact typed input under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] for malformed, over-bound, truncated, or + /// trailing bytes. + #[allow(clippy::unused_self)] + pub fn decode_input(&self, bytes: &[u8]) -> Result { + decode_from_bytes(bytes) + } + + /// Encode one exact typed output under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] when the typed value violates its generated + /// bound or cannot be represented by the selected codec. + #[allow(clippy::unused_self)] + pub fn encode_output(&self, output: &Output) -> Result, CodecError> { + encode_to_vec(output) + } + + /// Decode one exact typed output under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] for malformed, over-bound, truncated, or + /// trailing bytes. + #[allow(clippy::unused_self)] + pub fn decode_output(&self, bytes: &[u8]) -> Result { + decode_from_bytes(bytes) + } + + /// Encode a typed input and wrap it in the canonical Echo EINT v1 + /// envelope for this matched operation id. + /// + /// # Errors + /// + /// Returns [`GeneratedInvocationError::Codec`] when the input violates + /// its generated value contract, or + /// [`GeneratedInvocationError::Envelope`] when Echo refuses envelope + /// construction. + pub fn pack_intent(&self, input: &Input) -> Result, GeneratedInvocationError> { + let vars = self + .encode_input(input) + .map_err(GeneratedInvocationError::Codec)?; + pack_intent_v1(self.operation_id(), &vars) + .map_err(GeneratedInvocationError::Envelope) + } + } + + fn matches_operation(view: GraphView<'_>, scope: &NodeId) -> bool { + matches_eint_op(view, scope, OPERATION_ID) + } + + /// Compare assembled bundle claims to an independent exact host pin and to + /// this generated helper's semantic identities. + /// + /// This pure equality/consistency preflight neither authenticates the pin, + /// admits the bundle, nor installs a package. Those remain separate trusted + /// host and Echo runtime crossings. + pub fn bind_contract_bundle<'a>( + expected: ExpectedContractBundleIdentityV1<'a>, + identity: &ContractBundleIdentityV1<'a>, + ) -> Result, BindingMismatchKind> { + if expected.semantic_digest_domain != SEMANTIC_BUNDLE_DIGEST_DOMAIN + || identity.semantic_digest_domain != SEMANTIC_BUNDLE_DIGEST_DOMAIN + || expected.release_digest_domain != RELEASE_BUNDLE_DIGEST_DOMAIN + || identity.release_digest_domain != RELEASE_BUNDLE_DIGEST_DOMAIN + { + return Err(BindingMismatchKind::BundleDigestDomain); + } + if !is_sha256_review(expected.semantic_digest) + || !is_sha256_review(expected.release_digest) + || !is_sha256_review(identity.semantic_digest) + || !is_sha256_review(identity.release_digest) + { + return Err(BindingMismatchKind::BundleDigest); + } + if identity.semantic_digest != expected.semantic_digest { + return Err(BindingMismatchKind::SemanticBundleDigest); + } + if identity.release_digest != expected.release_digest { + return Err(BindingMismatchKind::ReleaseBundleDigest); + } + if identity.operation_coordinate != OPERATION_COORDINATE + || identity.operation_domain != OPERATION_DOMAIN + { + return Err(BindingMismatchKind::Operation); + } + if identity.operation_id_law != OPERATION_ID_LAW || identity.operation_id != OPERATION_ID { + return Err(BindingMismatchKind::OperationId); + } + if identity.value_codec != VALUE_CODEC_ID { + return Err(BindingMismatchKind::Codec); + } + if identity.target_ir_coordinate != TARGET_IR_COORDINATE + || identity.target_ir_digest_domain != TARGET_IR_DIGEST_DOMAIN + || identity.target_ir_digest != TARGET_IR_DIGEST + { + return Err(BindingMismatchKind::TargetIr); + } + if identity.target_profile_coordinate != TARGET_PROFILE_COORDINATE + || identity.target_profile_digest_domain != TARGET_PROFILE_DIGEST_DOMAIN + || identity.target_profile_digest != TARGET_PROFILE_DIGEST + { + return Err(BindingMismatchKind::TargetProfile); + } + if identity.target_bundle_profile_coordinate != TARGET_BUNDLE_PROFILE_COORDINATE + || identity.target_bundle_profile_digest_domain + != TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN + || identity.target_bundle_profile_digest != TARGET_BUNDLE_PROFILE_DIGEST + { + return Err(BindingMismatchKind::TargetBundleProfile); + } + if identity.echo_contract_abi_version != ECHO_CONTRACT_ABI_VERSION { + return Err(BindingMismatchKind::EchoAbi); + } + if identity.helper_api_version != CONTRACT_HOST_HELPER_API_VERSION { + return Err(BindingMismatchKind::HelperApi); + } + if identity.provider_schema_coordinate != PROVIDER_SCHEMA_COORDINATE + || identity.provider_schema_sha256_hex != PROVIDER_SCHEMA_SHA256_HEX + || identity.input_schema != INPUT_SCHEMA + || identity.output_schema != OUTPUT_SCHEMA + || identity.type_schema_domain != TYPE_SCHEMA_DOMAIN + || identity.obstruction_coordinate != OBSTRUCTION_COORDINATE + || identity.obstruction_domain != OBSTRUCTION_DOMAIN + || identity.effect_failure_schema != EFFECT_FAILURE_SCHEMA + || identity.obstruction_payload_schema != OBSTRUCTION_PAYLOAD_SCHEMA + { + return Err(BindingMismatchKind::Schema); + } + if identity.generated_artifact_profile != GENERATED_ARTIFACT_PROFILE + || identity.generated_artifact_profile_digest_domain + != GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN + || identity.generated_artifact_profile_digest + != GENERATED_ARTIFACT_PROFILE_DIGEST + { + return Err(BindingMismatchKind::GeneratedArtifactProfile); + } + if identity.operation_profile != OPERATION_PROFILE + || identity.operation_profile_domain != OPERATION_PROFILE_DOMAIN + || identity.operation_profiles_coordinate != OPERATION_PROFILES_COORDINATE + || identity.operation_profiles_digest_domain != OPERATION_PROFILES_DIGEST_DOMAIN + || identity.operation_profiles_digest != OPERATION_PROFILES_DIGEST + { + return Err(BindingMismatchKind::OperationProfile); + } + if identity.footprint_obligation != FOOTPRINT_OBLIGATION + || identity.footprint_algebra != FOOTPRINT_ALGEBRA + || identity.footprint_algebra_digest_domain != FOOTPRINT_ALGEBRA_DIGEST_DOMAIN + || identity.footprint_algebra_digest != FOOTPRINT_ALGEBRA_DIGEST + { + return Err(BindingMismatchKind::Footprint); + } + Ok(RegistrationDescriptorV1 { + contract_bundle: *identity, + }) + } + + fn is_sha256_review(value: &str) -> bool { + let Some(hex) = value.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 + && hex + .as_bytes() + .iter() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + } +} +"#; + + TEMPLATE + .replace("__TARGET_IR_DIGEST__", &digest_review(target_ir_digest)) + .replace( + "__TARGET_PROFILE_DIGEST__", + &digest_review(target_profile_digest), + ) + .replace( + "__TARGET_BUNDLE_PROFILE_DIGEST__", + TARGET_BUNDLE_PROFILE_DIGEST, + ) +} + +fn render_review_payload( + target_ir_digest: &Digest, + target_profile_digest: &Digest, + generated_artifact_digest: &Digest, +) -> String { + format!( + concat!( + "{{\"apiVersion\":\"echo.generated-helper-review/v1\",", + "\"authoritative\":false,", + "\"operation\":\"a.b@1.t\",", + "\"targetIr\":{{\"coordinate\":\"{}\",\"digestDomain\":\"{}\",", + "\"digest\":\"{}\"}},", + "\"targetProfile\":{{\"id\":\"echo.dpo@1\",\"digest\":\"{}\"}},", + "\"targetBundleProfile\":{{\"id\":\"{}\",\"digest\":\"{}\"}},", + "\"generatedArtifact\":{{\"coordinate\":\"{}\",\"digestDomain\":\"{}\",", + "\"digest\":\"{}\"}},", + "\"contractBundle\":{{\"binding\":\"explicit-after-assembly\",", + "\"semanticDigestDomain\":\"{}\",\"semanticDigest\":null,", + "\"releaseDigestDomain\":\"{}\",\"releaseDigest\":null}},", + "\"runtimeAuthority\":false}}\n" + ), + TARGET_IR_COORDINATE, + TARGET_IR_ARTIFACT_DOMAIN, + digest_review(target_ir_digest), + digest_review(target_profile_digest), + TARGET_BUNDLE_PROFILE, + TARGET_BUNDLE_PROFILE_DIGEST, + GENERATED_ARTIFACT_ROLE, + GENERATED_ARTIFACT_DOMAIN, + digest_review(generated_artifact_digest), + SEMANTIC_BUNDLE_DIGEST_DOMAIN, + RELEASE_BUNDLE_DIGEST_DOMAIN, + ) +} + +fn decode_sha256_review(value: &str) -> Option> { + let hex = value.strip_prefix("sha256:")?; + if hex.len() != 64 { + return None; + } + hex.as_bytes() + .chunks_exact(2) + .map(|pair| { + let high = hex_nibble(pair[0])?; + let low = hex_nibble(pair[1])?; + Some((high << 4) | low) + }) + .collect() +} + +const fn hex_nibble(value: u8) -> Option { + match value { + b'0'..=b'9' => Some(value - b'0'), + b'a'..=b'f' => Some(value - b'a' + 10), + _ => None, + } +} + +fn invalid_output_artifact(subject: &str, code: &str, message: &str) -> ProviderRefusalV1 { + refusal( + ProviderRefusalKind::InvalidSemanticArtifact, + subject, + code, + message, + ) } fn lower_core( @@ -589,7 +1610,7 @@ fn lower_core( Ok(canonical_map([ ("kind", canonical_text("targetIrArtifact")), - ("domain", canonical_text(INNER_TARGET_IR_DOMAIN)), + ("domain", canonical_text(TARGET_IR_COORDINATE)), ( "targetProfile", canonical_map([ diff --git a/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs b/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs new file mode 100644 index 00000000..f4abc28c --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs @@ -0,0 +1,715 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Generated Echo helper projection for one admitted Edict operation. +//! Final Edict contract-bundle identity is bound explicitly after assembly. + +/// Namespaced generated contract and invocation surface for `a.b@1.t`. +pub mod echo_dpo { + use echo_registry_api::{ + OpKind, ProviderBundleIdentityV1, ProviderDigestIdentityV1, + ProviderFootprintIdentityV1, ProviderOperationV1, ProviderRegistryV1, + ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, ProviderValueContractV1, + }; + use echo_wasm_abi::codec::{ + decode_from_bytes, encode_to_vec, CodecError, Decode, Encode, Reader, Writer, + }; + use echo_wasm_abi::{pack_intent_v1, EnvelopeError}; + use warp_core::{ + matches_eint_op, propose_provider_contract_package_v1, ContractPackageIdentity, + GeneratedProviderMutationDispatchV1, GraphView, NodeId, + ProviderContractPackageProposalV1, ProviderMutationHooksV1, + ProviderMutationImplementationIdentityV1, ProviderPackageProposalError, + }; + + /// Exact Edict-authored semantic operation coordinate. + pub const OPERATION_COORDINATE: &str = "a.b@1.t"; + /// Semantic domain owning the operation coordinate. + pub const OPERATION_DOMAIN: &str = "echo.edict-provider/operation/v1"; + /// Echo-owned law that derives the persisted operation id. + pub const OPERATION_ID_LAW: &str = "echo.semantic-operation-id.fnv1-32/v1"; + /// Exact persisted operation id carried by the generated-artifact profile. + pub const OPERATION_ID: u32 = 3_389_142_194; + /// Exact Echo value codec carried by the generated-artifact profile. + pub const VALUE_CODEC_ID: &str = "le-binary-v1"; + /// Exact input schema coordinate owned by the generated-artifact profile. + pub const INPUT_SCHEMA: &str = "a.b@1.Input"; + /// Exact output schema coordinate owned by the generated-artifact profile. + pub const OUTPUT_SCHEMA: &str = "a.b@1.Output"; + /// Shared semantic domain owning the exact operation type schemas. + pub const TYPE_SCHEMA_DOMAIN: &str = "echo.edict-provider/value/v1"; + /// Exact typed obstruction coordinate for the reviewed failure mapping. + pub const OBSTRUCTION_COORDINATE: &str = "domain.WriteRejected"; + /// Semantic domain owning the typed obstruction coordinate. + pub const OBSTRUCTION_DOMAIN: &str = "echo.edict-provider/obstruction/v1"; + /// Exact target failure payload schema before obstruction mapping. + pub const EFFECT_FAILURE_SCHEMA: &str = "target.replace.rejected"; + /// Exact domain obstruction payload schema after obstruction mapping. + pub const OBSTRUCTION_PAYLOAD_SCHEMA: &str = "domain.WriteRejected.Payload"; + /// Semantic coordinate carried by the emitted Target IR artifact. + pub const TARGET_IR_COORDINATE: &str = "echo.span-ir/v1"; + /// Digest-framing domain for the complete Target IR artifact envelope. + pub const TARGET_IR_DIGEST_DOMAIN: &str = "edict.target-ir.artifact/v1"; + /// Exact domain-framed identity of the emitted Target IR artifact. + pub const TARGET_IR_DIGEST: &str = "sha256:2244345f046448c7b519ade05a167137659361ed144b46315ea32dabfbad85fc"; + /// Exact target-profile coordinate. + pub const TARGET_PROFILE_COORDINATE: &str = "echo.dpo@1"; + /// Digest-framing domain for the target-profile artifact. + pub const TARGET_PROFILE_DIGEST_DOMAIN: &str = "edict.target-profile/v1"; + /// Exact domain-framed identity of the target profile. + pub const TARGET_PROFILE_DIGEST: &str = "sha256:ad7f10e1843f4b3d2c08b11d69df103f9c0b1b7388ae26bb364cc87106cd419e"; + /// Semantic profile for Echo contract bundles; not a bundle occurrence. + pub const TARGET_BUNDLE_PROFILE_COORDINATE: &str = "echo.dpo.bundle/v1"; + /// Digest-framing domain for the target-bundle profile artifact. + pub const TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN: &str = "echo.dpo.bundle/v1"; + /// Exact domain-framed identity of the target-bundle profile. + pub const TARGET_BUNDLE_PROFILE_DIGEST: &str = "sha256:aa0438bcc6ef14ee6cb6d4976622f6080381d731459dcb7b9102595c9bed92c0"; + /// Echo contract ABI targeted by this generated helper. + pub const ECHO_CONTRACT_ABI_VERSION: u32 = 1; + /// Contract-host helper API targeted by this generated helper. + pub const CONTRACT_HOST_HELPER_API_VERSION: u32 = 1; + /// Exact self-contained provider CDDL coordinate. + pub const PROVIDER_SCHEMA_COORDINATE: &str = "echo.provider-artifacts.cddl@1"; + /// Raw SHA-256 of the exact self-contained provider CDDL bytes. + pub const PROVIDER_SCHEMA_SHA256_HEX: &str = + "e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"; + /// Exact generated-artifact profile coordinate owning operation schemas. + pub const GENERATED_ARTIFACT_PROFILE: &str = "echo.dpo.registration/v1"; + /// Digest-framing domain for the generated-artifact profile. + pub const GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN: &str = + "echo.generated-artifact-profile/v1"; + /// Domain-framed identity of the generated-artifact profile. + pub const GENERATED_ARTIFACT_PROFILE_DIGEST: &str = + "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d"; + /// Exact semantic operation profile selected by the authored operation. + pub const OPERATION_PROFILE: &str = "continuum.profile.write/v1"; + /// Semantic domain owning the selected operation profile. + pub const OPERATION_PROFILE_DOMAIN: &str = + "echo.edict-provider/operation-profile/v1"; + /// Exact operation-profiles document coordinate. + pub const OPERATION_PROFILES_COORDINATE: &str = "echo.dpo.operation-profiles/v1"; + /// Digest-framing domain for the operation-profiles document. + pub const OPERATION_PROFILES_DIGEST_DOMAIN: &str = "echo.dpo.operation-profiles/v1"; + /// Domain-framed identity of the operation-profiles document. + pub const OPERATION_PROFILES_DIGEST: &str = + "sha256:53256c51f6c817a77cc8694458bf9d3891abd15b9c94f79ca97d920d3c5f0416"; + /// Abstract footprint obligation carried across lowering. + pub const FOOTPRINT_OBLIGATION: &str = "target.replace.footprint"; + /// Exact target footprint-algebra coordinate. + pub const FOOTPRINT_ALGEBRA: &str = "echo.dpo.footprint/v1"; + /// Digest-framing domain for the target footprint algebra. + pub const FOOTPRINT_ALGEBRA_DIGEST_DOMAIN: &str = "echo.dpo.footprint/v1"; + /// Domain-framed identity of the target footprint algebra. + pub const FOOTPRINT_ALGEBRA_DIGEST: &str = + "sha256:f47bb65867e78099ddcfd6ae7af83870df8823f974a496a111ed94e5d785c769"; + /// Edict domain for the semantic contract-bundle digest proposition. + pub const SEMANTIC_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.semantic/v1"; + /// Edict domain for the release contract-bundle digest proposition. + pub const RELEASE_BUNDLE_DIGEST_DOMAIN: &str = "edict.bundle.release/v1"; + + const MUTATION_RULE_NAME: &str = concat!( + "cmd/contract/", + "e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c", + "/3389142194/a.b@1.t" + ); + const PROVIDER_OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { + coordinate: OPERATION_COORDINATE, + semantic_domain: OPERATION_DOMAIN, + kind: OpKind::Mutation, + operation_id_law: OPERATION_ID_LAW, + operation_id: OPERATION_ID, + input: ProviderValueContractV1 { + schema_coordinate: INPUT_SCHEMA, + schema_domain: TYPE_SCHEMA_DOMAIN, + codec_id: VALUE_CODEC_ID, + }, + output: ProviderValueContractV1 { + schema_coordinate: OUTPUT_SCHEMA, + schema_domain: TYPE_SCHEMA_DOMAIN, + codec_id: VALUE_CODEC_ID, + }, + target_failure_schema: EFFECT_FAILURE_SCHEMA, + obstruction: ProviderSemanticIdentityV1 { + coordinate: OBSTRUCTION_COORDINATE, + semantic_domain: OBSTRUCTION_DOMAIN, + }, + obstruction_payload_schema: OBSTRUCTION_PAYLOAD_SCHEMA, + target_ir: ProviderDigestIdentityV1 { + coordinate: TARGET_IR_COORDINATE, + digest_domain: TARGET_IR_DIGEST_DOMAIN, + digest: TARGET_IR_DIGEST, + }, + target_profile: ProviderDigestIdentityV1 { + coordinate: TARGET_PROFILE_COORDINATE, + digest_domain: TARGET_PROFILE_DIGEST_DOMAIN, + digest: TARGET_PROFILE_DIGEST, + }, + generated_artifact_profile: ProviderDigestIdentityV1 { + coordinate: GENERATED_ARTIFACT_PROFILE, + digest_domain: GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN, + digest: GENERATED_ARTIFACT_PROFILE_DIGEST, + }, + operation_profile: ProviderSemanticIdentityV1 { + coordinate: OPERATION_PROFILE, + semantic_domain: OPERATION_PROFILE_DOMAIN, + }, + operation_profiles: ProviderDigestIdentityV1 { + coordinate: OPERATION_PROFILES_COORDINATE, + digest_domain: OPERATION_PROFILES_DIGEST_DOMAIN, + digest: OPERATION_PROFILES_DIGEST, + }, + footprint: ProviderFootprintIdentityV1 { + obligation: FOOTPRINT_OBLIGATION, + algebra_coordinate: FOOTPRINT_ALGEBRA, + algebra_digest_domain: FOOTPRINT_ALGEBRA_DIGEST_DOMAIN, + algebra_digest: FOOTPRINT_ALGEBRA_DIGEST, + }, + }]; + + const ID_MAX_SCALAR_VALUES: usize = 16; + const ID_MAX_UTF8_BYTES: usize = ID_MAX_SCALAR_VALUES * 4; + + /// Exact bounded value of semantic type `a.b@1.Id`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Id(String); + + impl Id { + /// Construct an id after enforcing the authored Unicode-scalar bound. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `value` contains more + /// than sixteen Unicode scalar values. + pub fn new(value: impl Into) -> Result { + let value = value.into(); + if value.chars().count() > ID_MAX_SCALAR_VALUES { + return Err(CodecError::StringTooLong); + } + Ok(Self(value)) + } + + /// Borrow the exact raw UTF-8 value without normalization. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume this value and return its exact raw UTF-8 string. + pub fn into_string(self) -> String { + self.0 + } + } + + impl Encode for Id { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + if self.0.chars().count() > ID_MAX_SCALAR_VALUES { + return Err(CodecError::StringTooLong); + } + writer.write_len_prefixed_bytes(self.0.as_bytes()) + } + } + + impl Decode for Id { + fn decode(reader: &mut Reader<'_>) -> Result { + let bytes = reader.read_len_prefixed_bytes(ID_MAX_UTF8_BYTES)?; + let value = core::str::from_utf8(bytes).map_err(|_| CodecError::InvalidUtf8)?; + Self::new(String::from(value)) + } + } + + /// Exact typed input for semantic operation `a.b@1.t`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Input { + id: Id, + } + + impl Input { + /// Construct a validated operation input. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `id` exceeds its + /// authored Unicode-scalar bound. + pub fn new(id: impl Into) -> Result { + Ok(Self { id: Id::new(id)? }) + } + + /// Borrow the exact raw UTF-8 id. + pub fn id(&self) -> &str { + self.id.as_str() + } + } + + impl Encode for Input { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + self.id.encode(writer) + } + } + + impl Decode for Input { + fn decode(reader: &mut Reader<'_>) -> Result { + Ok(Self { + id: Id::decode(reader)?, + }) + } + } + + /// Exact typed output for semantic operation `a.b@1.t`. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Output { + id: Id, + } + + impl Output { + /// Construct a validated operation output. + /// + /// # Errors + /// + /// Returns [`CodecError::StringTooLong`] when `id` exceeds its + /// authored Unicode-scalar bound. + pub fn new(id: impl Into) -> Result { + Ok(Self { id: Id::new(id)? }) + } + + /// Borrow the exact raw UTF-8 id. + pub fn id(&self) -> &str { + self.id.as_str() + } + } + + impl Encode for Output { + fn encode(&self, writer: &mut Writer) -> Result<(), CodecError> { + self.id.encode(writer) + } + } + + impl Decode for Output { + fn decode(reader: &mut Reader<'_>) -> Result { + Ok(Self { + id: Id::decode(reader)?, + }) + } + } + + /// Stable failure produced while constructing one canonical invocation. + #[derive(Debug, Eq, PartialEq)] + pub enum GeneratedInvocationError { + /// The typed input violates its generated codec contract. + Codec(CodecError), + /// The canonical Echo intent envelope could not be constructed. + Envelope(EnvelopeError), + } + + /// Independent host pin for the final assembled bundle identity. + /// + /// This value is explicit expected evidence, not an admission token. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ExpectedContractBundleIdentityV1<'a> { + /// Digest proposition domain for `semantic_digest`. + pub semantic_digest_domain: &'a str, + /// Exact semantic-layer bundle digest expected by the host. + pub semantic_digest: &'a str, + /// Digest proposition domain for `release_digest`. + pub release_digest_domain: &'a str, + /// Exact release-layer bundle digest expected by the host. + pub release_digest: &'a str, + } + + /// Untrusted identity and semantic claims read from one assembled bundle. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ContractBundleIdentityV1<'a> { + /// Digest proposition domain for `semantic_digest`. + pub semantic_digest_domain: &'a str, + /// Claimed semantic-layer bundle digest computed by Edict. + pub semantic_digest: &'a str, + /// Digest proposition domain for `release_digest`. + pub release_digest_domain: &'a str, + /// Claimed release-layer bundle digest computed by Edict. + pub release_digest: &'a str, + /// Semantic operation coordinate carried by the bundle. + pub operation_coordinate: &'a str, + /// Semantic domain owning the operation coordinate. + pub operation_domain: &'a str, + /// Operation-id law claimed by the generated-artifact profile. + pub operation_id_law: &'a str, + /// Persisted operation id claimed by the generated-artifact profile. + pub operation_id: u32, + /// Echo value codec claimed by the generated-artifact profile. + pub value_codec: &'a str, + /// Semantic coordinate carried by the Target IR artifact. + pub target_ir_coordinate: &'a str, + /// Digest-framing domain for the Target IR artifact. + pub target_ir_digest_domain: &'a str, + /// Target IR digest carried by the bundle. + pub target_ir_digest: &'a str, + /// Target-profile coordinate carried by the bundle. + pub target_profile_coordinate: &'a str, + /// Digest-framing domain for the target-profile artifact. + pub target_profile_digest_domain: &'a str, + /// Target-profile digest carried by the bundle. + pub target_profile_digest: &'a str, + /// Target-bundle-profile coordinate carried by the bundle. + pub target_bundle_profile_coordinate: &'a str, + /// Digest-framing domain for the target-bundle-profile artifact. + pub target_bundle_profile_digest_domain: &'a str, + /// Target-bundle-profile digest carried by the bundle. + pub target_bundle_profile_digest: &'a str, + /// Echo contract ABI claimed by the generated registry. + pub echo_contract_abi_version: u32, + /// Contract-host helper API claimed by the generated registry. + pub helper_api_version: u32, + /// Provider CDDL coordinate claimed by the assembled bundle. + pub provider_schema_coordinate: &'a str, + /// Raw provider CDDL SHA-256 claimed by the assembled bundle. + pub provider_schema_sha256_hex: &'a str, + /// Input schema coordinate claimed for this operation. + pub input_schema: &'a str, + /// Output schema coordinate claimed for this operation. + pub output_schema: &'a str, + /// Semantic domain owning all claimed operation type schemas. + pub type_schema_domain: &'a str, + /// Typed obstruction coordinate claimed for the reviewed failure. + pub obstruction_coordinate: &'a str, + /// Semantic domain owning the typed obstruction coordinate. + pub obstruction_domain: &'a str, + /// Target failure payload schema claimed before obstruction mapping. + pub effect_failure_schema: &'a str, + /// Domain obstruction payload schema claimed after obstruction mapping. + pub obstruction_payload_schema: &'a str, + /// Generated-artifact profile coordinate claimed by the bundle. + pub generated_artifact_profile: &'a str, + /// Digest-framing domain for the generated-artifact profile. + pub generated_artifact_profile_digest_domain: &'a str, + /// Generated-artifact profile digest claimed by the bundle. + pub generated_artifact_profile_digest: &'a str, + /// Semantic operation profile claimed by the bundle. + pub operation_profile: &'a str, + /// Semantic domain owning the operation profile. + pub operation_profile_domain: &'a str, + /// Operation-profiles document coordinate claimed by the bundle. + pub operation_profiles_coordinate: &'a str, + /// Digest-framing domain for the operation-profiles document. + pub operation_profiles_digest_domain: &'a str, + /// Operation-profiles document digest claimed by the bundle. + pub operation_profiles_digest: &'a str, + /// Abstract footprint obligation claimed for this operation. + pub footprint_obligation: &'a str, + /// Footprint-algebra coordinate claimed by the bundle. + pub footprint_algebra: &'a str, + /// Digest-framing domain for the footprint algebra. + pub footprint_algebra_digest_domain: &'a str, + /// Footprint-algebra digest claimed by the bundle. + pub footprint_algebra_digest: &'a str, + } + + /// Stable reason an assembled bundle cannot bind this generated helper. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub enum BindingMismatchKind { + /// A bundle digest is framed under the wrong proposition domain. + BundleDigestDomain, + /// A semantic or release bundle digest is not a typed SHA-256 review value. + BundleDigest, + /// The assembled semantic-bundle digest differs from the host pin. + SemanticBundleDigest, + /// The assembled release-bundle digest differs from the host pin. + ReleaseBundleDigest, + /// The bundle names a different semantic operation. + Operation, + /// The bundle names a different persisted operation-id proposition. + OperationId, + /// The bundle names a different Echo value codec. + Codec, + /// The bundle names a different Target IR artifact. + TargetIr, + /// The bundle names a different target profile. + TargetProfile, + /// The bundle names a different target-bundle profile. + TargetBundleProfile, + /// The generated registry targets a different Echo contract ABI. + EchoAbi, + /// The generated registry targets a different contract-host helper API. + HelperApi, + /// The bundle names different provider or operation schema identities. + Schema, + /// The bundle names a different generated-artifact profile. + GeneratedArtifactProfile, + /// The bundle names a different semantic operation profile. + OperationProfile, + /// The bundle names a different footprint obligation or algebra. + Footprint, + } + + /// Exact generated registration binding, still without runtime authority. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct RegistrationDescriptorV1<'a> { + contract_bundle: ContractBundleIdentityV1<'a>, + } + + impl<'a> RegistrationDescriptorV1<'a> { + /// Return the exact bundle claims that matched the explicit host pin. + pub const fn contract_bundle(&self) -> &ContractBundleIdentityV1<'a> { + &self.contract_bundle + } + + /// Return the exact persisted operation id matched by this descriptor. + pub const fn operation_id(&self) -> u32 { + self.contract_bundle.operation_id + } + + /// Return the exact provider-generic registry claims retained by this + /// already matched descriptor. + /// + /// This constructs descriptive evidence only. It does not admit or + /// install a registry. + pub const fn provider_registry(&self) -> ProviderRegistryV1<'a> { + ProviderRegistryV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: ProviderSchemaIdentityV1 { + coordinate: PROVIDER_SCHEMA_COORDINATE, + raw_sha256_hex: PROVIDER_SCHEMA_SHA256_HEX, + }, + target_bundle_profile: ProviderDigestIdentityV1 { + coordinate: TARGET_BUNDLE_PROFILE_COORDINATE, + digest_domain: TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN, + digest: TARGET_BUNDLE_PROFILE_DIGEST, + }, + bundle: ProviderBundleIdentityV1 { + semantic_digest_domain: self.contract_bundle.semantic_digest_domain, + semantic_digest: self.contract_bundle.semantic_digest, + release_digest_domain: self.contract_bundle.release_digest_domain, + release_digest: self.contract_bundle.release_digest, + }, + operations: &PROVIDER_OPERATIONS, + } + } + + /// Return the exact identity a host implementation must independently + /// claim before its callbacks can be proposed for this operation. + pub const fn mutation_implementation_identity( + &self, + ) -> ProviderMutationImplementationIdentityV1<'a> { + let registry = self.provider_registry(); + ProviderMutationImplementationIdentityV1 { + echo_contract_abi_version: registry.echo_contract_abi_version, + helper_api_version: registry.helper_api_version, + provider_schema: registry.provider_schema, + target_bundle_profile: registry.target_bundle_profile, + bundle: registry.bundle, + operation: PROVIDER_OPERATIONS[0], + } + } + + /// Construct one opaque package proposal from exact matched claims and + /// an explicit host executor/footprint binding. + /// + /// The result grants no runtime admission, registration, installation, + /// scheduling, execution, durability, or receipt authority. + /// + /// # Errors + /// + /// Returns [`ProviderPackageProposalError`] when occurrence metadata, + /// generated dispatch identity, or any host implementation claim does + /// not exactly match this descriptor. + pub fn propose_contract_package<'proposal>( + &'proposal self, + occurrence: ContractPackageIdentity<'proposal>, + hooks: ProviderMutationHooksV1<'proposal>, + ) -> Result, ProviderPackageProposalError> + where + 'a: 'proposal, + { + let registry: ProviderRegistryV1<'proposal> = self.provider_registry(); + propose_provider_contract_package_v1( + occurrence, + registry, + GeneratedProviderMutationDispatchV1::new( + OPERATION_ID, + MUTATION_RULE_NAME, + matches_operation, + ), + hooks, + ) + } + + /// Encode one exact typed input under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] when the typed value violates its generated + /// bound or cannot be represented by the selected codec. + #[allow(clippy::unused_self)] + pub fn encode_input(&self, input: &Input) -> Result, CodecError> { + encode_to_vec(input) + } + + /// Decode one exact typed input under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] for malformed, over-bound, truncated, or + /// trailing bytes. + #[allow(clippy::unused_self)] + pub fn decode_input(&self, bytes: &[u8]) -> Result { + decode_from_bytes(bytes) + } + + /// Encode one exact typed output under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] when the typed value violates its generated + /// bound or cannot be represented by the selected codec. + #[allow(clippy::unused_self)] + pub fn encode_output(&self, output: &Output) -> Result, CodecError> { + encode_to_vec(output) + } + + /// Decode one exact typed output under the matched value-codec claim. + /// + /// # Errors + /// + /// Returns [`CodecError`] for malformed, over-bound, truncated, or + /// trailing bytes. + #[allow(clippy::unused_self)] + pub fn decode_output(&self, bytes: &[u8]) -> Result { + decode_from_bytes(bytes) + } + + /// Encode a typed input and wrap it in the canonical Echo EINT v1 + /// envelope for this matched operation id. + /// + /// # Errors + /// + /// Returns [`GeneratedInvocationError::Codec`] when the input violates + /// its generated value contract, or + /// [`GeneratedInvocationError::Envelope`] when Echo refuses envelope + /// construction. + pub fn pack_intent(&self, input: &Input) -> Result, GeneratedInvocationError> { + let vars = self + .encode_input(input) + .map_err(GeneratedInvocationError::Codec)?; + pack_intent_v1(self.operation_id(), &vars) + .map_err(GeneratedInvocationError::Envelope) + } + } + + fn matches_operation(view: GraphView<'_>, scope: &NodeId) -> bool { + matches_eint_op(view, scope, OPERATION_ID) + } + + /// Compare assembled bundle claims to an independent exact host pin and to + /// this generated helper's semantic identities. + /// + /// This pure equality/consistency preflight neither authenticates the pin, + /// admits the bundle, nor installs a package. Those remain separate trusted + /// host and Echo runtime crossings. + pub fn bind_contract_bundle<'a>( + expected: ExpectedContractBundleIdentityV1<'a>, + identity: &ContractBundleIdentityV1<'a>, + ) -> Result, BindingMismatchKind> { + if expected.semantic_digest_domain != SEMANTIC_BUNDLE_DIGEST_DOMAIN + || identity.semantic_digest_domain != SEMANTIC_BUNDLE_DIGEST_DOMAIN + || expected.release_digest_domain != RELEASE_BUNDLE_DIGEST_DOMAIN + || identity.release_digest_domain != RELEASE_BUNDLE_DIGEST_DOMAIN + { + return Err(BindingMismatchKind::BundleDigestDomain); + } + if !is_sha256_review(expected.semantic_digest) + || !is_sha256_review(expected.release_digest) + || !is_sha256_review(identity.semantic_digest) + || !is_sha256_review(identity.release_digest) + { + return Err(BindingMismatchKind::BundleDigest); + } + if identity.semantic_digest != expected.semantic_digest { + return Err(BindingMismatchKind::SemanticBundleDigest); + } + if identity.release_digest != expected.release_digest { + return Err(BindingMismatchKind::ReleaseBundleDigest); + } + if identity.operation_coordinate != OPERATION_COORDINATE + || identity.operation_domain != OPERATION_DOMAIN + { + return Err(BindingMismatchKind::Operation); + } + if identity.operation_id_law != OPERATION_ID_LAW || identity.operation_id != OPERATION_ID { + return Err(BindingMismatchKind::OperationId); + } + if identity.value_codec != VALUE_CODEC_ID { + return Err(BindingMismatchKind::Codec); + } + if identity.target_ir_coordinate != TARGET_IR_COORDINATE + || identity.target_ir_digest_domain != TARGET_IR_DIGEST_DOMAIN + || identity.target_ir_digest != TARGET_IR_DIGEST + { + return Err(BindingMismatchKind::TargetIr); + } + if identity.target_profile_coordinate != TARGET_PROFILE_COORDINATE + || identity.target_profile_digest_domain != TARGET_PROFILE_DIGEST_DOMAIN + || identity.target_profile_digest != TARGET_PROFILE_DIGEST + { + return Err(BindingMismatchKind::TargetProfile); + } + if identity.target_bundle_profile_coordinate != TARGET_BUNDLE_PROFILE_COORDINATE + || identity.target_bundle_profile_digest_domain + != TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN + || identity.target_bundle_profile_digest != TARGET_BUNDLE_PROFILE_DIGEST + { + return Err(BindingMismatchKind::TargetBundleProfile); + } + if identity.echo_contract_abi_version != ECHO_CONTRACT_ABI_VERSION { + return Err(BindingMismatchKind::EchoAbi); + } + if identity.helper_api_version != CONTRACT_HOST_HELPER_API_VERSION { + return Err(BindingMismatchKind::HelperApi); + } + if identity.provider_schema_coordinate != PROVIDER_SCHEMA_COORDINATE + || identity.provider_schema_sha256_hex != PROVIDER_SCHEMA_SHA256_HEX + || identity.input_schema != INPUT_SCHEMA + || identity.output_schema != OUTPUT_SCHEMA + || identity.type_schema_domain != TYPE_SCHEMA_DOMAIN + || identity.obstruction_coordinate != OBSTRUCTION_COORDINATE + || identity.obstruction_domain != OBSTRUCTION_DOMAIN + || identity.effect_failure_schema != EFFECT_FAILURE_SCHEMA + || identity.obstruction_payload_schema != OBSTRUCTION_PAYLOAD_SCHEMA + { + return Err(BindingMismatchKind::Schema); + } + if identity.generated_artifact_profile != GENERATED_ARTIFACT_PROFILE + || identity.generated_artifact_profile_digest_domain + != GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN + || identity.generated_artifact_profile_digest + != GENERATED_ARTIFACT_PROFILE_DIGEST + { + return Err(BindingMismatchKind::GeneratedArtifactProfile); + } + if identity.operation_profile != OPERATION_PROFILE + || identity.operation_profile_domain != OPERATION_PROFILE_DOMAIN + || identity.operation_profiles_coordinate != OPERATION_PROFILES_COORDINATE + || identity.operation_profiles_digest_domain != OPERATION_PROFILES_DIGEST_DOMAIN + || identity.operation_profiles_digest != OPERATION_PROFILES_DIGEST + { + return Err(BindingMismatchKind::OperationProfile); + } + if identity.footprint_obligation != FOOTPRINT_OBLIGATION + || identity.footprint_algebra != FOOTPRINT_ALGEBRA + || identity.footprint_algebra_digest_domain != FOOTPRINT_ALGEBRA_DIGEST_DOMAIN + || identity.footprint_algebra_digest != FOOTPRINT_ALGEBRA_DIGEST + { + return Err(BindingMismatchKind::Footprint); + } + Ok(RegistrationDescriptorV1 { + contract_bundle: *identity, + }) + } + + fn is_sha256_review(value: &str) -> bool { + let Some(hex) = value.strip_prefix("sha256:") else { + return false; + }; + hex.len() == 64 + && hex + .as_bytes() + .iter() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + } +} diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs index 5c39826d..9a5b7999 100644 --- a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -7,9 +7,9 @@ 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, + lower, Artifact, BoundArtifact, Digest, DigestAlgorithm, LoweringOutputArtifact, + LoweringOutputKind, LoweringOutputRequest, LoweringRequestV1, ProtocolVersionV1, + ProviderRefusalKind, ResourceRef, ResponseLimitsV1, SemanticInput, SemanticInputKind, }; use sha2::{Digest as ShaDigest, Sha256}; @@ -17,6 +17,9 @@ const TARGET_PROFILE: &[u8] = include_bytes!("../resources/target-profile.echo-d 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 GENERATED_ARTIFACT_PROFILE_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor" +); const CORE_DOMAIN: &str = "edict.core.module/v1"; const TARGET_PROFILE_DOMAIN: &str = "edict.target-profile/v1"; @@ -25,7 +28,26 @@ 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 GENERATED_ARTIFACT_DOMAIN: &str = "echo.generated-artifact/v1"; +const REVIEW_PAYLOAD_DOMAIN: &str = "echo.review-payload/v1"; +const GENERATED_ARTIFACT_ROLE: &str = "generated.echo-dpo"; +const REVIEW_PAYLOAD_ROLE: &str = "review.echo-dpo"; const TARGET_IR_ROLE: &str = "target-ir.echo-dpo"; +const GENERATED_ARTIFACT_PROFILE: &str = "echo.dpo.registration/v1"; +const GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN: &str = "echo.generated-artifact-profile/v1"; +const GENERATED_ARTIFACT_PROFILE_DIGEST: &str = + "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d"; +const TARGET_BUNDLE_PROFILE: &str = "echo.dpo.bundle/v1"; +const TARGET_BUNDLE_PROFILE_DIGEST: &str = + "sha256:aa0438bcc6ef14ee6cb6d4976622f6080381d731459dcb7b9102595c9bed92c0"; +const GENERATED_SOURCE_MEDIA_TYPE: &str = "text/rust; charset=utf-8"; +const REVIEW_MEDIA_TYPE: &str = "application/json"; +const GENERATED_SOURCE_PATH: &str = "generated/echo_dpo.rs"; +const REVIEW_PATH: &str = "review/echo_dpo.json"; +const EXPECTED_PROVIDER_SCHEMA_SHA256_HEX: &str = + "e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"; +const EXPECTED_OPERATION_ID_LAW: &str = "echo.semantic-operation-id.fnv1-32/v1"; +const EXPECTED_OPERATION_ID: u32 = 3_389_142_194; const EDICT_ORACLE_CORE_HEX: &str = concat!( "a6657479706573a665496e707574a2646b696e64665265636f7264666669656c6473a16269646e612e6240312e496e70", @@ -399,6 +421,125 @@ fn request() -> LoweringRequestV1 { request_with_core(core_value(ordinary_result(), Some("target.replace"))) } +fn reviewed_edict_request() -> LoweringRequestV1 { + let core_bytes = hex::decode(EDICT_ORACLE_CORE_HEX).expect("oracle Core hex is valid"); + let mut request = request(); + request.core = bound("a.b@1", CORE_DOMAIN, core_bytes); + request +} + +fn output_request(role: &str, kind: LoweringOutputKind, domain: &str) -> LoweringOutputRequest { + LoweringOutputRequest { + role: role.to_owned(), + kind, + domain: domain.to_owned(), + } +} + +fn generated_artifact_request() -> LoweringOutputRequest { + output_request( + GENERATED_ARTIFACT_ROLE, + LoweringOutputKind::GeneratedArtifact, + GENERATED_ARTIFACT_DOMAIN, + ) +} + +fn review_payload_request() -> LoweringOutputRequest { + output_request( + REVIEW_PAYLOAD_ROLE, + LoweringOutputKind::ReviewPayload, + REVIEW_PAYLOAD_DOMAIN, + ) +} + +fn target_ir_request() -> LoweringOutputRequest { + output_request( + TARGET_IR_ROLE, + LoweringOutputKind::TargetIr, + OUTER_TARGET_IR_DOMAIN, + ) +} + +fn assert_exact_outputs( + outputs: &[LoweringOutputArtifact], + expected: &[(&str, LoweringOutputKind, &str)], +) { + assert_eq!(outputs.len(), expected.len()); + for (output, (role, kind, domain)) in outputs.iter().zip(expected) { + assert_eq!(output.role, *role); + assert_eq!(output.kind, *kind); + assert_eq!(output.artifact.domain, *domain); + } +} + +fn output_by_role<'a>( + outputs: &'a [LoweringOutputArtifact], + role: &str, +) -> &'a LoweringOutputArtifact { + outputs + .iter() + .find(|output| output.role == role) + .unwrap_or_else(|| panic!("output role `{role}` is absent")) +} + +fn output_envelope(output: &LoweringOutputArtifact) -> CanonicalValueV1 { + decode_canonical_cbor_v1(&output.artifact.bytes) + .unwrap_or_else(|_| panic!("output role `{}` is not canonical CBOR", output.role)) +} + +fn bytes_field<'a>(value: &'a CanonicalValueV1, field: &str) -> &'a [u8] { + let CanonicalValueV1::Bytes(bytes) = map_field(value, field) else { + panic!("map field `{field}` is not bytes"); + }; + bytes +} + +fn assert_sha256_resource_ref(value: &CanonicalValueV1, id: &str, digest: &str) { + assert_eq!(text_value(map_field(value, "id")), id); + let digest_bytes = hex::decode( + digest + .strip_prefix("sha256:") + .expect("expected review digest uses sha256"), + ) + .expect("expected review digest is hexadecimal"); + assert_eq!( + map_field(value, "digest"), + &CanonicalValueV1::Array(vec![text("sha256"), CanonicalValueV1::Bytes(digest_bytes),]) + ); +} + +fn generated_source_from(mut request: LoweringRequestV1) -> String { + request.requested_outputs = vec![generated_artifact_request()]; + let success = lower(request).expect("generated source lowers"); + let envelope = output_envelope(output_by_role(&success.outputs, GENERATED_ARTIFACT_ROLE)); + String::from_utf8(bytes_field(&envelope, "bytes").to_vec()).expect("generated source is UTF-8") +} + +fn generated_source() -> String { + generated_source_from(request()) +} + +fn reviewed_edict_generated_source() -> String { + generated_source_from(reviewed_edict_request()) +} + +#[test] +#[ignore = "explicit checked generated-helper regeneration"] +fn regenerate_checked_generated_helper() { + let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/generated_echo_dpo.rs"); + std::fs::write(&path, reviewed_edict_generated_source()) + .expect("failed to replace the checked generated-helper fixture"); +} + +fn review_json() -> String { + let mut request = request(); + request.requested_outputs = vec![review_payload_request()]; + let success = lower(request).expect("review payload lowers"); + let envelope = output_envelope(output_by_role(&success.outputs, REVIEW_PAYLOAD_ROLE)); + String::from_utf8(bytes_field(&envelope, "bytes").to_vec()).expect("review payload is UTF-8") +} + fn map_field<'a>(value: &'a CanonicalValueV1, field: &str) -> &'a CanonicalValueV1 { let CanonicalValueV1::Map(entries) = value else { panic!("value is not a map"); @@ -507,6 +648,7 @@ fn reviewed_edict_fixture_has_exact_builtin_wrapper_parity() { assert_eq!(core_bytes.len(), 1209); let mut request = request(); request.core = bound("a.b@1", CORE_DOMAIN, core_bytes); + let target_profile_digest = request.target_profile.reference.digest.bytes.clone(); assert_eq!( hex::encode(&request.core.reference.digest.bytes), "c3dbe413c78a82f6120e64c9a04bc94e2d79505f9e4b8a65c2bc26b408d775de" @@ -516,8 +658,19 @@ fn reviewed_edict_fixture_has_exact_builtin_wrapper_parity() { 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); + let prior_oracle = + hex::decode(EDICT_ORACLE_TARGET_IR_HEX).expect("oracle Target IR hex is valid"); + assert_eq!(prior_oracle.len(), 848); + let mut expected = decode_canonical_cbor_v1(&prior_oracle) + .expect("reviewed Edict oracle is canonical Target IR"); + *map_field_mut(map_field_mut(&mut expected, "targetProfile"), "digest") = + CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes(target_profile_digest), + ]); + let expected = + encode_canonical_cbor_v1(&expected).expect("rebound Edict oracle remains canonical"); + assert_ne!(prior_oracle, expected); assert_eq!(output.artifact.bytes, expected); let output_value = decode_canonical_cbor_v1(&output.artifact.bytes) @@ -525,7 +678,7 @@ fn reviewed_edict_fixture_has_exact_builtin_wrapper_parity() { assert_eq!( digest_canonical_value_v1(OUTER_TARGET_IR_DOMAIN, &output_value) .expect("oracle-parity output has a domain-framed digest"), - "sha256:b0d9e218f00a102d1e951c73e5063a9bbe6077e6c7468d171ec08b420e7b47da" + "sha256:2244345f046448c7b519ade05a167137659361ed144b46315ea32dabfbad85fc" ); } @@ -540,17 +693,428 @@ fn vendored_wit_is_the_frozen_edict_lowerer_contract() { } #[test] -fn output_overclaim_identifies_the_first_unsupported_role() { +fn target_ir_only_remains_an_exact_single_output() { + let success = lower(request()).expect("the existing Target IR-only request remains supported"); + assert_exact_outputs( + &success.outputs, + &[( + TARGET_IR_ROLE, + LoweringOutputKind::TargetIr, + OUTER_TARGET_IR_DOMAIN, + )], + ); +} + +#[test] +fn generated_artifact_only_emits_the_exact_declared_envelope() { 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(), - }); + request.requested_outputs = vec![generated_artifact_request()]; + + let success = lower(request).expect("the declared generated-artifact role is supported"); + assert_exact_outputs( + &success.outputs, + &[( + GENERATED_ARTIFACT_ROLE, + LoweringOutputKind::GeneratedArtifact, + GENERATED_ARTIFACT_DOMAIN, + )], + ); +} + +#[test] +fn review_payload_only_emits_the_exact_declared_envelope() { + let mut request = request(); + request.requested_outputs = vec![review_payload_request()]; + + let success = lower(request).expect("the declared review-payload role is supported"); + assert_exact_outputs( + &success.outputs, + &[( + REVIEW_PAYLOAD_ROLE, + LoweringOutputKind::ReviewPayload, + REVIEW_PAYLOAD_DOMAIN, + )], + ); +} + +#[test] +fn all_declared_outputs_emit_exact_sorted_correspondence() { + let mut request = request(); + request.requested_outputs = vec![ + generated_artifact_request(), + review_payload_request(), + target_ir_request(), + ]; + + let success = lower(request).expect("the exact declared output set is supported"); + assert_exact_outputs( + &success.outputs, + &[ + ( + GENERATED_ARTIFACT_ROLE, + LoweringOutputKind::GeneratedArtifact, + GENERATED_ARTIFACT_DOMAIN, + ), + ( + REVIEW_PAYLOAD_ROLE, + LoweringOutputKind::ReviewPayload, + REVIEW_PAYLOAD_DOMAIN, + ), + ( + TARGET_IR_ROLE, + LoweringOutputKind::TargetIr, + OUTER_TARGET_IR_DOMAIN, + ), + ], + ); +} + +#[test] +fn generated_artifact_profile_identity_is_domain_framed_not_a_raw_file_hash() { + let profile = decode_canonical_cbor_v1(GENERATED_ARTIFACT_PROFILE_BYTES) + .expect("the checked generated-artifact profile is canonical CBOR"); + let domain_framed = + digest_canonical_value_v1(GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN, &profile) + .expect("the checked profile has a domain-framed identity"); + let raw = format!( + "sha256:{}", + hex::encode(Sha256::digest(GENERATED_ARTIFACT_PROFILE_BYTES)) + ); + + assert_eq!(domain_framed, GENERATED_ARTIFACT_PROFILE_DIGEST); + assert_ne!(raw, GENERATED_ARTIFACT_PROFILE_DIGEST); +} + +#[test] +fn generated_and_review_envelopes_bind_exact_projection_metadata() { + let mut request = request(); + request.requested_outputs = vec![ + generated_artifact_request(), + review_payload_request(), + target_ir_request(), + ]; + let success = lower(request).expect("the exact declared output set lowers"); + + let generated = output_by_role(&success.outputs, GENERATED_ARTIFACT_ROLE); + assert_eq!( + generated.logical_path.as_deref(), + Some(GENERATED_SOURCE_PATH) + ); + let generated_envelope = output_envelope(generated); + let CanonicalValueV1::Map(generated_fields) = &generated_envelope else { + panic!("generated artifact envelope is not a map"); + }; + assert_eq!(generated_fields.len(), 5); + assert_eq!( + text_value(map_field(&generated_envelope, "apiVersion")), + GENERATED_ARTIFACT_DOMAIN + ); + assert_sha256_resource_ref( + map_field(&generated_envelope, "profile"), + GENERATED_ARTIFACT_PROFILE, + GENERATED_ARTIFACT_PROFILE_DIGEST, + ); + assert_eq!( + text_value(map_field(&generated_envelope, "operation")), + "a.b@1.t" + ); + assert_eq!( + text_value(map_field(&generated_envelope, "mediaType")), + GENERATED_SOURCE_MEDIA_TYPE + ); + assert!(bytes_field(&generated_envelope, "bytes") + .starts_with(b"// SPDX-License-Identifier: Apache-2.0\n")); + + let review = output_by_role(&success.outputs, REVIEW_PAYLOAD_ROLE); + assert_eq!(review.logical_path.as_deref(), Some(REVIEW_PATH)); + let review_envelope = output_envelope(review); + let CanonicalValueV1::Map(review_fields) = &review_envelope else { + panic!("review payload envelope is not a map"); + }; + assert_eq!(review_fields.len(), 5); + assert_eq!( + text_value(map_field(&review_envelope, "apiVersion")), + REVIEW_PAYLOAD_DOMAIN + ); + assert_eq!( + map_field(&review_envelope, "authoritative"), + &CanonicalValueV1::Bool(false) + ); + let generated_digest = + digest_canonical_value_v1(GENERATED_ARTIFACT_DOMAIN, &generated_envelope) + .expect("the corresponding generated artifact has a domain-framed digest"); + assert_sha256_resource_ref( + map_field(&review_envelope, "subject"), + GENERATED_ARTIFACT_ROLE, + &generated_digest, + ); + assert_eq!( + text_value(map_field(&review_envelope, "mediaType")), + REVIEW_MEDIA_TYPE + ); + let review_bytes = bytes_field(&review_envelope, "bytes"); + assert!(review_bytes.starts_with(b"{\"apiVersion\":\"echo.generated-helper-review/v1\"")); + let target_ir = output_by_role(&success.outputs, TARGET_IR_ROLE); + let target_ir_value = decode_canonical_cbor_v1(&target_ir.artifact.bytes) + .expect("the corresponding Target IR output is canonical CBOR"); + let target_ir_digest = digest_canonical_value_v1(OUTER_TARGET_IR_DOMAIN, &target_ir_value) + .expect("the corresponding Target IR has a domain-framed digest"); + assert!(String::from_utf8_lossy(review_bytes).contains(&target_ir_digest)); + assert!(String::from_utf8_lossy(review_bytes).contains(&generated_digest)); + assert!(review_bytes.ends_with(b"}\n")); +} - 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 generated_and_review_bytes_are_invariant_under_requested_output_supersets() { + let mut generated_request = request(); + generated_request.requested_outputs = vec![generated_artifact_request()]; + let generated_only = lower(generated_request).expect("generated-only request lowers"); + + let mut review_request = request(); + review_request.requested_outputs = vec![review_payload_request()]; + let review_only = lower(review_request).expect("review-only request lowers"); + + let mut all_request = request(); + all_request.requested_outputs = vec![ + generated_artifact_request(), + review_payload_request(), + target_ir_request(), + ]; + let all = lower(all_request).expect("full sorted request lowers"); + + assert_eq!( + output_by_role(&generated_only.outputs, GENERATED_ARTIFACT_ROLE), + output_by_role(&all.outputs, GENERATED_ARTIFACT_ROLE) + ); + assert_eq!( + output_by_role(&review_only.outputs, REVIEW_PAYLOAD_ROLE), + output_by_role(&all.outputs, REVIEW_PAYLOAD_ROLE) + ); +} + +#[test] +fn generated_source_exposes_explicit_post_assembly_bundle_binding() { + let source = generated_source(); + for required_surface in [ + "pub struct ContractBundleIdentityV1", + "pub struct RegistrationDescriptorV1", + "pub enum BindingMismatchKind", + "pub fn bind_contract_bundle", + ] { + assert!( + source.contains(required_surface), + "generated source is missing `{required_surface}`" + ); + } +} + +#[test] +fn generated_source_binds_the_expected_provider_schema_identity() { + let source = generated_source(); + + assert!( + source.contains(&format!("\"{EXPECTED_PROVIDER_SCHEMA_SHA256_HEX}\"")), + "generated source did not bind expected provider schema SHA-256 \ + {EXPECTED_PROVIDER_SCHEMA_SHA256_HEX}" + ); +} + +#[test] +fn generated_source_exposes_the_expected_profile_owned_operation_identity() { + let source = generated_source(); + + assert!( + source.contains(&format!( + "pub const OPERATION_ID_LAW: &str = \"{EXPECTED_OPERATION_ID_LAW}\";" + )), + "generated source did not expose the expected operation-id law" + ); + assert!( + source.contains("pub const OPERATION_ID: u32 = 3_389_142_194;"), + "generated source did not expose the expected operation id" + ); + assert_eq!(EXPECTED_OPERATION_ID, 3_389_142_194); +} + +#[test] +fn generated_source_exposes_the_profile_bound_value_codec() { + let source = generated_source(); + + for required_surface in [ + "pub const VALUE_CODEC_ID: &str = \"le-binary-v1\";", + "pub struct Id", + "pub struct Input", + "pub struct Output", + "pub fn encode_input", + "pub fn decode_output", + "pub fn pack_intent", + ] { + assert!( + source.contains(required_surface), + "generated source is missing `{required_surface}`" + ); + } +} + +#[test] +fn generated_source_constructs_a_non_authoritative_provider_package_proposal() { + let source = generated_source(); + + for required_surface in [ + "ProviderRegistryV1", + "ProviderMutationImplementationIdentityV1", + "pub const fn provider_registry", + "pub const fn mutation_implementation_identity", + "pub fn propose_contract_package", + "GeneratedProviderMutationDispatchV1::new", + "propose_provider_contract_package_v1", + ] { + assert!( + source.contains(required_surface), + "generated source is missing `{required_surface}`" + ); + } + + for forbidden_surface in [ + "RegistryProvider", + "OpDef", + "RewriteRule", + "InstalledContractPackage", + "Engine", + "TrustedRuntimeHost", + "register_contract_package", + "dispatch_optic_intent", + "submit_intent", + "tick_once", + ] { + assert!( + !source.contains(forbidden_surface), + "generated source exposes forbidden authority or facade `{forbidden_surface}`" + ); + } +} + +#[test] +fn generated_bundle_binding_is_a_bounded_executable_consumer_contract() { + let source = reviewed_edict_generated_source(); + assert_eq!( + source, + include_str!("fixtures/generated_echo_dpo.rs"), + "the checked generated helper must match the lowerer's exact output" + ); + + for forbidden_surface in [ + "pack_intent_raw_vars", + "raw_vars", + "register_contract_package", + "dispatch_optic_intent", + "submit_intent", + "tick_once", + ] { + assert!( + !source.contains(forbidden_surface), + "generated source exposes forbidden authority or bypass `{forbidden_surface}`" + ); + } +} + +#[test] +fn contract_bundle_binding_separates_semantic_and_release_digests() { + let source = generated_source(); + for line in source + .lines() + .filter(|line| line.contains(TARGET_BUNDLE_PROFILE_DIGEST)) + { + assert!( + !line.contains("CONTRACT_BUNDLE_DIGEST"), + "the target-bundle-profile digest was mislabeled as a final contract-bundle digest" + ); + } + assert!(source.contains(TARGET_BUNDLE_PROFILE)); + assert!(source.contains("TARGET_BUNDLE_PROFILE_DIGEST")); + assert!( + source.contains("semantic_digest"), + "contract-bundle identity lacks its semantic digest" + ); + assert!( + source.contains("release_digest"), + "contract-bundle identity lacks its release digest" + ); +} + +#[test] +fn review_declares_explicit_post_assembly_contract_bundle_binding() { + let review = review_json(); + assert!(review.contains(&format!( + "\"targetBundleProfile\":{{\"id\":\"{TARGET_BUNDLE_PROFILE}\",\"digest\":\"{TARGET_BUNDLE_PROFILE_DIGEST}\"}}" + ))); + assert!( + review.contains("\"contractBundle\":"), + "review does not distinguish the final contract bundle from its target-bundle profile" + ); + assert!( + review.contains("\"binding\":\"explicit-after-assembly\""), + "review does not state the explicit post-assembly binding law" + ); +} + +#[test] +fn invalid_output_requests_refuse_with_the_stable_first_subject() { + let cases = [ + ( + "wrong domain", + vec![output_request( + TARGET_IR_ROLE, + LoweringOutputKind::TargetIr, + "wrong.target-ir/v1", + )], + TARGET_IR_ROLE, + ), + ( + "wrong kind", + vec![output_request( + TARGET_IR_ROLE, + LoweringOutputKind::GeneratedArtifact, + OUTER_TARGET_IR_DOMAIN, + )], + TARGET_IR_ROLE, + ), + ( + "unknown role", + vec![output_request( + "unknown.echo-dpo", + LoweringOutputKind::TargetIr, + OUTER_TARGET_IR_DOMAIN, + )], + "unknown.echo-dpo", + ), + ( + "duplicate role", + vec![target_ir_request(), target_ir_request()], + TARGET_IR_ROLE, + ), + ( + "out-of-order roles", + vec![target_ir_request(), generated_artifact_request()], + GENERATED_ARTIFACT_ROLE, + ), + ]; + + for (label, requested_outputs, expected_subject) in cases { + let mut request = request(); + request.requested_outputs = requested_outputs; + let refusal = lower(request).expect_err(label); + assert_eq!( + refusal.kind, + ProviderRefusalKind::UnsupportedOutputRole, + "{label}" + ); + assert_eq!( + refusal.subject.as_deref(), + Some(expected_subject), + "{label}" + ); + } } #[test] diff --git a/crates/echo-edict-provider-verifier/README.md b/crates/echo-edict-provider-verifier/README.md index 301bf0df..32e4199b 100644 --- a/crates/echo-edict-provider-verifier/README.md +++ b/crates/echo-edict-provider-verifier/README.md @@ -21,9 +21,9 @@ target profile's diagnostic ABI and every emitted report consume one shared admitted identity. The `wasm32` guest adapter vendors Edict's exact frozen `edict:target-provider/verifier@1.0.0` WIT world and performs only exhaustive -transport-to-model conversion. Its reproducibly built 189,515-byte checked +transport-to-model conversion. Its reproducibly built 189,922-byte checked component has SHA-256 -`11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952`. +`4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a`. Component identity and admitted host replay remain separate propositions: the pinned Edict host preflights the request artifacts and declared output schema, invokes the checked component, then admits and manifests each returned accepted diff --git a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor index 807c2186..ef4787da 100644 --- a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor +++ b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor @@ -1 +1 @@ -fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file +fsourcedkindmtargetProfilefdigestfsha256X ?K=,i? s&6LqAjcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor index a3a7b681..bba8cf44 100644 Binary files a/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor and b/crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor differ diff --git a/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor b/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor index bb7a2d6e..a1b381f2 100644 --- a/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor +++ b/crates/echo-edict-provider-verifier/resources/generated-artifact-profile.echo-dpo-registration.cbor @@ -1 +1 @@ -etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputjvalueCodeclle-binary-v1koperationId4mopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1noperationIdLawx%echo.semantic-operation-id.fnv1-32/v1 \ No newline at end of file diff --git a/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor index 354071e7..af0722fb 100644 Binary files a/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor and b/crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor b/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor index b3ca7861..28493267 100644 Binary files a/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor and b/crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor differ diff --git a/crates/echo-edict-provider-verifier/src/semantic_resources.rs b/crates/echo-edict-provider-verifier/src/semantic_resources.rs index 36356c76..223a7e18 100644 --- a/crates/echo-edict-provider-verifier/src/semantic_resources.rs +++ b/crates/echo-edict-provider-verifier/src/semantic_resources.rs @@ -13,19 +13,19 @@ const PROFILE: ResourceSpec = ResourceSpec { coordinate: "echo.dpo@1", domain: "edict.target-profile/v1", bytes: include_bytes!("../resources/target-profile.echo-dpo.cbor"), - framed_sha256: "f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0", + framed_sha256: "ad7f10e1843f4b3d2c08b11d69df103f9c0b1b7388ae26bb364cc87106cd419e", }; const LAWPACK: ResourceSpec = ResourceSpec { coordinate: "echo.dpo-lawpack@1", domain: "edict.lawpack/v1", bytes: include_bytes!("../resources/lawpack.echo-dpo.cbor"), - framed_sha256: "47006be4c2b569f13759078c560fe508e158b429cd9d4c5e47d9a7ea86b983b2", + framed_sha256: "2a1631ae5fe2e11d09ccdfb94e3498f3236a3d615ee217e6cd5843efcd284987", }; const GENERATED_PROFILE: ResourceSpec = ResourceSpec { coordinate: "echo.dpo.registration/v1", domain: "echo.generated-artifact-profile/v1", bytes: include_bytes!("../resources/generated-artifact-profile.echo-dpo-registration.cbor"), - framed_sha256: "3377304d8634681821cd958427e0b8baccc37b7b08bfb342d988a08571eb83ab", + framed_sha256: "ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d", }; const LAWPACK_EXPORTS: ResourceSpec = ResourceSpec { coordinate: "echo.dpo-lawpack.exports@1", @@ -1583,15 +1583,15 @@ mod tests { let expected_raw = [ ( PROFILE, - "95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb", + "cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85", ), ( LAWPACK, - "df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad", + "679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1", ), ( GENERATED_PROFILE, - "8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3", + "4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34", ), ( LAWPACK_EXPORTS, diff --git a/crates/echo-edict-provider-verifier/tests/verifier_contract.rs b/crates/echo-edict-provider-verifier/tests/verifier_contract.rs index 801b3ba9..45ec57e8 100644 --- a/crates/echo-edict-provider-verifier/tests/verifier_contract.rs +++ b/crates/echo-edict-provider-verifier/tests/verifier_contract.rs @@ -183,11 +183,7 @@ fn request_with_target_ir( } fn request() -> VerificationRequestV1 { - request_with_target_ir( - hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) - .expect("Target IR fixture hex is valid"), - limits(64 * 1024), - ) + request_with_target_ir(canonical_bytes(&target_ir_value()), limits(64 * 1024)) } fn core_value() -> CanonicalValueV1 { @@ -199,11 +195,21 @@ fn core_value() -> CanonicalValueV1 { } fn target_ir_value() -> CanonicalValueV1 { - decode_canonical_cbor_v1( + let mut target_ir = decode_canonical_cbor_v1( &hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) .expect("Target IR fixture hex is valid"), ) - .expect("Target IR fixture is canonical") + .expect("Target IR fixture is canonical"); + let target_profile_digest = bound("echo.dpo@1", TARGET_PROFILE_DOMAIN, TARGET_PROFILE.to_vec()) + .reference + .digest + .bytes; + *map_field_mut(map_field_mut(&mut target_ir, "targetProfile"), "digest") = + CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes(target_profile_digest), + ]); + target_ir } fn bind_core(request: &mut VerificationRequestV1, core: &CanonicalValueV1) { @@ -402,19 +408,19 @@ fn packaged_semantic_resource_bytes_are_pinned() { let resources = [ ( TARGET_PROFILE, - "95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb", + "cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85", ), ( LAWPACK, - "df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad", + "679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1", ), ( TARGET_AUTHORITY, - "d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86", + "e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e", ), ( LAWPACK_AUTHORITY, - "3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96", + "022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc", ), ]; @@ -425,11 +431,7 @@ fn packaged_semantic_resource_bytes_are_pinned() { #[test] fn changed_target_intrinsic_is_rejected() { - let mut target_ir = decode_canonical_cbor_v1( - &hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) - .expect("Target IR fixture hex is valid"), - ) - .expect("Target IR fixture is canonical"); + let mut target_ir = target_ir_value(); let intent = map_field_mut(map_field_mut(&mut target_ir, "intents"), "t"); let CanonicalValueV1::Array(steps) = map_field_mut(intent, "steps") else { panic!("steps is not an array"); @@ -470,8 +472,7 @@ fn output_overclaim_names_the_first_unsupported_role() { fn repeated_verification_is_byte_identical() { let first = verify(request()).expect("first verification succeeds"); let second = verify(request_with_target_ir( - hex::decode(include_str!("fixtures/edict-target-ir.hex").trim()) - .expect("Target IR fixture hex is valid"), + canonical_bytes(&target_ir_value()), limits(1024 * 1024), )) .expect("second verification succeeds"); diff --git a/crates/echo-registry-api/README.md b/crates/echo-registry-api/README.md index 614a479d..1ece1cb9 100644 --- a/crates/echo-registry-api/README.md +++ b/crates/echo-registry-api/README.md @@ -3,15 +3,36 @@ # echo-registry-api -Generic registry interface for Echo WASM helpers. Provides the trait and data -types (`RegistryProvider`, `RegistryInfo`, `OpDef`) that an application-specific -registry crate implements. `warp-wasm` links only to this interface so Echo -stays generic; apps supply their own registry at build time. +Provider-neutral registry vocabulary for Echo-generated helpers. The crate has +two deliberately separate surfaces: + +- the Wesley/GraphQL compatibility surface (`RegistryProvider`, + `RegistryInfo`, and `OpDef`) that an application-specific registry crate + implements for `warp-wasm`; and +- the Edict provider surface (`ProviderRegistryV1`, `ProviderOperationV1`, and + their identity records), which borrows exact semantic, bundle, schema, + profile, Target IR, codec, obstruction, and footprint claims from a generated + helper. + +Neither surface gives this crate application semantics. `warp-wasm` links only +to the compatibility interface, while provider package preflight consumes the +separate borrowed Edict vocabulary without importing a GraphQL facade. `OpDef` preserves authored operation directive metadata as JSON. Echo admission tooling can interpret entries such as `wes_footprint`, but this crate only carries the data so the generic runtime boundary stays application-neutral. +## Operation identifiers + +`stable_semantic_operation_id_v1(...)` owns the generic, versioned derivation +from an exact semantic coordinate and query/mutation kind. The top two `u32` +values are reserved for Echo protocol envelopes: scheduler control at +`u32::MAX` and witnessed suffix import at `u32::MAX - 1`. +`is_reserved_operation_id(...)` is the shared fail-closed predicate for +generators and application-blind envelope construction. Generators must also +detect collisions across their complete operation set; a numeric schema range +cannot prove either derivation or collision freedom. + ## Contract artifact verification Hosts can call `verify_contract_artifact(...)` against a generated @@ -41,3 +62,20 @@ application adapters still own domain validation before packing EINT bytes. Generated compatibility metadata is install-time evidence only. It does not grant execution authority, query rights, or scheduler control, and it does not replace semantic reading identity. + +## Provider-generic registry + +`ProviderRegistryV1` is descriptive borrowed evidence retained by an opaque +provider package proposal. It contains no Wesley generator metadata, GraphQL +field vocabulary, executor capability, installation token, or runtime +authority. Each `ProviderOperationV1` binds the complete operation proposition: +semantic coordinate and kind, persisted id law and value, distinct input and +output schema/codec contracts, failure-to-obstruction mapping, Target IR, +target and generated profiles, operation profile, and abstract footprint +identity. + +`ProviderRegistryV1::operation_by_id(...)` fails closed: it returns `None` when +the id is absent _or_ duplicated instead of choosing one conflicting claim. +Constructing, inspecting, or matching this registry does not authenticate a +bundle, install a package, register an operation, invoke a callback, or prove +that a host implementation has the semantics it claims. diff --git a/crates/echo-registry-api/src/lib.rs b/crates/echo-registry-api/src/lib.rs index c9c3fc33..20ce5542 100644 --- a/crates/echo-registry-api/src/lib.rs +++ b/crates/echo-registry-api/src/lib.rs @@ -1,13 +1,24 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Minimal, generic registry interface for Echo WASM helpers. +//! Provider-neutral registry vocabulary for Echo-generated helpers. //! -//! The registry provider is supplied by the application (generated from the -//! GraphQL/Wesley IR). Echo core and `warp-wasm` depend only on this crate and -//! **must not** embed app-specific registries. +//! The crate keeps two surfaces separate: the application-supplied +//! [`RegistryProvider`] compatibility interface generated from GraphQL/Wesley +//! IR, and the borrowed provider-generic [`ProviderRegistryV1`] evidence emitted +//! from admitted Edict artifacts. Neither surface owns application semantics or +//! grants runtime installation authority. Echo core and `warp-wasm` depend only +//! on this crate and **must not** embed app-specific registries. #![no_std] +mod provider; + +pub use provider::{ + ProviderBundleIdentityV1, ProviderDigestIdentityV1, ProviderFootprintIdentityV1, + ProviderOperationV1, ProviderRegistryV1, ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, + ProviderValueContractV1, +}; + /// Local contract ABI version supported by this registry API. pub const ECHO_CONTRACT_ABI_VERSION: u32 = 1; /// Generated contract-host helper API version supported by Echo. @@ -220,6 +231,74 @@ pub enum OpKind { Mutation, } +/// Stable identifier for Echo's versioned universal little-endian value codec. +/// +/// The byte law is owned by ADR 0017 and implemented by `echo-wasm-abi`. +pub const LITTLE_ENDIAN_CODEC_V1_ID: &str = "le-binary-v1"; + +/// Versioned Echo law for deriving persisted operation ids from semantic +/// coordinates and generic operation kinds. +pub const SEMANTIC_OPERATION_ID_LAW_V1: &str = "echo.semantic-operation-id.fnv1-32/v1"; + +/// Echo operation id reserved for privileged scheduler-control envelopes. +pub const RESERVED_CONTROL_OPERATION_ID: u32 = u32::MAX; + +/// Echo operation id reserved for witnessed-suffix-import envelopes. +pub const RESERVED_IMPORT_SUFFIX_OPERATION_ID: u32 = u32::MAX - 1; + +/// Returns whether an operation id is reserved for an Echo protocol envelope. +/// +/// Application contracts and generated semantic operations must not use these +/// ids. Echo-owned privileged paths remain responsible for constructing the +/// corresponding protocol envelopes. +#[must_use] +pub const fn is_reserved_operation_id(operation_id: u32) -> bool { + operation_id == RESERVED_CONTROL_OPERATION_ID + || operation_id == RESERVED_IMPORT_SUFFIX_OPERATION_ID +} + +/// Derives a persisted Echo operation id from an exact semantic coordinate. +/// +/// The byte preimage is the UTF-8 law coordinate, one zero delimiter, the +/// generic kind byte (`0` for queries and `1` for mutations), and the exact +/// UTF-8 semantic coordinate. FNV-1 32-bit processes that complete preimage. +/// This law is deliberately domain-separated from the legacy Wesley derivation +/// over GraphQL field names. +/// +/// Generators remain responsible for refusing Echo protocol-reserved ids and +/// for detecting collisions across the complete generated operation set. They +/// must not salt, probe, or rename coordinates to repair a collision. +#[must_use] +pub const fn stable_semantic_operation_id_v1(kind: OpKind, semantic_coordinate: &str) -> u32 { + let mut hash = 2_166_136_261_u32; + let law = SEMANTIC_OPERATION_ID_LAW_V1.as_bytes(); + let mut index = 0; + while index < law.len() { + hash = semantic_operation_id_fnv1_step(hash, law[index]); + index += 1; + } + hash = semantic_operation_id_fnv1_step(hash, 0); + hash = semantic_operation_id_fnv1_step( + hash, + match kind { + OpKind::Query => 0, + OpKind::Mutation => 1, + }, + ); + + let coordinate = semantic_coordinate.as_bytes(); + index = 0; + while index < coordinate.len() { + hash = semantic_operation_id_fnv1_step(hash, coordinate[index]); + index += 1; + } + hash +} + +const fn semantic_operation_id_fnv1_step(hash: u32, byte: u8) -> u32 { + hash.wrapping_mul(16_777_619) ^ byte as u32 +} + /// Descriptor for a single operation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OpDef { diff --git a/crates/echo-registry-api/src/provider.rs b/crates/echo-registry-api/src/provider.rs new file mode 100644 index 00000000..01be6cc6 --- /dev/null +++ b/crates/echo-registry-api/src/provider.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! Provider-generic semantic registry vocabulary. +//! +//! These borrowed values describe exact provider artifact claims. Constructing +//! or inspecting them does not authenticate, admit, install, register, invoke, +//! or execute any operation. + +use crate::OpKind; + +/// Exact identity of one digest-framed provider artifact. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderDigestIdentityV1<'a> { + /// Semantic or artifact coordinate named by the digest claim. + pub coordinate: &'a str, + /// Proposition domain used to frame the digest. + pub digest_domain: &'a str, + /// Exact encoded digest value, including its algorithm prefix. + pub digest: &'a str, +} + +/// Exact identity of the admitted provider schema used by generated helpers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderSchemaIdentityV1<'a> { + /// Coordinate of the provider schema artifact. + pub coordinate: &'a str, + /// Lowercase hexadecimal SHA-256 of the exact raw schema bytes. + pub raw_sha256_hex: &'a str, +} + +/// Exact coordinate and semantic domain of one provider-owned meaning. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderSemanticIdentityV1<'a> { + /// Provider-owned semantic coordinate. + pub coordinate: &'a str, + /// Domain that defines the coordinate's meaning. + pub semantic_domain: &'a str, +} + +/// Schema and codec claims for one operation value boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderValueContractV1<'a> { + /// Semantic schema coordinate for the value. + pub schema_coordinate: &'a str, + /// Semantic domain that defines the value schema. + pub schema_domain: &'a str, + /// Exact codec identifier selected for the value bytes. + pub codec_id: &'a str, +} + +/// Independent semantic-layer and release-layer provider bundle identities. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderBundleIdentityV1<'a> { + /// Proposition domain used to frame the semantic bundle digest. + pub semantic_digest_domain: &'a str, + /// Exact semantic bundle digest. + pub semantic_digest: &'a str, + /// Proposition domain used to frame the release bundle digest. + pub release_digest_domain: &'a str, + /// Exact release bundle digest. + pub release_digest: &'a str, +} + +/// Exact abstract footprint obligation and its owning algebra artifact. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderFootprintIdentityV1<'a> { + /// Abstract footprint obligation authored for the operation. + pub obligation: &'a str, + /// Coordinate of the footprint algebra artifact. + pub algebra_coordinate: &'a str, + /// Proposition domain used to frame the footprint algebra digest. + pub algebra_digest_domain: &'a str, + /// Exact footprint algebra digest. + pub algebra_digest: &'a str, +} + +/// Exact semantic, generated, and target claims for one provider operation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderOperationV1<'a> { + /// Semantic operation coordinate. + pub coordinate: &'a str, + /// Domain that defines the operation coordinate. + pub semantic_domain: &'a str, + /// Whether the operation mutates state or performs a bounded read. + pub kind: OpKind, + /// Law used to derive the persisted operation identifier. + pub operation_id_law: &'a str, + /// Exact persisted operation identifier. + pub operation_id: u32, + /// Input schema and codec claims. + pub input: ProviderValueContractV1<'a>, + /// Output schema and codec claims. + pub output: ProviderValueContractV1<'a>, + /// Target-owned schema for a failure before obstruction mapping. + pub target_failure_schema: &'a str, + /// Provider-owned typed obstruction produced by failure mapping. + pub obstruction: ProviderSemanticIdentityV1<'a>, + /// Provider-owned payload schema for the typed obstruction. + pub obstruction_payload_schema: &'a str, + /// Exact Target IR artifact identity. + pub target_ir: ProviderDigestIdentityV1<'a>, + /// Exact target-profile artifact identity. + pub target_profile: ProviderDigestIdentityV1<'a>, + /// Exact generated-artifact-profile identity. + pub generated_artifact_profile: ProviderDigestIdentityV1<'a>, + /// Semantic operation profile selected for this operation. + pub operation_profile: ProviderSemanticIdentityV1<'a>, + /// Exact operation-profiles document identity. + pub operation_profiles: ProviderDigestIdentityV1<'a>, + /// Exact footprint obligation and algebra claims. + pub footprint: ProviderFootprintIdentityV1<'a>, +} + +/// Borrowed provider-generic registry retained by a package proposal. +/// +/// This registry intentionally contains no GraphQL facade, Wesley generator +/// metadata, runtime installation token, or executor authority. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderRegistryV1<'a> { + /// Echo contract ABI version targeted by generated helpers. + pub echo_contract_abi_version: u32, + /// Contract-host helper API version targeted by generated helpers. + pub helper_api_version: u32, + /// Exact raw provider schema identity. + pub provider_schema: ProviderSchemaIdentityV1<'a>, + /// Exact target-bundle-profile artifact identity. + pub target_bundle_profile: ProviderDigestIdentityV1<'a>, + /// Independent semantic-layer and release-layer bundle identities. + pub bundle: ProviderBundleIdentityV1<'a>, + /// Provider operations described by this registry. + pub operations: &'a [ProviderOperationV1<'a>], +} + +impl<'a> ProviderRegistryV1<'a> { + /// Look up one operation by its exact persisted identifier. + #[must_use] + pub fn operation_by_id(&self, operation_id: u32) -> Option<&ProviderOperationV1<'a>> { + let mut found = None; + for operation in self.operations { + if operation.operation_id != operation_id { + continue; + } + if found.is_some() { + return None; + } + found = Some(operation); + } + found + } +} diff --git a/crates/echo-registry-api/tests/provider_registry.rs b/crates/echo-registry-api/tests/provider_registry.rs new file mode 100644 index 00000000..9f732ff8 --- /dev/null +++ b/crates/echo-registry-api/tests/provider_registry.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Provider-generic registry contract witnesses. + +use echo_registry_api::{ + OpKind, ProviderBundleIdentityV1, ProviderDigestIdentityV1, ProviderFootprintIdentityV1, + ProviderOperationV1, ProviderRegistryV1, ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, + ProviderValueContractV1, CONTRACT_HOST_HELPER_API_VERSION, ECHO_CONTRACT_ABI_VERSION, + LITTLE_ENDIAN_CODEC_V1_ID, SEMANTIC_OPERATION_ID_LAW_V1, +}; + +const TARGET_IR: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.span-ir/v1", + digest_domain: "edict.target-ir.artifact/v1", + digest: "sha256:target-ir", +}; + +const TARGET_PROFILE: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo@1", + digest_domain: "edict.target-profile/v1", + digest: "sha256:target-profile", +}; + +const GENERATED_PROFILE: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo.registration/v1", + digest_domain: "echo.generated-artifact-profile/v1", + digest: "sha256:generated-profile", +}; + +const OPERATION_PROFILES: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo.operation-profiles/v1", + digest_domain: "echo.dpo.operation-profiles/v1", + digest: "sha256:operation-profiles", +}; + +const OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { + coordinate: "a.b@1.t", + semantic_domain: "echo.edict-provider/operation/v1", + kind: OpKind::Mutation, + operation_id_law: SEMANTIC_OPERATION_ID_LAW_V1, + operation_id: 3_389_142_194, + input: ProviderValueContractV1 { + schema_coordinate: "a.b@1.Input", + schema_domain: "echo.edict-provider/value/v1", + codec_id: LITTLE_ENDIAN_CODEC_V1_ID, + }, + output: ProviderValueContractV1 { + schema_coordinate: "a.b@1.Output", + schema_domain: "echo.edict-provider/value/v1", + codec_id: LITTLE_ENDIAN_CODEC_V1_ID, + }, + target_failure_schema: "target.replace.rejected", + obstruction: ProviderSemanticIdentityV1 { + coordinate: "domain.WriteRejected", + semantic_domain: "echo.edict-provider/obstruction/v1", + }, + obstruction_payload_schema: "domain.WriteRejected.Payload", + target_ir: TARGET_IR, + target_profile: TARGET_PROFILE, + generated_artifact_profile: GENERATED_PROFILE, + operation_profile: ProviderSemanticIdentityV1 { + coordinate: "continuum.profile.write/v1", + semantic_domain: "echo.edict-provider/operation-profile/v1", + }, + operation_profiles: OPERATION_PROFILES, + footprint: ProviderFootprintIdentityV1 { + obligation: "target.replace.footprint", + algebra_coordinate: "echo.dpo.footprint/v1", + algebra_digest_domain: "echo.dpo.footprint/v1", + algebra_digest: "sha256:footprint-algebra", + }, +}]; + +#[test] +fn provider_registry_represents_semantic_operation_without_graphql_facade() { + let registry = ProviderRegistryV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: ProviderSchemaIdentityV1 { + coordinate: "echo.provider-artifacts.cddl@1", + raw_sha256_hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + target_bundle_profile: ProviderDigestIdentityV1 { + coordinate: "echo.dpo.bundle/v1", + digest_domain: "echo.dpo.bundle/v1", + digest: "sha256:target-bundle-profile", + }, + bundle: ProviderBundleIdentityV1 { + semantic_digest_domain: "edict.bundle.semantic/v1", + semantic_digest: "sha256:semantic-bundle", + release_digest_domain: "edict.bundle.release/v1", + release_digest: "sha256:release-bundle", + }, + operations: &OPERATIONS, + }; + + assert_eq!( + registry.operation_by_id(3_389_142_194), + Some(&OPERATIONS[0]) + ); + let operation = &OPERATIONS[0]; + + assert_eq!(operation.coordinate, "a.b@1.t"); + assert_eq!(operation.kind, OpKind::Mutation); + assert_eq!(operation.input.codec_id, LITTLE_ENDIAN_CODEC_V1_ID); + assert!(registry.operation_by_id(7).is_none()); +} + +#[test] +fn provider_registry_does_not_select_an_ambiguous_operation_id() { + let operations = [ + OPERATIONS[0], + ProviderOperationV1 { + coordinate: "a.b@1.conflicting", + ..OPERATIONS[0] + }, + ]; + let registry = ProviderRegistryV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: ProviderSchemaIdentityV1 { + coordinate: "echo.provider-artifacts.cddl@1", + raw_sha256_hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + target_bundle_profile: ProviderDigestIdentityV1 { + coordinate: "echo.dpo.bundle/v1", + digest_domain: "echo.dpo.bundle/v1", + digest: "sha256:target-bundle-profile", + }, + bundle: ProviderBundleIdentityV1 { + semantic_digest_domain: "edict.bundle.semantic/v1", + semantic_digest: "sha256:semantic-bundle", + release_digest_domain: "edict.bundle.release/v1", + release_digest: "sha256:release-bundle", + }, + operations: &operations, + }; + + assert!(registry.operation_by_id(3_389_142_194).is_none()); +} diff --git a/crates/echo-registry-api/tests/semantic_operation_id_contract.rs b/crates/echo-registry-api/tests/semantic_operation_id_contract.rs new file mode 100644 index 00000000..e8072d06 --- /dev/null +++ b/crates/echo-registry-api/tests/semantic_operation_id_contract.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Executable contract for generic semantic operation identifiers. + +use echo_registry_api::{ + is_reserved_operation_id, stable_semantic_operation_id_v1, OpKind, + RESERVED_CONTROL_OPERATION_ID, RESERVED_IMPORT_SUFFIX_OPERATION_ID, + SEMANTIC_OPERATION_ID_LAW_V1, +}; + +const MUTATION_ID: u32 = stable_semantic_operation_id_v1(OpKind::Mutation, "a.b@1.t"); +const READ_ID: u32 = stable_semantic_operation_id_v1(OpKind::Query, "a.b@1.read"); + +fn legacy_wesley_fnv1_id(kind_rank: u8, field_name: &str) -> u32 { + let mut hash = 2_166_136_261_u32; + for byte in std::iter::once(kind_rank).chain(field_name.bytes()) { + hash = hash.wrapping_mul(16_777_619) ^ u32::from(byte); + } + hash +} + +#[test] +fn semantic_operation_id_v1_is_domain_separated_and_byte_exact() { + assert_eq!( + SEMANTIC_OPERATION_ID_LAW_V1, + "echo.semantic-operation-id.fnv1-32/v1" + ); + assert_eq!(MUTATION_ID, 3_389_142_194); + assert_eq!(READ_ID, 2_012_636_359); + assert_eq!(RESERVED_CONTROL_OPERATION_ID, u32::MAX); + assert_eq!(RESERVED_IMPORT_SUFFIX_OPERATION_ID, u32::MAX - 1); + assert!(is_reserved_operation_id(RESERVED_CONTROL_OPERATION_ID)); + assert!(is_reserved_operation_id( + RESERVED_IMPORT_SUFFIX_OPERATION_ID + )); + assert!(!is_reserved_operation_id(u32::MAX - 2)); + assert_ne!(MUTATION_ID, READ_ID); + + assert_ne!(MUTATION_ID, legacy_wesley_fnv1_id(1, "a.b@1.t")); + assert_ne!( + stable_semantic_operation_id_v1(OpKind::Query, "same.coordinate"), + stable_semantic_operation_id_v1(OpKind::Mutation, "same.coordinate") + ); + assert_ne!( + stable_semantic_operation_id_v1(OpKind::Mutation, "\u{00e9}"), + stable_semantic_operation_id_v1(OpKind::Mutation, "e\u{0301}") + ); +} diff --git a/crates/echo-wasm-abi/Cargo.toml b/crates/echo-wasm-abi/Cargo.toml index 4f2829bb..38ae9ee0 100644 --- a/crates/echo-wasm-abi/Cargo.toml +++ b/crates/echo-wasm-abi/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["echo", "warp", "wasm", "abi", "dto"] categories = ["wasm", "encoding", "data-structures"] [dependencies] +echo-registry-api = { workspace = true } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } ciborium = { version = "0.2", default-features = false } serde-value = { version = "0.7" } diff --git a/crates/echo-wasm-abi/src/lib.rs b/crates/echo-wasm-abi/src/lib.rs index f64151cf..7d33abb9 100644 --- a/crates/echo-wasm-abi/src/lib.rs +++ b/crates/echo-wasm-abi/src/lib.rs @@ -72,13 +72,14 @@ mod witnessed_suffix_tests; pub mod codec; /// Reserved EINT op id for privileged control intents. -pub const CONTROL_INTENT_V1_OP_ID: u32 = u32::MAX; +pub const CONTROL_INTENT_V1_OP_ID: u32 = echo_registry_api::RESERVED_CONTROL_OPERATION_ID; /// Reserved Echo-owned EINT op id for proposing witnessed suffix import. /// /// Transport arrival is host I/O until the bundle is wrapped in this canonical /// intent envelope and admitted through Echo scheduling. -pub const IMPORT_SUFFIX_INTENT_V1_OP_ID: u32 = u32::MAX - 1; +pub const IMPORT_SUFFIX_INTENT_V1_OP_ID: u32 = + echo_registry_api::RESERVED_IMPORT_SUFFIX_OPERATION_ID; /// Errors produced by the Intent Envelope parser. #[derive(Debug, PartialEq, Eq)] @@ -93,7 +94,7 @@ pub enum EnvelopeError { Malformed, /// Payload length exceeds u32::MAX. PayloadTooLarge, - /// Public application envelopes may not use the reserved control op id. + /// Public application envelopes may not use Echo protocol-reserved op ids. ReservedOpId, } @@ -105,7 +106,7 @@ impl core::fmt::Display for EnvelopeError { Self::LengthMismatch => f.write_str("envelope length mismatch"), Self::Malformed => f.write_str("malformed envelope"), Self::PayloadTooLarge => f.write_str("payload exceeds u32::MAX"), - Self::ReservedOpId => f.write_str("reserved control op id is not allowed here"), + Self::ReservedOpId => f.write_str("reserved Echo protocol op id is not allowed here"), } } } @@ -128,10 +129,10 @@ fn pack_envelope_v1_raw(op_id: u32, vars: &[u8]) -> Result, EnvelopeErro /// /// # Errors /// Returns [`EnvelopeError::PayloadTooLarge`] if `vars.len()` exceeds -/// `u32::MAX`, or [`EnvelopeError::ReservedOpId`] if `op_id` is the reserved -/// control envelope id. +/// `u32::MAX`, or [`EnvelopeError::ReservedOpId`] if `op_id` is reserved for an +/// Echo protocol envelope. pub fn pack_intent_v1(op_id: u32, vars: &[u8]) -> Result, EnvelopeError> { - if op_id == CONTROL_INTENT_V1_OP_ID { + if echo_registry_api::is_reserved_operation_id(op_id) { return Err(EnvelopeError::ReservedOpId); } pack_envelope_v1_raw(op_id, vars) @@ -227,7 +228,7 @@ pub fn pack_import_suffix_intent_v1( request: &kernel_port::ImportSuffixRequest, ) -> Result, EnvelopeError> { let bytes = encode_cbor(request).map_err(|_| EnvelopeError::Malformed)?; - pack_intent_v1(IMPORT_SUFFIX_INTENT_V1_OP_ID, &bytes) + pack_envelope_v1_raw(IMPORT_SUFFIX_INTENT_V1_OP_ID, &bytes) } /// Unpacks and validates a witnessed suffix import proposal from EINT v1 bytes. @@ -506,11 +507,13 @@ mod tests { } #[test] - fn test_pack_intent_rejects_reserved_control_op_id() { - assert_eq!( - pack_intent_v1(CONTROL_INTENT_V1_OP_ID, b"reserved"), - Err(EnvelopeError::ReservedOpId) - ); + fn test_pack_intent_rejects_reserved_echo_protocol_op_ids() { + for operation_id in [CONTROL_INTENT_V1_OP_ID, IMPORT_SUFFIX_INTENT_V1_OP_ID] { + assert_eq!( + pack_intent_v1(operation_id, b"reserved"), + Err(EnvelopeError::ReservedOpId) + ); + } } #[test] @@ -618,7 +621,7 @@ mod tests { #[test] fn test_import_suffix_intent_rejects_malformed_payload() { - let packed = pack_intent_v1(IMPORT_SUFFIX_INTENT_V1_OP_ID, &[0xff]).unwrap(); + let packed = pack_envelope_v1_raw(IMPORT_SUFFIX_INTENT_V1_OP_ID, &[0xff]).unwrap(); assert_eq!( unpack_import_suffix_intent_v1(&packed), diff --git a/crates/echo-wesley-gen/Cargo.toml b/crates/echo-wesley-gen/Cargo.toml index a72cd6dd..22b5c474 100644 --- a/crates/echo-wesley-gen/Cargo.toml +++ b/crates/echo-wesley-gen/Cargo.toml @@ -22,6 +22,7 @@ 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 } +echo-registry-api = { workspace = true } hex = "0.4" proc-macro2 = "1.0" quote = "1.0" diff --git a/crates/echo-wesley-gen/README.md b/crates/echo-wesley-gen/README.md index f7d3286e..e57dceea 100644 --- a/crates/echo-wesley-gen/README.md +++ b/crates/echo-wesley-gen/README.md @@ -8,8 +8,9 @@ functions from Wesley contract data. Wesley is the compiler seam between authored application contracts and Echo's generic runtime. Generated application helpers build canonical intent/query -requests. Generated contract-host helpers install mutation handlers and -read-only query observers. Neither surface gives application code tick +requests. Generated contract-host helpers build mutation-rule and read-only +observer material for a trusted Echo host to inspect and install; the helpers +do not install themselves. Neither surface gives application code tick authority. The preferred input is GraphQL SDL lowered directly through the published @@ -142,7 +143,7 @@ unique expected inventory before resolving the root, caps that inventory at 256 files and 64 MiB, caps an actual scan at 1,024 entries, and never opens or reads an unexpected regular file. -`echo-edict-provider-assets` maintains the exact 35-file package-local carrier +`echo-edict-provider-assets` maintains the exact 38-file package-local carrier tree under `assets/v1/`. The physical carrier names are packaging locations, not replacement source identities: generator provenance continues to name the original repository-relative authored paths. Read-only mode requires every @@ -151,8 +152,21 @@ to match their checked package copies, and can prove that `cargo package --list` selects exactly the complete carrier inventory. Explicit `--write` mode copies authoritative owners without requiring the temporarily stale package copy, allowing the honest staged sequence artifact generation, carrier sync, package -generation, then final carrier corroboration. It never discovers a preferred -owner or normalizes authored bytes. +generation, then final carrier corroboration. Each fixed owner leaf is opened +without following its final symbolic link and read twice through the same +retained descriptor; file-type, length, or byte disagreement refuses a moving +owner. It never discovers a preferred owner or normalizes authored bytes. The +20-file generator source closure and its carriers include the exact manifest +and implementation occurrences for both `echo-edict-canonical` and +`echo-registry-api`, including the provider-generic registry vocabulary, so +provenance binds the canonicalization, operation-id, and registry laws actually +executed by provider generation. + +`echo-edict-provider-assets -- --sync-component-resources` adds the lowerer and +verifier resource trees to the ordinary package-carrier operation. Without +`--write` it checks both the carrier and exact component resources; with +`--write` it synchronizes both. `--check-package-list` composes with the same +invocation, so no accepted flag is silently skipped. The isolated `tests/edict-provider-host-v1` gate pins Edict revision `c75c3f550d049485ba00eae0dc272c6dd6aca11f` and consumes the exact checked @@ -190,8 +204,7 @@ cat ir.json | cargo run -p echo-wesley-gen -- # Write to a file cat ir.json | cargo run -p echo-wesley-gen -- --out generated.rs -# Emit std-only warp-core contract-host helpers for installed mutation handlers -# and query observers +# Emit std-only warp-core contract-host material for trusted host installation cat ir.json | cargo run -p echo-wesley-gen -- --contract-host --out generated.rs # Rebuild the checked Edict provider artifact corpus from exact inputs @@ -221,21 +234,43 @@ cargo +1.90.0 run --locked -p echo-wesley-gen \ operation argument shape, and hosts can verify them through `echo_registry_api::verify_contract_artifact` before treating the generated artifact as compile-time-certified. +- Edict provider profiles carry the Echo-owned + `echo.semantic-operation-id.fnv1-32/v1` law and exact `u32` id derived from + the semantic operation coordinate plus generic query/mutation kind. The law + is separate from GraphQL field identity and reserves the top two ids for Echo + protocol envelopes: `u32::MAX` for scheduler control and `u32::MAX - 1` for + witnessed suffix import. Generation refuses either reserved result and + package-local collisions without salting or probing. The generated CDDL + constrains `operationId` to `0..4294967293`; that numeric-domain proof does not + replace semantic recomputation of the coordinate-and-kind law or complete-set + collision checking. Carrying the id in canonical package content does not + register, install, authorize, or execute an operation. +- The checked Edict helper implements the profile-owned `le-binary-v1` codec + with distinct bounded `Id`, `Input`, and `Output` types, fail-closed decoding, + and canonical EINT v1 packing. Its EINT `vars` bytes remain opaque and + codec-owned. After exact bundle binding it exposes a borrowed provider-generic + registry and can preflight one explicit host mutation implementation into an + opaque package proposal. The proposal cross-binds Target IR, bundle, profile, + value, obstruction, ABI, helper, rule, and footprint identities but neither + proves arbitrary callback semantics nor installs itself. Query refusal is + specific to this mutation proposal; authored reads remain a separate bounded + observer/optic path. - GraphQL SDL operation ids are derived deterministically and fail closed on - collision. The generator never increments a collided id because operation ids - are persisted ABI. + collision or either Echo protocol reservation. The generator never increments + a collided id because operation ids are persisted ABI. - Generated query optic helpers use Echo ABI's domain-separated BLAKE3 `query_vars_digest_v1(...)`; ad hoc variable digests are not accepted for retained reading identity. -- `--contract-host` emits opt-in, std-only mutation helpers for installing - generated operations as `warp-core` command rules. The generated surface +- `--contract-host` emits opt-in, std-only mutation helpers that construct + `warp-core` command rules for trusted host installation. The generated surface matches scheduler-materialized EINT runtime ingress events by op id, decodes typed vars, provides the base runtime-ingress read footprint, and builds a `RewriteRule` from host-supplied executor and footprint functions. It does not generate the application mutation body or grant application code tick authority. -- `--contract-host` also emits std-only query observer helpers for installing - generated queries as read-only `warp-core::ContractQueryObserver` instances. +- `--contract-host` also emits std-only query observer helpers that construct + read-only `warp-core::ContractQueryObserver` instances for trusted host + installation. The generated surface stamps deterministic authored observer plan identity, decodes typed vars from observer context with `Result`, and accepts a host closure that returns `ContractQueryObserverResult` or diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm index c98bfb79..5f536455 100644 Binary files a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm and b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm differ diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm index 65128439..5e1224f6 100644 Binary files a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm and b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm differ diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 8d25de7f..2c481917 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json index 3c71a269..a298a191 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/review.provider-generation.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","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:b9c6d845e151f455bc7d0d6e7d209e7fbe92e3fc592145227cbb8e9b3e59c7b2","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","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:2874523930a963f81204df9c39aca6f9b62edbd45e5512ef6903346f7efa7c3e","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor index 807c2186..ef4787da 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor @@ -1 +1 @@ -fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file +fsourcedkindmtargetProfilefdigestfsha256X ?K=,i? s&6LqAjcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor index a3a7b681..bba8cf44 100644 Binary files a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor and b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor differ diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor index bb7a2d6e..a1b381f2 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor @@ -1 +1 @@ -etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputjvalueCodeclle-binary-v1koperationId4mopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1noperationIdLawx%echo.semantic-operation-id.fnv1-32/v1 \ No newline at end of file diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor index 354071e7..af0722fb 100644 Binary files a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor and b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor differ diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl index 900e2fc2..380abbe7 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl @@ -800,6 +800,7 @@ target-ir-step = { generated-artifact-profile = { apiVersion: "echo.generated-artifact-profile/v1", targetProfile: tstr, + operationIdLaw: "echo.semantic-operation-id.fnv1-32/v1", types: { * tstr => echo-generated-type }, operations: { * tstr => generated-operation }, } @@ -817,6 +818,8 @@ echo-generated-record = { echo-generated-record-field = { name: tstr, type: tstr } generated-operation = { + operationId: 0..4294967293, + valueCodec: "le-binary-v1", inputType: tstr, outputType: tstr, effect: tstr, diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor index b3ca7861..28493267 100644 Binary files a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor and b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor differ diff --git a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json index b95686ea..8782587e 100644 --- a/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json +++ b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/provider-manifest.echo.json @@ -3,7 +3,7 @@ "providerAbi": "edict:target-provider@1.0.0", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:38dd0532d4353f6260e8aaeafb149457b42f8ad94bb107ecbb74bf02f0051a92" + "digest": "sha256:6d99fc4b4a56268b053226f17dfaa946bec6a3328219393c21fdb3398db6a925" }, "artifacts": [ { @@ -11,7 +11,7 @@ "artifactKind": "authorityFacts", "resource": { "coordinate": "echo.dpo-authority-facts@1", - "digest": "sha256:5c30871b115e82415980714281e250c2fda281afb4a4fe16c2917f93e86182a4" + "digest": "sha256:724d9f0cf79d2f3815902b1aba8d811add4c00f45c93a7c8c6b05ff2f7ed4c29" }, "source": { "kind": "generated", @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -30,7 +30,7 @@ "artifactKind": "authorityFacts", "resource": { "coordinate": "echo.dpo-lawpack-authority-facts@1", - "digest": "sha256:37591478584a88d3dc08a620873d803a2d6fcf5dc70767aae3587e5862ee99e9" + "digest": "sha256:147a07c38d257f6c949ad3890f3071049b79771b268d2e0d0fb1ad846e235209" }, "source": { "kind": "generated", @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -49,7 +49,7 @@ "artifactKind": "generatedArtifactProfile", "resource": { "coordinate": "echo.dpo.registration/v1", - "digest": "sha256:3377304d8634681821cd958427e0b8baccc37b7b08bfb342d988a08571eb83ab" + "digest": "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d" }, "source": { "kind": "generated", @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -68,7 +68,7 @@ "artifactKind": "lawpack", "resource": { "coordinate": "echo.dpo-lawpack@1", - "digest": "sha256:47006be4c2b569f13759078c560fe508e158b429cd9d4c5e47d9a7ea86b983b2" + "digest": "sha256:2a1631ae5fe2e11d09ccdfb94e3498f3236a3d615ee217e6cd5843efcd284987" }, "source": { "kind": "generated", @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326" + "digest": "sha256:9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326" + "digest": "sha256:9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:5cb085e1a35d01363a629b6933e7e88e96b38a2d841848d68320e9fb0fba8580" + "digest": "sha256:830978048785a7b59ab5ebc65ab10a09211c43394bd3e970534f2dc4d6c25982" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:b9b5c7715089ba24c6ce15d9f8674a7ab2ed95225ff8127952eb4d3065d32f6c" + "digest": "sha256:c5092ee28a0fe0df0265e5f0313e72212b96578c069276ecbd534a6bc3fc9017" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af" + "digest": "sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -159,7 +159,7 @@ "artifactKind": "targetProfile", "resource": { "coordinate": "echo.dpo@1", - "digest": "sha256:f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0" + "digest": "sha256:ad7f10e1843f4b3d2c08b11d69df103f9c0b1b7388ae26bb364cc87106cd419e" }, "source": { "kind": "generated", @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -178,13 +178,13 @@ "artifactKind": "verifier", "resource": { "coordinate": "echo.dpo.verifier/component@1", - "digest": "sha256:11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952" + "digest": "sha256:4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.verifier/component@1", - "digest": "sha256:11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952" + "digest": "sha256:4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a" } } } diff --git a/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source b/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source index 87729c4c..dee1e866 100644 --- a/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source +++ b/crates/echo-wesley-gen/assets/v1/repository/Cargo.lock.source @@ -737,6 +737,7 @@ version = "0.1.0" dependencies = [ "blake3", "ciborium", + "echo-registry-api", "half", "proptest", "serde", @@ -757,6 +758,7 @@ dependencies = [ "ciborium", "clap", "echo-edict-canonical", + "echo-registry-api", "hex", "prettyplease", "proc-macro2", diff --git a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-edict-canonical/src/lib.rs.source b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-edict-canonical/src/lib.rs.source index 53815795..891f42b8 100644 --- a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-edict-canonical/src/lib.rs.source +++ b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-edict-canonical/src/lib.rs.source @@ -187,6 +187,25 @@ pub fn digest_canonical_value_v1( domain: &str, value: &CanonicalValueV1, ) -> Result { + Ok(format!( + "sha256:{}", + hex::encode(digest_canonical_value_bytes_v1(domain, value)?) + )) +} + +/// Computes the typed bytes of an Edict v1 domain-framed SHA-256 digest. +/// +/// This is the wire-form counterpart of [`digest_canonical_value_v1`]. The +/// preimage and authority boundary are identical; only the returned rendering +/// differs. +/// +/// # Errors +/// +/// Returns a stable failure for an empty domain or an unencodable value. +pub fn digest_canonical_value_bytes_v1( + domain: &str, + value: &CanonicalValueV1, +) -> Result<[u8; 32], CanonicalValueError> { if domain.is_empty() { return Err(CanonicalValueError::new( CanonicalValueErrorKind::UnsupportedValue, @@ -203,7 +222,7 @@ pub fn digest_canonical_value_v1( ))?); preimage.extend(encode_canonical_cbor_v1(value)?); - Ok(format!("sha256:{}", hex::encode(Sha256::digest(preimage)))) + Ok(Sha256::digest(preimage).into()) } fn encode_value( diff --git a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/Cargo.toml.source b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/Cargo.toml.source new file mode 100644 index 00000000..906db407 --- /dev/null +++ b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/Cargo.toml.source @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# © James Ross Ω FLYING•ROBOTS +[package] +name = "echo-registry-api" +version = "0.1.0" +edition = "2021" +rust-version = "1.90.0" +license = "Apache-2.0" +description = "Generic registry interface for Echo WASM helpers (schema hash, codec, ops catalog)" +repository = "https://github.com/flyingrobots/echo" +readme = "README.md" +keywords = ["echo", "wasm", "registry"] +categories = ["wasm", "development-tools"] + +[dependencies] + +[features] +default = ["std"] +std = [] + +[lints] +workspace = true diff --git a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/lib.rs.source b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/lib.rs.source new file mode 100644 index 00000000..20ce5542 --- /dev/null +++ b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/lib.rs.source @@ -0,0 +1,934 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Provider-neutral registry vocabulary for Echo-generated helpers. +//! +//! The crate keeps two surfaces separate: the application-supplied +//! [`RegistryProvider`] compatibility interface generated from GraphQL/Wesley +//! IR, and the borrowed provider-generic [`ProviderRegistryV1`] evidence emitted +//! from admitted Edict artifacts. Neither surface owns application semantics or +//! grants runtime installation authority. Echo core and `warp-wasm` depend only +//! on this crate and **must not** embed app-specific registries. + +#![no_std] + +mod provider; + +pub use provider::{ + ProviderBundleIdentityV1, ProviderDigestIdentityV1, ProviderFootprintIdentityV1, + ProviderOperationV1, ProviderRegistryV1, ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, + ProviderValueContractV1, +}; + +/// Local contract ABI version supported by this registry API. +pub const ECHO_CONTRACT_ABI_VERSION: u32 = 1; +/// Generated contract-host helper API version supported by Echo. +pub const CONTRACT_HOST_HELPER_API_VERSION: u32 = 1; + +/// Codec identifier used by the registry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RegistryInfo { + /// Echo contract ABI version targeted by the generated artifact. + pub echo_abi_version: u32, + /// Canonical codec identifier (e.g., "cbor-canon-v1"). + pub codec_id: &'static str, + /// Registry schema version for breaking changes in layout. + pub registry_version: u32, + /// Hex-encoded schema hash (lowercase, 64 chars). + pub schema_sha256_hex: &'static str, + /// Wesley generator version that emitted this registry. + pub wesley_generator_version: &'static str, + /// Generated contract-host helper API version. + pub helper_api_version: u32, +} + +/// Trust posture assigned after a generated contract artifact has been verified. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContractArtifactTrustPosture { + /// The registry artifact matched expected schema, codec, and registry + /// version, but the host policy did not request or prove the stronger + /// compile-time footprint certificate posture. + MetadataVerified, + /// The registry artifact matched the expected schema, codec, registry + /// version, and footprint certificate set supplied by the host policy. + CompileTimeCertified, +} + +/// One footprint certificate expected by the host for a generated operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExpectedFootprintCertificate<'a> { + /// Operation identifier that must exist in the generated registry. + pub op_id: u32, + /// Expected footprint certificate hash for the operation. + pub certificate_hash_hex: &'a str, + /// Optional expected generated artifact hash for the operation. + pub artifact_hash_hex: Option<&'a str>, +} + +/// Host policy for verifying a generated contract artifact before admission. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ContractArtifactVerificationPolicy<'a> { + /// Expected Echo contract ABI version. + pub echo_abi_version: u32, + /// Expected codec identifier for the generated artifact. + pub codec_id: &'a str, + /// Expected registry layout version for the generated artifact. + pub registry_version: u32, + /// Expected schema hash for the generated artifact. + pub schema_sha256_hex: &'a str, + /// Expected Wesley generator version. + pub wesley_generator_version: &'a str, + /// Expected contract-host helper API version. + pub helper_api_version: u32, + /// Expected footprint certificates keyed by operation id. + pub footprint_certificates: &'a [ExpectedFootprintCertificate<'a>], + /// Require every mutation op to carry a footprint certificate named by this policy. + pub require_mutation_footprint_certificates: bool, +} + +/// Successful generated contract artifact verification result. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VerifiedContractArtifact { + /// Registry metadata that was verified against host policy. + pub info: RegistryInfo, + /// Trust posture assigned to the generated artifact. + pub posture: ContractArtifactTrustPosture, +} + +/// Rejection returned when a generated contract artifact fails verification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContractArtifactRejection<'a> { + /// The generated artifact targets a different Echo ABI version. + EchoAbiVersionMismatch { + /// Expected Echo ABI version. + expected: u32, + /// Actual Echo ABI version from the registry. + actual: u32, + }, + /// The registry codec id did not match host policy. + CodecIdMismatch { + /// Expected codec id. + expected: &'a str, + /// Actual codec id from the registry. + actual: &'static str, + }, + /// The registry layout version did not match host policy. + RegistryVersionMismatch { + /// Expected registry layout version. + expected: u32, + /// Actual registry layout version. + actual: u32, + }, + /// The registry schema hash did not match host policy. + SchemaHashMismatch { + /// Expected schema hash. + expected: &'a str, + /// Actual schema hash from the registry. + actual: &'static str, + }, + /// The generated artifact came from an unsupported Wesley generator version. + WesleyGeneratorVersionMismatch { + /// Expected Wesley generator version. + expected: &'a str, + /// Actual Wesley generator version from the registry. + actual: &'static str, + }, + /// The generated helper API version does not match Echo's contract-host API. + HelperApiVersionMismatch { + /// Expected helper API version. + expected: u32, + /// Actual helper API version from the registry. + actual: u32, + }, + /// Host policy named an operation id that the registry does not contain. + MissingOperation { + /// Missing operation identifier. + op_id: u32, + }, + /// A required footprint certificate was missing from the operation. + MissingFootprintCertificate { + /// Operation identifier missing the certificate. + op_id: u32, + }, + /// A footprint certificate names a different operation id than its registry entry. + FootprintCertificateOpMismatch { + /// Operation identifier from the registry entry. + op_id: u32, + /// Operation identifier from the certificate. + certificate_op_id: u32, + }, + /// A footprint certificate names a different operation name than its registry entry. + FootprintCertificateNameMismatch { + /// Operation identifier whose certificate mismatched. + op_id: u32, + /// Operation name from the registry entry. + expected: &'static str, + /// Operation name from the certificate. + actual: &'static str, + }, + /// A footprint certificate was created for a different schema hash. + FootprintCertificateSchemaMismatch { + /// Operation identifier whose certificate mismatched. + op_id: u32, + /// Expected schema hash. + expected: &'static str, + /// Schema hash from the certificate. + actual: &'static str, + }, + /// A footprint certificate hash did not match host policy. + FootprintCertificateHashMismatch { + /// Operation identifier whose certificate mismatched. + op_id: u32, + /// Expected certificate hash. + expected: &'a str, + /// Certificate hash from the registry artifact. + actual: &'static str, + }, + /// A generated artifact hash did not match host policy. + FootprintArtifactHashMismatch { + /// Operation identifier whose artifact hash mismatched. + op_id: u32, + /// Expected generated artifact hash. + expected: &'a str, + /// Generated artifact hash from the registry artifact. + actual: &'static str, + }, + /// Host policy requires certified mutations and this mutation was uncertified. + UncertifiedMutation { + /// Operation identifier missing the certificate. + op_id: u32, + /// Operation name missing the certificate. + op_name: &'static str, + }, + /// Host policy requires certified mutations and this mutation certificate + /// was not named in the expected certificate set. + UnverifiedMutationFootprintCertificate { + /// Operation identifier missing from the expected certificate set. + op_id: u32, + /// Operation name missing from the expected certificate set. + op_name: &'static str, + }, +} + +/// Error codes for wasm helpers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HelperError { + /// No registry provider installed. + NoRegistry, + /// Unknown operation ID. + UnknownOp, + /// Input did not match schema (unknown key, missing required, wrong type, enum mismatch). + InvalidInput, + /// Internal failure (encoding). + Internal, +} + +/// Operation kind (query or mutation/command). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpKind { + /// Read-only operation. + Query, + /// State-mutating operation. + Mutation, +} + +/// Stable identifier for Echo's versioned universal little-endian value codec. +/// +/// The byte law is owned by ADR 0017 and implemented by `echo-wasm-abi`. +pub const LITTLE_ENDIAN_CODEC_V1_ID: &str = "le-binary-v1"; + +/// Versioned Echo law for deriving persisted operation ids from semantic +/// coordinates and generic operation kinds. +pub const SEMANTIC_OPERATION_ID_LAW_V1: &str = "echo.semantic-operation-id.fnv1-32/v1"; + +/// Echo operation id reserved for privileged scheduler-control envelopes. +pub const RESERVED_CONTROL_OPERATION_ID: u32 = u32::MAX; + +/// Echo operation id reserved for witnessed-suffix-import envelopes. +pub const RESERVED_IMPORT_SUFFIX_OPERATION_ID: u32 = u32::MAX - 1; + +/// Returns whether an operation id is reserved for an Echo protocol envelope. +/// +/// Application contracts and generated semantic operations must not use these +/// ids. Echo-owned privileged paths remain responsible for constructing the +/// corresponding protocol envelopes. +#[must_use] +pub const fn is_reserved_operation_id(operation_id: u32) -> bool { + operation_id == RESERVED_CONTROL_OPERATION_ID + || operation_id == RESERVED_IMPORT_SUFFIX_OPERATION_ID +} + +/// Derives a persisted Echo operation id from an exact semantic coordinate. +/// +/// The byte preimage is the UTF-8 law coordinate, one zero delimiter, the +/// generic kind byte (`0` for queries and `1` for mutations), and the exact +/// UTF-8 semantic coordinate. FNV-1 32-bit processes that complete preimage. +/// This law is deliberately domain-separated from the legacy Wesley derivation +/// over GraphQL field names. +/// +/// Generators remain responsible for refusing Echo protocol-reserved ids and +/// for detecting collisions across the complete generated operation set. They +/// must not salt, probe, or rename coordinates to repair a collision. +#[must_use] +pub const fn stable_semantic_operation_id_v1(kind: OpKind, semantic_coordinate: &str) -> u32 { + let mut hash = 2_166_136_261_u32; + let law = SEMANTIC_OPERATION_ID_LAW_V1.as_bytes(); + let mut index = 0; + while index < law.len() { + hash = semantic_operation_id_fnv1_step(hash, law[index]); + index += 1; + } + hash = semantic_operation_id_fnv1_step(hash, 0); + hash = semantic_operation_id_fnv1_step( + hash, + match kind { + OpKind::Query => 0, + OpKind::Mutation => 1, + }, + ); + + let coordinate = semantic_coordinate.as_bytes(); + index = 0; + while index < coordinate.len() { + hash = semantic_operation_id_fnv1_step(hash, coordinate[index]); + index += 1; + } + hash +} + +const fn semantic_operation_id_fnv1_step(hash: u32, byte: u8) -> u32 { + hash.wrapping_mul(16_777_619) ^ byte as u32 +} + +/// Descriptor for a single operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OpDef { + /// Operation kind. + pub kind: OpKind, + /// Operation name (GraphQL name). + pub name: &'static str, + /// Persisted operation identifier. + pub op_id: u32, + /// Argument descriptors. + pub args: &'static [ArgDef], + /// Result type name (GraphQL return type). + pub result_ty: &'static str, + /// Preserved operation directive metadata as JSON. + /// + /// Echo-specific admission tooling can interpret entries such as + /// `wes_footprint`; the generic registry API only carries the authored + /// directive data. + pub directives_json: &'static str, + /// Optional compile-time footprint certificate emitted by Wesley tooling. + /// + /// Hosts can compare the certificate hash during registry load and treat a + /// match as the proof that this generated artifact is carrying the declared + /// footprint it was compiled with. Echo core still treats the footprint as + /// data; domain-specific meaning belongs to the generated application/module. + pub footprint_certificate: Option<&'static FootprintCertificate>, +} + +/// Compile-time footprint certificate for one generated operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FootprintCertificate { + /// Operation identifier covered by this certificate. + pub op_id: u32, + /// Operation name covered by this certificate. + pub op_name: &'static str, + /// Hex-encoded schema hash used as the certificate basis. + pub schema_sha256_hex: &'static str, + /// Lowercase hex BLAKE3 hash of the generated operation artifact manifest. + /// + /// This is intentionally stronger than the declared read/write footprint: it + /// is expected to include the generated artifact family identity that + /// produced the operation helper, plus the operation shape covered by this + /// certificate. + pub artifact_hash_hex: &'static str, + /// Lowercase hex BLAKE3 hash of the full footprint certificate preimage. + pub certificate_hash_hex: &'static str, + /// Declared read resources, sorted and deduplicated by the generator. + pub reads: &'static [&'static str], + /// Declared write resources, sorted and deduplicated by the generator. + pub writes: &'static [&'static str], +} + +impl OpDef { + /// Return true when this operation carries a footprint certificate matching + /// the expected certificate hash and the registry schema hash. + /// + /// Hosts call this once while loading a generated registry artifact. A + /// successful match means the operation's declared footprint was certified + /// against the same schema hash the registry reports. + pub fn footprint_certificate_matches( + &self, + schema_sha256_hex: &str, + expected_certificate_hash_hex: &str, + ) -> bool { + let Some(certificate) = self.footprint_certificate else { + return false; + }; + + if certificate.op_id != self.op_id { + return false; + } + if certificate.op_name != self.name { + return false; + } + if certificate.schema_sha256_hex != schema_sha256_hex { + return false; + } + + certificate.certificate_hash_hex == expected_certificate_hash_hex + } +} + +/// Argument descriptor (flat; sufficient for strict object validation). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArgDef { + /// Field name. + pub name: &'static str, + /// GraphQL base type name. + pub ty: &'static str, + /// Whether the field is required. + pub required: bool, + /// Whether the field is a list. + pub list: bool, +} + +/// Enum descriptor (for validating enum string values). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnumDef { + /// Enum name. + pub name: &'static str, + /// Allowed values (uppercase GraphQL names). + pub values: &'static [&'static str], +} + +/// Object descriptor for result validation (optional; fields may be empty for now). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ObjectDef { + /// Object name. + pub name: &'static str, + /// Fields on the object. + pub fields: &'static [ArgDef], +} + +/// Application-supplied registry provider. +/// +/// Implemented by a generated crate in the application build. `warp-wasm` +/// should link against that provider to validate op IDs, expose registry +/// metadata, and (eventually) drive schema-typed encoding/decoding. +pub trait RegistryProvider: Sync { + /// Return registry metadata (codec, version, schema hash). + fn info(&self) -> RegistryInfo; + + /// Look up an operation by ID. + fn op_by_id(&self, op_id: u32) -> Option<&'static OpDef>; + + /// Return all operations (sorted by op_id for deterministic iteration). + fn all_ops(&self) -> &'static [OpDef]; + + /// Return all enums (for validating enum values). + fn all_enums(&self) -> &'static [EnumDef]; + + /// Return all objects (for result validation). + fn all_objects(&self) -> &'static [ObjectDef]; +} + +/// Verify a generated contract registry against host artifact policy. +/// +/// This check is intentionally application-neutral. It proves only that the +/// loaded generated registry matches the host's expected schema, codec, registry +/// layout, and footprint certificate identities. Domain payload validation still +/// belongs to the generated application adapter for this slice. +pub fn verify_contract_artifact<'a>( + registry: &dyn RegistryProvider, + policy: &ContractArtifactVerificationPolicy<'a>, +) -> Result> { + let info = registry.info(); + if info.echo_abi_version != policy.echo_abi_version { + return Err(ContractArtifactRejection::EchoAbiVersionMismatch { + expected: policy.echo_abi_version, + actual: info.echo_abi_version, + }); + } + if info.codec_id != policy.codec_id { + return Err(ContractArtifactRejection::CodecIdMismatch { + expected: policy.codec_id, + actual: info.codec_id, + }); + } + if info.registry_version != policy.registry_version { + return Err(ContractArtifactRejection::RegistryVersionMismatch { + expected: policy.registry_version, + actual: info.registry_version, + }); + } + if info.schema_sha256_hex != policy.schema_sha256_hex { + return Err(ContractArtifactRejection::SchemaHashMismatch { + expected: policy.schema_sha256_hex, + actual: info.schema_sha256_hex, + }); + } + if info.wesley_generator_version != policy.wesley_generator_version { + return Err(ContractArtifactRejection::WesleyGeneratorVersionMismatch { + expected: policy.wesley_generator_version, + actual: info.wesley_generator_version, + }); + } + if info.helper_api_version != policy.helper_api_version { + return Err(ContractArtifactRejection::HelperApiVersionMismatch { + expected: policy.helper_api_version, + actual: info.helper_api_version, + }); + } + + for expected in policy.footprint_certificates { + let op = registry.op_by_id(expected.op_id).ok_or( + ContractArtifactRejection::MissingOperation { + op_id: expected.op_id, + }, + )?; + verify_expected_footprint_certificate(op, info.schema_sha256_hex, expected)?; + } + + if policy.require_mutation_footprint_certificates { + for op in registry.all_ops() { + if op.kind == OpKind::Mutation { + if op.footprint_certificate.is_none() { + return Err(ContractArtifactRejection::UncertifiedMutation { + op_id: op.op_id, + op_name: op.name, + }); + } + if !policy + .footprint_certificates + .iter() + .any(|expected| expected.op_id == op.op_id) + { + return Err( + ContractArtifactRejection::UnverifiedMutationFootprintCertificate { + op_id: op.op_id, + op_name: op.name, + }, + ); + } + } + } + } + + let posture = if policy.require_mutation_footprint_certificates { + ContractArtifactTrustPosture::CompileTimeCertified + } else { + ContractArtifactTrustPosture::MetadataVerified + }; + + Ok(VerifiedContractArtifact { info, posture }) +} + +fn verify_expected_footprint_certificate<'a>( + op: &OpDef, + schema_sha256_hex: &'static str, + expected: &ExpectedFootprintCertificate<'a>, +) -> Result<(), ContractArtifactRejection<'a>> { + let certificate = op + .footprint_certificate + .ok_or(ContractArtifactRejection::MissingFootprintCertificate { op_id: op.op_id })?; + + if certificate.op_id != op.op_id { + return Err(ContractArtifactRejection::FootprintCertificateOpMismatch { + op_id: op.op_id, + certificate_op_id: certificate.op_id, + }); + } + if certificate.op_name != op.name { + return Err( + ContractArtifactRejection::FootprintCertificateNameMismatch { + op_id: op.op_id, + expected: op.name, + actual: certificate.op_name, + }, + ); + } + if certificate.schema_sha256_hex != schema_sha256_hex { + return Err( + ContractArtifactRejection::FootprintCertificateSchemaMismatch { + op_id: op.op_id, + expected: schema_sha256_hex, + actual: certificate.schema_sha256_hex, + }, + ); + } + if certificate.certificate_hash_hex != expected.certificate_hash_hex { + return Err( + ContractArtifactRejection::FootprintCertificateHashMismatch { + op_id: op.op_id, + expected: expected.certificate_hash_hex, + actual: certificate.certificate_hash_hex, + }, + ); + } + if let Some(expected_artifact_hash_hex) = expected.artifact_hash_hex { + if certificate.artifact_hash_hex != expected_artifact_hash_hex { + return Err(ContractArtifactRejection::FootprintArtifactHashMismatch { + op_id: op.op_id, + expected: expected_artifact_hash_hex, + actual: certificate.artifact_hash_hex, + }); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + verify_contract_artifact, ArgDef, ContractArtifactRejection, ContractArtifactTrustPosture, + ContractArtifactVerificationPolicy, ExpectedFootprintCertificate, FootprintCertificate, + ObjectDef, OpDef, OpKind, RegistryInfo, RegistryProvider, VerifiedContractArtifact, + }; + + const SCHEMA_SHA256_HEX: &str = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const CERTIFICATE_HASH_HEX: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const ARTIFACT_HASH_HEX: &str = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + static READS: &[&str] = &["CounterValue"]; + static WRITES: &[&str] = &["CounterValue"]; + static FOOTPRINT_CERTIFICATE: FootprintCertificate = FootprintCertificate { + op_id: 1001, + op_name: "increment", + schema_sha256_hex: SCHEMA_SHA256_HEX, + artifact_hash_hex: ARTIFACT_HASH_HEX, + certificate_hash_hex: CERTIFICATE_HASH_HEX, + reads: READS, + writes: WRITES, + }; + static INCREMENT_ARGS: &[ArgDef] = &[ArgDef { + name: "input", + ty: "IncrementInput", + required: true, + list: false, + }]; + static OPS_WITH_CERTIFICATE: &[OpDef] = &[ + OpDef { + kind: OpKind::Mutation, + name: "increment", + op_id: 1001, + args: INCREMENT_ARGS, + result_ty: "CounterValue", + directives_json: "{}", + footprint_certificate: Some(&FOOTPRINT_CERTIFICATE), + }, + OpDef { + kind: OpKind::Query, + name: "counterValue", + op_id: 1002, + args: &[], + result_ty: "CounterValue", + directives_json: "{}", + footprint_certificate: None, + }, + ]; + static OPS_WITHOUT_CERTIFICATE: &[OpDef] = &[OpDef { + kind: OpKind::Mutation, + name: "increment", + op_id: 1001, + args: INCREMENT_ARGS, + result_ty: "CounterValue", + directives_json: "{}", + footprint_certificate: None, + }]; + static OPS_QUERY_ONLY: &[OpDef] = &[OpDef { + kind: OpKind::Query, + name: "counterValue", + op_id: 1002, + args: &[], + result_ty: "CounterValue", + directives_json: "{}", + footprint_certificate: None, + }]; + + struct StaticRegistry { + ops: &'static [OpDef], + } + + impl RegistryProvider for StaticRegistry { + fn info(&self) -> RegistryInfo { + RegistryInfo { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + } + } + + fn op_by_id(&self, op_id: u32) -> Option<&'static OpDef> { + self.ops.iter().find(|op| op.op_id == op_id) + } + + fn all_ops(&self) -> &'static [OpDef] { + self.ops + } + + fn all_enums(&self) -> &'static [super::EnumDef] { + &[] + } + + fn all_objects(&self) -> &'static [ObjectDef] { + &[] + } + } + + #[test] + fn verifier_accepts_matching_registry_and_expected_certificate() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let expected_certificates = [ExpectedFootprintCertificate { + op_id: 1001, + certificate_hash_hex: CERTIFICATE_HASH_HEX, + artifact_hash_hex: Some(ARTIFACT_HASH_HEX), + }]; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &expected_certificates, + require_mutation_footprint_certificates: true, + }; + + let result = verify_contract_artifact(®istry, &policy); + + assert_eq!( + result, + Ok(VerifiedContractArtifact { + info: registry.info(), + posture: ContractArtifactTrustPosture::CompileTimeCertified, + }) + ); + } + + #[test] + fn verifier_rejects_certificate_hash_mismatch() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let expected_certificates = [ExpectedFootprintCertificate { + op_id: 1001, + certificate_hash_hex: "wrong", + artifact_hash_hex: Some(ARTIFACT_HASH_HEX), + }]; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &expected_certificates, + require_mutation_footprint_certificates: true, + }; + + let result = verify_contract_artifact(®istry, &policy); + + assert_eq!( + result, + Err( + ContractArtifactRejection::FootprintCertificateHashMismatch { + op_id: 1001, + expected: "wrong", + actual: CERTIFICATE_HASH_HEX, + } + ) + ); + } + + #[test] + fn verifier_rejects_echo_abi_version_mismatch() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 2, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: false, + }; + + assert_eq!( + verify_contract_artifact(®istry, &policy), + Err(ContractArtifactRejection::EchoAbiVersionMismatch { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn verifier_rejects_wesley_generator_version_mismatch() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/9.9.9", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: false, + }; + + assert_eq!( + verify_contract_artifact(®istry, &policy), + Err(ContractArtifactRejection::WesleyGeneratorVersionMismatch { + expected: "echo-wesley-gen/9.9.9", + actual: "echo-wesley-gen/0.1.0", + }) + ); + } + + #[test] + fn verifier_rejects_helper_api_version_mismatch() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 2, + footprint_certificates: &[], + require_mutation_footprint_certificates: false, + }; + + assert_eq!( + verify_contract_artifact(®istry, &policy), + Err(ContractArtifactRejection::HelperApiVersionMismatch { + expected: 2, + actual: 1, + }) + ); + } + + #[test] + fn verifier_does_not_compile_time_certify_empty_policy() { + let registry = StaticRegistry { + ops: OPS_QUERY_ONLY, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: false, + }; + + assert_eq!( + verify_contract_artifact(®istry, &policy), + Ok(VerifiedContractArtifact { + info: registry.info(), + posture: ContractArtifactTrustPosture::MetadataVerified, + }) + ); + } + + #[test] + fn verifier_does_not_compile_time_certify_weak_mutation_policy() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: false, + }; + + assert_eq!( + verify_contract_artifact(®istry, &policy), + Ok(VerifiedContractArtifact { + info: registry.info(), + posture: ContractArtifactTrustPosture::MetadataVerified, + }) + ); + } + + #[test] + fn verifier_rejects_uncertified_mutation_when_policy_requires_it() { + let registry = StaticRegistry { + ops: OPS_WITHOUT_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: true, + }; + + let result = verify_contract_artifact(®istry, &policy); + + assert_eq!( + result, + Err(ContractArtifactRejection::UncertifiedMutation { + op_id: 1001, + op_name: "increment", + }) + ); + } + + #[test] + fn verifier_rejects_mutation_certificate_not_named_by_policy() { + let registry = StaticRegistry { + ops: OPS_WITH_CERTIFICATE, + }; + let policy = ContractArtifactVerificationPolicy { + echo_abi_version: 1, + codec_id: "cbor-canon-v1", + registry_version: 1, + schema_sha256_hex: SCHEMA_SHA256_HEX, + wesley_generator_version: "echo-wesley-gen/0.1.0", + helper_api_version: 1, + footprint_certificates: &[], + require_mutation_footprint_certificates: true, + }; + + let result = verify_contract_artifact(®istry, &policy); + + assert_eq!( + result, + Err( + ContractArtifactRejection::UnverifiedMutationFootprintCertificate { + op_id: 1001, + op_name: "increment", + } + ) + ); + } +} diff --git a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/provider.rs.source b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/provider.rs.source new file mode 100644 index 00000000..01be6cc6 --- /dev/null +++ b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-registry-api/src/provider.rs.source @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! Provider-generic semantic registry vocabulary. +//! +//! These borrowed values describe exact provider artifact claims. Constructing +//! or inspecting them does not authenticate, admit, install, register, invoke, +//! or execute any operation. + +use crate::OpKind; + +/// Exact identity of one digest-framed provider artifact. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderDigestIdentityV1<'a> { + /// Semantic or artifact coordinate named by the digest claim. + pub coordinate: &'a str, + /// Proposition domain used to frame the digest. + pub digest_domain: &'a str, + /// Exact encoded digest value, including its algorithm prefix. + pub digest: &'a str, +} + +/// Exact identity of the admitted provider schema used by generated helpers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderSchemaIdentityV1<'a> { + /// Coordinate of the provider schema artifact. + pub coordinate: &'a str, + /// Lowercase hexadecimal SHA-256 of the exact raw schema bytes. + pub raw_sha256_hex: &'a str, +} + +/// Exact coordinate and semantic domain of one provider-owned meaning. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderSemanticIdentityV1<'a> { + /// Provider-owned semantic coordinate. + pub coordinate: &'a str, + /// Domain that defines the coordinate's meaning. + pub semantic_domain: &'a str, +} + +/// Schema and codec claims for one operation value boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderValueContractV1<'a> { + /// Semantic schema coordinate for the value. + pub schema_coordinate: &'a str, + /// Semantic domain that defines the value schema. + pub schema_domain: &'a str, + /// Exact codec identifier selected for the value bytes. + pub codec_id: &'a str, +} + +/// Independent semantic-layer and release-layer provider bundle identities. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderBundleIdentityV1<'a> { + /// Proposition domain used to frame the semantic bundle digest. + pub semantic_digest_domain: &'a str, + /// Exact semantic bundle digest. + pub semantic_digest: &'a str, + /// Proposition domain used to frame the release bundle digest. + pub release_digest_domain: &'a str, + /// Exact release bundle digest. + pub release_digest: &'a str, +} + +/// Exact abstract footprint obligation and its owning algebra artifact. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderFootprintIdentityV1<'a> { + /// Abstract footprint obligation authored for the operation. + pub obligation: &'a str, + /// Coordinate of the footprint algebra artifact. + pub algebra_coordinate: &'a str, + /// Proposition domain used to frame the footprint algebra digest. + pub algebra_digest_domain: &'a str, + /// Exact footprint algebra digest. + pub algebra_digest: &'a str, +} + +/// Exact semantic, generated, and target claims for one provider operation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderOperationV1<'a> { + /// Semantic operation coordinate. + pub coordinate: &'a str, + /// Domain that defines the operation coordinate. + pub semantic_domain: &'a str, + /// Whether the operation mutates state or performs a bounded read. + pub kind: OpKind, + /// Law used to derive the persisted operation identifier. + pub operation_id_law: &'a str, + /// Exact persisted operation identifier. + pub operation_id: u32, + /// Input schema and codec claims. + pub input: ProviderValueContractV1<'a>, + /// Output schema and codec claims. + pub output: ProviderValueContractV1<'a>, + /// Target-owned schema for a failure before obstruction mapping. + pub target_failure_schema: &'a str, + /// Provider-owned typed obstruction produced by failure mapping. + pub obstruction: ProviderSemanticIdentityV1<'a>, + /// Provider-owned payload schema for the typed obstruction. + pub obstruction_payload_schema: &'a str, + /// Exact Target IR artifact identity. + pub target_ir: ProviderDigestIdentityV1<'a>, + /// Exact target-profile artifact identity. + pub target_profile: ProviderDigestIdentityV1<'a>, + /// Exact generated-artifact-profile identity. + pub generated_artifact_profile: ProviderDigestIdentityV1<'a>, + /// Semantic operation profile selected for this operation. + pub operation_profile: ProviderSemanticIdentityV1<'a>, + /// Exact operation-profiles document identity. + pub operation_profiles: ProviderDigestIdentityV1<'a>, + /// Exact footprint obligation and algebra claims. + pub footprint: ProviderFootprintIdentityV1<'a>, +} + +/// Borrowed provider-generic registry retained by a package proposal. +/// +/// This registry intentionally contains no GraphQL facade, Wesley generator +/// metadata, runtime installation token, or executor authority. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderRegistryV1<'a> { + /// Echo contract ABI version targeted by generated helpers. + pub echo_contract_abi_version: u32, + /// Contract-host helper API version targeted by generated helpers. + pub helper_api_version: u32, + /// Exact raw provider schema identity. + pub provider_schema: ProviderSchemaIdentityV1<'a>, + /// Exact target-bundle-profile artifact identity. + pub target_bundle_profile: ProviderDigestIdentityV1<'a>, + /// Independent semantic-layer and release-layer bundle identities. + pub bundle: ProviderBundleIdentityV1<'a>, + /// Provider operations described by this registry. + pub operations: &'a [ProviderOperationV1<'a>], +} + +impl<'a> ProviderRegistryV1<'a> { + /// Look up one operation by its exact persisted identifier. + #[must_use] + pub fn operation_by_id(&self, operation_id: u32) -> Option<&ProviderOperationV1<'a>> { + let mut found = None; + for operation in self.operations { + if operation.operation_id != operation_id { + continue; + } + if found.is_some() { + return None; + } + found = Some(operation); + } + found + } +} diff --git a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-wesley-gen/Cargo.toml.source b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-wesley-gen/Cargo.toml.source index a72cd6dd..22b5c474 100644 --- a/crates/echo-wesley-gen/assets/v1/repository/crates/echo-wesley-gen/Cargo.toml.source +++ b/crates/echo-wesley-gen/assets/v1/repository/crates/echo-wesley-gen/Cargo.toml.source @@ -22,6 +22,7 @@ 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 } +echo-registry-api = { workspace = true } hex = "0.4" proc-macro2 = "1.0" quote = "1.0" diff --git a/crates/echo-wesley-gen/src/bin/echo-edict-provider-assets.rs b/crates/echo-wesley-gen/src/bin/echo-edict-provider-assets.rs index 6ca0f91b..1980cd8d 100644 --- a/crates/echo-wesley-gen/src/bin/echo-edict-provider-assets.rs +++ b/crates/echo-wesley-gen/src/bin/echo-edict-provider-assets.rs @@ -4,10 +4,16 @@ //! Synchronizes exact package-local assets for the Echo Edict provider tools. use anyhow::{anyhow, bail, Context, Result}; +use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt}; +use cap_std::{ + ambient_authority, + fs::{Dir, OpenOptions}, +}; use clap::Parser; use echo_wesley_gen::provider_corpus::{diff_exact_corpus_files_v1, ProviderArtifactCorpusFileV1}; use echo_wesley_gen::provider_corpus_fs::{read_actual_corpus, write_corpus}; use std::ffi::OsString; +use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -43,7 +49,7 @@ impl AssetSpec { } } -const ASSETS: [AssetSpec; 35] = [ +const ASSETS: [AssetSpec; 38] = [ AssetSpec::new( "edict-provider/contracts/v1/edict-provider-contracts.cddl", "schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl", @@ -194,6 +200,18 @@ const ASSETS: [AssetSpec; 35] = [ "repository/crates/echo-edict-canonical/src/lib.rs.source", "crates/echo-edict-canonical/src/lib.rs", ), + AssetSpec::new( + "repository/crates/echo-registry-api/Cargo.toml.source", + "crates/echo-registry-api/Cargo.toml", + ), + AssetSpec::new( + "repository/crates/echo-registry-api/src/lib.rs.source", + "crates/echo-registry-api/src/lib.rs", + ), + AssetSpec::new( + "repository/crates/echo-registry-api/src/provider.rs.source", + "crates/echo-registry-api/src/provider.rs", + ), AssetSpec::new( "repository/crates/echo-wesley-gen/Cargo.toml.source", "crates/echo-wesley-gen/Cargo.toml", @@ -204,6 +222,88 @@ const ASSETS: [AssetSpec; 35] = [ ), ]; +const LOWERER_RESOURCES: [AssetSpec; 4] = [ + AssetSpec::new( + "authority-facts.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor", + ), + AssetSpec::new( + "authority-facts.echo-lawpack.cbor", + "schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor", + ), + AssetSpec::new( + "lawpack.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor", + ), + AssetSpec::new( + "target-profile.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor", + ), +]; + +const VERIFIER_RESOURCES: [AssetSpec; 15] = [ + AssetSpec::new( + "authority-facts.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor", + ), + AssetSpec::new( + "authority-facts.echo-lawpack.cbor", + "schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor", + ), + AssetSpec::new( + "generated-artifact-profile.echo-dpo-registration.cbor", + "schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor", + ), + AssetSpec::new( + "lawpack.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor", + ), + AssetSpec::new( + "resource.lawpack-exports.cbor", + "schemas/edict-provider/generated/v1/resources/resource.lawpack-exports.cbor", + ), + AssetSpec::new( + "resource.lawpack-target-adapter.cbor", + "schemas/edict-provider/generated/v1/resources/resource.lawpack-target-adapter.cbor", + ), + AssetSpec::new( + "resource.lawpack-verifier.cbor", + "schemas/edict-provider/generated/v1/resources/resource.lawpack-verifier.cbor", + ), + AssetSpec::new( + "resource.target-cost-algebra.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-cost-algebra.cbor", + ), + AssetSpec::new( + "resource.target-footprint-algebra.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-footprint-algebra.cbor", + ), + AssetSpec::new( + "resource.target-intrinsics.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-intrinsics.cbor", + ), + AssetSpec::new( + "resource.target-ir.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor", + ), + AssetSpec::new( + "resource.target-obstruction-taxonomy.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-obstruction-taxonomy.cbor", + ), + AssetSpec::new( + "resource.target-operation-profiles.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-operation-profiles.cbor", + ), + AssetSpec::new( + "resource.target-verifier-contract.cbor", + "schemas/edict-provider/generated/v1/resources/resource.target-verifier-contract.cbor", + ), + AssetSpec::new( + "target-profile.echo-dpo.cbor", + "schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor", + ), +]; + #[derive(Parser)] #[command( author, @@ -226,6 +326,18 @@ struct Args { /// Override the carrier root, primarily for isolated check/write witnesses. #[arg(long)] assets_root: Option, + + /// Also synchronize the exact lowerer and verifier component resources. + #[arg(long)] + sync_component_resources: bool, + + /// Override the lowerer resource root for an isolated witness. + #[arg(long, requires = "sync_component_resources")] + lowerer_resources_root: Option, + + /// Override the verifier resource root for an isolated witness. + #[arg(long, requires = "sync_component_resources")] + verifier_resources_root: Option, } fn main() -> Result<()> { @@ -238,6 +350,12 @@ fn main() -> Result<()> { let assets_root = args .assets_root .unwrap_or_else(|| crate_root.join("assets/v1")); + let lowerer_resources_root = args + .lowerer_resources_root + .unwrap_or_else(|| workspace_root.join("crates/echo-edict-provider-lowerer/resources")); + let verifier_resources_root = args + .verifier_resources_root + .unwrap_or_else(|| workspace_root.join("crates/echo-edict-provider-verifier/resources")); // Write mode intentionally permits a stale package copy so authoritative // generated/component owners can break the regeneration cycle. Read-only // check mode still requires every distribution copy to corroborate them. @@ -255,6 +373,27 @@ fn main() -> Result<()> { check_package_list(&workspace_root, &expected)?; println!("Cargo package selects the exact provider asset tree"); } + if args.sync_component_resources { + let lowerer_resources = load_expected_files(&workspace_root, &LOWERER_RESOURCES, false)?; + let verifier_resources = load_expected_files(&workspace_root, &VERIFIER_RESOURCES, false)?; + if args.write { + write_corpus(&lowerer_resources_root, &lowerer_resources)?; + write_corpus(&verifier_resources_root, &verifier_resources)?; + println!("Provider component resources synchronized"); + } else { + check_exact_files( + &lowerer_resources_root, + &lowerer_resources, + "Lowerer component resource", + )?; + check_exact_files( + &verifier_resources_root, + &verifier_resources, + "Verifier component resource", + )?; + println!("Provider component resources are current"); + } + } Ok(()) } @@ -262,9 +401,17 @@ fn load_expected_assets( root: &Path, require_corroboration: bool, ) -> Result> { - let mut files = Vec::with_capacity(ASSETS.len()); + load_expected_files(root, &ASSETS, require_corroboration) +} + +fn load_expected_files( + root: &Path, + specs: &[AssetSpec], + require_corroboration: bool, +) -> Result> { + let mut files = Vec::with_capacity(specs.len()); let mut total_bytes = 0usize; - for spec in ASSETS { + for spec in specs { let bytes = read_owner(root, spec.owner)?; total_bytes = total_bytes .checked_add(bytes.len()) @@ -292,10 +439,36 @@ fn load_expected_assets( } fn read_owner(root: &Path, relative_path: &str) -> Result> { + read_owner_with_between_reads_hook(root, relative_path, || Ok(())) +} + +fn read_owner_with_between_reads_hook( + root: &Path, + relative_path: &str, + between_reads_hook: impl FnOnce() -> Result<()>, +) -> Result> { let path = root.join(relative_path); - let metadata = std::fs::symlink_metadata(&path) + let directory = Dir::open_ambient_dir(root, ambient_authority()).with_context(|| { + format!( + "failed to open provider asset owner root {}", + root.display() + ) + })?; + let mut options = OpenOptions::new(); + options.read(true); + options.follow(FollowSymlinks::No); + let mut file = directory + .open_with(relative_path, &options) + .with_context(|| { + format!( + "failed to open provider asset owner without following symbolic links: {}", + path.display() + ) + })?; + let metadata = file + .metadata() .with_context(|| format!("failed to inspect provider asset owner {}", path.display()))?; - if metadata.file_type().is_symlink() || !metadata.is_file() { + if !metadata.is_file() { bail!( "provider asset owner is not a regular non-symlink file: {}", path.display() @@ -307,11 +480,41 @@ fn read_owner(root: &Path, relative_path: &str) -> Result> { path.display() ); } - let bytes = std::fs::read(&path) + let mut bytes = Vec::new(); + (&mut file) + .take(MAX_OWNER_FILE_BYTES + 1) + .read_to_end(&mut bytes) .with_context(|| format!("failed to read provider asset owner {}", path.display()))?; - if bytes.len() as u64 != metadata.len() { + between_reads_hook()?; + file.seek(SeekFrom::Start(0)).with_context(|| { + format!( + "failed to rewind provider asset owner for corroborating read {}", + path.display() + ) + })?; + let mut corroborating_bytes = Vec::with_capacity(bytes.len()); + (&mut file) + .take(MAX_OWNER_FILE_BYTES + 1) + .read_to_end(&mut corroborating_bytes) + .with_context(|| { + format!( + "failed to corroborate provider asset owner bytes {}", + path.display() + ) + })?; + let final_metadata = file.metadata().with_context(|| { + format!( + "failed to re-inspect provider asset owner {}", + path.display() + ) + })?; + if !final_metadata.is_file() + || final_metadata.len() != metadata.len() + || bytes.len() as u64 != metadata.len() + || corroborating_bytes != bytes + { bail!( - "provider asset owner changed length while being read: {}", + "provider asset owner changed while being read: {}", path.display() ); } @@ -319,17 +522,25 @@ fn read_owner(root: &Path, relative_path: &str) -> Result> { } fn check_assets(root: &Path, expected: &[ProviderArtifactCorpusFileV1]) -> Result<()> { + check_exact_files(root, expected, "Package-local provider asset") +} + +fn check_exact_files( + root: &Path, + expected: &[ProviderArtifactCorpusFileV1], + label: &str, +) -> Result<()> { let actual = read_actual_corpus(root, expected)?; let drift = diff_exact_corpus_files_v1(expected, &actual)?; if drift.is_empty() { return Ok(()); } - eprintln!("Package-local provider asset drift:"); + eprintln!("{label} drift:"); for entry in &drift { eprintln!(" {}: {}", entry.kind().as_str(), entry.relative_path()); } - bail!("package-local provider assets are not current") + bail!("{label} files are not current") } fn check_package_list( @@ -390,8 +601,12 @@ fn check_package_asset_inventory( #[cfg(test)] #[allow(clippy::expect_used)] mod tests { - use super::{check_package_asset_inventory, load_expected_assets, ASSETS}; + use super::{ + check_package_asset_inventory, load_expected_assets, read_owner, + read_owner_with_between_reads_hook, ASSETS, + }; use anyhow::Result; + use std::io::{Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -456,6 +671,57 @@ mod tests { Ok(()) } + #[cfg(unix)] + #[test] + fn owner_read_refuses_final_symlink_at_open_boundary() -> Result<()> { + use std::os::unix::fs::symlink; + + let sequence = NEXT_TEST_DIRECTORY.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "echo-provider-asset-owner-symlink-{}-{sequence}", + std::process::id() + )); + std::fs::create_dir_all(&root)?; + std::fs::write(root.join("owner"), b"exact owner bytes")?; + symlink("owner", root.join("carrier"))?; + + let error = read_owner(&root, "carrier").expect_err("final symlink must be refused"); + assert!(format!("{error:#}") + .contains("failed to open provider asset owner without following symbolic links")); + + std::fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn owner_read_refuses_same_length_in_place_rewrite() -> Result<()> { + let sequence = NEXT_TEST_DIRECTORY.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "echo-provider-asset-owner-rewrite-{}-{sequence}", + std::process::id() + )); + std::fs::create_dir_all(&root)?; + let owner = root.join("owner"); + let original = [b'a'; 4096]; + let replacement = [b'b'; 4096]; + std::fs::write(&owner, original)?; + + let error = read_owner_with_between_reads_hook(&root, "owner", || { + let mut writer = std::fs::OpenOptions::new().write(true).open(&owner)?; + writer.seek(SeekFrom::Start(0))?; + writer.write_all(&replacement)?; + writer.flush()?; + Ok(()) + }) + .expect_err("same-length in-place owner rewrite must be refused"); + assert!(error + .to_string() + .contains("provider asset owner changed while being read")); + + std::fs::remove_dir_all(root)?; + Ok(()) + } + fn materialize_owner_fixture(root: &Path) -> Result<()> { let source_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); for spec in ASSETS { diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs index 8ee10072..83ffbc72 100644 --- a/crates/echo-wesley-gen/src/main.rs +++ b/crates/echo-wesley-gen/src/main.rs @@ -5,6 +5,7 @@ use anyhow::{bail, Result}; use clap::Parser; +use echo_registry_api::{is_reserved_operation_id, LITTLE_ENDIAN_CODEC_V1_ID}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::collections::BTreeMap; @@ -22,9 +23,8 @@ mod ir; use ir::{OpKind, TypeKind, WesleyIR}; const ECHO_IR_VERSION: &str = "echo-ir/v1"; -const DEFAULT_CODEC_ID: &str = "le-binary-v1"; +const DEFAULT_CODEC_ID: &str = LITTLE_ENDIAN_CODEC_V1_ID; const DEFAULT_REGISTRY_VERSION: u32 = 1; -const RESERVED_CONTROL_OP_ID: u32 = u32::MAX; const WESLEY_CORE_VERSION: &str = "0.3.0-alpha.1"; #[derive(Parser)] @@ -109,9 +109,9 @@ fn echo_ir_from_schema_sdl(schema_sdl: &str) -> Result { operation.field_name ); } - if op_id == RESERVED_CONTROL_OP_ID { + if is_reserved_operation_id(op_id) { bail!( - "generated operation id for {:?} `{}` uses Echo's reserved control op id; \ + "generated operation id for {:?} `{}` uses reserved Echo protocol op id {op_id}; \ add explicit operation ids upstream before generating Echo artifacts", operation.operation_type, operation.field_name @@ -1161,11 +1161,12 @@ fn generate_rust(ir: &WesleyIR, args: &Args) -> Result { fn validate_operation_ids(ir: &WesleyIR) -> Result<()> { for op in &ir.ops { - if op.op_id == RESERVED_CONTROL_OP_ID { + if is_reserved_operation_id(op.op_id) { bail!( - "operation `{}` uses Echo's reserved control op id {RESERVED_CONTROL_OP_ID}; \ - application contracts must not generate scheduler control intents", - op.name + "operation `{}` uses reserved Echo protocol op id {}; \ + application contracts must not generate Echo protocol intents", + op.name, + op.op_id ); } } diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index c52afb72..bc397d7b 100644 --- a/crates/echo-wesley-gen/src/provider_artifacts.rs +++ b/crates/echo-wesley-gen/src/provider_artifacts.rs @@ -9,12 +9,17 @@ //! generated self-contained schema and, when Edict owns the format, the //! independently admitted upstream root. +use std::collections::BTreeMap; use std::fmt; use std::sync::Arc; use cddl_cat::cbor::validate_cbor; use cddl_cat::context::BasicContext; use cddl_cat::flatten::flatten_from_str; +use echo_registry_api::{ + is_reserved_operation_id, stable_semantic_operation_id_v1, OpKind, LITTLE_ENDIAN_CODEC_V1_ID, + SEMANTIC_OPERATION_ID_LAW_V1, +}; use serde_json::{json, Map as JsonMap, Value as JsonValue}; use wesley_core::{ GenerationArtifactReferenceV1, GenerationContractError, GenerationContractErrorKind, @@ -49,6 +54,7 @@ const PROVIDER_SCHEMA_SUFFIX: &str = r#" generated-artifact-profile = { apiVersion: "echo.generated-artifact-profile/v1", targetProfile: tstr, + operationIdLaw: "echo.semantic-operation-id.fnv1-32/v1", types: { * tstr => echo-generated-type }, operations: { * tstr => generated-operation }, } @@ -66,6 +72,8 @@ echo-generated-record = { echo-generated-record-field = { name: tstr, type: tstr } generated-operation = { + operationId: 0..4294967293, + valueCodec: "le-binary-v1", inputType: tstr, outputType: tstr, effect: tstr, @@ -235,6 +243,10 @@ pub enum ProviderArtifactGenerationErrorKind { DigestConstructionFailed, /// A validated semantic declaration needed by projection was absent. SemanticProjectionMismatch, + /// A semantic operation derived an Echo protocol-reserved operation id. + ReservedOperationId, + /// Two semantic operations derived the same persisted Echo operation id. + OperationIdCollision, /// Wesley rejected an emitted exact-byte content reference. WesleyContractRejected, } @@ -249,6 +261,8 @@ impl ProviderArtifactGenerationErrorKind { Self::OwningRootRejected => "owning-root-rejected", Self::DigestConstructionFailed => "digest-construction-failed", Self::SemanticProjectionMismatch => "semantic-projection-mismatch", + Self::ReservedOperationId => "reserved-operation-id", + Self::OperationIdCollision => "operation-id-collision", Self::WesleyContractRejected => "wesley-contract-rejected", } } @@ -1808,6 +1822,7 @@ fn build_generated_profile( ); } let mut operations = JsonMap::new(); + let mut operation_ids = BTreeMap::new(); for operation in &source.operations { let profile = source .profiles @@ -1820,10 +1835,28 @@ fn build_generated_profile( &operation.profile, ) })?; - let invocation_kind = match profile.optic_template.optic_kind { - OpticKind::Revelation => "observer", - OpticKind::AffectReintegration => "mutation", + let (invocation_kind, operation_kind) = match profile.optic_template.optic_kind { + OpticKind::Revelation => ("observer", OpKind::Query), + OpticKind::AffectReintegration => ("mutation", OpKind::Mutation), }; + let operation_id = + stable_semantic_operation_id_v1(operation_kind, &operation.identity.coordinate); + if is_reserved_operation_id(operation_id) { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ReservedOperationId, + &operation.identity.coordinate, + SEMANTIC_OPERATION_ID_LAW_V1, + )); + } + if let Some(conflicting_coordinate) = + operation_ids.insert(operation_id, operation.identity.coordinate.as_str()) + { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::OperationIdCollision, + &operation.identity.coordinate, + conflicting_coordinate, + )); + } let implementation = implementation_projection(operation)?; let obstruction_mappings = operation .obstruction_mappings @@ -1838,6 +1871,8 @@ fn build_generated_profile( operations.insert( operation.identity.coordinate.clone(), json!({ + "operationId": operation_id, + "valueCodec": LITTLE_ENDIAN_CODEC_V1_ID, "inputType": operation.input_type, "outputType": operation.output_type, "effect": operation.effect, @@ -1857,6 +1892,7 @@ fn build_generated_profile( source.target_profile_projection.id, source.target_profile_projection.version ), + "operationIdLaw": SEMANTIC_OPERATION_ID_LAW_V1, "types": types, "operations": operations, })) diff --git a/crates/echo-wesley-gen/src/provider_corpus.rs b/crates/echo-wesley-gen/src/provider_corpus.rs index 46d13814..9ef09a58 100644 --- a/crates/echo-wesley-gen/src/provider_corpus.rs +++ b/crates/echo-wesley-gen/src/provider_corpus.rs @@ -349,7 +349,7 @@ 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]); 17] = [ + let sources: [(&str, &[u8]); 20] = [ ( "Cargo.lock", include_bytes!("../assets/v1/repository/Cargo.lock.source"), @@ -366,6 +366,20 @@ pub fn checked_provider_generator_source_bundle_v1( "crates/echo-edict-canonical/src/lib.rs", include_bytes!("../assets/v1/repository/crates/echo-edict-canonical/src/lib.rs.source"), ), + ( + "crates/echo-registry-api/Cargo.toml", + include_bytes!("../assets/v1/repository/crates/echo-registry-api/Cargo.toml.source"), + ), + ( + "crates/echo-registry-api/src/lib.rs", + include_bytes!("../assets/v1/repository/crates/echo-registry-api/src/lib.rs.source"), + ), + ( + "crates/echo-registry-api/src/provider.rs", + include_bytes!( + "../assets/v1/repository/crates/echo-registry-api/src/provider.rs.source" + ), + ), ( "crates/echo-wesley-gen/Cargo.toml", include_bytes!("../assets/v1/repository/crates/echo-wesley-gen/Cargo.toml.source"), diff --git a/crates/echo-wesley-gen/tests/generation.rs b/crates/echo-wesley-gen/tests/generation.rs index 546bf491..9675a2f9 100644 --- a/crates/echo-wesley-gen/tests/generation.rs +++ b/crates/echo-wesley-gen/tests/generation.rs @@ -1405,8 +1405,9 @@ fn assert_generated_crate_checks(crate_dir: &Path) { } #[test] -fn test_reserved_control_op_id_fails_closed() { - let ir = r#"{ +fn test_reserved_echo_protocol_op_ids_fail_closed() { + for op_id in [u32::MAX - 1, u32::MAX] { + let ir = r#"{ "ir_version": "echo-ir/v1", "schema_sha256": "abc123", "codec_id": "le-binary-v1", @@ -1420,24 +1421,26 @@ fn test_reserved_control_op_id_fails_closed() { { "kind": "MUTATION", "name": "startScheduler", - "op_id": 4294967295, + "op_id": __OP_ID__, "args": [], "result_type": "RunStatus" } ] - }"#; + }"# + .replace("__OP_ID__", &op_id.to_string()); - let output = run_wesley_gen(ir); - assert!( - !output.status.success(), - "reserved control op id must fail closed" - ); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains("reserved control op id"), - "stderr did not explain reserved control id: {}", - String::from_utf8_lossy(&output.stderr) - ); + let output = run_wesley_gen(&ir); + assert!( + !output.status.success(), + "reserved Echo protocol op id {op_id} must fail closed" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("reserved Echo protocol op id"), + "stderr did not explain reserved Echo protocol id {op_id}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } } #[test] diff --git a/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs index 481d754f..29d0de45 100644 --- a/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs +++ b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs @@ -36,11 +36,14 @@ const CONTRACT_CDDL: &[u8] = const CONTRACT_MANIFEST: &[u8] = include_bytes!("../assets/v1/edict-provider/contracts/v1/manifest.json"); -const GENERATOR_SOURCE_PATHS: [&str; 17] = [ +const GENERATOR_SOURCE_PATHS: [&str; 20] = [ "Cargo.lock", "Cargo.toml", "crates/echo-edict-canonical/Cargo.toml", "crates/echo-edict-canonical/src/lib.rs", + "crates/echo-registry-api/Cargo.toml", + "crates/echo-registry-api/src/lib.rs", + "crates/echo-registry-api/src/provider.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/crates/echo-wesley-gen/tests/provider_package_assets.rs b/crates/echo-wesley-gen/tests/provider_package_assets.rs index cdb32216..5df581b9 100644 --- a/crates/echo-wesley-gen/tests/provider_package_assets.rs +++ b/crates/echo-wesley-gen/tests/provider_package_assets.rs @@ -7,7 +7,9 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::sync::atomic::{AtomicU64, Ordering}; -const EXPECTED_ASSET_FILE_COUNT: usize = 35; +const EXPECTED_ASSET_FILE_COUNT: usize = 38; +const EXPECTED_LOWERER_RESOURCE_COUNT: usize = 4; +const EXPECTED_VERIFIER_RESOURCE_COUNT: usize = 15; static NEXT_TEMP_DIRECTORY: AtomicU64 = AtomicU64::new(0); struct TestDirectory(PathBuf); @@ -107,3 +109,72 @@ fn asset_check_reports_drift_without_repair_and_write_restores_exact_bytes() { String::from_utf8_lossy(&final_check.stderr) ); } + +#[test] +fn component_resource_check_reports_drift_and_write_restores_exact_bytes() { + let directory = TestDirectory::new("component-resources"); + let assets_root = directory.path().join("assets-v1"); + let lowerer_root = directory.path().join("lowerer-resources"); + let verifier_root = directory.path().join("verifier-resources"); + let assets_root_text = assets_root.to_string_lossy(); + let lowerer_root_text = lowerer_root.to_string_lossy(); + let verifier_root_text = verifier_root.to_string_lossy(); + let roots = [ + "--assets-root", + assets_root_text.as_ref(), + "--check-package-list", + "--sync-component-resources", + "--lowerer-resources-root", + lowerer_root_text.as_ref(), + "--verifier-resources-root", + verifier_root_text.as_ref(), + ]; + + let mut write_arguments = vec!["--write"]; + write_arguments.extend(roots); + let written = run_assets(&write_arguments); + assert!( + written.status.success(), + "component resource write failed:\n{}", + String::from_utf8_lossy(&written.stderr) + ); + let written_stdout = String::from_utf8_lossy(&written.stdout); + assert!(written_stdout.contains("Package-local provider assets synchronized")); + assert!(written_stdout.contains("Cargo package selects the exact provider asset tree")); + assert!(written_stdout.contains("Provider component resources synchronized")); + assert_eq!(count_files(&assets_root), EXPECTED_ASSET_FILE_COUNT); + assert_eq!(count_files(&lowerer_root), EXPECTED_LOWERER_RESOURCE_COUNT); + assert_eq!( + count_files(&verifier_root), + EXPECTED_VERIFIER_RESOURCE_COUNT + ); + + let changed_path = verifier_root.join("generated-artifact-profile.echo-dpo-registration.cbor"); + std::fs::write(&changed_path, b"tampered verifier resource") + .expect("verifier resource is deliberately changed"); + let checked = run_assets(&roots); + assert!(!checked.status.success()); + assert!(String::from_utf8_lossy(&checked.stderr) + .contains("changed: generated-artifact-profile.echo-dpo-registration.cbor")); + assert_eq!( + std::fs::read(&changed_path).expect("changed verifier resource remains readable"), + b"tampered verifier resource" + ); + + let restored = run_assets(&write_arguments); + assert!( + restored.status.success(), + "component resource restore failed:\n{}", + String::from_utf8_lossy(&restored.stderr) + ); + let final_check = run_assets(&roots); + assert!( + final_check.status.success(), + "restored component resources do not check:\n{}", + String::from_utf8_lossy(&final_check.stderr) + ); + let final_stdout = String::from_utf8_lossy(&final_check.stdout); + assert!(final_stdout.contains("Package-local provider assets are current")); + assert!(final_stdout.contains("Cargo package selects the exact provider asset tree")); + assert!(final_stdout.contains("Provider component resources are current")); +} diff --git a/crates/echo-wesley-gen/tests/provider_package_corpus.rs b/crates/echo-wesley-gen/tests/provider_package_corpus.rs index 92b29e22..25aa5115 100644 --- a/crates/echo-wesley-gen/tests/provider_package_corpus.rs +++ b/crates/echo-wesley-gen/tests/provider_package_corpus.rs @@ -11,9 +11,9 @@ use std::sync::atomic::{AtomicU64, Ordering}; use sha2::{Digest as _, Sha256}; const EXPECTED_PROVIDER_DIGEST: &str = - "sha256:38dd0532d4353f6260e8aaeafb149457b42f8ad94bb107ecbb74bf02f0051a92"; + "sha256:6d99fc4b4a56268b053226f17dfaa946bec6a3328219393c21fdb3398db6a925"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "a65771b9430edd6b6ea6de727bf7569a491cfcc8bcd636d081d3e536557bfc5c"; + "0256679771de98841e1594a4133b24a328a9924c765fe2545e182565494ea6c0"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs b/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs index 48bfa3c6..e344f532 100644 --- a/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs +++ b/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs @@ -3,6 +3,10 @@ #![allow(clippy::expect_used, clippy::panic)] //! Deterministic primary artifact generation for the Echo Edict provider. +use echo_registry_api::{ + stable_semantic_operation_id_v1, OpKind, LITTLE_ENDIAN_CODEC_V1_ID, + SEMANTIC_OPERATION_ID_LAW_V1, +}; use echo_wesley_gen::provider_artifacts::{ generate_provider_primary_artifacts_v1, ProviderArtifactGenerationErrorKind, ProviderPrimaryArtifactsV1, SchemaValidatedCanonicalProviderOutputV1, @@ -470,6 +474,93 @@ fn budget_change_moves_only_lawpack_authority_facts() { ); } +#[test] +fn generated_profile_carries_the_echo_owned_semantic_operation_id() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let registration = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("registration profile exists") + .canonical_value(); + + assert_eq!( + text_value(map_field(registration, "operationIdLaw")), + SEMANTIC_OPERATION_ID_LAW_V1 + ); + let operation = map_field(map_field(registration, "operations"), "a.b@1.t"); + assert_eq!( + integer_value(map_field(operation, "operationId")), + i128::from(stable_semantic_operation_id_v1(OpKind::Mutation, "a.b@1.t")) + ); +} + +#[test] +fn generated_profile_binds_the_exact_echo_value_codec() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let registration = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("registration profile exists") + .canonical_value(); + let operation = map_field(map_field(registration, "operations"), "a.b@1.t"); + + assert_eq!( + text_value(map_field(operation, "valueCodec")), + LITTLE_ENDIAN_CODEC_V1_ID + ); +} + +#[test] +fn semantic_operation_ids_fail_closed_on_reserved_and_colliding_results() { + let pack = admitted_pack(); + + for coordinate in ["a.b@1.cs2esoh", "a.b@1.0ctdaa8i"] { + let mut reserved = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + reserved["operations"][0]["identity"]["coordinate"] = json!(coordinate); + let reserved_bytes = serde_json::to_vec(&reserved).expect("reserved-id source serializes"); + let reserved_input = build_input(&reserved_bytes, &pack); + let reserved_error = generate_provider_primary_artifacts_v1(&reserved_input, &pack) + .expect_err("every Echo-reserved protocol id must refuse"); + assert_eq!( + ( + reserved_error.kind(), + reserved_error.subject(), + reserved_error.reference(), + ), + ( + ProviderArtifactGenerationErrorKind::ReservedOperationId, + coordinate, + SEMANTIC_OPERATION_ID_LAW_V1, + ) + ); + } + + let mut colliding = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + let mut second = colliding["operations"][0].clone(); + colliding["operations"][0]["identity"]["coordinate"] = json!("a.b@1.3n8plpiea7"); + second["identity"]["coordinate"] = json!("a.b@1.lmdfcgrl2h"); + colliding["operations"] + .as_array_mut() + .expect("operations are an array") + .push(second); + let colliding_bytes = serde_json::to_vec(&colliding).expect("colliding-id source serializes"); + let colliding_input = build_input(&colliding_bytes, &pack); + let collision_error = generate_provider_primary_artifacts_v1(&colliding_input, &pack) + .expect_err("package-local operation-id collisions must refuse"); + assert_eq!( + ( + collision_error.kind(), + collision_error.subject(), + collision_error.reference(), + ), + ( + ProviderArtifactGenerationErrorKind::OperationIdCollision, + "a.b@1.lmdfcgrl2h", + "a.b@1.3n8plpiea7", + ) + ); +} + #[test] fn revelation_profiles_generate_bounded_observer_invocations() { let pack = admitted_pack(); @@ -494,6 +585,14 @@ fn revelation_profiles_generate_bounded_observer_invocations() { text_value(map_field(operation, "invocationKind")), "observer" ); + assert_eq!( + integer_value(map_field(operation, "operationId")), + i128::from(stable_semantic_operation_id_v1(OpKind::Query, "a.b@1.t")) + ); + assert_ne!( + integer_value(map_field(operation, "operationId")), + i128::from(stable_semantic_operation_id_v1(OpKind::Mutation, "a.b@1.t")) + ); let profiles = map_field( generated @@ -1024,6 +1123,28 @@ fn generated_schema_distinguishes_canonical_wire_from_root_admission() { ) ); + for invalid_operation_id in [ + i128::from(u32::MAX - 1), + i128::from(u32::MAX), + i128::from(u32::MAX) + 1, + ] { + let mut invalid_id = profile.canonical_value().clone(); + *map_field_mut( + map_field_mut(map_field_mut(&mut invalid_id, "operations"), "a.b@1.t"), + "operationId", + ) = CanonicalValueV1::Integer(invalid_operation_id); + let invalid_id_bytes = encode_canonical_cbor_v1(&invalid_id) + .expect("invalid operation id remains canonical CBOR"); + let invalid_id_error = generated + .schema() + .validate_root_bytes(profile.owning_root(), &invalid_id_bytes) + .expect_err("the owning root must constrain operation ids to application u32 values"); + assert_eq!( + invalid_id_error.kind(), + ProviderArtifactGenerationErrorKind::OwningRootRejected + ); + } + let mut trailing = profile.canonical_bytes().to_vec(); trailing.push(0xf6); let canonical_error = generated diff --git a/crates/warp-core/README.md b/crates/warp-core/README.md index 1f821cd0..3d10e39c 100644 --- a/crates/warp-core/README.md +++ b/crates/warp-core/README.md @@ -59,6 +59,36 @@ The `warp-core` crate also contains a small “website kernel spike” used by t - `Engine::ingest_intent(intent_bytes)` and `Engine::ingest_inbox_event(seq, payload)` remain legacy compatibility helpers for isolated tests and older spike call sites. +## Generated provider package proposals + +`warp-core` now accepts the provider-neutral registry emitted by the Edict +helper and can preflight one mutation into an opaque +`ProviderContractPackageProposalV1`. The constructor syntactically bounds the +host-owned runtime package occurrence to a nonempty name and version plus a raw +lowercase SHA-256 claim; it does not authenticate or semantically cross-bind +that occurrence. Separately, it fails closed unless the provider registry and +explicit host implementation agree on the complete operation id, Target IR, +semantic/release bundles, target/generated/operation profiles, provider and +value schemas, `le-binary-v1` codec, obstruction mapping, ABI, helper API, and +footprint identities, and unless the generated dispatch agrees on the exact +operation id and canonical rule name. + +The generated helper owns the distinct typed input/output codecs and canonical +EINT construction; `warp-core` treats EINT `vars` as codec-owned opaque bytes. +The host supplies only its semantic mutation effects. Echo always adds the +generated matcher's mandatory ingress-EINT read to the proposed rule footprint +and conservatively enables every factor bit, so a host cannot omit those +Echo-owned matcher reads. Matching identities and footprints are preflight +claims, not proof that arbitrary host callback code implements the declared +semantics. + +The proposal is deliberately non-installing: it grants no admission, +registration, scheduling, execution, durability, receipt, or observation +authority. A trusted Echo host must perform the later package admission and +installation crossing. This constructor is mutation-specific and rejects query +operations. Authored reads remain on the separate bounded observer/optic path; +they are never represented as synthetic mutations. + ## Documentation - Core engine specs live in `docs/`: diff --git a/crates/warp-core/src/contract_host.rs b/crates/warp-core/src/contract_host.rs index a19e6672..447947b6 100644 --- a/crates/warp-core/src/contract_host.rs +++ b/crates/warp-core/src/contract_host.rs @@ -64,19 +64,18 @@ pub fn eint_vars_for_op<'a>( /// /// Contract handlers should extend this footprint with any handler-specific /// graph, edge, attachment, or port writes they emit. This helper deliberately -/// declares no write authority by itself. +/// declares no write authority by itself. The scope and attachment reads are +/// unconditional because observing their absence is still a read. #[must_use] pub fn runtime_ingress_eint_read_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { let warp_id = view.warp_id(); let mut n_read = NodeSet::default(); let mut a_read = AttachmentSet::default(); - if view.node(scope).is_some() { - n_read.insert_with_warp(warp_id, *scope); - a_read.insert(AttachmentKey::node_alpha(NodeKey { - warp_id, - local_id: *scope, - })); - } + n_read.insert_with_warp(warp_id, *scope); + a_read.insert(AttachmentKey::node_alpha(NodeKey { + warp_id, + local_id: *scope, + })); Footprint { n_read, n_write: NodeSet::default(), diff --git a/crates/warp-core/src/footprint.rs b/crates/warp-core/src/footprint.rs index 9c6d8f77..baaf8d73 100644 --- a/crates/warp-core/src/footprint.rs +++ b/crates/warp-core/src/footprint.rs @@ -274,6 +274,22 @@ pub struct Footprint { } impl Footprint { + /// Add every resource claim from `other` to this footprint. + /// + /// Set membership is unioned and the coarse factor mask is combined with + /// bitwise OR. This operation never removes or weakens an existing claim. + pub fn union_assign(&mut self, other: &Self) { + self.n_read.0.extend(other.n_read.0.iter().copied()); + self.n_write.0.extend(other.n_write.0.iter().copied()); + self.e_read.0.extend(other.e_read.0.iter().copied()); + self.e_write.0.extend(other.e_write.0.iter().copied()); + self.a_read.0.extend(other.a_read.0.iter().copied()); + self.a_write.0.extend(other.a_write.0.iter().copied()); + self.b_in.0.extend(other.b_in.0.iter().copied()); + self.b_out.0.extend(other.b_out.0.iter().copied()); + self.factor_mask |= other.factor_mask; + } + /// Derives the admission-side bounded site for this footprint. /// /// The first cut preserves the full declared claim and derives the site's @@ -360,6 +376,84 @@ mod tests { use super::*; use crate::ident::make_warp_id; + #[test] + fn footprint_union_assign_preserves_every_resource_plane_and_mask_bit() { + let warp_id = make_warp_id("test/footprint/union"); + let left_node = NodeId(blake3::hash(b"union-left-node").into()); + let right_node = NodeId(blake3::hash(b"union-right-node").into()); + let left_edge = EdgeId(blake3::hash(b"union-left-edge").into()); + let right_edge = EdgeId(blake3::hash(b"union-right-edge").into()); + let left_node_key = NodeKey { + warp_id, + local_id: left_node, + }; + let right_node_key = NodeKey { + warp_id, + local_id: right_node, + }; + + let mut left = Footprint { + factor_mask: 0b0001, + ..Footprint::default() + }; + left.n_read.insert(left_node_key); + left.n_write.insert(left_node_key); + left.e_read.insert(EdgeKey { + warp_id, + local_id: left_edge, + }); + left.e_write.insert(EdgeKey { + warp_id, + local_id: left_edge, + }); + left.a_read.insert(AttachmentKey::node_alpha(left_node_key)); + left.a_write + .insert(AttachmentKey::node_alpha(left_node_key)); + left.b_in + .insert(warp_id, pack_port_key(&left_node, 1, true)); + left.b_out + .insert(warp_id, pack_port_key(&left_node, 2, false)); + + let mut right = Footprint { + factor_mask: 0b0100, + ..Footprint::default() + }; + right.n_read.insert(right_node_key); + right.n_write.insert(right_node_key); + right.e_read.insert(EdgeKey { + warp_id, + local_id: right_edge, + }); + right.e_write.insert(EdgeKey { + warp_id, + local_id: right_edge, + }); + right + .a_read + .insert(AttachmentKey::node_alpha(right_node_key)); + right + .a_write + .insert(AttachmentKey::node_alpha(right_node_key)); + right + .b_in + .insert(warp_id, pack_port_key(&right_node, 3, true)); + right + .b_out + .insert(warp_id, pack_port_key(&right_node, 4, false)); + + left.union_assign(&right); + + assert_eq!(left.factor_mask, 0b0101); + assert_eq!(left.n_read.len(), 2); + assert_eq!(left.n_write.len(), 2); + assert_eq!(left.e_read.len(), 2); + assert_eq!(left.e_write.len(), 2); + assert_eq!(left.a_read.len(), 2); + assert_eq!(left.a_write.len(), 2); + assert_eq!(left.b_in.len(), 2); + assert_eq!(left.b_out.len(), 2); + } + #[test] fn pack_port_key_is_stable_and_distinct_by_inputs() { let a = NodeId(blake3::hash(b"node-a").into()); diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 2abbb3e8..cce29291 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -152,6 +152,7 @@ mod plurality_law; pub mod proof; mod provenance_codec; mod provenance_store; +mod provider_contract; mod receipt; mod record; mod retained_evidence; @@ -271,6 +272,12 @@ pub use payload::{ encode_motion_payload_q32_32, encode_motion_payload_v0, motion_payload_type_id, motion_payload_type_id_v0, }; +pub use provider_contract::{ + propose_provider_contract_package_v1, GeneratedProviderMutationDispatchV1, + ProviderContractPackageProposalV1, ProviderMutationExecuteFnV1, ProviderMutationFootprintFnV1, + ProviderMutationHooksV1, ProviderMutationHostV1, ProviderMutationImplementationIdentityV1, + ProviderMutationMatchFnV1, ProviderPackageProposalError, ProviderPackageProposalErrorKind, +}; // --- Plurality law types --- pub use plurality_law::{ PluralityLawAuthorization, PluralityLawCard, PluralityLawCardError, PluralityLawConcealment, diff --git a/crates/warp-core/src/provider_contract.rs b/crates/warp-core/src/provider_contract.rs new file mode 100644 index 00000000..e8e28cf5 --- /dev/null +++ b/crates/warp-core/src/provider_contract.rs @@ -0,0 +1,615 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! Provider-generic, non-installing contract package proposals. +//! +//! A proposal retains exact generated identities and one explicit host +//! implementation binding. It grants no admission, installation, scheduling, +//! execution, durability, receipt, or observation authority. + +use core::fmt; + +use echo_registry_api::{ + is_reserved_operation_id, OpKind, ProviderBundleIdentityV1, ProviderDigestIdentityV1, + ProviderOperationV1, ProviderRegistryV1, ProviderSchemaIdentityV1, +}; + +use crate::contract_host::runtime_ingress_eint_read_footprint; +use crate::contract_registry::{ContractMutationHandler, ContractPackageIdentity}; +use crate::footprint::Footprint; +use crate::graph_view::GraphView; +use crate::ident::{make_type_id, NodeId}; +use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; +use crate::TickDelta; + +/// Generated matcher callback for one proposed provider mutation. +pub type ProviderMutationMatchFnV1 = for<'view> fn(GraphView<'view>, &NodeId) -> bool; + +/// Explicit host executor callback for one proposed provider mutation. +pub type ProviderMutationExecuteFnV1 = for<'view> fn(GraphView<'view>, &NodeId, &mut TickDelta); + +/// Explicit host footprint callback for one proposed provider mutation. +pub type ProviderMutationFootprintFnV1 = for<'view> fn(GraphView<'view>, &NodeId) -> Footprint; + +/// Host-owned semantic implementation for one generated provider mutation. +/// +/// Echo wraps the effect footprint returned here with the mandatory runtime +/// ingress EINT read used by generated matchers. Implementations therefore +/// describe only their semantic graph effects and cannot omit that Echo-owned +/// read from a proposed rule. +pub trait ProviderMutationHostV1 { + /// Execute the semantic mutation during a scheduler-owned tick. + fn execute(view: GraphView<'_>, scope: &NodeId, delta: &mut TickDelta); + + /// Compute the semantic effect footprint beyond Echo's ingress EINT read. + fn effect_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint; +} + +fn execute_provider_host( + view: GraphView<'_>, + scope: &NodeId, + delta: &mut TickDelta, +) { + H::execute(view, scope, delta); +} + +fn compute_provider_host_footprint( + view: GraphView<'_>, + scope: &NodeId, +) -> Footprint { + let mut footprint = runtime_ingress_eint_read_footprint(view, scope); + footprint.union_assign(&H::effect_footprint(view, scope)); + footprint.factor_mask = u64::MAX; + footprint +} + +/// Exact identities claimed by one host mutation implementation. +/// +/// Equality against the generated registry detects accidental cross-binding. +/// It does not prove that arbitrary Rust callbacks semantically implement the +/// claims; that stronger proposition belongs to admitted implementation and +/// conformance evidence. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ProviderMutationImplementationIdentityV1<'a> { + /// Echo contract ABI version the implementation expects. + pub echo_contract_abi_version: u32, + /// Contract-host helper API version the implementation expects. + pub helper_api_version: u32, + /// Exact provider schema identity the implementation expects. + pub provider_schema: ProviderSchemaIdentityV1<'a>, + /// Exact target-bundle-profile identity the implementation expects. + pub target_bundle_profile: ProviderDigestIdentityV1<'a>, + /// Exact semantic and release bundle identities the implementation expects. + pub bundle: ProviderBundleIdentityV1<'a>, + /// Exact operation and target claims the implementation expects. + pub operation: ProviderOperationV1<'a>, +} + +/// One explicit host implementation and exact identity binding. +pub struct ProviderMutationHooksV1<'a> { + identity: ProviderMutationImplementationIdentityV1<'a>, + executor: ProviderMutationExecuteFnV1, + compute_footprint: ProviderMutationFootprintFnV1, +} + +impl<'a> ProviderMutationHooksV1<'a> { + /// Bind an independently claimed implementation identity to one host type. + /// + /// The stored footprint callback always unions the host's semantic effect + /// claim with Echo's mandatory runtime-ingress EINT read. + #[must_use] + pub const fn for_host( + identity: ProviderMutationImplementationIdentityV1<'a>, + ) -> Self { + Self { + identity, + executor: execute_provider_host::, + compute_footprint: compute_provider_host_footprint::, + } + } + + /// Return the implementation identity asserted by the host. + #[must_use] + pub const fn identity(&self) -> &ProviderMutationImplementationIdentityV1<'a> { + &self.identity + } +} + +/// Generated scheduler dispatch metadata for one mutation. +/// +/// This is code-generator infrastructure. The proposal constructor validates +/// its operation and deterministic rule name before retaining its matcher. +#[doc(hidden)] +pub struct GeneratedProviderMutationDispatchV1 { + operation_id: u32, + rule_name: &'static str, + matcher: ProviderMutationMatchFnV1, +} + +impl GeneratedProviderMutationDispatchV1 { + /// Construct generated dispatch metadata for proposal preflight. + #[must_use] + pub const fn new( + operation_id: u32, + rule_name: &'static str, + matcher: ProviderMutationMatchFnV1, + ) -> Self { + Self { + operation_id, + rule_name, + matcher, + } + } +} + +/// Stable reason a provider package proposal failed before installation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderPackageProposalErrorKind { + /// The host supplied no runtime package name. + EmptyPackageName, + /// The host supplied no runtime package version. + EmptyPackageVersion, + /// The host package artifact hash was not lowercase raw SHA-256. + MalformedPackageArtifactHash, + /// The registry does not describe exactly one supported operation. + UnsupportedOperationCount, + /// The registry operation is not a mutation supported by this constructor. + UnsupportedOperationKind, + /// The registry selected an Echo-reserved operation identifier. + ReservedOperationId, + /// Generated dispatch or host implementation operation claims differ. + OperationMismatch, + /// Echo contract ABI claims differ. + EchoAbiMismatch, + /// Contract-host helper API claims differ. + HelperApiMismatch, + /// Provider schema claims differ. + SchemaMismatch, + /// Semantic or release bundle claims differ. + BundleMismatch, + /// Target-bundle-profile claims differ. + TargetBundleProfileMismatch, + /// Target IR claims differ. + TargetIrMismatch, + /// Target-profile claims differ. + TargetProfileMismatch, + /// Generated-artifact-profile claims differ. + GeneratedArtifactProfileMismatch, + /// Input or output codec claims differ. + CodecMismatch, + /// Failure or typed-obstruction claims differ. + ObstructionMismatch, + /// Semantic operation-profile claims differ. + OperationProfileMismatch, + /// Footprint obligation or algebra claims differ. + FootprintMismatch, + /// Generated dispatch names a non-canonical rule identity. + RuleIdentityMismatch, +} + +/// Structured provider package proposal failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderPackageProposalError { + kind: ProviderPackageProposalErrorKind, + subject: String, + reference: Option, +} + +impl ProviderPackageProposalError { + fn new( + kind: ProviderPackageProposalErrorKind, + subject: impl Into, + reference: Option, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference, + } + } + + /// Return the stable failure category. + #[must_use] + pub const fn kind(&self) -> ProviderPackageProposalErrorKind { + self.kind + } + + /// Return the stable subject that failed preflight. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Return the optional conflicting value or claim name. + #[must_use] + pub fn reference(&self) -> Option<&str> { + self.reference.as_deref() + } +} + +impl fmt::Display for ProviderPackageProposalError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider package proposal failed for {}", + self.subject + )?; + if let Some(reference) = &self.reference { + write!(formatter, " ({reference})")?; + } + Ok(()) + } +} + +impl std::error::Error for ProviderPackageProposalError {} + +/// Opaque provider package proposal with no runtime installation authority. +pub struct ProviderContractPackageProposalV1<'a> { + occurrence: ContractPackageIdentity<'a>, + registry: ProviderRegistryV1<'a>, + mutation_handler: ContractMutationHandler, +} + +impl<'a> ProviderContractPackageProposalV1<'a> { + /// Return the host-owned package occurrence claim. + #[must_use] + pub const fn occurrence(&self) -> &ContractPackageIdentity<'a> { + &self.occurrence + } + + /// Return the exact provider registry retained by the proposal. + #[must_use] + pub const fn registry(&self) -> &ProviderRegistryV1<'a> { + &self.registry + } + + /// Iterate the mutation operation identifiers retained by the proposal. + pub fn mutation_operation_ids(&self) -> impl ExactSizeIterator + '_ { + core::iter::once(self.mutation_handler.op_id) + } +} + +/// Construct one pure provider mutation package proposal. +/// +/// The function compares every generated registry claim with the independently +/// supplied host implementation identity before materializing a private, +/// conservative scheduler rule. It performs no registration or runtime I/O. +/// +/// # Errors +/// +/// Returns [`ProviderPackageProposalError`] when occurrence metadata is +/// malformed, the registry is not exactly one mutation, any identity differs, +/// or generated dispatch metadata is inconsistent. +pub fn propose_provider_contract_package_v1<'a>( + occurrence: ContractPackageIdentity<'a>, + registry: ProviderRegistryV1<'a>, + dispatch: GeneratedProviderMutationDispatchV1, + hooks: ProviderMutationHooksV1<'a>, +) -> Result, ProviderPackageProposalError> { + validate_occurrence(occurrence)?; + if registry.operations.len() != 1 { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::UnsupportedOperationCount, + "registry.operations", + Some(registry.operations.len().to_string()), + )); + } + + let operation = ®istry.operations[0]; + if operation.kind != OpKind::Mutation { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::UnsupportedOperationKind, + operation.coordinate, + Some("query".to_owned()), + )); + } + if is_reserved_operation_id(operation.operation_id) { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::ReservedOperationId, + operation.coordinate, + Some(operation.operation_id.to_string()), + )); + } + if dispatch.operation_id != operation.operation_id { + return Err(mismatch( + ProviderPackageProposalErrorKind::OperationMismatch, + operation, + "dispatch.operationId", + )); + } + + validate_implementation_identity(®istry, operation, &hooks.identity)?; + let expected_rule_name = format!( + "cmd/contract/{}/{}/{}", + registry.provider_schema.raw_sha256_hex, operation.operation_id, operation.coordinate + ); + if dispatch.rule_name != expected_rule_name { + return Err(mismatch( + ProviderPackageProposalErrorKind::RuleIdentityMismatch, + operation, + dispatch.rule_name, + )); + } + + let mutation_handler = materialize_mutation_handler( + operation.operation_id, + dispatch, + hooks.executor, + hooks.compute_footprint, + ); + + Ok(ProviderContractPackageProposalV1 { + occurrence, + registry, + mutation_handler, + }) +} + +fn materialize_mutation_handler( + operation_id: u32, + dispatch: GeneratedProviderMutationDispatchV1, + executor: ProviderMutationExecuteFnV1, + compute_footprint: ProviderMutationFootprintFnV1, +) -> ContractMutationHandler { + ContractMutationHandler { + op_id: operation_id, + rule: RewriteRule { + id: make_type_id(dispatch.rule_name).0, + name: dispatch.rule_name, + left: PatternGraph { nodes: Vec::new() }, + matcher: dispatch.matcher, + executor, + compute_footprint, + factor_mask: u64::MAX, + conflict_policy: ConflictPolicy::Abort, + join_fn: None, + }, + } +} + +fn validate_occurrence( + occurrence: ContractPackageIdentity<'_>, +) -> Result<(), ProviderPackageProposalError> { + if occurrence.package_name.is_empty() { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::EmptyPackageName, + "package.name", + None, + )); + } + if occurrence.package_version.is_empty() { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::EmptyPackageVersion, + "package.version", + None, + )); + } + if !is_raw_sha256(occurrence.artifact_hash_hex) { + return Err(ProviderPackageProposalError::new( + ProviderPackageProposalErrorKind::MalformedPackageArtifactHash, + "package.artifactHash", + Some(occurrence.artifact_hash_hex.to_owned()), + )); + } + Ok(()) +} + +fn validate_implementation_identity( + registry: &ProviderRegistryV1<'_>, + operation: &ProviderOperationV1<'_>, + identity: &ProviderMutationImplementationIdentityV1<'_>, +) -> Result<(), ProviderPackageProposalError> { + if identity.echo_contract_abi_version != registry.echo_contract_abi_version { + return Err(mismatch( + ProviderPackageProposalErrorKind::EchoAbiMismatch, + operation, + "echoContractAbiVersion", + )); + } + if identity.helper_api_version != registry.helper_api_version { + return Err(mismatch( + ProviderPackageProposalErrorKind::HelperApiMismatch, + operation, + "helperApiVersion", + )); + } + if identity.provider_schema != registry.provider_schema { + return Err(mismatch( + ProviderPackageProposalErrorKind::SchemaMismatch, + operation, + "providerSchema", + )); + } + if identity.target_bundle_profile != registry.target_bundle_profile { + return Err(mismatch( + ProviderPackageProposalErrorKind::TargetBundleProfileMismatch, + operation, + "targetBundleProfile", + )); + } + if identity.bundle != registry.bundle { + return Err(mismatch( + ProviderPackageProposalErrorKind::BundleMismatch, + operation, + "bundle", + )); + } + compare_operation_identity(operation, &identity.operation) +} + +fn compare_operation_identity( + expected: &ProviderOperationV1<'_>, + actual: &ProviderOperationV1<'_>, +) -> Result<(), ProviderPackageProposalError> { + if expected.coordinate != actual.coordinate + || expected.semantic_domain != actual.semantic_domain + || expected.kind != actual.kind + || expected.operation_id_law != actual.operation_id_law + || expected.operation_id != actual.operation_id + { + return Err(mismatch( + ProviderPackageProposalErrorKind::OperationMismatch, + expected, + "operation", + )); + } + if expected.input.codec_id != actual.input.codec_id + || expected.output.codec_id != actual.output.codec_id + { + return Err(mismatch( + ProviderPackageProposalErrorKind::CodecMismatch, + expected, + "valueCodec", + )); + } + if expected.input.schema_coordinate != actual.input.schema_coordinate + || expected.input.schema_domain != actual.input.schema_domain + || expected.output.schema_coordinate != actual.output.schema_coordinate + || expected.output.schema_domain != actual.output.schema_domain + { + return Err(mismatch( + ProviderPackageProposalErrorKind::SchemaMismatch, + expected, + "operationSchema", + )); + } + if expected.target_ir != actual.target_ir { + return Err(mismatch( + ProviderPackageProposalErrorKind::TargetIrMismatch, + expected, + "targetIr", + )); + } + if expected.target_profile != actual.target_profile { + return Err(mismatch( + ProviderPackageProposalErrorKind::TargetProfileMismatch, + expected, + "targetProfile", + )); + } + if expected.generated_artifact_profile != actual.generated_artifact_profile { + return Err(mismatch( + ProviderPackageProposalErrorKind::GeneratedArtifactProfileMismatch, + expected, + "generatedArtifactProfile", + )); + } + if expected.target_failure_schema != actual.target_failure_schema + || expected.obstruction != actual.obstruction + || expected.obstruction_payload_schema != actual.obstruction_payload_schema + { + return Err(mismatch( + ProviderPackageProposalErrorKind::ObstructionMismatch, + expected, + "obstruction", + )); + } + if expected.operation_profile != actual.operation_profile + || expected.operation_profiles != actual.operation_profiles + { + return Err(mismatch( + ProviderPackageProposalErrorKind::OperationProfileMismatch, + expected, + "operationProfile", + )); + } + if expected.footprint != actual.footprint { + return Err(mismatch( + ProviderPackageProposalErrorKind::FootprintMismatch, + expected, + "footprint", + )); + } + Ok(()) +} + +fn mismatch( + kind: ProviderPackageProposalErrorKind, + operation: &ProviderOperationV1<'_>, + reference: &str, +) -> ProviderPackageProposalError { + ProviderPackageProposalError::new(kind, operation.coordinate, Some(reference.to_owned())) +} + +fn is_raw_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn matcher(_view: GraphView<'_>, _scope: &NodeId) -> bool { + false + } + + fn executor(_view: GraphView<'_>, _scope: &NodeId, _delta: &mut TickDelta) {} + + fn footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { + let mut footprint = Footprint::default(); + footprint.n_write.insert_with_warp(view.warp_id(), *scope); + footprint + } + + struct TestHost; + + impl ProviderMutationHostV1 for TestHost { + fn execute(view: GraphView<'_>, scope: &NodeId, delta: &mut TickDelta) { + executor(view, scope, delta); + } + + fn effect_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { + footprint(view, scope) + } + } + + #[test] + fn provider_proposal_materializes_a_conservative_private_rule() { + let handler = materialize_mutation_handler( + 7, + GeneratedProviderMutationDispatchV1::new(7, "cmd/provider/test", matcher), + execute_provider_host::, + compute_provider_host_footprint::, + ); + + assert_eq!(handler.op_id, 7); + assert_eq!(handler.rule.id, make_type_id("cmd/provider/test").0); + assert_eq!(handler.rule.name, "cmd/provider/test"); + assert!(handler.rule.left.nodes.is_empty()); + assert!(std::ptr::fn_addr_eq( + handler.rule.matcher, + matcher as ProviderMutationMatchFnV1 + )); + assert!(std::ptr::fn_addr_eq( + handler.rule.executor, + execute_provider_host:: as ProviderMutationExecuteFnV1 + )); + assert!(std::ptr::fn_addr_eq( + handler.rule.compute_footprint, + compute_provider_host_footprint:: as ProviderMutationFootprintFnV1 + )); + let warp_id = crate::ident::WarpId([1; 32]); + let scope = NodeId([2; 32]); + let mut store = crate::graph::GraphStore::new(warp_id); + store.insert_node( + scope, + crate::record::NodeRecord { + ty: make_type_id("provider-test-ingress"), + }, + ); + let declared = (handler.rule.compute_footprint)(GraphView::new(&store), &scope); + assert_eq!(declared.n_read.len(), 1); + assert_eq!(declared.a_read.len(), 1); + assert_eq!(declared.n_write.len(), 1); + assert_eq!(declared.factor_mask, u64::MAX); + assert_eq!(handler.rule.factor_mask, u64::MAX); + assert!(matches!( + handler.rule.conflict_policy, + ConflictPolicy::Abort + )); + assert!(handler.rule.join_fn.is_none()); + } +} diff --git a/crates/warp-core/tests/contract_host_footprint_tests.rs b/crates/warp-core/tests/contract_host_footprint_tests.rs new file mode 100644 index 00000000..fb6eea06 --- /dev/null +++ b/crates/warp-core/tests/contract_host_footprint_tests.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Contract-host footprint soundness tests. + +use warp_core::{ + make_node_id, make_warp_id, runtime_ingress_eint_read_footprint, AttachmentKey, + AttachmentValue, Footprint, GraphStore, GraphView, NodeId, NodeKey, +}; + +fn assert_ingress_scope_reads(footprint: &Footprint, warp_id: warp_core::WarpId, scope: NodeId) { + let node = NodeKey { + warp_id, + local_id: scope, + }; + assert_eq!( + footprint.n_read.iter().copied().collect::>(), + vec![node] + ); + assert_eq!( + footprint.a_read.iter().copied().collect::>(), + vec![AttachmentKey::node_alpha(node)] + ); +} + +#[test] +fn runtime_ingress_footprint_declares_absent_scope_reads() { + let warp_id = make_warp_id("test/contract-host/absent-scope"); + let scope = make_node_id("test/contract-host/absent-scope/event"); + let store = GraphStore::new(warp_id); + + let footprint = runtime_ingress_eint_read_footprint(GraphView::new(&store), &scope); + + assert_ingress_scope_reads(&footprint, warp_id, scope); +} + +#[test] +fn runtime_ingress_footprint_declares_orphan_attachment_reads() { + let warp_id = make_warp_id("test/contract-host/orphan-attachment"); + let scope = make_node_id("test/contract-host/orphan-attachment/event"); + let mut store = GraphStore::new(warp_id); + store.set_node_attachment( + scope, + Some(AttachmentValue::Descend(make_warp_id( + "test/contract-host/orphan-attachment/target", + ))), + ); + + let footprint = runtime_ingress_eint_read_footprint(GraphView::new(&store), &scope); + + assert_ingress_scope_reads(&footprint, warp_id, scope); +} diff --git a/crates/warp-core/tests/generated_provider_helper_msrv_tests.rs b/crates/warp-core/tests/generated_provider_helper_msrv_tests.rs new file mode 100644 index 00000000..2a3befb2 --- /dev/null +++ b/crates/warp-core/tests/generated_provider_helper_msrv_tests.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Echo-MSRV compile witness for the checked generated provider helper. +#![allow(clippy::expect_used)] +#![allow(dead_code)] + +#[rustfmt::skip] +#[path = "../../echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs"] +mod checked_generated_helper; + +use checked_generated_helper::echo_dpo as generated; + +#[test] +fn checked_generated_provider_helper_compiles_in_echo_rust_1_90_graph() { + let operation_id: u32 = generated::OPERATION_ID; + assert_eq!(operation_id, 3_389_142_194); + let input = generated::Input::new("echo-msrv").expect("fixture input is within its bound"); + assert_eq!(input.id(), "echo-msrv"); +} diff --git a/crates/warp-core/tests/provider_contract_package_proposal_tests.rs b/crates/warp-core/tests/provider_contract_package_proposal_tests.rs new file mode 100644 index 00000000..b2448439 --- /dev/null +++ b/crates/warp-core/tests/provider_contract_package_proposal_tests.rs @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Non-authoritative provider package proposal witnesses. + +use echo_registry_api::{ + OpKind, ProviderBundleIdentityV1, ProviderDigestIdentityV1, ProviderFootprintIdentityV1, + ProviderOperationV1, ProviderRegistryV1, ProviderSchemaIdentityV1, ProviderSemanticIdentityV1, + ProviderValueContractV1, CONTRACT_HOST_HELPER_API_VERSION, ECHO_CONTRACT_ABI_VERSION, + LITTLE_ENDIAN_CODEC_V1_ID, RESERVED_CONTROL_OPERATION_ID, SEMANTIC_OPERATION_ID_LAW_V1, +}; +use warp_core::{ + propose_provider_contract_package_v1, ContractPackageIdentity, Footprint, + GeneratedProviderMutationDispatchV1, GraphView, NodeId, ProviderMutationHooksV1, + ProviderMutationHostV1, ProviderMutationImplementationIdentityV1, + ProviderPackageProposalErrorKind, TickDelta, +}; + +use std::sync::atomic::{AtomicUsize, Ordering}; + +const OPERATION_ID: u32 = 3_389_142_194; +const RAW_SCHEMA_SHA256: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const DIGEST: &str = "sha256:1111111111111111111111111111111111111111111111111111111111111111"; +const RULE_NAME: &str = concat!( + "cmd/contract/", + "0000000000000000000000000000000000000000000000000000000000000000", + "/3389142194/a.b@1.t" +); + +const TARGET_IR: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.span-ir/v1", + digest_domain: "edict.target-ir.artifact/v1", + digest: DIGEST, +}; +const TARGET_PROFILE: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo@1", + digest_domain: "edict.target-profile/v1", + digest: DIGEST, +}; +const GENERATED_PROFILE: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo.registration/v1", + digest_domain: "echo.generated-artifact-profile/v1", + digest: DIGEST, +}; +const OPERATION_PROFILES: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo.operation-profiles/v1", + digest_domain: "echo.dpo.operation-profiles/v1", + digest: DIGEST, +}; +const TARGET_BUNDLE_PROFILE: ProviderDigestIdentityV1<'static> = ProviderDigestIdentityV1 { + coordinate: "echo.dpo.bundle/v1", + digest_domain: "echo.dpo.bundle/v1", + digest: DIGEST, +}; +const PROVIDER_SCHEMA: ProviderSchemaIdentityV1<'static> = ProviderSchemaIdentityV1 { + coordinate: "echo.provider-artifacts.cddl@1", + raw_sha256_hex: RAW_SCHEMA_SHA256, +}; +const BUNDLE: ProviderBundleIdentityV1<'static> = ProviderBundleIdentityV1 { + semantic_digest_domain: "edict.bundle.semantic/v1", + semantic_digest: DIGEST, + release_digest_domain: "edict.bundle.release/v1", + release_digest: DIGEST, +}; +const OPERATION: ProviderOperationV1<'static> = ProviderOperationV1 { + coordinate: "a.b@1.t", + semantic_domain: "echo.edict-provider/operation/v1", + kind: OpKind::Mutation, + operation_id_law: SEMANTIC_OPERATION_ID_LAW_V1, + operation_id: OPERATION_ID, + input: ProviderValueContractV1 { + schema_coordinate: "a.b@1.Input", + schema_domain: "echo.edict-provider/value/v1", + codec_id: LITTLE_ENDIAN_CODEC_V1_ID, + }, + output: ProviderValueContractV1 { + schema_coordinate: "a.b@1.Output", + schema_domain: "echo.edict-provider/value/v1", + codec_id: LITTLE_ENDIAN_CODEC_V1_ID, + }, + target_failure_schema: "target.replace.rejected", + obstruction: ProviderSemanticIdentityV1 { + coordinate: "domain.WriteRejected", + semantic_domain: "echo.edict-provider/obstruction/v1", + }, + obstruction_payload_schema: "domain.WriteRejected.Payload", + target_ir: TARGET_IR, + target_profile: TARGET_PROFILE, + generated_artifact_profile: GENERATED_PROFILE, + operation_profile: ProviderSemanticIdentityV1 { + coordinate: "continuum.profile.write/v1", + semantic_domain: "echo.edict-provider/operation-profile/v1", + }, + operation_profiles: OPERATION_PROFILES, + footprint: ProviderFootprintIdentityV1 { + obligation: "target.replace.footprint", + algebra_coordinate: "echo.dpo.footprint/v1", + algebra_digest_domain: "echo.dpo.footprint/v1", + algebra_digest: DIGEST, + }, +}; +const OPERATIONS: [ProviderOperationV1<'static>; 1] = [OPERATION]; +const REGISTRY: ProviderRegistryV1<'static> = ProviderRegistryV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: PROVIDER_SCHEMA, + target_bundle_profile: TARGET_BUNDLE_PROFILE, + bundle: BUNDLE, + operations: &OPERATIONS, +}; +const IMPLEMENTATION_IDENTITY: ProviderMutationImplementationIdentityV1<'static> = + ProviderMutationImplementationIdentityV1 { + echo_contract_abi_version: ECHO_CONTRACT_ABI_VERSION, + helper_api_version: CONTRACT_HOST_HELPER_API_VERSION, + provider_schema: PROVIDER_SCHEMA, + target_bundle_profile: TARGET_BUNDLE_PROFILE, + bundle: BUNDLE, + operation: OPERATION, + }; +const OCCURRENCE: ContractPackageIdentity<'static> = ContractPackageIdentity { + package_name: "a.b", + package_version: "1.0.0", + artifact_hash_hex: RAW_SCHEMA_SHA256, +}; + +static MATCHER_CALLS: AtomicUsize = AtomicUsize::new(0); +static EXECUTOR_CALLS: AtomicUsize = AtomicUsize::new(0); +static FOOTPRINT_CALLS: AtomicUsize = AtomicUsize::new(0); + +fn matcher(_view: GraphView<'_>, _scope: &NodeId) -> bool { + false +} + +fn execute(_view: GraphView<'_>, _scope: &NodeId, _delta: &mut TickDelta) {} + +fn footprint(_view: GraphView<'_>, _scope: &NodeId) -> Footprint { + Footprint::default() +} + +fn counting_matcher(_view: GraphView<'_>, _scope: &NodeId) -> bool { + MATCHER_CALLS.fetch_add(1, Ordering::SeqCst); + false +} + +fn counting_execute(_view: GraphView<'_>, _scope: &NodeId, _delta: &mut TickDelta) { + EXECUTOR_CALLS.fetch_add(1, Ordering::SeqCst); +} + +fn counting_footprint(_view: GraphView<'_>, _scope: &NodeId) -> Footprint { + FOOTPRINT_CALLS.fetch_add(1, Ordering::SeqCst); + Footprint::default() +} + +struct TestHost; + +impl ProviderMutationHostV1 for TestHost { + fn execute(view: GraphView<'_>, scope: &NodeId, delta: &mut TickDelta) { + execute(view, scope, delta); + } + + fn effect_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { + footprint(view, scope) + } +} + +struct CountingHost; + +impl ProviderMutationHostV1 for CountingHost { + fn execute(view: GraphView<'_>, scope: &NodeId, delta: &mut TickDelta) { + counting_execute(view, scope, delta); + } + + fn effect_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { + counting_footprint(view, scope) + } +} + +fn proposal_with<'a>( + occurrence: ContractPackageIdentity<'a>, + registry: ProviderRegistryV1<'a>, + dispatch: GeneratedProviderMutationDispatchV1, + identity: &ProviderMutationImplementationIdentityV1<'a>, +) -> Result, warp_core::ProviderPackageProposalError> +{ + propose_provider_contract_package_v1( + occurrence, + registry, + dispatch, + ProviderMutationHooksV1::for_host::(*identity), + ) +} + +fn assert_identity_mismatch( + identity: &ProviderMutationImplementationIdentityV1<'static>, + expected: ProviderPackageProposalErrorKind, +) { + let error = proposal_with( + OCCURRENCE, + REGISTRY, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + identity, + ) + .err() + .map(|error| error.kind()); + assert_eq!(error, Some(expected)); +} + +#[test] +fn provider_proposal_binds_exactly_one_generated_mutation( +) -> Result<(), warp_core::ProviderPackageProposalError> { + let proposal = propose_provider_contract_package_v1( + OCCURRENCE, + REGISTRY, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + ProviderMutationHooksV1::for_host::(IMPLEMENTATION_IDENTITY), + )?; + + assert_eq!(proposal.occurrence(), &OCCURRENCE); + assert_eq!(proposal.registry(), ®ISTRY); + assert_eq!( + proposal.mutation_operation_ids().collect::>(), + vec![OPERATION_ID] + ); + Ok(()) +} + +#[test] +fn provider_proposal_refuses_every_mismatched_implementation_claim() { + let mut identity = IMPLEMENTATION_IDENTITY; + identity.echo_contract_abi_version += 1; + assert_identity_mismatch(&identity, ProviderPackageProposalErrorKind::EchoAbiMismatch); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.helper_api_version += 1; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::HelperApiMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.provider_schema.raw_sha256_hex = "wrong"; + assert_identity_mismatch(&identity, ProviderPackageProposalErrorKind::SchemaMismatch); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.target_bundle_profile.digest = "sha256:wrong"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::TargetBundleProfileMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.bundle.semantic_digest = "sha256:wrong"; + assert_identity_mismatch(&identity, ProviderPackageProposalErrorKind::BundleMismatch); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.coordinate = "a.b@1.other"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::OperationMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.input.codec_id = "wrong-codec/v1"; + assert_identity_mismatch(&identity, ProviderPackageProposalErrorKind::CodecMismatch); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.output.schema_coordinate = "a.b@1.OtherOutput"; + assert_identity_mismatch(&identity, ProviderPackageProposalErrorKind::SchemaMismatch); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.target_ir.digest = "sha256:wrong"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::TargetIrMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.target_profile.digest = "sha256:wrong"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::TargetProfileMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.generated_artifact_profile.digest = "sha256:wrong"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::GeneratedArtifactProfileMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.obstruction.coordinate = "domain.Other"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::ObstructionMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.operation_profiles.digest = "sha256:wrong"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::OperationProfileMismatch, + ); + + let mut identity = IMPLEMENTATION_IDENTITY; + identity.operation.footprint.obligation = "target.other.footprint"; + assert_identity_mismatch( + &identity, + ProviderPackageProposalErrorKind::FootprintMismatch, + ); +} + +#[test] +fn provider_proposal_refuses_invalid_occurrence_registry_and_dispatch() { + for (occurrence, expected) in [ + ( + ContractPackageIdentity { + package_name: "", + ..OCCURRENCE + }, + ProviderPackageProposalErrorKind::EmptyPackageName, + ), + ( + ContractPackageIdentity { + package_version: "", + ..OCCURRENCE + }, + ProviderPackageProposalErrorKind::EmptyPackageVersion, + ), + ( + ContractPackageIdentity { + artifact_hash_hex: "ABC", + ..OCCURRENCE + }, + ProviderPackageProposalErrorKind::MalformedPackageArtifactHash, + ), + ] { + let kind = proposal_with( + occurrence, + REGISTRY, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + &IMPLEMENTATION_IDENTITY, + ) + .err() + .map(|error| error.kind()); + assert_eq!(kind, Some(expected)); + } + + let empty_registry = ProviderRegistryV1 { + operations: &[], + ..REGISTRY + }; + let kind = proposal_with( + OCCURRENCE, + empty_registry, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + &IMPLEMENTATION_IDENTITY, + ) + .err() + .map(|error| error.kind()); + assert_eq!( + kind, + Some(ProviderPackageProposalErrorKind::UnsupportedOperationCount) + ); + + let duplicate_registry = ProviderRegistryV1 { + operations: &[OPERATION, OPERATION], + ..REGISTRY + }; + let kind = proposal_with( + OCCURRENCE, + duplicate_registry, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + &IMPLEMENTATION_IDENTITY, + ) + .err() + .map(|error| error.kind()); + assert_eq!( + kind, + Some(ProviderPackageProposalErrorKind::UnsupportedOperationCount) + ); + + let query = ProviderOperationV1 { + kind: OpKind::Query, + ..OPERATION + }; + let query_registry = ProviderRegistryV1 { + operations: &[query], + ..REGISTRY + }; + let kind = proposal_with( + OCCURRENCE, + query_registry, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, matcher), + &IMPLEMENTATION_IDENTITY, + ) + .err() + .map(|error| error.kind()); + assert_eq!( + kind, + Some(ProviderPackageProposalErrorKind::UnsupportedOperationKind) + ); + + let reserved = ProviderOperationV1 { + operation_id: RESERVED_CONTROL_OPERATION_ID, + ..OPERATION + }; + let reserved_registry = ProviderRegistryV1 { + operations: &[reserved], + ..REGISTRY + }; + let kind = proposal_with( + OCCURRENCE, + reserved_registry, + GeneratedProviderMutationDispatchV1::new(RESERVED_CONTROL_OPERATION_ID, RULE_NAME, matcher), + &ProviderMutationImplementationIdentityV1 { + operation: reserved, + ..IMPLEMENTATION_IDENTITY + }, + ) + .err() + .map(|error| error.kind()); + assert_eq!( + kind, + Some(ProviderPackageProposalErrorKind::ReservedOperationId) + ); + + for (dispatch, expected) in [ + ( + GeneratedProviderMutationDispatchV1::new(OPERATION_ID + 1, RULE_NAME, matcher), + ProviderPackageProposalErrorKind::OperationMismatch, + ), + ( + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, "wrong-rule", matcher), + ProviderPackageProposalErrorKind::RuleIdentityMismatch, + ), + ] { + let kind = proposal_with(OCCURRENCE, REGISTRY, dispatch, &IMPLEMENTATION_IDENTITY) + .err() + .map(|error| error.kind()); + assert_eq!(kind, Some(expected)); + } +} + +#[test] +fn provider_proposal_construction_invokes_no_runtime_callback( +) -> Result<(), warp_core::ProviderPackageProposalError> { + MATCHER_CALLS.store(0, Ordering::SeqCst); + EXECUTOR_CALLS.store(0, Ordering::SeqCst); + FOOTPRINT_CALLS.store(0, Ordering::SeqCst); + + let _proposal = propose_provider_contract_package_v1( + OCCURRENCE, + REGISTRY, + GeneratedProviderMutationDispatchV1::new(OPERATION_ID, RULE_NAME, counting_matcher), + ProviderMutationHooksV1::for_host::(IMPLEMENTATION_IDENTITY), + )?; + + assert_eq!(MATCHER_CALLS.load(Ordering::SeqCst), 0); + assert_eq!(EXECUTOR_CALLS.load(Ordering::SeqCst), 0); + assert_eq!(FOOTPRINT_CALLS.load(Ordering::SeqCst), 0); + Ok(()) +} diff --git a/docs/NORTHSTAR.md b/docs/NORTHSTAR.md index 3a0ba5f0..acd0a75b 100644 --- a/docs/NORTHSTAR.md +++ b/docs/NORTHSTAR.md @@ -10,13 +10,17 @@ not "make Echo know editors." It is: ```text Applications hold product-facing capabilities. -Wesley compiles lawful optic claims. +Edict preserves authored operation semantics through admitted provider paths. +Wesley compiles supported lawful optic claims. Echo registers, admits, obstructs, instruments, and witnesses those claims. ``` Echo must stay runtime-shaped. Product nouns belong above Echo. Compiler -authority belongs to Wesley. Protocol publication belongs later, after the seam -has been proven locally. +authority belongs to the compiler responsible for each crossing: Edict owns +admitted operation semantics and provider contracts, while Wesley owns its +optic and compatibility compiler artifacts. Echo owns Echo-specific meanings, +runtime admission, installation, execution, and evidence. Protocol publication +belongs later, after the seam has been proven locally. The graph is not the substrate. The graph is a reading. The substrate is witnessed causal history. @@ -43,18 +47,22 @@ runtime boundary. ## Ownership boundaries -| Object | Owner | Echo posture | -| :---------------------------- | :----------------------------- | :--------------------------------------------------------------------------- | -| `OpticArtifact` | Wesley | Echo verifies and stores it | -| `OpticRegistrationDescriptor` | Wesley | Echo accepts it as registration input, not authority | -| `OpticArtifactHandle` | Echo | Echo returns it after verified registration | -| `OpticRequirements` | Wesley-authored, Echo-enforced | Echo stores internally and checks at invocation | -| `CapabilityGrant` | Host, user, or quorum | Echo validates it against stored artifact requirements | -| `CapabilityPresentation` | Caller | Echo checks it at invocation time | -| `LawWitness` | Echo | Echo emits runtime evidence | -| `ReadingEnvelope` | Echo | Echo returns observed runtime evidence and payload posture | -| WARP-TTD | Debug observer | Inspects admission chains, witnesses, receipts, readings, and causal history | -| WARPDrive | Materializer | Exposes filesystem-compatible readings for legacy tools | +| Object | Owner | Echo posture | +| :-------------------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------ | +| Edict semantic source | Application through Edict | Echo accepts only admitted, explicitly supplied compiler input | +| Provider Target IR | Echo provider through Edict | Generated and independently verified evidence, not installed runtime law | +| Provider publication package/descriptor | Compiler-emitted | Trusted host verifies publication claims and binds runtime material; it carries no Echo authority | +| `InstalledContractPackage` | Trusted host, then Echo | Echo verifies it at installation; application code receives no install authority | +| `OpticArtifact` | Wesley | Echo verifies and stores it | +| `OpticRegistrationDescriptor` | Wesley | Echo accepts it as registration input, not authority | +| `OpticArtifactHandle` | Echo | Echo returns it after verified registration | +| `OpticRequirements` | Wesley-authored, Echo-enforced | Echo stores internally and checks at invocation | +| `CapabilityGrant` | Host, user, or quorum | Echo validates it against stored artifact requirements | +| `CapabilityPresentation` | Caller | Echo checks it at invocation time | +| `LawWitness` | Echo | Echo emits runtime evidence | +| `ReadingEnvelope` | Echo | Echo returns observed runtime evidence and payload posture | +| WARP-TTD | Debug observer | Inspects admission chains, witnesses, receipts, readings, and causal history | +| WARPDrive | Materializer | Exposes filesystem-compatible readings for legacy tools | The handle proves registration, not authority. @@ -65,11 +73,21 @@ requirements. ## Platform flow +Two compiler lanes meet Echo at explicit, independent crossings. For mutating +provider operations, Edict admits exact authored semantics, the Echo provider +lowers and independently verifies Echo Target IR, and the compiler publishes +a digest-locked package and descriptor. That publication is not installation: +a trusted host must still verify the package, bind an implementation, and ask +Echo to install it. Authored reads must retain lawful read semantics and be +executed through bounded observers; they must not become synthetic mutations. + +The optic/read registration target is: + ```text Application declares GraphQL operation -> Wesley compiles OpticArtifact -> Wesley emits artifact hash and requirements digest - -> App registers artifact with Echo + -> Trusted host submits the verified artifact to Echo -> Echo verifies artifact identity -> Echo stores requirements internally -> Echo returns opaque OpticArtifactHandle @@ -229,16 +247,22 @@ are replaceable. Next Echo work should proceed in this order: -1. Land the artifact registration boundary. -2. Accept a Wesley-style artifact registration descriptor. -3. Verify artifact hash, schema id, operation id, and requirements digest. -4. Store requirements internally. -5. Return an opaque Echo-owned `OpticArtifactHandle`. -6. Add an invocation skeleton using handle, variables, basis/aperture request, and capability presentation. -7. Obstruct unknown handles, mismatched operation ids, missing capabilities, and expired capabilities. -8. Emit specific `LawWitness` records for registration and admission checks. -9. Keep jedit unchanged until Echo exposes the new boundary. -10. Update jedit only below the optic/session adapter line. +1. Accept a compiler-generated registration descriptor at the trusted-host + boundary. +2. Verify exact package, Target IR, schema, codec where declared, operation, + and requirements identities. +3. Bind host-supplied mutation executors or bounded read observers and install + them through Echo's existing package boundary. +4. Store requirements internally and return an opaque Echo-owned handle only + after successful installation. +5. Add invocation using the handle, canonical variables, explicit + basis/aperture request, and capability presentation. +6. Obstruct unknown handles, mismatched operation ids, missing capabilities, + and expired capabilities. +7. Emit specific `LawWitness` records for installation, admission, and + execution checks. +8. Keep jedit unchanged until Echo exposes the generated boundary. +9. Update jedit only below the optic/session adapter line. ## Non-goals @@ -246,7 +270,7 @@ Do not implement these before the registration and invocation boundary is witnessed: - Continuum schema publication -- public package or crate release +- generalized runtime/plugin publication or mutable provider discovery - generalized plugin loading - dynamic contract execution - full collaboration semantics diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index e9b683f4..7dbe8f0d 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -7,18 +7,22 @@ This page explains how applications use Echo without turning Echo into an application framework. Echo is a deterministic witnessed causal substrate. Applications own product -semantics. Wesley compiles authored GraphQL contracts into generated Rust code -that can talk to Echo through generic intent and observation DTOs. A bounded -optic is the product contract shape, but the current generated query execution -path is still the lower-level observation primitive described below. +semantics. Edict admits authored operation semantics and coordinates +provider-specific lowering and verification. Wesley compiles supported +GraphQL and optic contracts. Their generated artifacts can cross into Echo +only through generic, trusted-host-controlled intent, observation, and package +boundaries. A bounded optic is the read-contract shape, but the current Wesley +query fixture still uses the lower-level observation primitive described +below. This is Echo's concrete implementation of the WARP compiler seam: authored -contract nouns lower into generated request helpers and contract-host helpers, -while Echo core remains generic. See the current +contract nouns lower through approved compiler paths into generated artifacts, +request helpers, and contract-host helpers, while Echo core remains generic. +See the current [WARP optics](../topics/WarpOptics.md) model for the WARP-paper-to-Echo noun map. -The current installed-contract query path is: +The current Wesley compatibility query fixture is: ```text Application UI / adapter @@ -40,7 +44,7 @@ Echo must not gain privileged application nouns. Names such as `ReplaceRange`, `JeditBuffer`, `RenameSymbol`, `DeadSymbols`, `GraftProjection`, or `CounterIncrement` may appear in authored -contracts, Wesley-generated code, tests for generated families, application +contracts, compiler-generated code, tests for generated families, application adapters, and product documentation. They must not become Echo substrate APIs. Echo-owned APIs stay generic: @@ -72,15 +76,18 @@ Echo, contracts, and applications have different jobs. flowchart TB app["Application"] contract["Authored contract"] - wesley["Wesley compiler"] - generated["Generated Rust client and registry"] + edict["Edict provider path"] + wesley["Wesley compatibility path"] + generated["Generated artifacts and adapters"] echo["Echo substrate"] cas["echo-cas"] app -->|"owns UI, workflows, product policy"| contract - contract -->|"declares domain nouns, ops, reads"| wesley - wesley -->|"emits DTOs, codecs, op ids, registry"| generated - generated -->|"packs EINT; builds raw and optic query DTOs"| echo + contract -->|"declares admitted operations and law"| edict + contract -->|"declares supported GraphQL and optics"| wesley + edict -->|"emits verified artifacts, package, helper"| generated + wesley -->|"emits DTOs, codecs, op ids, fixture registry"| generated + generated -->|"trusted host verifies, binds, and installs"| echo echo -->|"admits, schedules, witnesses, observes"| cas cas -->|"stores retained content, witnesses, cached readings"| echo @@ -111,13 +118,25 @@ Contracts own: - domain emission law; - domain-specific reading payloads. -Wesley-generated code owns the typed bridge between those contract nouns and +Compiler-generated code owns the typed bridge between those contract nouns and Echo's generic runtime surfaces. That bridge has two separate faces: -| Surface | Responsibility | -| :-------------------- | :---------------------------------------------------------------------- | -| Application helpers | Build EINT bytes plus raw and optic-shaped query request DTOs. | -| Contract-host helpers | Install generated mutation handler rules and read-only query observers. | +| Surface | Responsibility | +| :-------------------- | :----------------------------------------------------------------------- | +| Application helpers | Encode typed values, build EINT bytes, and build bounded read requests. | +| Contract-host helpers | Bind host implementations and propose mutation or read package material. | + +The current implementations have not yet closed both faces. Wesley's +compatibility fixture emits direct bootstrap helpers. The Edict provider path +emits verified publication artifacts plus a pure helper projection with the +profile-owned `le-binary-v1` typed codecs, canonical EINT packing, a borrowed +provider-generic registry, and a fail-closed mutation package proposal that +binds one explicit host implementation. The helper can bind and propose; it +cannot authenticate or install its own proposal. A trusted Echo host still owns +runtime package admission and installation. The current proposal is +mutation-specific; generated reads remain a separate bounded observer/optic +crossing. Neither compiler path may claim that generated authority facts are +Echo runtime authority. ## External Edict Provider Artifacts @@ -132,6 +151,8 @@ Echo GraphQL and checked semantic declarations -> provider-owned lowerer and verifier components -> #655 generated provider manifest and digest-locked Echo provider package -> Edict's runtime-neutral provider host + -> exact requested Target IR, generated-helper, and non-authoritative review + projections after their own host admission ``` The first checked non-GraphQL source is @@ -219,14 +240,18 @@ proofs establish schema, identity-graph, component-contract, and request readiness only. They still do not install, authorize, schedule, execute, commit, observe, or receipt anything in Echo. -The publishable Rust crate uses a separate 35-file package-local carrier tree +The publishable Rust crate uses a separate 38-file package-local carrier tree for exact repository sources and provider bytes that would otherwise live above the crate root. Carrier locations never replace the logical authored paths in generation provenance. Generated artifacts and components remain authoritative; their package copies are corroborating release occurrences. This distinction permits staged regeneration without allowing a stale package to block the carrier update required to rebuild that package. Final checks require owner, -carrier, package copy, and Cargo archive inventory to agree exactly. +carrier, package copy, and Cargo archive inventory to agree exactly. The source +closure explicitly enumerates 20 files, including the exact canonicalization, +semantic-operation-id, and provider-registry dependency manifests and +implementations; a dependency implementation cannot move while the claimed +generator occurrence remains fixed. The target-profile lowerer and verifier resources are generated declarative contract documents. They do not select executable implementations. The package @@ -260,6 +285,79 @@ crossing proves translation only. Package admission, installation, invocation, commitment, observation, and receipt authority remain separate Echo runtime crossings. +The #656 native lowerer model adds two projections beside the Target IR output: +an exact generated Rust helper envelope and a permanently non-authoritative +review envelope. Requested outputs must be an exact lexicographically sorted +subset of the three declared role/kind/domain triples. The helper and review +separately name the semantic Target IR coordinate `echo.span-ir/v1` and its +artifact digest domain `edict.target-ir.artifact/v1`; neither identity may stand +in for the other. The review subjects the exact generated artifact, so its +evidence changes when the generated envelope changes. + +The `echo.dpo.bundle/v1` identity is only a target-bundle profile, not a final +contract-bundle occurrence. Because the generated artifact participates in the +bundle's semantic identity, embedding final bundle digests in that artifact or +its review would create an identity cycle. The review therefore records +`edict.bundle.semantic/v1` and `edict.bundle.release/v1` as the two bundle +digest propositions, leaves their digest values absent, and declares +`explicit-after-assembly` binding. After assembly, a caller supplies an +independent expected pin and untrusted actual bundle claims. The generated +helper checks exact domains, typed digest form, expected-versus-actual bundle +digests, and the operation coordinate, Target IR, Echo ABI, helper API, provider +and operation schemas, target/generated/operation profiles, +and abstract footprint +obligation/algebra identities before returning a descriptor with private state. +Every framed resource claim includes and compares its coordinate, digest domain, +and digest; a bare hash cannot stand in for that proposition. +The footprint claim remains abstract; the helper does not counterfeit a static +read/write certificate. This is an equality and semantic-consistency proof +only: it does not authenticate the pin, schema-admit the outputs, construct or +install an Echo package, or grant runtime authority. The canonical generated +profile/package now carries the separately versioned Echo-owned +`echo.semantic-operation-id.fnv1-32/v1` law and exact persisted id derived from +the semantic coordinate and generic query/mutation kind. The top two operation +ids remain Echo-owned protocol reservations: `u32::MAX` for scheduler control +and `u32::MAX - 1` for witnessed suffix import. Either reserved result and any +package-local collision refuse without salting or probing. CDDL admission +constrains the numeric application-id domain only; generation must independently +recompute the coordinate-and-kind law and collision-check the complete set. +Emitted source carries public expected constants for that packaged proposition +without re-derivation, checks the law and id as untrusted bundle claims, and lets +the descriptor expose the matched id claim after the pure preflight succeeds. +The descriptor remains evidence, not a registry entry, installation token, or +runtime authority. The generated-artifact profile now owns the exact +`le-binary-v1` byte law. Generated Rust implements distinct `Id`, `Input`, and +`Output` types, preserves raw UTF-8 under the authored scalar bound, and rejects +malformed, over-bound, truncated, or trailing bytes. The descriptor exposes +typed encode/decode methods and packs an `Input` into canonical EINT v1 under +the matched operation id. Echo treats EINT `vars` as codec-owned opaque bytes; +canonical CBOR is specific to the Wesley path rather than a universal +operation-variable contract. + +The descriptor also exposes a borrowed `ProviderRegistryV1` whose operation +record retains the complete Target IR, bundle, target/generated/operation +profile, provider/value schema, codec, obstruction, operation-id, ABI, +helper-API, and footprint proposition. It can bind the generated matcher and +canonical rule name to one independently identified host mutation +implementation, then return an opaque `ProviderContractPackageProposalV1` only +when every claim agrees. This is fail-closed cross-binding, not proof that +arbitrary callback code implements the declared semantics. The proposal does +not authenticate or install itself; a trusted Echo host still owns package +admission and installation. Its constructor supports mutations and refuses a +`Query`. Authored reads continue through a separate bounded observer/optic +crossing and may not be encoded as synthetic mutations. + +Both refreshed components have crossed reproducible checked promotion. The +lowerer is 189,668 bytes with SHA-256 +`9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af`; the +verifier is 189,922 bytes with SHA-256 +`4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a`. +The generated envelope crosses pinned-host CDDL admission under its owning +root, and the isolated host helper witness covers exact binding, codec refusal +and round trips, EINT packing, the borrowed registry, and the non-installing +proposal. Pinned-host admission of the review envelope remains a separate +crossing. + The first executable verifier independently implements the frozen `edict:target-provider/verifier@1.0.0` world. It compares the exact Core and Target IR relation under the target profile and ordered semantic inputs rather @@ -439,14 +537,14 @@ contract helpers. ```mermaid sequenceDiagram participant UI as Application UI - participant Gen as Wesley-generated client + participant Gen as Generated contract client participant ABI as echo-wasm-abi participant Echo as Echo KernelPort participant Runtime as Echo runtime - UI->>Gen: build typed contract operation variables - Gen->>ABI: encode_cbor(contract operation variables) - ABI-->>Gen: canonical vars bytes + UI->>Gen: build typed contract operation input + Gen->>Gen: encode with the profile-owned value codec + Gen-->>Gen: exact opaque vars bytes Gen->>ABI: pack_intent_v1(op_id, vars bytes) ABI-->>Gen: EINT v1 bytes Gen->>Echo: dispatch_intent(EINT bytes) @@ -456,7 +554,7 @@ sequenceDiagram Gen-->>UI: application-level result handling ``` -For a toy mutation: +For a toy Wesley mutation: ```graphql mutation increment(input: IncrementInput): CounterValue @@ -470,19 +568,32 @@ let intent = pack_increment_intent(&__echo_wesley_generated::IncrementVars { })?; ``` -The generated helper performs the deterministic boundary work: +The Wesley helper performs this deterministic boundary work: ```text __echo_wesley_generated::IncrementVars -> encode_cbor(...) - -> canonical vars bytes + -> canonical-CBOR vars bytes selected by that Wesley contract -> pack_intent_v1(OP_INCREMENT, &vars_bytes) -> EINT v1 bytes ``` +The checked Edict helper follows the same EINT envelope law but a different, +profile-owned value law: + +```text +echo_dpo::Input + -> le-binary-v1 typed encoding + -> exact opaque vars bytes + -> RegistrationDescriptorV1::pack_intent(...) + -> EINT v1 bytes +``` + Echo receives the EINT bytes. Echo does not need to know that the operation was called `increment` or that the payload contained an `IncrementInput`. That -meaning belongs to the generated contract layer and the application. +meaning and the `vars` codec belong to the generated contract layer and the +application. Canonical EINT framing does not make every operation payload +canonical CBOR. ## Dispatch Is Synchronous In Code, Not A Domain RPC @@ -521,8 +632,8 @@ must not hide app mutation in unrecorded global state. ## Read Path -The product contract shape is a bounded optic, but the two generated query -surfaces have different current behavior: +The product contract shape is a bounded optic, but the two Wesley-generated +query surfaces have different current behavior: - `*_observation_request(...)` builds a raw `ObservationRequest`. The installed contract host can execute this lower-level QueryView path through @@ -537,10 +648,19 @@ application path. Raw observation is the current integration primitive, while the bounded optic remains the product contract shape. Generated contract-host query observer helpers are separate from application -query request builders. They install read-only host observers behind -`warp-core`'s `ContractQueryObserver` boundary. Those observers receive -read-only context; they do not receive mutable runtime, scheduler control, -`TickDelta`, or write authority. +query request builders. They describe read-only observers that a trusted Echo +host may install behind `warp-core`'s `ContractQueryObserver` boundary; the +helpers do not confer installation authority themselves. Those observers +receive read-only context; they do not receive mutable runtime, scheduler +control, `TickDelta`, or write authority. + +The current Edict `ProviderContractPackageProposalV1` constructor is a different, +mutation-only surface. Its typed `UnsupportedOperationKind` refusal for a +`Query` applies only to mutation proposal construction. It is not a claim that +all provider operations are mutations. An authored read must lower into lawful +target read semantics—potentially an optic executed through a bounded +observer—and use the separate read installation and observation path. It must +not be wrapped in a no-op or synthetic mutation. ```mermaid sequenceDiagram @@ -550,7 +670,7 @@ sequenceDiagram participant Runtime as Echo runtime UI->>Gen: request generated query - Gen->>Gen: canonicalize query variables + Gen->>Gen: encode query variables with the Wesley codec Gen->>Gen: build raw ObservationRequest Gen->>Echo: observe(request) Echo->>Runtime: resolve QueryView coordinate and projection @@ -585,9 +705,10 @@ capability/law posture of a separate optic request. ## Registry Handshake Applications need to know whether their generated client matches the installed -host. +host. Echo currently has two separate registry vocabularies; they must not be +collapsed merely because both describe operations. -Echo already exposes registry metadata surfaces: +The existing WASM/Wesley compatibility path exposes host metadata surfaces: - `get_registry_info`; - `get_codec_id`; @@ -602,7 +723,15 @@ Wesley-generated code already exposes generated registry information: - enum and object descriptors; - static `REGISTRY`. -The first handshake is intentionally small: +The Edict provider path instead exposes a borrowed `ProviderRegistryV1` only +after exact bundle binding. It is provider-generic and carries no GraphQL +facade or Wesley generator metadata. Each operation record binds the semantic +coordinate and kind, persisted id, distinct input/output codec contracts, +failure and obstruction identities, Target IR, profiles, bundle identities, +and abstract footprint claim. Duplicate operation-id lookup fails closed. This +registry is proposal evidence, not installed-host metadata or a runtime token. + +The first Wesley handshake is intentionally small: ```mermaid flowchart LR @@ -624,6 +753,13 @@ reserved control-op usage. Host-side generated-payload validation is deferred until a RED proves that Echo itself must reject malformed app payloads at the host boundary. +For the current Edict mutation path, the generated descriptor and an explicit +host implementation must agree on every registry identity before Echo will even +retain an opaque package proposal. That preflight neither validates arbitrary +callback semantics nor installs the proposal. A trusted Echo host remains +responsible for authenticating the occurrence and crossing into +`InstalledContractPackage`. + ## Admission Security Ramp The registry handshake is compatibility evidence, not production security. @@ -642,7 +778,7 @@ sequenceDiagram participant Policy as Observer/ingress policy participant Echo as Echo runtime - Gen->>Gen: canonicalize op vars + Gen->>Gen: encode vars with the declared profile codec Gen->>Gen: bind op, vars, artifact, target, session Gen->>Host: authenticated intent submission Host->>Policy: verify artifact posture and session authority @@ -850,8 +986,8 @@ In words: Application: I know what the user wants. -Generated Wesley code: - I know the contract schema and how to encode/decode operations. +Generated contract code: + I know the authored contract schema and its exact encoding/decoding law. Echo ABI: I accept canonical intent bytes and explicit observation DTOs. @@ -869,8 +1005,12 @@ Application: For a `jedit`-style application: ```text -jedit owns text/editor semantics. -Wesley compiles those semantics into generated contract types and helpers. +jedit owns text/editor domain meaning. +Edict owns authored operation semantics and lowers supported mutations through +verified Echo Target IR. Authored reads retain lawful read/optic semantics and +execute through bounded observers; Wesley remains the neutral compiler owner +for the supported optic and compatibility path. Generated contract code carries +the exact types and helpers. Echo receives canonical intents and emits witnessed readings through the current raw query path. Generated optic queries remain typed obstructions. jedit renders buffers, cursors, diffs, diagnostics, history, and UI. diff --git a/docs/architecture/echo-optics-adapter-notes.md b/docs/architecture/echo-optics-adapter-notes.md index 497d85fd..3cb2c62a 100644 --- a/docs/architecture/echo-optics-adapter-notes.md +++ b/docs/architecture/echo-optics-adapter-notes.md @@ -77,10 +77,11 @@ still lower into Echo as generic Optics requests: - generated decoding helpers decode observer payload bytes after Echo has emitted a reading. -Generated contract-host helpers are a separate surface. They install host-owned -mutation rule constructors and read-only query observer constructors against -generic `warp-core` boundaries. They must not become application tick authority -or application-owned runtime control. +Generated contract-host helpers are a separate surface. They expose host-owned +mutation-rule and read-only query-observer constructors against generic +`warp-core` boundaries for a trusted Echo host to inspect and install. The +helpers do not confer installation authority and must not become application +tick authority or application-owned runtime control. Authored optics may declare retained consequence obligations, including receipt obligations. They do not create ticks or tick receipts. Echo's trusted diff --git a/docs/case-studies/JimAndEcho.md b/docs/case-studies/JimAndEcho.md index 2dc5303c..bc749a2f 100644 --- a/docs/case-studies/JimAndEcho.md +++ b/docs/case-studies/JimAndEcho.md @@ -20,10 +20,10 @@ which structures are projections, and what happens when a step fails. This document uses three labels: -| Label | Meaning | -| --- | --- | -| Implemented | The named `warp-core` API and recovery behavior exist now. | -| Application contract | Jim must provide this domain behavior; Echo stays generic. | +| Label | Meaning | +| ------------------------- | ------------------------------------------------------------------------------ | +| Implemented | The named `warp-core` API and recovery behavior exist now. | +| Application contract | Jim must provide this domain behavior; Echo stays generic. | | Architectural destination | The boundary is required, but the current implementation has not completed it. | That distinction prevents an example from becoming accidental architecture. @@ -44,12 +44,12 @@ The stable application rule is narrower and already enforceable: Jim and Echo interact cleanly only when four different things remain distinct. -| Kind | Jim example | Authority posture | -| --- | --- | --- | -| Witnessed causal history | The admitted replace operation, its text rewrite, receipt, and save anchor | Semantic history; Echo controls admission and causal ordering. | -| Durable commit material | Runtime WAL transactions and commit markers | Current crash-recovery and linearization mechanism. | -| Materialized reading | Rope projection, visible text window, syntax spans, line index, rendered screen | Derived from a named basis; rebuildable when support remains available. | -| Disposable accelerator | Pending-intent index, layout cache, highlighted-line cache | Never authority; discard or rebuild on mismatch. | +| Kind | Jim example | Authority posture | +| ------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Witnessed causal history | The admitted replace operation, its text rewrite, receipt, and save anchor | Semantic history; Echo controls admission and causal ordering. | +| Durable commit material | Runtime WAL transactions and commit markers | Current crash-recovery and linearization mechanism. | +| Materialized reading | Rope projection, visible text window, syntax spans, line index, rendered screen | Derived from a named basis; rebuildable when support remains available. | +| Disposable accelerator | Pending-intent index, layout cache, highlighted-line cache | Never authority; discard or rebuild on mismatch. | A host file such as `notes.txt` is also a materialization. Writing it does not replace Echo history. Conversely, an accepted edit is not the same thing as a @@ -64,7 +64,7 @@ framework. They meet through explicit contracts. flowchart TB user["User"] ui["Jim UI and interaction model
JIM-OWNED"] - client["Generated contract client and Jim adapters
JIM / WESLEY-OWNED"] + client["Generated contract client and Jim adapters
EDICT-GENERATED / JIM ADAPTER"] app["TrustedRuntimeApp
ECHO APP CAPABILITY"] host["TrustedRuntimeHost
ECHO-OWNED"] contract["Installed Jim contract
JIM SEMANTICS"] @@ -89,18 +89,18 @@ flowchart TB wal -. "recovers committed history evidence" .-> history ``` -| Participant | Owns | Must not own | -| --- | --- | --- | -| Jim UI | Key interpretation, modes, commands, status text, viewport policy | Tick creation, WAL append, Echo receipt identity | -| Jim contract | `ReplaceRange`, rope facts, text invariants, inverse law, domain checkpoint meaning | Generic scheduler policy, Echo admission claims | -| Wesley-generated bridge | Typed operations, codecs, operation ids, package metadata | Handwritten product policy or hidden authority | -| `TrustedRuntimeApp` | App-safe proposal and observation capability | Package installation, ticket fabrication, scheduler stepping, WAL mutation | -| `TrustedRuntimeHost` | Package registration, WAL configuration, support policy, staging, scheduler cadence, recovery | Jim-specific text semantics | -| Scheduler | Basis pinning, deterministic eligibility and order, tick opportunity | Meaning of a text replacement | -| Installed Jim contract | Proposed DPO rewrite and typed refusal under Jim law | Commit publication or receipt minting | -| Runtime WAL | Atomic recoverable transaction material | A second semantic model independent of causal history | -| Observation service | Basis- and aperture-bounded readings with evidence posture | Ambient mutable access to all state | -| CAS | Content-addressed bytes and artifacts | Meaning, admission, retention policy, or causal order by itself | +| Participant | Owns | Must not own | +| ------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Jim UI | Key interpretation, modes, commands, status text, viewport policy | Tick creation, WAL append, Echo receipt identity | +| Jim contract | `ReplaceRange`, rope facts, text invariants, inverse law, domain checkpoint meaning | Generic scheduler policy, Echo admission claims | +| Generated contract bridge | Typed Edict-authored operations, exact codecs where declared, operation ids, clients, and package metadata | Handwritten product policy, hidden authority, or Jim-specific runtime admission | +| `TrustedRuntimeApp` | App-safe proposal and observation capability | Package installation, ticket fabrication, scheduler stepping, WAL mutation | +| `TrustedRuntimeHost` | Package registration, WAL configuration, support policy, staging, scheduler cadence, recovery | Jim-specific text semantics | +| Scheduler | Basis pinning, deterministic eligibility and order, tick opportunity | Meaning of a text replacement | +| Installed Jim contract | Proposed DPO rewrite and typed refusal under Jim law | Commit publication or receipt minting | +| Runtime WAL | Atomic recoverable transaction material | A second semantic model independent of causal history | +| Observation service | Basis- and aperture-bounded readings with evidence posture | Ambient mutable access to all state | +| CAS | Content-addressed bytes and artifacts | Meaning, admission, retention policy, or causal order by itself | ## Application Lifecycle @@ -140,18 +140,24 @@ bootstrap evidence. Recovery must be able to reproduce or validate them; a fresh process may not silently choose a different starting document and then replay old receipts over it. -### 2. Install The Jim Contract +### 2. Propose, Then Install The Application Contract -Implemented host API: +Existing trusted-host installation API (a later crossing): ```rust -host.register_contract_package(jim_package)?; +host.register_contract_package(installed_package)?; ``` -The package contains generated registry metadata plus Jim-authored mutation -handlers and query observers. Echo verifies and installs the package. Jim owns -the meaning of operations such as `ReplaceRange`; Echo sees a verified generic -contract operation, canonical intent bytes, declared support, and a handler. +Edict's generated helper and explicit host bindings currently produce an opaque +`ProviderContractPackageProposalV1`. The proposal retains generated registry +metadata and one host-supplied mutation binding after pure identity preflight, +but it does not authenticate the package occurrence, construct an +`InstalledContractPackage`, register anything, or mint runtime authority. A +separately tracked trusted-host admission flow must authenticate and admit the +proposal, construct the runtime package, and only then call the installation API +above. Jim retains its domain meaning; Echo receives an admitted generic +contract operation, canonical intent bytes, declared support, and its host +binding only after that later crossing. Package installation is host authority. The application-facing handle cannot replace the package after it has submitted work. @@ -257,17 +263,17 @@ stateDiagram-v2 NotAccepted --> [*] ``` -| Lifecycle term | Precise claim | Durable posture | -| --- | --- | --- | -| Proposed | Jim has canonical intent bytes in caller memory. | No | -| Accepted pending | Echo committed acceptance plus the retained envelope. | Yes | -| Staged | Trusted host projected accepted work into ticketed runtime ingress. | Rebuildable from retained evidence; not independent authority | -| Eligible | At one pinned basis, support and scheduler policy permit consideration. | A reading, not an eternal flag | -| Selected | Deterministic scheduler chose the intent for a tentative tick bundle. | Normally recorded with settlement, not a separate precommit fact | -| Applied | The application rewrite and its receipt committed. | Yes | -| Rejected | Named law produced a committed refusal or conflict outcome. | Yes | -| Obstructed | Required support or evidence prevented a lawful success claim. | Retained when the corresponding outcome commits | -| Deferred | The intent remains accepted but is not eligible at this basis. | Acceptance remains durable; eligibility is recomputed | +| Lifecycle term | Precise claim | Durable posture | +| ---------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | +| Proposed | Jim has canonical intent bytes in caller memory. | No | +| Accepted pending | Echo committed acceptance plus the retained envelope. | Yes | +| Staged | Trusted host projected accepted work into ticketed runtime ingress. | Rebuildable from retained evidence; not independent authority | +| Eligible | At one pinned basis, support and scheduler policy permit consideration. | A reading, not an eternal flag | +| Selected | Deterministic scheduler chose the intent for a tentative tick bundle. | Normally recorded with settlement, not a separate precommit fact | +| Applied | The application rewrite and its receipt committed. | Yes | +| Rejected | Named law produced a committed refusal or conflict outcome. | Yes | +| Obstructed | Required support or evidence prevented a lawful success claim. | Retained when the corresponding outcome commits | +| Deferred | The intent remains accepted but is not eligible at this basis. | Acceptance remains durable; eligibility is recomputed | `AcceptedPending` is the ACK boundary exposed by the current trusted runtime. `Eligible` can change when the scheduler basis changes, so Echo must derive it @@ -311,7 +317,7 @@ sequenceDiagram autonumber actor U as User participant UI as Jim UI
JIM-OWNED - participant GC as Generated Client
JIM/WESLEY + participant GC as Generated Client
EDICT-GENERATED / JIM ADAPTER participant A as TrustedRuntimeApp
ECHO APP API participant R as WorldlineRuntime
ECHO RUNTIME participant W as Runtime WAL
ECHO DURABILITY @@ -378,21 +384,21 @@ sequenceDiagram ### Step Table -| Step | Owner | Input | Authoritative change | Failure behavior | -| ---: | --- | --- | --- | --- | -| 1 | Jim UI | Key event and current UI mode | None | Invalid key mapping remains a UI event. | -| 2 | Jim | Cursor projection and rope reading | None | A stale or unavailable coordinate obstructs intent creation. | -| 3 | Generated client | Typed `ReplaceRange` | None | Encoding failure produces no proposal. | -| 4 | Echo app API | Canonical ingress envelope | Tentative runtime intake only | No WAL means explicit `RuntimeWalUnavailable`. | -| 5 | Echo WAL | Acceptance record and retained envelope | Committed accepted-submission evidence | On failure, runtime intake rolls back. | -| 6 | Trusted host | Submission id and Echo ticket | Ticketed ingress projection | Unknown or unsupported submission is rejected. | -| 7 | Scheduler | Pinned runtime basis and candidates | Tentative tick plan | Jim cannot choose bundle membership. | -| 8 | Jim contract | Basis `H41`, range, replacement | Proposed rope rewrite | Stale basis or invalid UTF-8 yields typed refusal. | -| 9 | Echo | Contract result and causal parents | Tentative outcome, receipt, provenance | Inconsistent evidence aborts the tick. | -| 10 | Echo WAL | Receipt, correlation, replayable state delta | Committed scheduler-tick evidence | Runtime and provenance roll back if commit fails. | -| 11 | Jim | Submission id | None; reads committed outcome | Pending remains pending; missing is not rejection. | -| 12 | Observer | Basis `H42`, aperture, budget | None; returns a reading | Missing support yields typed obstruction. | -| 13 | Jim UI | Reading payload | Terminal pixels only | Rendering never mutates text authority. | +| Step | Owner | Input | Authoritative change | Failure behavior | +| ---: | ---------------- | -------------------------------------------- | -------------------------------------- | ------------------------------------------------------------ | +| 1 | Jim UI | Key event and current UI mode | None | Invalid key mapping remains a UI event. | +| 2 | Jim | Cursor projection and rope reading | None | A stale or unavailable coordinate obstructs intent creation. | +| 3 | Generated client | Typed `ReplaceRange` | None | Encoding failure produces no proposal. | +| 4 | Echo app API | Canonical ingress envelope | Tentative runtime intake only | No WAL means explicit `RuntimeWalUnavailable`. | +| 5 | Echo WAL | Acceptance record and retained envelope | Committed accepted-submission evidence | On failure, runtime intake rolls back. | +| 6 | Trusted host | Submission id and Echo ticket | Ticketed ingress projection | Unknown or unsupported submission is rejected. | +| 7 | Scheduler | Pinned runtime basis and candidates | Tentative tick plan | Jim cannot choose bundle membership. | +| 8 | Jim contract | Basis `H41`, range, replacement | Proposed rope rewrite | Stale basis or invalid UTF-8 yields typed refusal. | +| 9 | Echo | Contract result and causal parents | Tentative outcome, receipt, provenance | Inconsistent evidence aborts the tick. | +| 10 | Echo WAL | Receipt, correlation, replayable state delta | Committed scheduler-tick evidence | Runtime and provenance roll back if commit fails. | +| 11 | Jim | Submission id | None; reads committed outcome | Pending remains pending; missing is not rejection. | +| 12 | Observer | Basis `H42`, aperture, budget | None; returns a reading | Missing support yields typed obstruction. | +| 13 | Jim UI | Reading payload | Terminal pixels only | Rendering never mutates text authority. | The current filesystem host accepts at most one newly correlated scheduler outcome per `tick_once()` call. If one pass would require multiple filesystem @@ -612,13 +618,13 @@ shutdown gate based on Echo evidence, not classic dirty-buffer state. Before normal process exit, Jim should distinguish: -| State | Exit meaning | -| --- | --- | -| Key exists only in UI event queue | Not durable; finish submission or report failure. | -| Submission has WAL-backed acceptance | Recoverable as pending even if not yet applied. | -| Scheduler receipt and replayable state delta committed | Applied edit and provenance are recoverable. | -| Host file projection is older than current head | Text is safe in Echo, but external file is not current. | -| Cursor/session projection is not modeled causally | Text recovers, but exact UI session does not. | +| State | Exit meaning | +| ------------------------------------------------------ | ------------------------------------------------------- | +| Key exists only in UI event queue | Not durable; finish submission or report failure. | +| Submission has WAL-backed acceptance | Recoverable as pending even if not yet applied. | +| Scheduler receipt and replayable state delta committed | Applied edit and provenance are recoverable. | +| Host file projection is older than current head | Text is safe in Echo, but external file is not current. | +| Cursor/session projection is not modeled causally | Text recovers, but exact UI session does not. | The ideal interactive loop submits each meaningful edit through the WAL-backed path before presenting it as safely accepted. It may batch rendering and @@ -648,12 +654,12 @@ flowchart TD ### Recovery Outcomes -| Recovered posture | Jim behavior | -| --- | --- | -| `accepted_pending` | Keep the intent pending and permit normal scheduler consideration after startup gates pass. | -| `decided_applied` | Reconstruct the rope transition and show the resulting text reading. | -| `decided_rejected` | Preserve the proposal and refusal evidence; do not apply text. | -| `obstructed` | Explain missing or incompatible evidence; do not guess state. | +| Recovered posture | Jim behavior | +| ------------------ | -------------------------------------------------------------------------------------------- | +| `accepted_pending` | Keep the intent pending and permit normal scheduler consideration after startup gates pass. | +| `decided_applied` | Reconstruct the rope transition and show the resulting text reading. | +| `decided_rejected` | Preserve the proposal and refusal evidence; do not apply text. | +| `obstructed` | Explain missing or incompatible evidence; do not guess state. | | `recovery_faulted` | Refuse writable startup until the durable-history problem is repaired or explicitly handled. | Recovery does not rerun Jim's contract to rediscover what probably happened. @@ -666,23 +672,23 @@ performance event, not lost history. ## Failure Matrix -| Failure | Required outcome | -| --- | --- | -| Intent encoding fails | No submission exists. | -| Runtime WAL unavailable | WAL-backed app submission returns a typed error before acceptance. | -| Acceptance append fails before commit | Restore pre-submission runtime; do not return a durable handle. | -| Process dies after acceptance commit | Recover the exact retained envelope as accepted pending. | -| Ticket/support resolution fails | Preserve accepted proposal; expose a typed obstruction or pending posture. | -| Jim contract refuses stale basis | Commit the refusal posture as designed; do not rewrite current text. | -| Scheduler evidence is internally inconsistent | Abort and restore pre-tick runtime/provenance. | -| Tick append fails before commit | Do not expose the tentative applied outcome. | -| Filesystem reports an error after commit | Re-scan; return success only if the exact committed evidence recovers. | -| Observation cache is stale | Rebuild or return typed obstruction; never mutate authority to match cache. | -| File export fails | Echo text history remains intact; save/export posture remains failed. | -| Anchor basis is stale | Request the current basis and reconsider the save operation; do not silently retarget. | -| Anchor root lacks host support | Refuse admission; application claims cannot grant their own support. | -| Anchor WAL commit fails | Publish no anchor authority. | -| Recovery finds malformed or incomplete evidence | Fail closed with a recovery obstruction. | +| Failure | Required outcome | +| ----------------------------------------------- | -------------------------------------------------------------------------------------- | +| Intent encoding fails | No submission exists. | +| Runtime WAL unavailable | WAL-backed app submission returns a typed error before acceptance. | +| Acceptance append fails before commit | Restore pre-submission runtime; do not return a durable handle. | +| Process dies after acceptance commit | Recover the exact retained envelope as accepted pending. | +| Ticket/support resolution fails | Preserve accepted proposal; expose a typed obstruction or pending posture. | +| Jim contract refuses stale basis | Commit the refusal posture as designed; do not rewrite current text. | +| Scheduler evidence is internally inconsistent | Abort and restore pre-tick runtime/provenance. | +| Tick append fails before commit | Do not expose the tentative applied outcome. | +| Filesystem reports an error after commit | Re-scan; return success only if the exact committed evidence recovers. | +| Observation cache is stale | Rebuild or return typed obstruction; never mutate authority to match cache. | +| File export fails | Echo text history remains intact; save/export posture remains failed. | +| Anchor basis is stale | Request the current basis and reconsider the save operation; do not silently retarget. | +| Anchor root lacks host support | Refuse admission; application claims cannot grant their own support. | +| Anchor WAL commit fails | Publish no anchor authority. | +| Recovery finds malformed or incomplete evidence | Fail closed with a recovery obstruction. | ## Current Implementation Versus Architectural Destination @@ -732,12 +738,21 @@ The Jim example generalizes into a practical checklist. 4. Define bounded observers and their evidence requirements. 5. Keep application nouns out of Echo core. -### Generate And Install The Bridge - -1. Compile the authored contract with Wesley. -2. Package generated registry metadata, codecs, handlers, and observers. -3. Let the trusted host verify and register the package. -4. Give product code only generated clients and `TrustedRuntimeApp`. +### Generate, Propose, And Later Install The Bridge + +1. Author Jim operation semantics in Edict, keeping mutating DPO semantics + distinct from bounded read/optic semantics. +2. Admit the exact source and run the target-specific lowerer and independent + verifier without rediscovering or inventing meaning. +3. Generate operation ids, exact codecs where declared, clients, descriptors, + digests, and provenance; bind supported mutation handlers into an opaque, + non-installing provider proposal while keeping bounded observers on their + separate read path. +4. Pass the preflighted proposal into the separately tracked trusted-host + admission and installation flow; that later crossing must authenticate the + package occurrence, construct an `InstalledContractPackage`, and register it + before any runtime authority exists. +5. Give product code only generated clients and `TrustedRuntimeApp`. ### Submit Work @@ -802,19 +817,19 @@ Specific violations include: ## Source Map -| Concern | Current implementation or documentation | -| --- | --- | -| App-safe and host-owned runtime surfaces | [`trusted_runtime_host.rs`](../../crates/warp-core/src/trusted_runtime_host.rs) | -| Runtime scheduling and ingress | [`coordinator.rs`](../../crates/warp-core/src/coordinator.rs) | -| Causal WAL transaction and recovery contracts | [`causal_wal.rs`](../../crates/warp-core/src/causal_wal.rs) | -| Causal-anchor values and identity | [`causal_anchor.rs`](../../crates/warp-core/src/causal_anchor.rs) | -| Causal-anchor architecture | [Causal Anchors](../topics/CausalAnchors.md) | -| WAL durability and recovery | [WAL](../topics/WAL.md) | -| Application package boundary | [Application Contract Hosting](../architecture/application-contract-hosting.md) | -| Bounded observation model | [WARP Optics](../topics/WarpOptics.md) | -| Inverse intent admission | [Contract Inverse Admission](../topics/ContractInverseAdmission.md) | -| Executable trusted-host witnesses | [`trusted_runtime_host_loop_tests.rs`](../../crates/warp-core/tests/trusted_runtime_host_loop_tests.rs) | -| External anchor consumer witness | [`causal_anchor_external_consumer_tests.rs`](../../crates/warp-core/tests/causal_anchor_external_consumer_tests.rs) | +| Concern | Current implementation or documentation | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| App-safe and host-owned runtime surfaces | [`trusted_runtime_host.rs`](../../crates/warp-core/src/trusted_runtime_host.rs) | +| Runtime scheduling and ingress | [`coordinator.rs`](../../crates/warp-core/src/coordinator.rs) | +| Causal WAL transaction and recovery contracts | [`causal_wal.rs`](../../crates/warp-core/src/causal_wal.rs) | +| Causal-anchor values and identity | [`causal_anchor.rs`](../../crates/warp-core/src/causal_anchor.rs) | +| Causal-anchor architecture | [Causal Anchors](../topics/CausalAnchors.md) | +| WAL durability and recovery | [WAL](../topics/WAL.md) | +| Application package boundary | [Application Contract Hosting](../architecture/application-contract-hosting.md) | +| Bounded observation model | [WARP Optics](../topics/WarpOptics.md) | +| Inverse intent admission | [Contract Inverse Admission](../topics/ContractInverseAdmission.md) | +| Executable trusted-host witnesses | [`trusted_runtime_host_loop_tests.rs`](../../crates/warp-core/tests/trusted_runtime_host_loop_tests.rs) | +| External anchor consumer witness | [`causal_anchor_external_consumer_tests.rs`](../../crates/warp-core/tests/causal_anchor_external_consumer_tests.rs) | ## Final Model diff --git a/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md b/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md index b5c2b8c7..017d0381 100644 --- a/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md +++ b/docs/invariants/DECLARATIVE-RULE-AUTHORSHIP.md @@ -7,16 +7,20 @@ ## Invariant -Echo's deterministic execution path admits user-authored rewrite logic only as -Wesley-compiled declarative IR. Native executable rewrite code is a -trusted/bootstrap-only engine implementation surface, not a public user -extension boundary. - -For Echo, that declarative boundary is the GraphQL-authored shared family -surface: graph entities, graph rewrites, declared footprints, and any types -that cross Rust, TypeScript, WASM, process, or network boundaries. The rest of -the runtime may remain handwritten Rust as long as it does not become a shadow -authority for user-authored rewrite law. +Echo's deterministic execution path admits user-authored operation law only as +compiler-produced declarative semantics and verified Echo-targeted artifacts +from an approved contract path. Current approved authoring systems include +Edict and Wesley. Native executable rewrite code is a trusted/bootstrap-only +engine implementation surface, not a public user extension boundary. + +For Echo, that declarative boundary includes Edict-authored semantic operations, +lawpacks, target profiles, and schemas as well as supported Wesley-authored +GraphQL and optic contracts. Mutating operations may lower into Echo Span/DPO +IR; effect-free operations must retain lawful read semantics and may execute +through bounded observers. Declared footprints and any types that cross Rust, +TypeScript, WASM, process, or network boundaries remain part of the authored +contract. The rest of the runtime may remain handwritten Rust as long as it +does not become a shadow authority for user-authored operation law. ## Rulings @@ -25,29 +29,35 @@ RFC 2119 convention. ### R1 — User-authored rewrite law is declarative -User-authored rewrite logic MUST enter Echo as Wesley-compiled declarative IR. -User-authored rewrite law MUST NOT enter the deterministic path as handwritten -native callbacks, function pointers, or ad hoc executable host code. +User-authored operation law MUST enter Echo through an approved declarative +compiler path, deterministic lowering, independent verification where required, +digest-bound packaging, and explicit Echo admission. It MUST NOT enter the +deterministic path as handwritten native callbacks, function pointers, or ad +hoc executable host code. -GraphQL-authored shared families are the intended source for user-authored -graph entities, graph rewrites, declared footprints, and cross-boundary types -that Echo exposes to hosts, `warp-ttd`, or other engines. +Edict-authored operations and supported Wesley-authored GraphQL/optic contracts +are current declarative sources. Their generated artifacts may describe +semantic operations, reads, declared footprints, and cross-boundary types that +Echo exposes to hosts, `warp-ttd`, or other engines. Compiler or package identity +is evidence for its named crossing; it is not runtime installation, execution, +or receipt authority. Echo MAY keep handwritten Rust for engine internals, storage, scheduler mechanics, and other local implementation details, but those details MUST NOT -replace the shared family as the public authoring boundary for rewrite law or -cross-boundary types. +replace the approved declarative contract and compiler path as the public +authoring boundary for rewrite law or cross-boundary types. ### R1a — Declared footprints are part of the authored boundary -When a user-authored graph rewrite enters Echo through the shared family -surface, its declared footprint is part of the contract, not optional -commentary. +When a user-authored graph rewrite enters Echo through an approved declarative +contract and compiler path, its declared footprint is part of the contract, +not optional commentary. -The long-term target is for Wesley-generated Rust surfaces to make those -capability boundaries explicit enough that dishonest footprint use becomes -structurally impossible or a compile-time failure, rather than something Echo -discovers only after arbitrary native code has already been admitted. +The long-term target is for generated Rust, registry, and package surfaces from +approved compiler paths to make those capability boundaries explicit enough +that dishonest footprint use becomes structurally impossible or a compile-time +failure, rather than something Echo discovers only after arbitrary native code +has already been admitted. For dynamic rewrites, the declared boundary should be read as static at the level of slots, binding sources, closure operators, create/update surfaces, @@ -103,19 +113,21 @@ native code, Echo cannot fully guarantee that ambient state, hidden callbacks, or impure helper code will stay out of deterministic execution. The correct closure is not "be careful." The correct closure is to narrow the -user authoring boundary until it is declarative, inspectable, and compilable by -Wesley into a lawful rewrite substrate. +user authoring boundary until it is declarative, inspectable, deterministically +lowerable, independently verifiable where required, and explicitly admitted. -That does not require Wesley to compile the entire application. It requires -Wesley to own the lawful graph/rewrite boundary so Echo can keep runtime +That does not require one compiler to own the entire application. It requires +approved compiler paths such as Edict and Wesley to preserve their authored +semantic obligations through generated Echo artifacts so Echo can keep runtime internals handwritten without exposing arbitrary native author code on the deterministic path. ## Consequences - Echo may keep native `RewriteRule` internals for bootstrap and tests. -- Echo MUST move user-facing rewrite authorship toward Wesley-generated - declarative IR instead of stabilizing native rule callbacks as product API. +- Echo MUST move user-facing operation authorship through approved declarative + Edict or Wesley compiler paths instead of stabilizing native rule callbacks + as product API. - Determinism audit gates should scrutinize rule-authoring and host-boundary files more strictly than ordinary support code. - A sandboxed language is acceptable only as a front-end to the same @@ -124,6 +136,7 @@ deterministic path. ## Cross-references - [Generated rule authorship and footprints](../adr/0014-generated-rule-authorship-and-footprints.md) +- [Registry, provider, and host boundary](../adr/0015-registry-provider-host-boundary.md) - [Generated rules](../topics/GeneratedRules.md) - [WARP optics](../topics/WarpOptics.md) - [FIXED-TIMESTEP](./FIXED-TIMESTEP.md) diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 4747f14f..c82b21c6 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -5,8 +5,8 @@ Product and application rules are authored in contract languages. Hand-written Rust rule registration is not a supported product authoring surface. The final -package-shaped lowering corridor is a target boundary, not current end-to-end -implementation truth. +package-shaped lowering corridor is partially implemented, but it is not yet +current end-to-end runtime truth. ## Current Implementation @@ -16,15 +16,38 @@ handler, observer, and footprint helpers. Its integration fixture enables not yet package that output. It does not emit an `InstalledContractPackage` or exercise package verification. +The Edict provider path authenticates exact authored semantic source and +generation settings, emits canonical provider artifacts, lowers the supported +mutation closure into `echo.span-ir/v1` Target IR, and independently verifies +that crossing. It publishes checked lowerer and verifier components inside a +digest-locked provider package with exact provenance and non-authoritative +review evidence. The lowerer also emits a requested-only Rust helper that binds +the exact Target IR, bundle propositions, profiles, schemas, ABI versions, +footprint obligation, obstruction mapping, and semantic operation identity +through a pure descriptor preflight. The generated-artifact profile owns the +`le-binary-v1` value codec; the helper implements distinct typed `Id`, `Input`, +and `Output` encoders/decoders, rejects malformed or trailing bytes, and packs +the typed input into canonical EINT v1. The EINT `vars` payload remains opaque, +codec-owned bytes rather than a universal canonical-CBOR value. + +That provider package and descriptor are compiler/publication artifacts. They +are not an Echo registry entry, `InstalledContractPackage`, installation token, +submitted intent, execution receipt, or observation. The descriptor exposes a +borrowed, provider-generic registry and can combine its generated matcher with +one explicitly identity-bound host implementation to produce an opaque, +non-installing provider package proposal. Proposal preflight fails closed across +the complete operation, Target IR, bundle, profile, schema, codec, obstruction, +ABI, helper API, and footprint claims. Matching callback claims are +cross-binding evidence, not proof that arbitrary callback code implements the +claimed semantics. + Echo separately implements `InstalledContractPackage` verification, `Engine::register_contract_package`, scheduler-owned execution for registered -handlers, and `rule_pack_id` stamping. No current generator joins Wesley output -to that package-registration path. - -The Edict bridge is fixture-only. It accepts a narrow `echo.span-ir/v1` Target -IR subset and produces deterministic attempt-receipt fixtures. - -The Edict bridge does not admit a package or execute scheduler work. +handlers, and `rule_pack_id` stamping. A trusted Echo host still has to consume +the opaque provider proposal, make its own admission decision, and perform the +actual installation crossing; generated code cannot install itself. The Edict +provider package is admitted by its compiler/host contracts, but it is not +thereby admitted or installed by Echo. `native_rule_bootstrap` is a Cargo feature gate and repository policy boundary. Default builds omit raw rule constructors and public registration methods, but @@ -39,19 +62,26 @@ The required end state is: ```text Wesley or Edict source --> compiler Target IR --> generated Rust handlers, observers, and footprints --> generated registry and package metadata +-> verified mutation Target IR or lawful read/observer semantics +-> generated Rust handlers, bounded observers, and footprints +-> generated typed codecs, EINT helpers, registry, and package metadata +-> opaque provider package proposal with explicit host binding -> InstalledContractPackage verification -> Engine::register_contract_package -> scheduler-owned execution -> receipt / reading evidence carrying package and rule-pack identity ``` -Both authoring systems need package emitters that consume their verified source -or Target IR and produce the same package-shaped runtime surface. That work must -reuse Echo's registration and execution path; it must not create a second -execution engine. +Both authoring systems still need generated bridges into Echo's runtime package +surface. Wesley needs a package emitter. Edict already emits a digest-locked +provider publication package, codec-bound mutation client, borrowed registry, +and fail-closed package proposal. It still needs the trusted host crossing that +admits and installs that proposal as an `InstalledContractPackage`, plus the +separate generated bounded-observer path for authored reads. The current +mutation proposal intentionally rejects `Query`; that refusal does not turn a +read into a mutation or eliminate the independent observer/optic corridor. All +installation work must reuse Echo's registration and execution path; it must +not create a second execution engine. ## Footprint Honesty @@ -62,9 +92,12 @@ generated Wesley and Edict packs with `footprint_enforce_release` so a generator cannot silently ship false declarations. The `footprint_enforce_release` qualification lane is not wired into CI. -No Wesley or Edict pack is currently release-qualified. Package emitters and a -positive generated-pack witness plus a deliberately false-footprint negative -oracle are prerequisites before either authoring system may claim that status. +No Wesley or Edict package is currently footprint-release-qualified. The +digest-locked Edict provider package proves publication identity and +reproducibility, not runtime installation or footprint qualification. Each +authoring path still needs a positive installed generated-pack witness plus a +deliberately false-footprint negative oracle before it may claim that status; +Wesley additionally still needs its package emitter. Conflicts remain explicit receipt rejections. A footprint failure must not trigger hidden retries or widen access. @@ -74,8 +107,10 @@ trigger hidden retries or widen access. - Default builds do not expose raw `RewriteRule` construction or public raw registration. - Product and adapter crates do not enable `native_rule_bootstrap`. -- Package-qualified generators register through `InstalledContractPackage`, - not an app-specific engine escape hatch. +- Generators emit artifacts, descriptors, and opaque proposals without + registering themselves. Trusted hosts register package-qualified generated + material through `InstalledContractPackage`, not an app-specific engine + escape hatch. - Registry/package identity, operation identity, codec/schema compatibility, and footprint metadata are verified before the engine mutates registration state. diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index cd0ff450..a4fe3d69 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -163,7 +163,11 @@ unsupported output-role overclaim remains a typed refusal with neither response nor manifest. Independent fresh-store replay and separate host processes reproduce all three completed outcomes identically. Both checked components remain uninstalled package material; neither they nor the generated -authority-facts documents are runtime Echo authority. +authority-facts documents are runtime Echo authority. The promoted lowerer is +189,668 bytes with SHA-256 +`9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af`; the +promoted verifier is 189,922 bytes with SHA-256 +`4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a`. External Edict contract inputs come from the checked [`contracts/v1/`](contracts/v1/README.md) publication merged in @@ -188,6 +192,66 @@ them with the generated semantic artifacts, and generates the package-root listed inside its own artifact inventory. Runtime `reviewPayload` invocation output is distinct from #652's build-time `reviewArtifact`. +The #656 native lowerer model declares three exact sorted outputs: +`generated.echo-dpo` / `echo.generated-artifact/v1`, `review.echo-dpo` / +`echo.review-payload/v1`, and `target-ir.echo-dpo` / +`edict.target-ir.artifact/v1`. The first two are canonical-CBOR envelopes at +`generated/echo_dpo.rs` and `review/echo_dpo.json`; the non-authoritative review +subjects the exact generated-artifact digest. `echo.span-ir/v1` remains the +semantic Target IR coordinate and `edict.target-ir.artifact/v1` its distinct +artifact output and digest domain. `echo.dpo.bundle/v1` is a target-bundle +profile rather than a contract-bundle occurrence. Final semantic and release +bundle identities are compared explicitly after assembly using the separate +`edict.bundle.semantic/v1` and `edict.bundle.release/v1` propositions. That +comparison also checks the semantic operation, Echo ABI and helper API, provider +and operation schemas, generated and operation profiles, and +the exact abstract +footprint obligation/algebra. It does not claim a concrete static read/write +footprint. Every framed resource is checked as a complete +coordinate/domain/digest identity. The comparison grants no admission, +installation, or runtime authority. The canonical generated-artifact profile +now carries `operationIdLaw: "echo.semantic-operation-id.fnv1-32/v1"` and the +exact persisted `operationId` for each semantic coordinate and generic +query/mutation kind. The top two ids are Echo protocol reservations: +`u32::MAX` for scheduler control and `u32::MAX - 1` for witnessed suffix import. +Either reserved result and any package-local collision refuse without salting +or probing. The owning CDDL limits `operationId` to `0..4294967293`, proving only +the numeric application domain; semantic generation must still recompute the +coordinate-and-kind law and collision-check the complete set. Generated source +now carries public expected constants for the packaged law and id, requires them +as untrusted bundle claims, and refuses disagreement before returning a +private-state descriptor that exposes the matched id claim. It does not +independently derive a runtime identity. + +The generated-artifact profile owns the exact `le-binary-v1` value-codec claim. +Generated Rust implements it with semantically distinct `Id`, `Input`, and +`Output` types, preserves raw UTF-8 under the authored scalar bound, and fails +closed on malformed, over-bound, truncated, or trailing bytes. Descriptor +methods encode/decode the exact input and output and `pack_intent(...)` wraps +the encoded input in canonical EINT v1. Echo treats the EINT `vars` bytes as +opaque bytes owned by that selected codec; canonical CBOR is not a universal +operation-variable law. + +The matched descriptor also exposes a borrowed provider-generic registry and +can bind its generated matcher to one explicitly identified host mutation +implementation. It produces only an opaque, non-installing package proposal +after comparing every Target IR, semantic/release bundle, target/generated/ +operation profile, provider/value schema, codec, obstruction, operation-id, +ABI, helper-API, rule-name, and footprint claim. Identity equality detects +cross-binding but does not prove arbitrary callback semantics. The proposal +does not authenticate, install, register, schedule, execute, persist, observe, +or receipt anything; those remain trusted Echo crossings. Its constructor is +mutation-specific and refuses a `Query`. Authored reads remain a separate +bounded observer/optic path and must not be lowered as synthetic mutations. + +Runtime `reviewPayload` remains distinct from Wesley `GenerationReviewV1`. +Both refreshed components have crossed checked promotion, and the actual pinned +host admits the generated envelope under its owning `generated-artifact` CDDL +root. The isolated host helper witness is green for exact bundle binding, typed +codec round trips and refusal cases, EINT packing, the borrowed registry, and +non-installing package proposal. The review envelope still requires its +independent host-side CDDL admission before that output is admitted. + The package closure contains the 22 generated files plus the exact lowerer and verifier components. Its Echo-owned provider digest binds the typed manifest routes, 24 domain-to-root bindings (nine invocation domains, the generated @@ -216,10 +280,12 @@ proofs without guest invocation. Schema-valid resource substitution, reference swaps, authority-source disagreement, and malformed contract material fail before execution. This does not grant Echo runtime authority. -The crate-local `assets/v1/` tree is an exact publication carrier for the same -provider bytes plus the repository sources needed for generator provenance. -Carrier paths are physical packaging details only; authored logical source paths -remain unchanged. `echo-edict-provider-assets --check-package-list` proves +The crate-local `assets/v1/` tree is an exact 38-file publication carrier for +the same provider bytes plus the repository sources needed for generator +provenance. The compile-time generator identity enumerates a 20-file source +closure, including the provider-generic registry implementation. Carrier paths +are physical packaging details only; authored logical source paths remain +unchanged. `echo-edict-provider-assets --check-package-list` proves owner/carrier/package agreement and exact Cargo archive selection. All generated files are derived artifacts. Their digests and review renderings diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index 6cd6b3e3..29d1ef32 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -4,9 +4,10 @@ # Echo Edict Provider Components v1 This directory checks the first executable Echo provider components for the -frozen Edict target-provider worlds. Issue #655 will bind these exact components -into the digest-locked provider package; their presence here does not claim that -Echo has installed, admitted, authorized, or executed them. +frozen Edict target-provider worlds. The digest-locked package assembled under +Issue #655 binds these exact components by digest. Checked presence and package +binding do not claim that Echo has installed, admitted, authorized, or executed +them. `lowerer.echo-dpo.component.wasm` implements `edict:target-provider/lowerer@1.0.0`. It was built from @@ -22,8 +23,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 131,458 bytes with SHA-256 -`d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326`. +The checked component is 189,668 bytes with SHA-256 +`9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af`. 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 @@ -34,9 +35,9 @@ capability imports. `verifier.echo-dpo.component.wasm` implements `edict:target-provider/verifier@1.0.0`. It uses the same immutable builder, authenticated Rust/Cargo identities, frozen WIT bytes, path-remapping law, and -`wit-component` version recorded above. Its checked component is 189,515 bytes +`wit-component` version recorded above. Its checked component is 189,922 bytes with SHA-256 -`11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952`. +`4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/verifier@1.0.0`. Its only imports are the frozen WIT's 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 c98bfb79..5f536455 100644 Binary files a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm and b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm differ diff --git a/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm index 65128439..5e1224f6 100644 Binary files a/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm and b/schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm differ diff --git a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json index 8d25de7f..2c481917 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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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 3c71a269..a298a191 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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","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:b9c6d845e151f455bc7d0d6e7d209e7fbe92e3fc592145227cbb8e9b3e59c7b2","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","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:2874523930a963f81204df9c39aca6f9b62edbd45e5512ef6903346f7efa7c3e","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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/primary/authority-facts.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor index 807c2186..ef4787da 100644 --- a/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor +++ b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor @@ -1 +1 @@ -fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file +fsourcedkindmtargetProfilefdigestfsha256X ?K=,i? s&6LqAjcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor index a3a7b681..bba8cf44 100644 Binary files a/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor and b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor differ diff --git a/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor b/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor index bb7a2d6e..a1b381f2 100644 --- a/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor +++ b/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor @@ -1 +1 @@ -etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputjvalueCodeclle-binary-v1koperationId4mopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1noperationIdLawx%echo.semantic-operation-id.fnv1-32/v1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor index 354071e7..af0722fb 100644 Binary files a/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor and b/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor differ diff --git a/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl b/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl index 900e2fc2..380abbe7 100644 --- a/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl +++ b/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl @@ -800,6 +800,7 @@ target-ir-step = { generated-artifact-profile = { apiVersion: "echo.generated-artifact-profile/v1", targetProfile: tstr, + operationIdLaw: "echo.semantic-operation-id.fnv1-32/v1", types: { * tstr => echo-generated-type }, operations: { * tstr => generated-operation }, } @@ -817,6 +818,8 @@ echo-generated-record = { echo-generated-record-field = { name: tstr, type: tstr } generated-operation = { + operationId: 0..4294967293, + valueCodec: "le-binary-v1", inputType: tstr, outputType: tstr, effect: tstr, diff --git a/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor index b3ca7861..28493267 100644 Binary files a/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor and b/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor differ diff --git a/schemas/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm index c98bfb79..5f536455 100644 Binary files a/schemas/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm and b/schemas/edict-provider/package/v1/components/lowerer.echo-dpo.component.wasm differ diff --git a/schemas/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm b/schemas/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm index 65128439..5e1224f6 100644 Binary files a/schemas/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm and b/schemas/edict-provider/package/v1/components/verifier.echo-dpo.component.wasm differ diff --git a/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 8d25de7f..2c481917 100644 --- a/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json +++ b/schemas/edict-provider/package/v1/generated/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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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/package/v1/generated/evidence/review.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json index 3c71a269..a298a191 100644 --- a/schemas/edict-provider/package/v1/generated/evidence/review.provider-generation.json +++ b/schemas/edict-provider/package/v1/generated/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:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e","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:b9c6d845e151f455bc7d0d6e7d209e7fbe92e3fc592145227cbb8e9b3e59c7b2","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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:e4984d3ce0d9195f23ee30c2d36b46814d0872e294266ac30959a5a95b1f5f7e"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:022658a64fa8f693388e3f21354ebe3125eba0ea1fedf140b7c87f0c76cf08dc"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:679b090774088b46413a4697a646f10c3627d9f7f698380237db72e0b11739a1"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c"}],"generationInputDigest":"sha256:aff2b3737d635728190e6252515d03a1c3a9b58031c6a126a9cc9007f3998aa8","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2","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:2874523930a963f81204df9c39aca6f9b62edbd45e5512ef6903346f7efa7c3e","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:09e621edf0ba0a21bb602d759d7c66a5db394065ab2bc3578a6b7c31bc1ab335"},{"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/package/v1/generated/primary/authority-facts.echo-dpo.cbor b/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor index 807c2186..ef4787da 100644 --- a/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor +++ b/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-dpo.cbor @@ -1 +1 @@ -fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file +fsourcedkindmtargetProfilefdigestfsha256X ?K=,i? s&6LqAjcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor b/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor index a3a7b681..bba8cf44 100644 Binary files a/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor and b/schemas/edict-provider/package/v1/generated/primary/authority-facts.echo-lawpack.cbor differ diff --git a/schemas/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor b/schemas/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor index bb7a2d6e..a1b381f2 100644 --- a/schemas/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor +++ b/schemas/edict-provider/package/v1/generated/primary/generated-artifact-profile.echo-dpo-registration.cbor @@ -1 +1 @@ -etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputjvalueCodeclle-binary-v1koperationId4mopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1noperationIdLawx%echo.semantic-operation-id.fnv1-32/v1 \ No newline at end of file diff --git a/schemas/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor b/schemas/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor index 354071e7..af0722fb 100644 Binary files a/schemas/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor and b/schemas/edict-provider/package/v1/generated/primary/lawpack.echo-dpo.cbor differ diff --git a/schemas/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl b/schemas/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl index 900e2fc2..380abbe7 100644 --- a/schemas/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl +++ b/schemas/edict-provider/package/v1/generated/primary/schema.echo-provider-artifacts.cddl @@ -800,6 +800,7 @@ target-ir-step = { generated-artifact-profile = { apiVersion: "echo.generated-artifact-profile/v1", targetProfile: tstr, + operationIdLaw: "echo.semantic-operation-id.fnv1-32/v1", types: { * tstr => echo-generated-type }, operations: { * tstr => generated-operation }, } @@ -817,6 +818,8 @@ echo-generated-record = { echo-generated-record-field = { name: tstr, type: tstr } generated-operation = { + operationId: 0..4294967293, + valueCodec: "le-binary-v1", inputType: tstr, outputType: tstr, effect: tstr, diff --git a/schemas/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor b/schemas/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor index b3ca7861..28493267 100644 Binary files a/schemas/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor and b/schemas/edict-provider/package/v1/generated/primary/target-profile.echo-dpo.cbor differ diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index b95686ea..8782587e 100644 --- a/schemas/edict-provider/package/v1/provider-manifest.echo.json +++ b/schemas/edict-provider/package/v1/provider-manifest.echo.json @@ -3,7 +3,7 @@ "providerAbi": "edict:target-provider@1.0.0", "provider": { "coordinate": "echo.edict-provider@1", - "digest": "sha256:38dd0532d4353f6260e8aaeafb149457b42f8ad94bb107ecbb74bf02f0051a92" + "digest": "sha256:6d99fc4b4a56268b053226f17dfaa946bec6a3328219393c21fdb3398db6a925" }, "artifacts": [ { @@ -11,7 +11,7 @@ "artifactKind": "authorityFacts", "resource": { "coordinate": "echo.dpo-authority-facts@1", - "digest": "sha256:5c30871b115e82415980714281e250c2fda281afb4a4fe16c2917f93e86182a4" + "digest": "sha256:724d9f0cf79d2f3815902b1aba8d811add4c00f45c93a7c8c6b05ff2f7ed4c29" }, "source": { "kind": "generated", @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -30,7 +30,7 @@ "artifactKind": "authorityFacts", "resource": { "coordinate": "echo.dpo-lawpack-authority-facts@1", - "digest": "sha256:37591478584a88d3dc08a620873d803a2d6fcf5dc70767aae3587e5862ee99e9" + "digest": "sha256:147a07c38d257f6c949ad3890f3071049b79771b268d2e0d0fb1ad846e235209" }, "source": { "kind": "generated", @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -49,7 +49,7 @@ "artifactKind": "generatedArtifactProfile", "resource": { "coordinate": "echo.dpo.registration/v1", - "digest": "sha256:3377304d8634681821cd958427e0b8baccc37b7b08bfb342d988a08571eb83ab" + "digest": "sha256:ff88be93c26cc533948d8a93601954dc391912d593ca1e96115c846cbf2c5b5d" }, "source": { "kind": "generated", @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -68,7 +68,7 @@ "artifactKind": "lawpack", "resource": { "coordinate": "echo.dpo-lawpack@1", - "digest": "sha256:47006be4c2b569f13759078c560fe508e158b429cd9d4c5e47d9a7ea86b983b2" + "digest": "sha256:2a1631ae5fe2e11d09ccdfb94e3498f3236a3d615ee217e6cd5843efcd284987" }, "source": { "kind": "generated", @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326" + "digest": "sha256:9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326" + "digest": "sha256:9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:5cb085e1a35d01363a629b6933e7e88e96b38a2d841848d68320e9fb0fba8580" + "digest": "sha256:830978048785a7b59ab5ebc65ab10a09211c43394bd3e970534f2dc4d6c25982" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:b9b5c7715089ba24c6ce15d9f8674a7ab2ed95225ff8127952eb4d3065d32f6c" + "digest": "sha256:c5092ee28a0fe0df0265e5f0313e72212b96578c069276ecbd534a6bc3fc9017" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af" + "digest": "sha256:e4d9239715011fb03891aaf710455ac6ef68d787fa1f27f7c3153df48337871c" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -159,7 +159,7 @@ "artifactKind": "targetProfile", "resource": { "coordinate": "echo.dpo@1", - "digest": "sha256:f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0" + "digest": "sha256:ad7f10e1843f4b3d2c08b11d69df103f9c0b1b7388ae26bb364cc87106cd419e" }, "source": { "kind": "generated", @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:e57827652e86865b38a614a84fdacf715a46c4c528e133bc2942df47b29d146e" + "digest": "sha256:4b38887f484e9de8cdffe89e87bd0edcad165122889a7761dfdae90c1af76af2" } } }, @@ -178,13 +178,13 @@ "artifactKind": "verifier", "resource": { "coordinate": "echo.dpo.verifier/component@1", - "digest": "sha256:11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952" + "digest": "sha256:4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.verifier/component@1", - "digest": "sha256:11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952" + "digest": "sha256:4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a" } } } diff --git a/scripts/verify-edict-provider-host-v1.sh b/scripts/verify-edict-provider-host-v1.sh index 06aa895f..56d6478f 100755 --- a/scripts/verify-edict-provider-host-v1.sh +++ b/scripts/verify-edict-provider-host-v1.sh @@ -44,6 +44,12 @@ ECHO_PROVIDER_LOWERER_COMPONENT="$component" \ --locked \ --test host_contract +CARGO_TARGET_DIR="$HOST_TARGET_DIR" \ + cargo +1.94.0 test \ + --manifest-path "$MANIFEST" \ + --locked \ + --test generated_helper_contract + CARGO_TARGET_DIR="$HOST_TARGET_DIR" \ cargo +1.94.0 test \ --manifest-path "$MANIFEST" \ diff --git a/tests/docs/test_generated_rule_truth.sh b/tests/docs/test_generated_rule_truth.sh index ecefa26c..6a950fc6 100755 --- a/tests/docs/test_generated_rule_truth.sh +++ b/tests/docs/test_generated_rule_truth.sh @@ -12,6 +12,7 @@ adr="docs/adr/0014-generated-rule-authorship-and-footprints.md" wesley_fixture="crates/echo-wesley-gen/tests/generation.rs" edict_bridge="crates/warp-core/src/edict_target_ir.rs" readme="README.md" +jim_case="docs/case-studies/JimAndEcho.md" fail() { echo "generated-rule-truth: $*" >&2 @@ -38,24 +39,43 @@ require_literal "$edict_bridge" "bundle admission, target plugin dispatch, or sc require_literal "$topic" 'Wesley currently emits raw `RewriteRule` builders' require_literal "$topic" 'does not emit an `InstalledContractPackage`' -require_literal "$topic" 'The Edict bridge is fixture-only' -require_literal "$topic" 'does not admit a package or execute scheduler work' +require_literal "$topic" 'The Edict provider path authenticates exact authored semantic source' +require_literal "$topic" 'non-installing provider package proposal' +require_literal "$topic" 'generated code cannot install itself' require_literal "$topic" '`native_rule_bootstrap` is a Cargo feature gate and repository policy boundary' require_literal "$topic" 'It is not an access-control or security seal' require_literal "$topic" 'The `footprint_enforce_release` qualification lane is not wired into CI.' -require_literal "$topic" 'No Wesley or Edict pack is currently release-qualified.' +require_literal "$topic" 'No Wesley or Edict package is currently footprint-release-qualified.' require_literal "$adr" 'The feature is a policy and compatibility boundary, not an access-control seal.' require_literal "$adr" 'This is a qualification requirement, not a claim that the lane is currently implemented.' reject_literal "$topic" "The supported flow is:" reject_literal "$topic" "The missing Edict bridge is a generator" +reject_literal "$topic" "The Edict bridge is fixture-only" +reject_literal "$topic" "does not admit a package or execute scheduler work" +reject_literal "$topic" "No Wesley or Edict pack is currently release-qualified." reject_literal "$adr" '`native_rule_bootstrap` remains sealed' reject_literal "$adr" "without the sealed feature" -require_literal "$readme" 'Wesley currently emits raw `RewriteRule` builders and generated helpers.' -require_literal "$readme" 'It does not emit an `InstalledContractPackage`' -require_literal "$readme" 'The package-shaped flow below is the target corridor, not a current end-to-end application path:' +require_literal "$readme" 'The Wesley compatibility path emits raw `RewriteRule` builders and generated' +require_literal "$readme" 'not emit an `InstalledContractPackage` or exercise package verification.' +require_literal "$readme" 'The helper performs pure, fail-closed preflight across exact package' +require_literal "$readme" 'opaque, non-installing provider package proposal' +require_literal "$readme" 'The helper does not construct an `InstalledContractPackage`, register or' +require_literal "$readme" "No generated bridge yet carries either compiler path across Echo's trusted-host" +require_literal "$readme" 'helper can encode typed input and pack a canonical intent for submission through' +require_literal "$readme" 'boundary into native scheduler execution.' +require_literal "$readme" 'The package-shaped flow below is partially implemented, but is not yet a' +reject_literal "$readme" 'It does not yet provide codec-bound invocation' +reject_literal "$readme" 'The package-shaped flow below is the target corridor, not a current end-to-end application path:' reject_literal "$readme" 'Wesley generates type-safe helpers, codecs, registry metadata, and host adapters.' +require_literal "$jim_case" 'currently produce an opaque' +require_literal "$jim_case" '`ProviderContractPackageProposalV1`. The proposal retains generated registry' +require_literal "$jim_case" 'separately tracked trusted-host admission flow' +require_literal "$jim_case" 'before any runtime authority exists.' +reject_literal "$jim_case" 'host.register_contract_package(jim_package)?;' +reject_literal "$jim_case" 'Let the trusted host verify, bind, and register the package' + echo "generated-rule-truth: current Wesley, Edict, and feature boundaries are explicit" diff --git a/tests/edict-provider-host-v1/Cargo.lock b/tests/edict-provider-host-v1/Cargo.lock index a386a14e..9523ce42 100644 --- a/tests/edict-provider-host-v1/Cargo.lock +++ b/tests/edict-provider-host-v1/Cargo.lock @@ -38,6 +38,18 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "async-trait" version = "0.1.89" @@ -49,6 +61,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "base64" version = "0.22.1" @@ -61,6 +79,20 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -79,6 +111,32 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.2.67" @@ -149,6 +207,12 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "cpp_demangle" version = "0.4.5" @@ -167,6 +231,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cranelift-assembler-x64" version = "0.133.1" @@ -352,11 +425,35 @@ dependencies = [ name = "echo-edict-provider-host-v1-witness" version = "0.0.0" dependencies = [ + "echo-registry-api", + "echo-wasm-abi", "edict-provider-host-wasmtime", "edict-provider-schema", "edict-syntax", "serde_json", "sha2", + "warp-core", +] + +[[package]] +name = "echo-registry-api" +version = "0.1.0" + +[[package]] +name = "echo-runtime-schema" +version = "0.1.0" + +[[package]] +name = "echo-wasm-abi" +version = "0.1.0" +dependencies = [ + "blake3", + "ciborium", + "echo-registry-api", + "half", + "serde", + "serde-value", + "thiserror 2.0.18", ] [[package]] @@ -700,6 +797,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.39.1" @@ -718,6 +824,15 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -872,6 +987,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -912,7 +1037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1063,6 +1188,29 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "warp-core" +version = "0.1.1" +dependencies = [ + "blake3", + "bytemuck", + "bytes", + "echo-registry-api", + "echo-runtime-schema", + "echo-wasm-abi", + "hex", + "rustc-hash", + "thiserror 1.0.69", + "warp-math", +] + +[[package]] +name = "warp-math" +version = "0.1.0" +dependencies = [ + "libm", +] + [[package]] name = "wasm-encoder" version = "0.251.0" diff --git a/tests/edict-provider-host-v1/Cargo.toml b/tests/edict-provider-host-v1/Cargo.toml index 886083e3..c923a1b4 100644 --- a/tests/edict-provider-host-v1/Cargo.toml +++ b/tests/edict-provider-host-v1/Cargo.toml @@ -16,6 +16,9 @@ resolver = "2" 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" } +echo-registry-api = { path = "../../crates/echo-registry-api" } +echo-wasm-abi = { path = "../../crates/echo-wasm-abi" } +warp-core = { path = "../../crates/warp-core" } serde_json = "=1.0.150" sha2 = "=0.10.9" diff --git a/tests/edict-provider-host-v1/tests/generated_helper_contract.rs b/tests/edict-provider-host-v1/tests/generated_helper_contract.rs new file mode 100644 index 00000000..94d17439 --- /dev/null +++ b/tests/edict-provider-host-v1/tests/generated_helper_contract.rs @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used)] +//! Cargo-backed consumer contract for the checked generated Echo helper. + +#[rustfmt::skip] +#[path = "../../../crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs"] +mod checked_generated_helper; + +use checked_generated_helper::echo_dpo as generated; +use echo_wasm_abi::codec::CodecError; +use warp_core::{ + ContractPackageIdentity, Footprint, GraphView, NodeId, ProviderMutationHooksV1, + ProviderMutationHostV1, ProviderPackageProposalErrorKind, TickDelta, +}; + +const SEMANTIC_DIGEST: &str = + "sha256:1111111111111111111111111111111111111111111111111111111111111111"; +const RELEASE_DIGEST: &str = + "sha256:2222222222222222222222222222222222222222222222222222222222222222"; +const PACKAGE_ARTIFACT_SHA256: &str = + "3333333333333333333333333333333333333333333333333333333333333333"; + +const fn exact_pin() -> generated::ExpectedContractBundleIdentityV1<'static> { + generated::ExpectedContractBundleIdentityV1 { + semantic_digest_domain: generated::SEMANTIC_BUNDLE_DIGEST_DOMAIN, + semantic_digest: SEMANTIC_DIGEST, + release_digest_domain: generated::RELEASE_BUNDLE_DIGEST_DOMAIN, + release_digest: RELEASE_DIGEST, + } +} + +const fn matching_identity() -> generated::ContractBundleIdentityV1<'static> { + generated::ContractBundleIdentityV1 { + semantic_digest_domain: generated::SEMANTIC_BUNDLE_DIGEST_DOMAIN, + semantic_digest: SEMANTIC_DIGEST, + release_digest_domain: generated::RELEASE_BUNDLE_DIGEST_DOMAIN, + release_digest: RELEASE_DIGEST, + operation_coordinate: generated::OPERATION_COORDINATE, + operation_domain: generated::OPERATION_DOMAIN, + operation_id_law: generated::OPERATION_ID_LAW, + operation_id: generated::OPERATION_ID, + value_codec: generated::VALUE_CODEC_ID, + target_ir_coordinate: generated::TARGET_IR_COORDINATE, + target_ir_digest_domain: generated::TARGET_IR_DIGEST_DOMAIN, + target_ir_digest: generated::TARGET_IR_DIGEST, + target_profile_coordinate: generated::TARGET_PROFILE_COORDINATE, + target_profile_digest_domain: generated::TARGET_PROFILE_DIGEST_DOMAIN, + target_profile_digest: generated::TARGET_PROFILE_DIGEST, + target_bundle_profile_coordinate: generated::TARGET_BUNDLE_PROFILE_COORDINATE, + target_bundle_profile_digest_domain: generated::TARGET_BUNDLE_PROFILE_DIGEST_DOMAIN, + target_bundle_profile_digest: generated::TARGET_BUNDLE_PROFILE_DIGEST, + echo_contract_abi_version: generated::ECHO_CONTRACT_ABI_VERSION, + helper_api_version: generated::CONTRACT_HOST_HELPER_API_VERSION, + provider_schema_coordinate: generated::PROVIDER_SCHEMA_COORDINATE, + provider_schema_sha256_hex: generated::PROVIDER_SCHEMA_SHA256_HEX, + input_schema: generated::INPUT_SCHEMA, + output_schema: generated::OUTPUT_SCHEMA, + type_schema_domain: generated::TYPE_SCHEMA_DOMAIN, + obstruction_coordinate: generated::OBSTRUCTION_COORDINATE, + obstruction_domain: generated::OBSTRUCTION_DOMAIN, + effect_failure_schema: generated::EFFECT_FAILURE_SCHEMA, + obstruction_payload_schema: generated::OBSTRUCTION_PAYLOAD_SCHEMA, + generated_artifact_profile: generated::GENERATED_ARTIFACT_PROFILE, + generated_artifact_profile_digest_domain: + generated::GENERATED_ARTIFACT_PROFILE_DIGEST_DOMAIN, + generated_artifact_profile_digest: generated::GENERATED_ARTIFACT_PROFILE_DIGEST, + operation_profile: generated::OPERATION_PROFILE, + operation_profile_domain: generated::OPERATION_PROFILE_DOMAIN, + operation_profiles_coordinate: generated::OPERATION_PROFILES_COORDINATE, + operation_profiles_digest_domain: generated::OPERATION_PROFILES_DIGEST_DOMAIN, + operation_profiles_digest: generated::OPERATION_PROFILES_DIGEST, + footprint_obligation: generated::FOOTPRINT_OBLIGATION, + footprint_algebra: generated::FOOTPRINT_ALGEBRA, + footprint_algebra_digest_domain: generated::FOOTPRINT_ALGEBRA_DIGEST_DOMAIN, + footprint_algebra_digest: generated::FOOTPRINT_ALGEBRA_DIGEST, + } +} + +fn bound_descriptor() -> generated::RegistrationDescriptorV1<'static> { + let descriptor = generated::bind_contract_bundle(exact_pin(), &matching_identity()) + .expect("the exact checked claims bind the generated descriptor"); + assert_eq!(*descriptor.contract_bundle(), matching_identity()); + descriptor +} + +const fn execute(_view: GraphView<'_>, _scope: &NodeId, _delta: &mut TickDelta) {} + +fn footprint(_view: GraphView<'_>, _scope: &NodeId) -> Footprint { + Footprint::default() +} + +struct TestHost; + +impl ProviderMutationHostV1 for TestHost { + fn execute(view: GraphView<'_>, scope: &NodeId, delta: &mut TickDelta) { + execute(view, scope, delta); + } + + fn effect_footprint(view: GraphView<'_>, scope: &NodeId) -> Footprint { + footprint(view, scope) + } +} + +const fn package_occurrence() -> ContractPackageIdentity<'static> { + ContractPackageIdentity { + package_name: "a.b", + package_version: "1.0.0", + artifact_hash_hex: PACKAGE_ARTIFACT_SHA256, + } +} + +#[test] +fn checked_generated_helper_refuses_the_wrong_value_codec_before_binding() { + let identity = generated::ContractBundleIdentityV1 { + value_codec: "wrong.codec/v1", + ..matching_identity() + }; + + assert_eq!( + generated::bind_contract_bundle(exact_pin(), &identity), + Err(generated::BindingMismatchKind::Codec) + ); +} + +#[test] +fn checked_generated_operation_codec_round_trips_exact_values() { + let descriptor = bound_descriptor(); + assert_eq!( + generated::Id::new("abc").expect("bounded id").into_string(), + "abc" + ); + let input = generated::Input::new("abc").expect("bounded ASCII input"); + let encoded = descriptor + .encode_input(&input) + .expect("bounded input encodes"); + assert_eq!(encoded, [3, 0, 0, 0, b'a', b'b', b'c']); + assert_eq!( + descriptor + .decode_input(&encoded) + .expect("canonical input decodes"), + input + ); + + let output = generated::Output::new("abc").expect("bounded output"); + assert_eq!(output.id(), "abc"); + let output_bytes = descriptor + .encode_output(&output) + .expect("bounded output encodes"); + assert_eq!( + descriptor + .decode_output(&output_bytes) + .expect("canonical output decodes"), + output + ); + + let sixteen_four_byte_scalars = "💩".repeat(16); + let boundary = generated::Input::new(sixteen_four_byte_scalars.clone()) + .expect("sixteen Unicode scalars are admitted regardless of UTF-8 width"); + let boundary_bytes = descriptor + .encode_input(&boundary) + .expect("the exact 64-byte UTF-8 boundary encodes"); + assert_eq!(&boundary_bytes[..4], &[64, 0, 0, 0]); + assert_eq!(boundary_bytes.len(), 68); + assert_eq!( + descriptor + .decode_input(&boundary_bytes) + .expect("the exact scalar and physical bounds decode") + .id(), + sixteen_four_byte_scalars + ); +} + +#[test] +fn checked_generated_operation_codec_rejects_invalid_or_non_exact_bytes() { + let descriptor = bound_descriptor(); + + assert_eq!( + generated::Input::new("12345678901234567"), + Err(CodecError::StringTooLong) + ); + assert_eq!( + generated::Input::new("💩".repeat(17)), + Err(CodecError::StringTooLong) + ); + assert_eq!( + descriptor.decode_input(&[1, 0, 0, 0, 0xff]), + Err(CodecError::InvalidUtf8) + ); + assert_eq!( + descriptor.decode_input(&[65, 0, 0, 0]), + Err(CodecError::LengthTooLarge) + ); + assert_eq!( + descriptor.decode_input(&[3, 0, 0, 0, b'a', b'b']), + Err(CodecError::OutOfBounds) + ); + assert_eq!( + descriptor.decode_input(&[1, 0, 0, 0, b'a', 0]), + Err(CodecError::Trailing) + ); +} + +#[test] +fn checked_generated_intent_uses_the_bound_operation_and_canonical_vars() { + let descriptor = bound_descriptor(); + let input = generated::Input::new("abc").expect("bounded input"); + let intent = descriptor + .pack_intent(&input) + .expect("typed canonical EINT construction succeeds"); + let expected = [ + 0x45, 0x49, 0x4e, 0x54, 0xb2, 0x34, 0x02, 0xca, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x61, 0x62, 0x63, + ]; + assert_eq!(intent, expected); + + let (operation_id, vars) = + echo_wasm_abi::unpack_intent_v1(&intent).expect("canonical EINT unpacks"); + assert_eq!(operation_id, generated::OPERATION_ID); + assert_eq!(vars, &expected[12..]); + assert_eq!( + descriptor + .decode_input(vars) + .expect("the exact EINT vars decode as the generated input"), + input + ); +} + +#[test] +fn checked_generated_helper_constructs_the_exact_non_authoritative_package_proposal() { + let descriptor = bound_descriptor(); + let registry = descriptor.provider_registry(); + let proposal = descriptor + .propose_contract_package( + package_occurrence(), + ProviderMutationHooksV1::for_host::( + descriptor.mutation_implementation_identity(), + ), + ) + .expect("exact implementation claims produce a package proposal"); + + assert_eq!(proposal.occurrence(), &package_occurrence()); + assert_eq!(proposal.registry(), ®istry); + assert_eq!( + proposal.mutation_operation_ids().collect::>(), + vec![generated::OPERATION_ID] + ); + assert_eq!(registry.operations.len(), 1); + assert_eq!( + registry.operations[0].coordinate, + generated::OPERATION_COORDINATE + ); + assert_eq!( + registry.operations[0].input.codec_id, + generated::VALUE_CODEC_ID + ); + assert_eq!(registry.bundle.semantic_digest, SEMANTIC_DIGEST); + assert_eq!(registry.bundle.release_digest, RELEASE_DIGEST); +} + +#[test] +fn checked_generated_helper_refuses_cross_bound_host_implementations() { + let descriptor = bound_descriptor(); + let mut identity = descriptor.mutation_implementation_identity(); + identity.operation.target_ir.digest = + "sha256:4444444444444444444444444444444444444444444444444444444444444444"; + let kind = descriptor + .propose_contract_package( + package_occurrence(), + ProviderMutationHooksV1::for_host::(identity), + ) + .err() + .map(|error| error.kind()); + assert_eq!( + kind, + Some(ProviderPackageProposalErrorKind::TargetIrMismatch) + ); + + let mut identity = descriptor.mutation_implementation_identity(); + identity.operation.input.codec_id = "wrong-codec/v1"; + let kind = descriptor + .propose_contract_package( + package_occurrence(), + ProviderMutationHooksV1::for_host::(identity), + ) + .err() + .map(|error| error.kind()); + assert_eq!(kind, Some(ProviderPackageProposalErrorKind::CodecMismatch)); +} diff --git a/tests/edict-provider-host-v1/tests/host_contract.rs b/tests/edict-provider-host-v1/tests/host_contract.rs index f8ab9758..7940fac9 100644 --- a/tests/edict-provider-host-v1/tests/host_contract.rs +++ b/tests/edict-provider-host-v1/tests/host_contract.rs @@ -57,6 +57,8 @@ const ECHO_SOURCE: &str = "package a.b@1;\n\ const LOWERABILITY_DOMAIN: &str = "edict.lowering-requirements/v1"; const TARGET_IR_ROLE: &str = "target-ir.echo-dpo"; +const GENERATED_ARTIFACT_ROLE: &str = "generated.echo-dpo"; +const GENERATED_ARTIFACT_DOMAIN: &str = "echo.generated-artifact/v1"; const LOWERER_ROLE: &str = "lowerer.echo-dpo"; const VERIFIER_ROLE: &str = "verifier.echo-dpo"; const VERIFIER_REPORT_ROLE: &str = "verifier-report.echo-dpo"; @@ -65,16 +67,24 @@ const DIAGNOSTIC_ABI_DIGEST: &str = "28fd72a98223153982ca084c29dbb1b2d430623967ab3b6db9d7fee668e614b9"; const SCHEMA_ROLE: &str = "schema.echo-provider-artifacts"; const RAW_TARGET_IR_SHA256: &str = - "41ae7a1d95e5068cb09ec581f16a90cc6e26a80f83ec073e86d5108c3a61ea41"; + "5922fe337e04d2df0b62f32b04b19409609dafbcc672d6e4ea593c83f4ffac53"; const DOMAIN_TARGET_IR_SHA256: &str = - "b0d9e218f00a102d1e951c73e5063a9bbe6077e6c7468d171ec08b420e7b47da"; -const TARGET_PROFILE_SHA256: &str = - "f41df38156625a05c1ee8bce652ffddf04e71b54fe027eeab9d255d0d8322db0"; + "2244345f046448c7b519ade05a167137659361ed144b46315ea32dabfbad85fc"; +const RAW_TARGET_PROFILE_SHA256: &str = + "cb5104802031e06d2e2802efe14ad23877dba2756684a5509c06a8de7bb9ec85"; +const DOMAIN_TARGET_PROFILE_SHA256: &str = + "ad7f10e1843f4b3d2c08b11d69df103f9c0b1b7388ae26bb364cc87106cd419e"; 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 GENERATED_ARTIFACT_PROFILE_BYTES: &[u8] = include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor" +); +const CHECKED_GENERATED_HELPER_BYTES: &[u8] = include_bytes!( + "../../../crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs" +); const TARGET_PROFILE_BYTES: &[u8] = include_bytes!( "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" ); @@ -367,6 +377,7 @@ fn echo_manifest(component_bytes: &'static [u8]) -> &'static TargetProviderManif }, ], schema_bindings: [ + (GENERATED_ARTIFACT_DOMAIN, "generated-artifact"), (AUTHORITY_FACTS_API_VERSION, "authority-facts"), (CORE_MODULE_DIGEST_DOMAIN, "core-module"), (PROVIDER_LAWPACK_ARTIFACT_DOMAIN, "lawpack-manifest"), @@ -397,6 +408,7 @@ fn echo_registry( bytes: Arc::from(SCHEMA_BYTES), }], [ + GENERATED_ARTIFACT_DOMAIN, AUTHORITY_FACTS_API_VERSION, CORE_MODULE_DIGEST_DOMAIN, PROVIDER_LAWPACK_ARTIFACT_DOMAIN, @@ -696,6 +708,16 @@ fn canonical_map_field_mut<'a>( .unwrap_or_else(|| panic!("canonical map field `{field}` is absent")) } +fn canonical_map_field<'a>(value: &'a CanonicalValue, field: &str) -> &'a CanonicalValue { + let CanonicalValue::Map(entries) = value else { + panic!("canonical value is not a map"); + }; + entries + .iter() + .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") } @@ -1166,7 +1188,7 @@ 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}")), + digest: Some(format!("sha256:{DOMAIN_TARGET_PROFILE_SHA256}")), }, target_ir_domain: ECHO_SPAN_IR_DOMAIN.to_owned(), operation_profiles: vec!["continuum.profile.write/v1".to_owned()], @@ -1206,9 +1228,10 @@ fn echo_component_matches_independent_edict_target_ir_bytes_and_digest() { hex(&provider_digest(CORE_MODULE_DIGEST_DOMAIN, &core_bytes).bytes), "c3dbe413c78a82f6120e64c9a04bc94e2d79505f9e4b8a65c2bc26b408d775de" ); + assert_eq!(raw_sha256(TARGET_PROFILE_BYTES), RAW_TARGET_PROFILE_SHA256); assert_eq!( hex(&provider_digest(TARGET_PROFILE_API_VERSION, TARGET_PROFILE_BYTES).bytes), - TARGET_PROFILE_SHA256 + DOMAIN_TARGET_PROFILE_SHA256 ); let (oracle_bytes, oracle_digest) = oracle_target_ir(&core); @@ -1261,6 +1284,88 @@ fn echo_component_matches_independent_edict_target_ir_bytes_and_digest() { assert!(replayed.refusal().is_none()); } +#[test] +fn echo_component_emits_profile_owned_operation_identity_through_the_actual_host() { + let core = echo_core(); + let (contract, mut request) = echo_request(&core, TARGET_IR_ROLE); + request.requested_outputs = vec![ProviderLoweringOutputRequest { + role: GENERATED_ARTIFACT_ROLE.to_owned(), + kind: ProviderLoweringOutputKind::GeneratedArtifact, + domain: GENERATED_ARTIFACT_DOMAIN.to_owned(), + }]; + let harness = echo_harness_with_request(contract, request); + let outcome = harness + .host + .invoke_lowerer( + &harness.prepared, + &harness.request, + harness.schema, + host_limits(), + ) + .expect("generated artifact 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, GENERATED_ARTIFACT_ROLE); + assert_eq!(output.kind, ProviderLoweringOutputKind::GeneratedArtifact); + assert_eq!(output.artifact.domain, GENERATED_ARTIFACT_DOMAIN); + assert_eq!( + output.logical_path.as_deref(), + Some("generated/echo_dpo.rs") + ); + + let envelope = decode_canonical_cbor(&output.artifact.bytes) + .expect("generated-artifact envelope is canonical CBOR"); + let CanonicalValue::Bytes(source_bytes) = canonical_map_field(&envelope, "bytes") else { + panic!("generated-artifact bytes field is not a byte string"); + }; + let source = std::str::from_utf8(source_bytes).expect("generated source is UTF-8"); + let expected_schema_sha256 = raw_sha256(SCHEMA_BYTES); + let profile = decode_canonical_cbor(GENERATED_ARTIFACT_PROFILE_BYTES) + .expect("generated-artifact profile is canonical CBOR"); + let CanonicalValue::Text(operation_id_law) = canonical_map_field(&profile, "operationIdLaw") + else { + panic!("operation-id law is not text"); + }; + let operation = canonical_map_field(canonical_map_field(&profile, "operations"), "a.b@1.t"); + let CanonicalValue::Integer(operation_id) = canonical_map_field(operation, "operationId") + else { + panic!("operation id is not an integer"); + }; + let CanonicalValue::Text(value_codec) = canonical_map_field(operation, "valueCodec") else { + panic!("operation value codec is not text"); + }; + + assert_eq!(source_bytes.as_slice(), CHECKED_GENERATED_HELPER_BYTES); + assert!(source.contains(&format!("\"{expected_schema_sha256}\""))); + assert!(source.contains(&format!( + "pub const OPERATION_ID_LAW: &str = \"{operation_id_law}\";" + ))); + let generated_operation_id = source + .lines() + .find_map(|line| { + let literal = line + .trim() + .strip_prefix("pub const OPERATION_ID: u32 = ")? + .strip_suffix(';')?; + literal.replace('_', "").parse::().ok() + }) + .expect("generated source declares one parseable operation id"); + assert_eq!(generated_operation_id, *operation_id); + assert!(source.contains(&format!( + "pub const VALUE_CODEC_ID: &str = \"{value_codec}\";" + ))); + + 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, GENERATED_ARTIFACT_ROLE); + assert_eq!(entry.kind, ProviderLoweringOutputKind::GeneratedArtifact); + assert_eq!(entry.domain, GENERATED_ARTIFACT_DOMAIN); +} + #[test] fn exact_merged_edict_host_fixtures_are_unchanged() { assert_eq!(FIXTURE_LOWERER_BYTES.len(), 28_007); diff --git a/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs b/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs index 158a1a23..94c611d9 100644 --- a/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs +++ b/tests/edict-provider-host-v1/tests/verifier_resource_sync.rs @@ -7,6 +7,24 @@ #[test] fn verifier_package_resources_equal_the_checked_generated_corpus() { let pairs: &[(&str, &[u8], &[u8])] = &[ + ( + "authority-facts.echo-dpo.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/authority-facts.echo-dpo.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor" + ), + ), + ( + "authority-facts.echo-lawpack.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/authority-facts.echo-lawpack.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor" + ), + ), ( "generated-artifact-profile.echo-dpo-registration.cbor", include_bytes!( @@ -16,6 +34,15 @@ fn verifier_package_resources_equal_the_checked_generated_corpus() { "../../../schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor" ), ), + ( + "lawpack.echo-dpo.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/lawpack.echo-dpo.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor" + ), + ), ( "resource.lawpack-exports.cbor", include_bytes!( @@ -106,6 +133,15 @@ fn verifier_package_resources_equal_the_checked_generated_corpus() { "../../../schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor" ), ), + ( + "target-profile.echo-dpo.cbor", + include_bytes!( + "../../../crates/echo-edict-provider-verifier/resources/target-profile.echo-dpo.cbor" + ), + include_bytes!( + "../../../schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor" + ), + ), ]; for (name, packaged, generated) in pairs { diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index 98ad9d0b..c567c794 100644 --- a/xtask/src/provider_lowerer_component.rs +++ b/xtask/src/provider_lowerer_component.rs @@ -53,7 +53,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 = - "d58a7838df9d7e65c72063aafad91df97cb21404be556c93e864234f3f846326"; + "9b5ecf1c87a4a98ad3d7381d85e3625649a4f9d62f80f74b2aca6ace26be14af"; pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; @@ -62,7 +62,7 @@ pub(crate) const VERIFIER_CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/verifier.echo-dpo.component.wasm"; /// Approved SHA-256 identity of the checked verifier component. pub(crate) const APPROVED_CHECKED_VERIFIER_COMPONENT_SHA256: &str = - "11fcaf291ffacac62800ab3fc198e29a23a591bcd30ca98828fa10a8ad1f1952"; + "4f99c07f1c6d682405945fbb3e36e5537b94c4a01bd1a4ec7f02ef8e9911249a"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct ProviderComponentSpec {