diff --git a/.ban-nondeterminism-allowlist b/.ban-nondeterminism-allowlist index 1e5829267..a08c03128 100644 --- a/.ban-nondeterminism-allowlist +++ b/.ban-nondeterminism-allowlist @@ -49,3 +49,5 @@ std-env crates/warp-core/benches/support/action_scene.rs benchmark driver reads std-fs crates/warp-core/benches/support/action_scene.rs benchmark driver creates and removes its own log directory; it is not linked into the runtime. time-instant crates/warp-core/benches/support/fixtures/think_append.rs benchmark fixture measures elapsed time outside the runtime; the timings decide no history and are excluded from validation. hand-hashed-table crates/warp-core/src/snapshot.rs state-root membership accelerator in private mod membership, whose only operations are begin and insert (no iterator, slice accessor or lookup, enforced by the compiler); exact key equality, no seed; its contents, capacity, probe counts, generation and fallback cannot affect the state root, which is independent of scratch history (tested against the ordered reference). + +std-fs crates/warp-core/tests/edict_effect_admission_tests/installation.rs owned filesystem WAL fixture directory creation and cleanup only; paths never enter program, policy, installation or receipt identities. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8b09bb3..0b9143b50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,8 +183,8 @@ jobs: cargo clippy -p warp-core --features host_test --test admission_comparison_tests -- -D warnings -D missing_docs - name: cargo clippy (scheduler-owned executable Actions) run: | - cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime --test executable_operation_pipeline_tests -- -D warnings -D missing_docs - cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test --test executable_operation_pipeline_tests -- -D warnings -D missing_docs + cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime --test executable_operation_pipeline_tests --test edict_effect_admission_tests -- -D warnings -D missing_docs + cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test --test executable_operation_pipeline_tests --test edict_effect_admission_tests -- -D warnings -D missing_docs - name: cargo clippy (compiler-produced compare-and-set recovery) run: | cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime --test edict_projected_cas_tests -- -D warnings -D missing_docs @@ -201,7 +201,7 @@ jobs: run: cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_profile --lib --tests --benches -- -D warnings - name: cargo clippy (compiler-produced pure evaluation) - run: cargo clippy -p warp-core --features trusted_runtime --lib --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests -- -D warnings -D missing_docs + run: cargo clippy -p warp-core --features trusted_runtime --lib --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_tests --test edict_effect_graph_schema_tests --test edict_effect_package_tests --test current_operation_publication_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests -- -D warnings -D missing_docs clippy-det-fixed: name: Clippy (det_fixed) @@ -378,8 +378,10 @@ jobs: run: cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test --test provider_contract_admission_tests - name: cargo test (scheduler-owned executable Actions) run: | - cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime --test executable_operation_pipeline_tests - cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test --test executable_operation_pipeline_tests + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime --test executable_operation_pipeline_tests --test edict_effect_admission_tests + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test --test executable_operation_pipeline_tests --test edict_effect_admission_tests + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime --doc admit_echo_operation_package_v2 + cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime --doc install_admitted_echo_operation_package_v2 - name: cargo test (compiler-produced compare-and-set recovery) run: | cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime --test edict_projected_cas_tests @@ -401,7 +403,7 @@ jobs: cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_profile,delta_validate --test application_root_profile_tests --test trusted_runtime_host_loop_tests --test executable_operation_pipeline_tests - name: cargo test (compiler-produced pure evaluation) run: | - cargo test -p warp-core --features trusted_runtime --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests + cargo test -p warp-core --features trusted_runtime --test edict_pure_evaluation_tests --test edict_pure_predicates_tests --test edict_pure_unsigned_subtraction_tests --test edict_unsigned_addition_tests --test edict_pure_byte_length_tests --test edict_byte_equality_tests --test edict_byte_slice_tests --test edict_node_read_tests --test edict_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_tests --test edict_effect_graph_schema_tests --test edict_effect_package_tests --test current_operation_publication_tests --test edict_byte_concat_tests --test edict_byte_literal_tests --test edict_source_functions_tests cargo test -p warp-core --features trusted_runtime --lib edict_pure:: - name: cargo test (delta validation and the canonical delta merge) run: | diff --git a/AGENTS.md b/AGENTS.md index 7ba7fe2f5..ce660f999 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,20 @@ tracing yields an explicit posture, never a successful all-zero receipt. - **NEVER** push to `main` without explicit permission. - Always use standard commits and regular pushes. +## One Current Version + +Keep exactly one supported version of each data structure, wire format, and +implementation in the main crates. Replace the previous version and update +its producers and consumers together. Old versions may exist only in separate +upgrade or migration tools, never behind compatibility wrappers, aliases, +feature flags, or version-dispatch branches in the main crates. + +The user confirmed on 2026-10-19 that there are no existing saved histories to +preserve. Do not invent a backward-compatibility requirement. Future persisted +data upgrades belong in explicit migration tools. See the current +[version policy](docs/topics/GeneratedRules.md#one-current-version-and-explicit-migration), +which supersedes earlier proposals and compatibility promises. + ## Repository Knowledge Model Repository knowledge has one owner for each kind of truth: diff --git a/CHANGELOG.md b/CHANGELOG.md index 721939aca..e7eb41f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,183 @@ ### Changed +- An authored effect package no longer carries a single `obstruction_coordinate`. + Its embedded exports declare every obstruction the program can return, and + admission checks each arm against them. The effect-package shape exists only + on this unreleased route, so it is amended in place; fixed-profile packages keep + the field, and their bytes and identities are unchanged. The lowerer's effect + publication no longer takes an obstruction input. + +- The authored route names `echo.dpo@1.node-atom-read`'s failures by Edict ABI + identifiers (`atomMissing`, `atomRequired`, `atomTypeMismatch`, + `atomByteBoundExceeded`). One Echo-owned table, scoped to that intrinsic, + translates each to the runtime tag Echo records; missing, unknown, repeated + or tag-spelled names refuse the package. The bounded-read route is unchanged. + +- `receipt_correlations_for_current_basis` compiles only where the trusted + runtime host that calls it does, removing a dead-code warning from + `trusted_runtime` builds without `native_rule_bootstrap`. + +- Make current effect publication preparation available as a data-only check + without trusted-host features, sharing the existing runtime checker while + retaining private host helpers and gated authority constructors. + +- Admit the current authored-effect envelope through the provider's owning + operation-package schema, sharing current package fields with anchored + operations and preserving kind-specific closure/projection requirements. + Program verification and runtime authority remain separate checks. + +- Added a pure current-format effect proposal serializer that preserves compiler + bundle bytes and derives embedded artifact identities. Synthetic controls + round-trip through the current runtime decoder; public producer integration, + independent verification and effect execution remain unfinished. + +- Required explicit bounded intrinsic-catalog bytes in authored-effect lowering + configurations and reject catalogs that do not reproduce the selected + target profile's owner-framed reference. This validates resource transport; + authored-effect body lowering and execution remain unfinished. + +- Provider configuration validation recognizes an explicit bounded declaration + for the current authored-effect program kind. Graph schema and footprint-law bytes, identity + recipe, resource limits and I/O budget are required. This declaration does not + implement the public effect lowerer/verifier or grant runtime authority. + +- Completed authored evaluations now retain their runtime owner's private + authority through frame consumption. Patch derivation refuses foreign-owner + completion even when program and parent coordinates match. This closes a + substitution gap because equal reachable-state roots can conceal different + detached store records. The authority is process-local and adds no wire format. + +- Trusted runtime owners can privately derive counterfactual patches from + completed authored evaluation at an exact unchanged full parent. Validation + checks all five mutation forms, schema and identity rules, typed write laws, + comparisons and endpoint presence while retaining evaluation and validation + costs. Derivation does not apply the patch or enable scheduler, scoped-history, + receipt/WAL or fresh-engine effect recovery. + +- Authored mutation emission now follows the fixed effect contract: one step, + 64 write bytes plus atom payload for node/atom effects, or 128 write bytes for + edges, and at most 1,024 emitted effects. Step/write/row admission is atomic, + failed operations expose no partial buffer, and retained usage includes write + charges even for Ensure intents that may later resolve without writes. This + replaces the buffering prototype's incorrect deferral of emission charges. + +- Completed authored evaluation now has one runtime-owned origin record binding + its exact program, canonical input basis, actual result, mutation intents and + recorded reads to the same parent. Whole authored evaluation requires an + unused frame and stops further evaluation on success. Consuming completion + refuses substituted input bindings, failed frames and read-export failures; + result storage is borrowed during evaluation and moved into completion. + Completion grants no installation or scheduler authority; the runtime owner + independently validates its origin when deriving a private patch. + +- Prepared authored programs can now emit all five typed mutation intents into + private frames under the explicit graph-effects profile. Fixed input layouts, + empty-record results and row reservations apply. Failed operations discard + partial emission. Emission does not modify the frontier or derive a patch; + the owner crossing separately resolves and validates intents. Physical + allocation guarantees and durable scheduler execution remain unfinished. + +- Runtime-bound private frames now evaluate prepared authored read/let/require + programs through the shared expression evaluator and typed recording adapter. + Exact footprint-law binding, graph-schema checks and cumulative component + budgets apply; authored failures retain read dependencies and terminate the + frame. Installed Action execution, the physical arena and decision recovery + remain unfinished. + +- Effect compiler bundles now require the pinned target profile and intrinsic + catalog. Preparation checks catalog registration and concrete signature/fault + agreement, then enforces the existing node/atom recording ABI independently + of those declarations. Fresh pins cannot redefine 32-byte addresses or fault + authority. Further intrinsic families, general signature unification, complete + profile obligations and installed effect execution remain unfinished. + +- Shared expression/helper inference now preserves nominal contract identity. + Effect expressions cannot interchange equal representations across contracts, + implicitly unwrap opaque values, or construct them from untyped literals. + Nominal declarations must bind their own Core identity. Wire validation keeps + the existing representation and value-metering rules; complete pure-main + static typing, explicit construction operations and installed effect execution remain + unfinished. + +- Effect compiler preparation now authenticates imported Core type declarations + in the selected lawpack namespace against pinned export definitions, including + bounds, record fields, nominal contract names and unused declarations. Fresh + pins cannot hide imported type redefinitions or unknown members. Named unsigned + aliases now use the actual compiler's canonical width-kind encoding; the + synthetic `Int`/`width` form and its decoder were replaced. + +- Effect compiler preparation now shares the pure expression and helper checker + for data values, predicates, effect inputs and results. Matching Core/Target + bytes cannot hide wrong representations, unpublished calls or invalid helper + arguments. Declared failure-arm constructors must preserve their exported + failure authority class. Complete type/source authentication, nominal and + intrinsic semantics, and installed effect Action execution remain unfinished. + +- Effect compiler preparation now binds the declared lawpack's exports and + adapter bytes and checks each lowered intrinsic and failure arm against that + adapter. Fresh artifact/package pins cannot hide a swapped failure tag or a + substituted intrinsic name. The sole compiler bundle now includes lowering + artifacts; source-to-Core proof, full intrinsic signatures and execution + remain unfinished. The inconsistent retained raw capture refuses; positive + correspondence tests use an explicitly repinned synthetic chain. + +- Effect compiler preparation now checks ordered Core/Target IR node + correspondence and exact lexical local identities. Fresh artifact/package + pins cannot hide altered inputs, effects, predicates, failure reasons, + definitions, or instruction order. Source and lowered failure-arm values + must correspond without duplication, and obstruction locals stay scoped to + their arms. One relation validator replaces the former ID-only order check; + full typing, complete intrinsic signatures and execution remain open. + +- Authored-effect payloads now bind a single canonical compiler-artifact bundle + containing Core, ordered Target IR and exports. Fresh package pins cannot + admit opaque bytes, false artifact identities, a different selected operation + or output type, inconsistent Core/Target metadata, or invalid instruction + ordering. This replaces the opaque-byte placeholder; full instruction + semantics and authored-effect Action execution remain unfinished. + +- Invocations now use one `EchoOperationInvocation`, codec, owner policy and + admitted token for fixed and authored-effect program kinds. The current + schema has an explicit kind and no invented anchor for effects; historical + invocation schemas and the duplicate effect decoder were removed. Owner + admission checks installed meaning, independent grants, all budget axes, + canonical input binding and the actual exact parent. Authored-effect Action + execution remains unfinished and refuses explicitly. + +- Installation now uses one `InstalledEchoOperation`, runtime-owner API, engine + catalog, retained record codec and recovery collection for all program kinds. + The duplicate effect installation module and schema-version dispatch were + removed. Lawpack evidence is optional for installed programs and receipts; + current receipt hashes bind its presence explicitly. Uncertain WAL errors + reconcile the exact installation evidence before registry publication. + +- Package admission now uses one runtime-owner method, opaque + `AdmittedExecutableOperationPackage`, validator and evidence identity recipe + for all supported program kinds. Version-specific admission methods and the + duplicate effect token/module were removed. Installation and retained recovery + consume the common checked evidence; this does not grant invocation rights. + +- Operation publications now use one `ExecutableOperationPackage`, one + `EchoOperationProgram` sum and one semantic closure. Fixed and authored-effect + program kinds share the current package/program schemas and identity domains; + the former V2 package type and decoder were removed. Program-kind validation + requires real lawpack bindings for fixed programs and forbids them for authored + effects. Invocation consolidation is complete; authored-effect Action execution + remains unfinished. + +- Recovery certificates now use one current operation-index recipe for empty, + installation-only, receipt-bearing and Action-bearing histories. It binds + count-prefixed retained sections, canonical installation ordering, and causal + receipt/outcome ordering. Historical index recipes and conditional switches + were removed; this does not consolidate the remaining package formats. + +- Operation package admission now uses one `EchoOperationAdmissionPolicy`, + one structured refusal contract, and shared authority/operation/budget checks. + Version-specific policy definitions were removed; runtime entry points, + retained recovery, compiler tooling, tests, and allocation witnesses use the + current API directly. Package and installation format consolidation remains + separate work. - A Core without a function table that contains `core.bytes.slice` or `core.integer.subtract` is now judged in full by the lowerer and verifier, including its declared budget, as addition already was. Previously such a @@ -350,6 +527,58 @@ ### Added +- Runtime-owned installation retains exact policy and package evidence in the + current WAL catalog and restores all program kinds through one fresh-host + recovery path. Conflicts and failed WAL appends publish no registry entry. + Installation recovery does not authorize or execute effects. + +- Runtime-owner package admission checks independently pinned publication, + operation, authority profile and budget policy, retaining checked + admission evidence under one shared recipe. Restricted application handles cannot admit packages. + This does not install or execute an effect program or grant invocation rights. + +- V2 invocation preparation binds canonical application input to its recorded + basis, selected package and operation, and checks declared input and budget + ceilings. It retains the V1 parent format without granting authority or + establishing frontier freshness. + +- Current effect publication preparation binds exact package/program bytes, validates + profile and Target IR equalities, and decodes the program's schema and exact + footprint-law contract together. This does not admit, install or execute the + effect program; all program kinds use the shared current publication codec. + +- Strict executable graph-schema preparation validates content/mutable node + addressing, node/atom compatibility and edge endpoint-type relations with + exact canonical shapes and ordered sets. It grants no graph access or Action + admission authority. + +- Typed private frames can export bounded canonical read-set images while + preserving their runtime parent and evaluation accounting. The aligned row + format has explicit wire ordering, typed absence, value digests and packed + range pairs. An allocation-free borrowed validator refuses malformed framing, + padding, duplicates and ranges. This does not add receipt/WAL formats, + authenticated effect admission or a physical interpreter arena. + +- Runtime-bound private frames can bind strict typed read laws. Actual node and + atom types authorize present values, named types authorize absence, and + descended reads witness authorized alpha-portal chains before target access. + Typed ranges retain whole-bucket limits and untyped entry points cannot bypass + a bound law. This remains host-private evaluation without a new admitted + effect profile, persisted read image or mutation authority. + +- Runtime-bound private frames interpret existing compiled node-atom reads with recorded presence/absence dependencies, shared metering and host-selected apertures. This does not admit a new authored-effect profile. + +- Bind private effect evaluation frames to the runtime-selected writer and exact commit parent, refusing substituted histories even when reading coordinates match. + +- Trusted-host range-read sessions record real source-node and range values at + an immutable basis, including absence and empty ranges. They reject forbidden + types before access and charge whole buckets before inspection. This is read + infrastructure for the unfinished authored-effect interpreter, without new + installation or mutation authority. Read storage reservations now have a + fixed ceiling, atomic charging with I/O and explicit host-allocation faults. + A trusted-host evaluation frame now shares cumulative work and storage with + the real pure interpreter, preserves each component's local ceiling and stops + after failure. Existing standalone APIs retain their accounting behavior. - Runtime operands for `core.bytes.slice` and `core.integer.subtract` under Edict's proven-range rules. The lowerer, the verifier and the native helper checker each require a dominating fact (an input constraint or an earlier diff --git a/README.md b/README.md index ce885a9a7..85d820fb1 100644 --- a/README.md +++ b/README.md @@ -172,10 +172,16 @@ ingress boundary, support provider-native reads, or turn package metadata into runtime authority. Echo separately retains the Wesley compatibility path for host-constructed `InstalledContractPackage` values. +Current operation invocations use one `EchoOperationInvocation` codec and owner +admission API across program kinds. Authored-effect invocations bind canonical +input to an actual exact parent and an independent owner grant, without a fixed +node anchor. Their compiled instruction execution and Action integration remain +unfinished and refuse explicitly. See the [current invocation contract](docs/topics/GeneratedRules.md#current-invocation-input-basis-and-owner-grant-admission). + Separately, Echo can now admit and install an exact canonical -`ExecutableOperationPackageV1`, admit an exact-basis invocation under explicit +`ExecutableOperationPackage`, admit an exact-basis invocation under explicit authority and delegated budget, evaluate its data-only -`EchoOperationProgramV1` privately during scheduler-owned Tick construction, +`EchoOperationProgram` privately during scheduler-owned Tick construction, and either commit its member consequence or return typed noncommit evidence. Two independent, same-basis executable Actions can enter one atomic Tick. The initial generic programs perform anchored typed-node alpha-attachment @@ -256,11 +262,11 @@ Edict source The current executable-operation runtime slice is: ```text -external compiler-produced ExecutableOperationPackageV1 bytes +external compiler-produced ExecutableOperationPackage bytes -> structurally separate accepted verification report -> exact manifest, target-adapter, and target-configuration closure -> Echo-owned package and invocation admission --> installed data-only EchoOperationProgramV1 +-> installed data-only EchoOperationProgram -> exact compiler-owned application-result projection -> exact canonical application input retained in the invocation -> canonical Action submission retained before acknowledgement @@ -591,10 +597,13 @@ for coincidental text. ### How Does Schema Evolution Work? 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. +package identity are explicit in admitted compiler output. Main crates support +one current version of each data structure and format. A replacement updates +the producers and consumers together. Old versions belong only in separate +upgrade or migration tools, which can convert and validate persisted data +before the current runtime consumes it. There are no existing saved histories +to preserve and no implemented migration tool is implied. See the +[version policy](docs/topics/GeneratedRules.md#one-current-version-and-explicit-migration). ### Where Should Contributors Start? diff --git a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs index 32550453d..daba1eeab 100644 --- a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs +++ b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs @@ -25,11 +25,11 @@ use sha2::{Digest, Sha256}; use warp_core::{ echo_operation_anchored_node_application_basis_v1, echo_operation_atom_value_digest_v1, echo_operation_package_id_v1, make_head_id, make_node_id, make_type_id, make_warp_id, - AtomPayload, AttachmentValue, EchoOperationAdmissionPolicyV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, - EchoOperationPreparationV1, EngineBuilder, GraphStore, InboxPolicy, NodeId, NodeKey, - NodeRecord, PlaybackMode, SchedulerKind, TrustedRuntimeHost, TypeId, WorldlineId, - WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, + AtomPayload, AttachmentValue, EchoOperationAdmissionPolicy, EchoOperationBudgetV1, + EchoOperationInvocation, EchoOperationInvocationAdmissionPolicy, EchoOperationPreparationV1, + EngineBuilder, GraphStore, InboxPolicy, NodeId, NodeKey, NodeRecord, PlaybackMode, + SchedulerKind, TrustedRuntimeHost, TypeId, WorldlineId, WorldlineRuntime, WorldlineState, + WriterHead, WriterHeadKey, }; /// Counts bytes requested while `ACTIVE` is set. @@ -182,9 +182,9 @@ fn preparing_an_action_does_not_copy_its_installed_package() { let bytes = package(); let package_len = bytes.len(); let admitted = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&bytes), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), @@ -192,13 +192,13 @@ fn preparing_an_action_does_not_copy_its_installed_package() { bytes, ) .expect("package admitted"); - host.install_admitted_echo_operation_package_v1(admitted) + host.install_admitted_echo_operation_package(admitted) .expect("install"); // Copying the installed operation allocates nothing. let installed = host .engine() - .installed_echo_operation_package_v1(echo_operation_package_id_v1(&package())) + .installed_echo_operation_package(echo_operation_package_id_v1(&package())) .expect("installed") .clone(); let (copy, copy_bytes) = measure(|| installed.clone()); @@ -226,7 +226,7 @@ fn preparing_an_action_does_not_copy_its_installed_package() { ])) .expect("input"); let invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set_with_application_input( + EchoOperationInvocation::anchored_node_attachment_compare_and_set_with_application_input( echo_operation_package_id_v1(&package()), OPERATION, basis, @@ -240,8 +240,8 @@ fn preparing_an_action_does_not_copy_its_installed_package() { .to_canonical_bytes() .expect("invocation"); let admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( profile("cas.echo.authority.local-demo/v1"), GRANT, budget(), diff --git a/crates/echo-edict-provider-lowerer/Cargo.toml b/crates/echo-edict-provider-lowerer/Cargo.toml index 64e6e667f..b3863ecf6 100644 --- a/crates/echo-edict-provider-lowerer/Cargo.toml +++ b/crates/echo-edict-provider-lowerer/Cargo.toml @@ -25,7 +25,8 @@ wit-bindgen = { version = "=0.58.0", default-features = false, features = ["macr [dev-dependencies] hex = "0.4" sha2 = "0.10" -warp-core = { workspace = true } +# Echo-owned native decoder oracle only; not a component/runtime dependency. +warp-core = { workspace = true, features = ["trusted_runtime", "native_rule_bootstrap"] } [lints] workspace = true diff --git a/crates/echo-edict-provider-lowerer/README.md b/crates/echo-edict-provider-lowerer/README.md index f4bc6d020..b358031df 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -202,3 +202,25 @@ Edict Git pins and uses Rust 1.96 for its Echo runtime dependency. These bytes implement a provider translation. They do not install a package, admit runtime authority, execute an operation, or attest an Echo consequence. Those remain explicit Echo runtime crossings. + +## Current effect proposal serialization + +The native Rust API `effect_publication::encode_effect_publication` serializes +untrusted proposals using the current `echo.operation-package/v1` and +`echo.operation-program/v1` effect kind. It preserves the compiler bundle's +exact bytes, derives embedded Core/Target-IR/exports identities, carries explicit +limits and graph/footprint data, and requires a declared obstruction coordinate. +Its requested `authority_profile_identity` is derived only from the +configuration's required `authorityProfile` label, by the same label digest +fixed operations use; no caller supplies it. +It uses the current `steps`, `read_bytes`, and `write_bytes` budget fields and +retains the actual lawpack inside the compiled bundle, without a duplicate outer +lawpack identity. + +The witness uses synthetic crate-local inputs and the current runtime decoder +as a separate native test oracle. Kernel dependencies and their trusted-host +features are dev dependencies only. The factory does not verify compiler +correspondence, graph-law semantics or authority. It is not yet called by the +public lowerer, and it does not change the frozen WIT interface or enable +installed effect execution. Producer integration and independent verification +remain unfinished. diff --git a/crates/echo-edict-provider-lowerer/src/effect_publication.rs b/crates/echo-edict-provider-lowerer/src/effect_publication.rs new file mode 100644 index 000000000..7bfaa895e --- /dev/null +++ b/crates/echo-edict-provider-lowerer/src/effect_publication.rs @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure serialization of current effect proposals, without verification or authority. + +/// Exact artifacts and explicit policy metadata for an untrusted publication. +#[derive(Clone, Copy)] +pub struct EffectPublicationInput<'a> { + /// Current canonical effect bundle containing retained compiler artifacts. + pub compiler_program: &'a [u8], + /// Coordinate of the exports artifact used as application-schema metadata. + pub application_schema_coordinate: &'a str, + /// Exact canonical source or authenticated source-closure artifact. + pub source_artifact: &'a [u8], + /// Either `edict.source/v1` or `edict.source-closure/v1`. + pub source_domain: &'a str, + /// Current canonical authored-effect configuration. + pub configuration: &'a [u8], +} + +/// Refusal to serialize malformed publication material. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EffectPublicationError { + /// Artifact shape, canonical bytes, or embedded identity material is invalid. + InvalidArtifact, + /// Explicit configuration or its resource limits are invalid. + InvalidConfiguration, + /// A required coordinate is missing or unbounded. + InvalidCoordinate, +} + +impl std::fmt::Display for EffectPublicationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::InvalidArtifact => "invalid effect publication artifact envelope", + Self::InvalidConfiguration => "invalid effect publication configuration or limits", + Self::InvalidCoordinate => "missing or unbounded effect publication coordinate", + }) + } +} +impl std::error::Error for EffectPublicationError {} + +/// Serializes the single current operation-package and effect-program formats. +/// +/// This constructs an untrusted proposal. Callers must independently verify +/// compiler correspondence, policy and authority before any runtime admission. +/// # Errors +/// Refuses malformed artifact/configuration envelopes, unsupported source +/// domains, undeclared coordinates and invalid limit fields. Graph-law semantics, +/// compiler correspondence and authority remain independent checks. +#[allow( + clippy::too_many_lines, + reason = "Keep the current wire projection and its identity recipes visible together." +)] +pub fn encode_effect_publication( + input: EffectPublicationInput<'_>, +) -> Result, EffectPublicationError> { + use EffectPublicationError::{InvalidArtifact as A, InvalidConfiguration as C}; + let bundle = decode(input.compiler_program).map_err(|_| A)?; + exact( + &bundle, + &[ + "schema", + "intent", + "core_artifact", + "target_ir_artifact", + "lawpack_exports_artifact", + "lowering_artifacts", + ], + A, + )?; + equals( + &bundle, + "schema", + "echo.compiler-produced-effect-program/v1", + A, + )?; + let intent_name = coordinate(field(&bundle, "intent", A)?)?; + let embedded = |key| { + let bytes = bytes(field(&bundle, key, A)?, A)?; + decode(bytes).map_err(|_| A) + }; + let core = embedded("core_artifact")?; + let target = embedded("target_ir_artifact")?; + let exports = embedded("lawpack_exports_artifact")?; + equals(&core, "apiVersion", "edict.core/v1", A)?; + equals(&target, "domain", "echo.span-ir/v2", A)?; + equals(&target, "kind", "orderedTargetIrArtifact", A)?; + let module = coordinate(field(&core, "coordinate", A)?)?; + equals(&target, "sourceCoreCoordinate", module, A)?; + let intent = field(field(&core, "intents", A)?, intent_name, A)?; + let output = coordinate(field(intent, "output", A)?)?; + let operation = format!("{module}.{intent_name}"); + coordinate(&text(&operation))?; + coordinate(&text(input.application_schema_coordinate))?; + let core_id = identity("edict.core.module/v1", &core)?; + let target_id = identity("edict.target-ir.artifact/v1", &target)?; + let exports_id = identity("edict.lawpack-exports/v1", &exports)?; + let core_reference = field(field(&target, "semanticClosure", A)?, "sourceCore", A)?; + exact(core_reference, &["id", "digest"], A)?; + equals(core_reference, "id", module, A)?; + if field(core_reference, "digest", A)? != &V::Array(vec![text("sha256"), core_id.clone()]) { + return Err(A); + } + let source = decode(input.source_artifact).map_err(|_| A)?; + match input.source_domain { + "edict.source/v1" => { + std::str::from_utf8(bytes(&source, A)?).map_err(|_| A)?; + } + "edict.source-closure/v1" => { + echo_edict_canonical::source_closure::source_closure_root_v1(&source, module) + .map_err(|_| A)?; + } + _ => return Err(A), + } + let source_id = identity(input.source_domain, &source)?; + let config = decode(input.configuration).map_err(|_| C)?; + exact( + &config, + &[ + "apiVersion", + "programKind", + "identityLaw", + "intrinsicCatalog", + "graphSchema", + "footprintLaw", + "maxInputBytes", + "maxResultBytes", + "maxAllocatedBytes", + "budgetCeiling", + "authorityProfile", + ], + C, + )?; + equals( + &config, + "apiVersion", + "echo.operation-lowering-configuration/v1", + C, + )?; + equals(&config, "programKind", "edict-effect-program/v1", C)?; + if number(field(&config, "identityLaw", C)?)? != 1 { + return Err(C); + } + // The requested authority class comes only from the configuration, through + // the same label digest fixed operations use. It is a request: the runtime + // owner's admission policy decides what the package may do. + let V::Text(authority_label) = field(&config, "authorityProfile", C)? else { + return Err(C); + }; + if authority_label.is_empty() { + return Err(C); + } + let authority = profile(authority_label); + let graph = policy_bytes(&config, "graphSchema")?; + let law = policy_bytes(&config, "footprintLaw")?; + let catalog = policy_bytes(&config, "intrinsicCatalog")?; + let catalog_value = decode(catalog).map_err(|_| C)?; + exact(&catalog_value, &["apiVersion", "intrinsics"], C)?; + equals( + &catalog_value, + "apiVersion", + "edict.target-profile.intrinsics/v1", + C, + )?; + if !matches!(field(&catalog_value, "intrinsics", C)?, V::Map(_)) { + return Err(C); + } + // This only preserves the chosen bytes. Their profile/signature relation is + // independently verified before admission, not asserted by this serializer. + if field(&bundle, "lowering_artifacts", A)? != &V::Null + && bytes( + field( + field(&bundle, "lowering_artifacts", A)?, + "intrinsics_artifact", + A, + )?, + A, + )? != catalog + { + return Err(C); + } + let max_input = limit(&config, "maxInputBytes", 65536)?; + let max_result = limit(&config, "maxResultBytes", 65536)?; + let max_allocated = limit(&config, "maxAllocatedBytes", 16 * 1024 * 1024)?; + let budget = field(&config, "budgetCeiling", C)?; + exact(budget, &["steps", "readBytes", "writeBytes"], C)?; + let steps = number(field(budget, "steps", C)?)?; + if steps == 0 { + return Err(C); + } + let reads = number(field(budget, "readBytes", C)?)?; + let writes = number(field(budget, "writeBytes", C)?)?; + let mut footprint = blake3::Hasher::new(); + footprint.update(b"echo:operation-footprint-law:v1\0"); + footprint.update(law); + let footprint = V::Bytes(footprint.finalize().as_bytes().to_vec()); + let program = map([ + ("schema", text("echo.operation-program/v1")), + ("kind", text("edict-effect-program/v1")), + ( + "interpreter_profile_identity", + profile("echo.operation-interpreter.edict-effect/v1"), + ), + ( + "intrinsic_profile_identity", + profile("echo.operation-intrinsics.edict-effect/v1"), + ), + ( + "compiled_program", + V::Bytes(input.compiler_program.to_vec()), + ), + ("target_ir_identity", target_id.clone()), + ("identity_law", V::Integer(1)), + ("graph_schema", V::Bytes(graph.to_vec())), + ("footprint_law", V::Bytes(law.to_vec())), + ("max_input_bytes", V::Integer(i128::from(max_input))), + ("max_result_bytes", V::Integer(i128::from(max_result))), + ("max_allocated_bytes", V::Integer(i128::from(max_allocated))), + ("output_type", text(output)), + ]); + let program = encode(&program).map_err(|_| A)?; + let mut package = vec![ + ("schema", text("echo.operation-package/v1")), + ("operation_coordinate", text(&operation)), + ("authority_profile_identity", authority), + ( + "semantic_closure", + map([ + ("edict_source_identity", source_id), + ("canonical_meaning_identity", core_id.clone()), + ("core_identity", core_id), + ("target_ir_identity", target_id), + ( + "application_schema_coordinate", + text(input.application_schema_coordinate), + ), + ("application_schema_identity", exports_id), + ]), + ), + ("program", V::Bytes(program)), + ( + "budget_ceiling", + map([ + ("steps", V::Integer(i128::from(steps))), + ("read_bytes", V::Integer(i128::from(reads))), + ("write_bytes", V::Integer(i128::from(writes))), + ]), + ), + ("footprint_contract_identity", footprint), + ]; + for (key, label) in [ + ( + "target_profile_identity", + "echo.operation-target.edict-effect/v1", + ), + ( + "interpreter_profile_identity", + "echo.operation-interpreter.edict-effect/v1", + ), + ( + "intrinsic_profile_identity", + "echo.operation-intrinsics.edict-effect/v1", + ), + ( + "application_basis_schema_identity", + "echo.operation.basis.edict-effect/v1", + ), + ( + "input_schema_identity", + "echo.operation.input.edict-effect/v1", + ), + ( + "result_schema_identity", + "echo.operation.result.edict-effect/v1", + ), + ( + "obstruction_schema_identity", + "echo.operation.obstruction.edict-effect/v1", + ), + ( + "result_interpretation_identity", + "echo.operation.result-interpretation.edict-effect/v1", + ), + ( + "obstruction_interpretation_identity", + "echo.operation.obstruction-interpretation.edict-effect/v1", + ), + ( + "evaluation_basis_schema_identity", + "echo.operation.evaluation-basis/v1", + ), + ] { + package.push((key, profile(label))); + } + encode(&map(package)).map_err(|_| A) +} + +use echo_edict_canonical::{ + decode_canonical_cbor_v1 as decode, digest_canonical_value_bytes_v1 as digest, + encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V, +}; +use EffectPublicationError as E; +fn text(s: &str) -> V { + V::Text(s.to_owned()) +} +fn map(entries: impl IntoIterator) -> V { + V::Map(entries.into_iter().map(|(k, v)| (text(k), v)).collect()) +} +fn field<'a>(v: &'a V, key: &str, e: E) -> Result<&'a V, E> { + let V::Map(fields) = v else { return Err(e) }; + fields + .iter() + .find_map(|(k, v)| (k == &text(key)).then_some(v)) + .ok_or(e) +} +fn exact(v: &V, keys: &[&str], e: E) -> Result<(), E> { + let V::Map(fields) = v else { return Err(e) }; + if fields.len() != keys.len() + || fields + .iter() + .any(|(k, _)| !matches!(k,V::Text(key) if keys.contains(&key.as_str()))) + { + return Err(e); + } + Ok(()) +} +fn equals(v: &V, key: &str, expected: &str, e: E) -> Result<(), E> { + (field(v, key, e)? == &text(expected)) + .then_some(()) + .ok_or(e) +} +fn coordinate(v: &V) -> Result<&str, E> { + match v { + V::Text(s) if !s.is_empty() && s.len() <= 1024 => Ok(s), + _ => Err(E::InvalidCoordinate), + } +} +fn bytes(v: &V, e: E) -> Result<&[u8], E> { + match v { + V::Bytes(b) => Ok(b), + _ => Err(e), + } +} +fn number(v: &V) -> Result { + match v { + V::Integer(n) => u64::try_from(*n).map_err(|_| E::InvalidConfiguration), + _ => Err(E::InvalidConfiguration), + } +} +fn policy_bytes<'a>(v: &'a V, key: &str) -> Result<&'a [u8], E> { + let b = bytes( + field(v, key, E::InvalidConfiguration)?, + E::InvalidConfiguration, + )?; + if b.is_empty() || b.len() > 16 * 1024 * 1024 { + return Err(E::InvalidConfiguration); + } + decode(b).map_err(|_| E::InvalidConfiguration)?; + Ok(b) +} +fn limit(v: &V, key: &str, max: u64) -> Result { + let n = number(field(v, key, E::InvalidConfiguration)?)?; + if n > max { + return Err(E::InvalidConfiguration); + } + Ok(n) +} +fn identity(domain: &str, value: &V) -> Result { + digest(domain, value) + .map(|v| V::Bytes(v.to_vec())) + .map_err(|_| E::InvalidArtifact) +} +fn profile(label: &str) -> V { + V::Bytes(crate::executable_operation::profile_digest(label).to_vec()) +} diff --git a/crates/echo-edict-provider-lowerer/src/executable_operation.rs b/crates/echo-edict-provider-lowerer/src/executable_operation.rs index b081ff4ea..8bacc360a 100644 --- a/crates/echo-edict-provider-lowerer/src/executable_operation.rs +++ b/crates/echo-edict-provider-lowerer/src/executable_operation.rs @@ -3,6 +3,7 @@ //! Generic data-only lowering into Echo's bounded executable-operation profile. mod bounded_read; +mod effect_resources; mod source_functions; use std::collections::BTreeSet; @@ -211,6 +212,10 @@ pub(super) fn lower(request: &LoweringRequestV1) -> Result Result<[u8; 32], ProviderRefusalV1> .map_err(|_| invalid_artifact(&bound.reference.coordinate, "digest must be 32 bytes")) } -fn profile_digest(label: &str) -> [u8; 32] { +pub(crate) fn profile_digest(label: &str) -> [u8; 32] { let mut hasher = Hasher::new(); hasher.update(b"echo:operation-profile:v1\0"); hasher.update(&(label.len() as u64).to_le_bytes()); diff --git a/crates/echo-edict-provider-lowerer/src/executable_operation/effect_resources.rs b/crates/echo-edict-provider-lowerer/src/executable_operation/effect_resources.rs new file mode 100644 index 000000000..470b7c82c --- /dev/null +++ b/crates/echo-edict-provider-lowerer/src/executable_operation/effect_resources.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Explicit catalog transport and profile binding, without execution authority. + +use super::{ + as_map, canonical_text, invalid_artifact, map_field, require_exact_fields, text_field, + validate_bound, BoundArtifact, CanonicalValueV1 as Value, ProviderRefusalV1, CONFIGURATION_ABI, +}; +use echo_edict_canonical::{decode_canonical_cbor_v1, digest_canonical_value_bytes_v1}; + +const SUBJECT: &str = "effect-target-resources"; +const CATALOG_DOMAIN: &str = "echo.dpo.intrinsics/v1"; +const MAX_CATALOG_BYTES: usize = 16 * 1024 * 1024; + +pub(super) fn is_configuration(value: &Value) -> bool { + text_field(value, "apiVersion") == Some(CONFIGURATION_ABI) + && text_field(value, "programKind") == Some("edict-effect-program/v1") +} + +fn invalid() -> ProviderRefusalV1 { + invalid_artifact( + SUBJECT, + "the explicit canonical intrinsic catalog must match the selected target profile", + ) +} + +pub(super) fn validate( + configuration: &Value, + profile: &BoundArtifact, +) -> Result<(), ProviderRefusalV1> { + let Some(Value::Bytes(bytes)) = map_field(configuration, "intrinsicCatalog") else { + return Err(invalid()); + }; + if bytes.is_empty() || bytes.len() > MAX_CATALOG_BYTES { + return Err(invalid()); + } + let catalog = decode_canonical_cbor_v1(bytes).map_err(|_| invalid())?; + require_exact_fields(&catalog, &["apiVersion", "intrinsics"], SUBJECT) + .map_err(|_| invalid())?; + if text_field(&catalog, "apiVersion") != Some("edict.target-profile.intrinsics/v1") + || map_field(&catalog, "intrinsics").and_then(as_map).is_none() + { + return Err(invalid()); + } + let profile = validate_bound(profile, "edict.target-profile/v1")?; + let reference = map_field(&profile, "intrinsics").ok_or_else(invalid)?; + require_exact_fields(reference, &["id", "digest"], SUBJECT).map_err(|_| invalid())?; + let identity = + digest_canonical_value_bytes_v1(CATALOG_DOMAIN, &catalog).map_err(|_| invalid())?; + let expected = Value::Array(vec![ + canonical_text("sha256"), + Value::Bytes(identity.to_vec()), + ]); + if text_field(reference, "id") != Some(CATALOG_DOMAIN) + || map_field(reference, "digest") != Some(&expected) + { + return Err(invalid()); + } + Ok(()) +} diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index 20d92851f..3c2bee86d 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -14,6 +14,7 @@ use echo_edict_canonical::{ #[cfg(target_arch = "wasm32")] mod component; +pub mod effect_publication; mod executable_operation; const PROVIDER_ABI: ProtocolVersionV1 = ProtocolVersionV1 { @@ -823,7 +824,7 @@ pub mod echo_dpo { 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 = - "0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"; + "8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"; /// 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. @@ -860,7 +861,7 @@ pub mod echo_dpo { const MUTATION_RULE_NAME: &str = concat!( "cmd/contract/", - "0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111", + "8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10", "/3389142194/a.b@1.t" ); const PROVIDER_OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { diff --git a/crates/echo-edict-provider-lowerer/tests/bounded_read.rs b/crates/echo-edict-provider-lowerer/tests/bounded_read.rs index 29b2d7cd0..c3a535b6b 100644 --- a/crates/echo-edict-provider-lowerer/tests/bounded_read.rs +++ b/crates/echo-edict-provider-lowerer/tests/bounded_read.rs @@ -360,3 +360,78 @@ fn compiler_authored_pair_keeps_independent_read_producers_and_ordered_guards() "second read moved ahead of the first guard" ); } + +// These tests alter only the configuration of the retained compiler request. +// They witness refusal boundaries, not a successful public effect compilation. +fn effect_catalog_request(catalog: Option>) -> LoweringRequestV1 { + let mut request = fixture(); + let mut fields = vec![ + ( + "apiVersion", + Value::Text("echo.operation-lowering-configuration/v1".into()), + ), + ("programKind", Value::Text("edict-effect-program/v1".into())), + ("identityLaw", Value::Integer(1)), + ("graphSchema", Value::Bytes(vec![0xa0])), + ("footprintLaw", Value::Bytes(vec![0xa0])), + ("maxInputBytes", Value::Integer(65536)), + ("maxResultBytes", Value::Integer(65536)), + ("maxAllocatedBytes", Value::Integer(16777216)), + ( + "budgetCeiling", + Value::Map(vec![ + (Value::Text("steps".into()), Value::Integer(10000)), + (Value::Text("readBytes".into()), Value::Integer(4096)), + (Value::Text("writeBytes".into()), Value::Integer(4096)), + ]), + ), + ]; + if let Some(bytes) = catalog { + fields.push(("intrinsicCatalog", Value::Bytes(bytes))); + } + let value = Value::Map( + fields + .into_iter() + .map(|(k, v)| (Value::Text(k.into()), v)) + .collect(), + ); + request.semantic_inputs[4].artifact = bound( + "echo.operation-lowering-configuration/v1", + "echo.operation-lowering-configuration/v1", + encode(&value).expect("effect configuration"), + ); + request +} +#[test] +fn effect_catalog_must_match_the_selected_profile_before_body_lowering() { + use echo_edict_provider_lowerer::ProviderRefusalKind as K; + let fixture_config = decode( + &hex::decode(include_str!("fixtures/current-effect-wire/configuration.hex").trim()) + .expect("crate-local catalog fixture"), + ) + .expect("canonical configuration"); + let Value::Bytes(catalog) = field(&fixture_config, "intrinsicCatalog") else { + panic!("catalog bytes") + }; + let catalog = catalog.clone(); + let control = lower(effect_catalog_request(Some(catalog.clone()))) + .expect_err("effect body lowering remains unsupported"); + assert_eq!(control.kind, K::UnsupportedSemantics); + assert_ne!(control.subject.as_deref(), Some("effect-target-resources")); + // Valid canonical catalog structure with a different profile digest. + let mut changed = decode(&catalog).expect("catalog"); + mutate(&mut changed, "intrinsics", |v| *v = Value::Map(vec![])); + for bytes in [ + None, + Some(vec![]), + Some(vec![0xff]), + Some(vec![0; 16 * 1024 * 1024 + 1]), + Some(vec![0xa0]), + Some(encode(&changed).expect("catalog")), + Some([catalog.as_slice(), &[0xf6]].concat()), + ] { + let refusal = lower(effect_catalog_request(bytes)).expect_err("catalog must refuse"); + assert_eq!(refusal.kind, K::InvalidSemanticArtifact); + assert_eq!(refusal.subject.as_deref(), Some("effect-target-resources")); + } +} diff --git a/crates/echo-edict-provider-lowerer/tests/current_effect_publication.rs b/crates/echo-edict-provider-lowerer/tests/current_effect_publication.rs new file mode 100644 index 000000000..d3c360cf1 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/current_effect_publication.rs @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Synthetic serialization controls, not public compiler or execution evidence. +use echo_edict_canonical::{decode_canonical_cbor_v1 as decode, CanonicalValueV1 as V}; +use echo_edict_provider_lowerer::effect_publication::{ + encode_effect_publication, EffectPublicationInput, +}; + +fn text(value: &str) -> V { + V::Text(value.into()) +} +fn map(fields: [(&str, V); N]) -> V { + V::Map(fields.into_iter().map(|(k, v)| (text(k), v)).collect()) +} +fn encoded(value: &V) -> Vec { + echo_edict_canonical::encode_canonical_cbor_v1(value).expect("canonical fixture") +} +fn id(value: u8) -> V { + V::Bytes(vec![value; 32]) +} +fn get<'a>(value: &'a V, key: &str) -> &'a V { + let V::Map(fields) = value else { panic!("map") }; + &fields + .iter() + .find(|(k, _)| k == &text(key)) + .expect("field") + .1 +} +fn set(value: &mut V, key: &str, replacement: V) { + let V::Map(fields) = value else { panic!("map") }; + fields + .iter_mut() + .find(|(k, _)| k == &text(key)) + .expect("field") + .1 = replacement; +} +fn decode_runtime( + value: &V, +) -> Result { + let bytes = encoded(value); + let expected = warp_core::echo_operation_package_id_v1(&bytes).as_hash(); + warp_core::ExecutableOperationPackage::prepare(&bytes, expected) +} +fn material() -> (Vec, Vec, Vec) { + ( + hex::decode(include_str!("fixtures/current-effect-wire/bundle.hex").trim()) + .expect("synthetic bundle"), + hex::decode(include_str!("fixtures/current-effect-wire/source.hex").trim()) + .expect("synthetic source"), + hex::decode(include_str!("fixtures/current-effect-wire/configuration.hex").trim()) + .expect("synthetic configuration"), + ) +} + +fn emit( + bundle: &[u8], + source: &[u8], + configuration: &[u8], +) -> Result, echo_edict_provider_lowerer::effect_publication::EffectPublicationError> { + encode_effect_publication(EffectPublicationInput { + compiler_program: bundle, + application_schema_coordinate: "app.Exports", + source_artifact: source, + source_domain: "edict.source/v1", + configuration, + }) +} + +#[test] +fn proposal_round_trips_the_current_runtime_decoder_without_rewriting_compiler_bytes() { + let (bundle, source, config) = material(); + let bytes = emit(&bundle, &source, &config).expect("current effect proposal serialization"); + assert_eq!(bytes, emit(&bundle, &source, &config).expect("repeat")); + let package = decode_runtime(&decode(&bytes).expect("current package")) + .expect("independent current runtime decoder accepts the wire control"); + assert_eq!( + package.to_canonical_bytes().expect("exact round trip"), + bytes + ); + let value = decode(&bytes).expect("package"); + let V::Bytes(program) = get(&value, "program") else { + panic!("program bytes") + }; + let program = decode(program).expect("program"); + assert_eq!(get(&program, "compiled_program"), &V::Bytes(bundle)); + let V::Map(closure) = get(&value, "semantic_closure") else { + panic!("closure") + }; + assert!(!closure.iter().any(|(k, _)| k == &text("lawpack_identity"))); + // Every obstruction the program can return is declared by its embedded + // exports and checked arm by arm at admission; no header picks one. + let V::Map(fields) = &value else { + panic!("package map") + }; + assert!(!fields + .iter() + .any(|(k, _)| k == &text("obstruction_coordinate"))); + assert_eq!(package.obstruction_coordinate(), None); + assert_eq!( + get(&value, "authority_profile_identity"), + &V::Bytes(label_digest("app.effect-writer@1")) + ); +} + +#[test] +fn malformed_or_cross_bound_artifacts_refuse_before_serialization() { + use echo_edict_provider_lowerer::effect_publication::EffectPublicationError as E; + let (bundle, source, config) = material(); + for bytes in [vec![], vec![0xff], [bundle.as_slice(), &[0xf6]].concat()] { + assert_eq!(emit(&bytes, &source, &config), Err(E::InvalidArtifact)); + } + let mut changed = decode(&bundle).expect("bundle"); + let V::Bytes(raw) = get(&changed, "core_artifact") else { + panic!("Core") + }; + let mut core = decode(raw).expect("Core"); + set(&mut core, "coordinate", text("different.app")); + set(&mut changed, "core_artifact", V::Bytes(encoded(&core))); + assert_eq!( + emit(&encoded(&changed), &source, &config), + Err(E::InvalidArtifact) + ); + let mut changed = decode(&bundle).expect("bundle"); + let V::Bytes(raw) = get(&changed, "target_ir_artifact") else { + panic!("Target IR") + }; + let mut target = decode(raw).expect("Target IR"); + let mut closure = get(&target, "semanticClosure").clone(); + let mut reference = get(&closure, "sourceCore").clone(); + set( + &mut reference, + "digest", + V::Array(vec![text("sha256"), id(99)]), + ); + set(&mut closure, "sourceCore", reference); + set(&mut target, "semanticClosure", closure); + set( + &mut changed, + "target_ir_artifact", + V::Bytes(encoded(&target)), + ); + assert_eq!( + emit(&encoded(&changed), &source, &config), + Err(E::InvalidArtifact) + ); +} + +#[test] +fn resource_limits_and_current_envelopes_are_explicit_and_closed() { + use echo_edict_provider_lowerer::effect_publication::EffectPublicationError as E; + let (bundle, source, config) = material(); + for (key, value) in [ + ("identityLaw", V::Integer(2)), + ("maxInputBytes", V::Integer(65537)), + ("maxResultBytes", V::Integer(-1)), + ("maxAllocatedBytes", V::Integer(16777217)), + ("graphSchema", V::Bytes(vec![])), + ("footprintLaw", V::Bytes(vec![0xff])), + ("intrinsicCatalog", V::Bytes(encoded(&map([])))), + ( + "apiVersion", + text("echo.operation-lowering-configuration/v2"), + ), + ] { + let mut changed = decode(&config).expect("config"); + set(&mut changed, key, value); + assert_eq!( + emit(&bundle, &source, &encoded(&changed)), + Err(E::InvalidConfiguration) + ); + } + let mut changed = decode(&config).expect("config"); + set( + &mut changed, + "budgetCeiling", + map([ + ("steps", V::Integer(0)), + ("readBytes", V::Integer(0)), + ("writeBytes", V::Integer(0)), + ]), + ); + assert_eq!( + emit(&bundle, &source, &encoded(&changed)), + Err(E::InvalidConfiguration) + ); + let mut changed = decode(&config).expect("valid config for extra-field control"); + let V::Map(fields) = &mut changed else { + panic!("config") + }; + fields.push((text("legacy"), V::Bool(true))); + assert_eq!( + emit(&bundle, &source, &encoded(&changed)), + Err(E::InvalidConfiguration) + ); + let mut old = decode(&bundle).expect("bundle"); + set( + &mut old, + "schema", + text("echo.compiler-produced-effect-program/v2"), + ); + assert_eq!( + emit(&encoded(&old), &source, &config), + Err(E::InvalidArtifact) + ); +} + +#[test] +fn explicit_zero_storage_limits_and_maximum_integer_budgets_remain_declarations() { + let (bundle, source, config) = material(); + let mut config = decode(&config).expect("configuration"); + for key in ["maxInputBytes", "maxResultBytes", "maxAllocatedBytes"] { + set(&mut config, key, V::Integer(0)); + } + set( + &mut config, + "budgetCeiling", + map([ + ("steps", V::Integer(i128::from(u64::MAX))), + ("readBytes", V::Integer(i128::from(u64::MAX))), + ("writeBytes", V::Integer(i128::from(u64::MAX))), + ]), + ); + let bytes = emit(&bundle, &source, &encoded(&config)).expect("bounded declarations"); + decode_runtime(&decode(&bytes).expect("package")).expect("wire-level preparation only"); +} + +#[test] +fn an_unknown_source_domain_refuses() { + use echo_edict_provider_lowerer::effect_publication::EffectPublicationError as E; + let (bundle, source, config) = material(); + let input = EffectPublicationInput { + compiler_program: &bundle, + application_schema_coordinate: "app.Exports", + source_artifact: &source, + source_domain: "edict.source/v1", + configuration: &config, + }; + assert_eq!( + encode_effect_publication(EffectPublicationInput { + source_domain: "edict.source/v2", + ..input + }), + Err(E::InvalidArtifact) + ); +} + +/// The `echo:operation-profile:v1` label digest, written out independently of +/// the lowerer: BLAKE3 over the domain, the label length as a little-endian +/// u64, and the label bytes. +fn label_digest(label: &str) -> Vec { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"echo:operation-profile:v1\0"); + hasher.update(&(label.len() as u64).to_le_bytes()); + hasher.update(label.as_bytes()); + hasher.finalize().as_bytes().to_vec() +} + +#[test] +fn authority_profile_identity_is_derived_from_the_configuration() { + let (bundle, source, config) = material(); + let package = decode(&emit(&bundle, &source, &config).expect("proposal")).expect("package"); + let declared = get(&decode(&config).expect("config"), "authorityProfile").clone(); + let V::Text(label) = declared else { + panic!("the fixture declares a text authority profile") + }; + assert_eq!( + get(&package, "authority_profile_identity"), + &V::Bytes(label_digest(&label)) + ); + + let mut other = decode(&config).expect("config"); + set(&mut other, "authorityProfile", text("app.other-writer@1")); + let changed = + decode(&emit(&bundle, &source, &encoded(&other)).expect("proposal")).expect("package"); + assert_eq!( + get(&changed, "authority_profile_identity"), + &V::Bytes(label_digest("app.other-writer@1")) + ); +} + +#[test] +fn authority_profile_is_required_text() { + use echo_edict_provider_lowerer::effect_publication::EffectPublicationError as E; + let (bundle, source, config) = material(); + for replacement in [text(""), V::Integer(1), V::Bytes(vec![1; 32])] { + let mut changed = decode(&config).expect("config"); + set(&mut changed, "authorityProfile", replacement); + assert_eq!( + emit(&bundle, &source, &encoded(&changed)), + Err(E::InvalidConfiguration) + ); + } + let V::Map(mut fields) = decode(&config).expect("config") else { + panic!("map") + }; + fields.retain(|(key, _)| key != &text("authorityProfile")); + assert_eq!( + emit(&bundle, &source, &encoded(&V::Map(fields))), + Err(E::InvalidConfiguration) + ); +} + +/// The synthetic package retained by warp-core and the provider schema tests +/// is exactly what this serializer emits for the wire-control material, so a +/// change to the package shape cannot leave a stale control behind. +#[test] +fn the_retained_publication_controls_are_this_serializers_output() { + let (bundle, source, config) = material(); + let emitted = hex::encode(emit(&bundle, &source, &config).expect("current emission")); + for retained in [ + include_str!("../../warp-core/tests/fixtures/current-effect-publication/package.hex"), + include_str!("../../echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex"), + ] { + assert_eq!(retained.trim(), emitted, "retained control is stale"); + } +} diff --git a/crates/echo-edict-provider-lowerer/tests/executable_operation_package.rs b/crates/echo-edict-provider-lowerer/tests/executable_operation_package.rs index cbd19daad..ed9f06632 100644 --- a/crates/echo-edict-provider-lowerer/tests/executable_operation_package.rs +++ b/crates/echo-edict-provider-lowerer/tests/executable_operation_package.rs @@ -15,8 +15,8 @@ use echo_edict_provider_lowerer::{ }; use warp_core::{ echo_operation_create_if_absent_target_profile_identity_v1, - echo_operation_target_profile_identity_v1, EchoOperationBudgetV1, EchoOperationProgramV1, - EchoOperationSemanticClosureV1, ExecutableOperationPackageV1, TypeId, + echo_operation_target_profile_identity_v1, EchoOperationBudgetV1, EchoOperationProgram, + EchoOperationSemanticClosure, ExecutableOperationPackage, TypeId, }; const TARGET_PROFILE: &[u8] = include_bytes!("../resources/target-profile.echo-dpo.cbor"); @@ -1292,7 +1292,7 @@ fn replace_projection(request: &mut LoweringRequestV1, value: CanonicalValueV1) fn expected_package( request: &LoweringRequestV1, names: FixtureNames<'_>, -) -> ExecutableOperationPackageV1 { +) -> ExecutableOperationPackage { expected_package_for(request, names, false) } @@ -1300,7 +1300,7 @@ fn expected_package_for( request: &LoweringRequestV1, names: FixtureNames<'_>, compare_and_set: bool, -) -> ExecutableOperationPackageV1 { +) -> ExecutableOperationPackage { let input = |kind: &SemanticInputKind| { &request .semantic_inputs @@ -1319,10 +1319,10 @@ fn expected_package_for( let core_identity = hash(&request.core.reference.digest); let node_type = TypeId(profile_digest(names.node_type)); let attachment_type = TypeId(profile_digest(names.attachment_type)); - let package = ExecutableOperationPackageV1::new( + let package = ExecutableOperationPackage::new( format!("{}.{}", names.application, names.intent), names.obstruction, - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( hash(&source.reference.digest), core_identity, core_identity, @@ -1340,13 +1340,13 @@ fn expected_package_for( profile_digest(names.authority), EchoOperationBudgetV1::new(16, 64, 320), if compare_and_set { - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( node_type, attachment_type, 256, ) } else { - EchoOperationProgramV1::anchored_node_attachment_create_if_absent( + EchoOperationProgram::anchored_node_attachment_create_if_absent( node_type, attachment_type, 256, diff --git a/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/README.md b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/README.md new file mode 100644 index 000000000..701a9610a --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/README.md @@ -0,0 +1,23 @@ + + + +# Current effect wire controls + +These are synthetic serialization inputs, not public compiler outputs. They +contain a zero-effect integer-result Core/ordered-IR control, one declared +obstruction, graph/footprint metadata, explicit limits and a source byte string +that says `synthetic serialization control`. + +They were materialized from the runtime-owner fixture helpers in +`crates/warp-core/tests/common/edict_effect_publication.rs` at +`41037d48b66fea9e88af1de96a502d2b1654b77c`, with the explicit effect +configuration and obstruction added by the lowerer serialization witness. +Each `.hex` file contains the canonical bytes of one whole input artifact. +Copies live in this crate so its source archive does not depend on another +crate's test files. + +The witness checks deterministic current-format serialization and the current +runtime decoder's exact round trip. It does not establish compiler correctness, +policy or profile authority, independent verifier acceptance, installed Action +execution, settlement or recovery. The public lowerer does not yet call this +serializer. diff --git a/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/bundle.hex b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/bundle.hex new file mode 100644 index 000000000..ec195edce --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/bundle.hex @@ -0,0 +1 @@ +a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0 diff --git a/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/configuration.hex b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/configuration.hex new file mode 100644 index 000000000..1bcf9f1ef --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/configuration.hex @@ -0,0 +1 @@ +ab6a61706956657273696f6e78286563686f2e6f7065726174696f6e2d6c6f776572696e672d636f6e66696775726174696f6e2f76316b6772617068536368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026b6964656e746974794c6177016b70726f6772616d4b696e647765646963742d6566666563742d70726f6772616d2f76316c666f6f747072696e744c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806d6275646765744365696c696e67a36573746570730a697265616442797465731910006a777269746542797465731910006d6d6178496e70757442797465731a000100006e6d6178526573756c7442797465731a0001000070617574686f7269747950726f66696c65736170702e6566666563742d777269746572403170696e7472696e736963436174616c6f675901a2a26a61706956657273696f6e782265646963742e7461726765742d70726f66696c652e696e7472696e736963732f76316a696e7472696e73696373a1726563686f2e64706f40312e7265706c616365ab6a6566666563744b696e64677265706c6163656a72657475726e547970656d612e6240312e526563656970746a7772697465436c617373677265706c6163656c636f737454656d706c617465737461726765742e7265706c6163652e636f73746c6775617264537570706f7274f56d617267756d656e7454797065738168612e6240312e49646e6566666563744661696c75726573a16872656a6563746564a26b7061796c6f616454797065777461726765742e7265706c6163652e72656a65637465646e617574686f72697479436c6173736e646f6d61696e4d61707061626c656e696e7472696e736963436c617373666566666563746e74797065506172616d65746572738071666f6f747072696e7454656d706c61746578187461726765742e7265706c6163652e666f6f747072696e74781b63616e5061727469636970617465496e41746f6d69634775617264f5716d6178416c6c6f636174656442797465731a01000000 diff --git a/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/source.hex b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/source.hex new file mode 100644 index 000000000..2f2c50fc9 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/source.hex @@ -0,0 +1 @@ +581f73796e7468657469632073657269616c697a6174696f6e20636f6e74726f6c 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 index 7ddb797a2..6a4dc7623 100644 --- a/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs @@ -71,7 +71,7 @@ pub mod echo_dpo { 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 = - "0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"; + "8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"; /// 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. @@ -108,7 +108,7 @@ pub mod echo_dpo { const MUTATION_RULE_NAME: &str = concat!( "cmd/contract/", - "0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111", + "8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10", "/3389142194/a.b@1.t" ); const PROVIDER_OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { diff --git a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs index f9b263dd2..a758a763b 100644 --- a/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs +++ b/crates/echo-edict-provider-lowerer/tests/lowerer_contract.rs @@ -46,7 +46,7 @@ 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 = - "0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"; + "8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"; const EXPECTED_OPERATION_ID_LAW: &str = "echo.semantic-operation-id.fnv1-32/v1"; const EXPECTED_OPERATION_ID: u32 = 3_389_142_194; 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 f40a43058..2fd68b516 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/generated/evidence/provenance.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 277972db6..6cb1b2c25 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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 4b88f8633..d1d16cc3b 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","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:ae741baed96ec6121aa06e366319d88cc8be6966e01a85f57a652173ce59b8a7","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","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:87294198ac6f94d68346fa49cd52b1c26a10cd51d91a079abacab81721eb2cb6","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file 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 8b4544430..dd8a38542 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 @@ -1174,10 +1174,10 @@ edict-source-bytes = bstr echo-operation-package = echo-anchored-operation-package / echo-compiler-produced-pure-operation-package / - echo-compiler-produced-read-operation-package + echo-compiler-produced-read-operation-package / + echo-authored-effect-operation-package -echo-anchored-operation-package = { - "application_result_projection": echo-operation-application-result-projection, +echo-current-operation-package-fields = ( "application_basis_schema_identity": bstr .size 32, "authority_profile_identity": bstr .size 32, "budget_ceiling": echo-operation-budget, @@ -1186,7 +1186,6 @@ echo-anchored-operation-package = { "interpreter_profile_identity": bstr .size 32, "input_schema_identity": bstr .size 32, "intrinsic_profile_identity": bstr .size 32, - "obstruction_coordinate": echo-nonempty-tstr, "obstruction_schema_identity": bstr .size 32, "obstruction_interpretation_identity": bstr .size 32, "operation_coordinate": echo-nonempty-tstr, @@ -1194,8 +1193,19 @@ echo-anchored-operation-package = { "result_schema_identity": bstr .size 32, "result_interpretation_identity": bstr .size 32, schema: "echo.operation-package/v1", - "semantic_closure": echo-operation-semantic-closure, "target_profile_identity": bstr .size 32, +) + +echo-anchored-operation-package = { + echo-current-operation-package-fields, + "application_result_projection": echo-operation-application-result-projection, + "obstruction_coordinate": echo-nonempty-tstr, + "semantic_closure": echo-operation-semantic-closure, +} + +echo-authored-effect-operation-package = { + echo-current-operation-package-fields, + "semantic_closure": echo-authored-effect-semantic-closure, } echo-compiler-produced-pure-operation-package = { @@ -1264,22 +1274,31 @@ echo-operation-budget = { "write_bytes": uint, } -echo-operation-semantic-closure = { +echo-operation-semantic-closure-fields = ( "application_schema_coordinate": echo-nonempty-tstr, "application_schema_identity": bstr .size 32, "canonical_meaning_identity": bstr .size 32, "core_identity": bstr .size 32, "edict_source_identity": bstr .size 32, + "target_ir_identity": bstr .size 32, +) + +echo-operation-semantic-closure = { + echo-operation-semantic-closure-fields, "lawpack_coordinate": echo-nonempty-tstr, "lawpack_identity": bstr .size 32, - "target_ir_identity": bstr .size 32, +} + +echo-authored-effect-semantic-closure = { + echo-operation-semantic-closure-fields, } echo-operation-lowering-configuration = echo-attachment-create-if-absent-lowering-configuration / echo-attachment-compare-and-set-lowering-configuration / echo-compiler-produced-bounded-pure-lowering-configuration / - echo-compiler-produced-bounded-read-lowering-configuration + echo-compiler-produced-bounded-read-lowering-configuration / + echo-authored-effect-lowering-configuration echo-compiler-produced-bounded-pure-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1", @@ -1293,6 +1312,27 @@ echo-compiler-produced-bounded-read-lowering-configuration = { maxReadBytes: 1..67108864, } +; This declares the current effect kind; schema admission grants no runtime authority. +; Payload semantics still require producer and runtime validation. +echo-authored-effect-lowering-configuration = { + apiVersion: "echo.operation-lowering-configuration/v1", + programKind: "edict-effect-program/v1", + identityLaw: 1, + intrinsicCatalog: bstr .size (1..16777216), + graphSchema: bstr .size (1..16777216), + footprintLaw: bstr .size (1..16777216), + maxInputBytes: 0..65536, + maxResultBytes: 0..65536, + maxAllocatedBytes: 0..16777216, + budgetCeiling: { + steps: 1..18446744073709551615, + readBytes: uint, + writeBytes: uint, + }, + ; The requested authority class; the runtime owner's policy grants authority. + authorityProfile: echo-nonempty-tstr, +} + echo-attachment-create-if-absent-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1" / "echo.operation-lowering-configuration/v2", authorityProfile: echo-nonempty-tstr, 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 8aae8e290..2ccb29373 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:e82a973ba7b0031a75958e3fa01480e2aeeaad1989a914214232896f3d7a246f" + "digest": "sha256:17cca54e166fe42c6dfbcb76f4a0d9c8ff3bc873e8dd2ac1d15bcf48b3ac183e" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b" + "digest": "sha256:498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b" + "digest": "sha256:498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:2bc9bf98d4f3ff91b31359afe60b30bbf6670c94ba2d36d04baecb686eec4d38" + "digest": "sha256:b7ee6e16eb67285dd0d3ef8a7c6eef0c99854526a2c7d56a249286fb66aedfd9" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:29612fb0642a1fea047e9d348e21abb776e3467ccad61fa9e07194e945f39191" + "digest": "sha256:2ad4fd2d8bb633dac05c3bbac2aad77446462ef5bf30f904c5ec5c2188b82527" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111" + "digest": "sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index 3a5dd794d..373e385bc 100644 --- a/crates/echo-wesley-gen/src/provider_artifacts.rs +++ b/crates/echo-wesley-gen/src/provider_artifacts.rs @@ -242,10 +242,10 @@ edict-source-bytes = bstr echo-operation-package = echo-anchored-operation-package / echo-compiler-produced-pure-operation-package / - echo-compiler-produced-read-operation-package + echo-compiler-produced-read-operation-package / + echo-authored-effect-operation-package -echo-anchored-operation-package = { - "application_result_projection": echo-operation-application-result-projection, +echo-current-operation-package-fields = ( "application_basis_schema_identity": bstr .size 32, "authority_profile_identity": bstr .size 32, "budget_ceiling": echo-operation-budget, @@ -254,7 +254,6 @@ echo-anchored-operation-package = { "interpreter_profile_identity": bstr .size 32, "input_schema_identity": bstr .size 32, "intrinsic_profile_identity": bstr .size 32, - "obstruction_coordinate": echo-nonempty-tstr, "obstruction_schema_identity": bstr .size 32, "obstruction_interpretation_identity": bstr .size 32, "operation_coordinate": echo-nonempty-tstr, @@ -262,8 +261,19 @@ echo-anchored-operation-package = { "result_schema_identity": bstr .size 32, "result_interpretation_identity": bstr .size 32, schema: "echo.operation-package/v1", - "semantic_closure": echo-operation-semantic-closure, "target_profile_identity": bstr .size 32, +) + +echo-anchored-operation-package = { + echo-current-operation-package-fields, + "application_result_projection": echo-operation-application-result-projection, + "obstruction_coordinate": echo-nonempty-tstr, + "semantic_closure": echo-operation-semantic-closure, +} + +echo-authored-effect-operation-package = { + echo-current-operation-package-fields, + "semantic_closure": echo-authored-effect-semantic-closure, } echo-compiler-produced-pure-operation-package = { @@ -332,22 +342,31 @@ echo-operation-budget = { "write_bytes": uint, } -echo-operation-semantic-closure = { +echo-operation-semantic-closure-fields = ( "application_schema_coordinate": echo-nonempty-tstr, "application_schema_identity": bstr .size 32, "canonical_meaning_identity": bstr .size 32, "core_identity": bstr .size 32, "edict_source_identity": bstr .size 32, + "target_ir_identity": bstr .size 32, +) + +echo-operation-semantic-closure = { + echo-operation-semantic-closure-fields, "lawpack_coordinate": echo-nonempty-tstr, "lawpack_identity": bstr .size 32, - "target_ir_identity": bstr .size 32, +} + +echo-authored-effect-semantic-closure = { + echo-operation-semantic-closure-fields, } echo-operation-lowering-configuration = echo-attachment-create-if-absent-lowering-configuration / echo-attachment-compare-and-set-lowering-configuration / echo-compiler-produced-bounded-pure-lowering-configuration / - echo-compiler-produced-bounded-read-lowering-configuration + echo-compiler-produced-bounded-read-lowering-configuration / + echo-authored-effect-lowering-configuration echo-compiler-produced-bounded-pure-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1", @@ -361,6 +380,27 @@ echo-compiler-produced-bounded-read-lowering-configuration = { maxReadBytes: 1..67108864, } +; This declares the current effect kind; schema admission grants no runtime authority. +; Payload semantics still require producer and runtime validation. +echo-authored-effect-lowering-configuration = { + apiVersion: "echo.operation-lowering-configuration/v1", + programKind: "edict-effect-program/v1", + identityLaw: 1, + intrinsicCatalog: bstr .size (1..16777216), + graphSchema: bstr .size (1..16777216), + footprintLaw: bstr .size (1..16777216), + maxInputBytes: 0..65536, + maxResultBytes: 0..65536, + maxAllocatedBytes: 0..16777216, + budgetCeiling: { + steps: 1..18446744073709551615, + readBytes: uint, + writeBytes: uint, + }, + ; The requested authority class; the runtime owner's policy grants authority. + authorityProfile: echo-nonempty-tstr, +} + echo-attachment-create-if-absent-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1" / "echo.operation-lowering-configuration/v2", authorityProfile: echo-nonempty-tstr, diff --git a/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/README.md b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/README.md new file mode 100644 index 000000000..599e3c9e2 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/README.md @@ -0,0 +1,8 @@ + + + +# Current effect publication schema control + +`package.hex` is a synthetic, zero-effect serialization control: the exact output of `effect_publication::encode_effect_publication` for the crate-local `current-effect-wire` controls in `echo-edict-provider-lowerer`. A lowerer test requires it to equal the current emission, so a change to the package shape cannot leave it stale. + +It is not a public compiler output, independent verifier result or execution receipt. Tests check that the current runtime decoder and the provider's owning metadata schema agree on its structure. The program remains opaque to that metadata schema; runtime preparation and semantic verification remain separate. diff --git a/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex new file mode 100644 index 000000000..07298284e --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex @@ -0,0 +1 @@ +b166736368656d6178196563686f2e6f7065726174696f6e2d7061636b6167652f76316770726f6772616d5907c4ad646b696e647765646963742d6566666563742d70726f6772616d2f763166736368656d6178196563686f2e6f7065726174696f6e2d70726f6772616d2f76316b6f75747075745f747970656a6170702e526573756c746c67726170685f736368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026c6964656e746974795f6c6177016d666f6f747072696e745f6c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806f6d61785f696e7075745f62797465731a0001000070636f6d70696c65645f70726f6772616d5904c9a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0706d61785f726573756c745f62797465731a00010000727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db0736d61785f616c6c6f63617465645f62797465731a01000000781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c6e6275646765745f6365696c696e67a36573746570730a6a726561645f62797465731910006b77726974655f62797465731910007073656d616e7469635f636c6f73757265a66d636f72655f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db07565646963745f736f757263655f6964656e746974795820fdf14fbb05ba25e820cd4a23b594e6648429940f7aed5218077eef841288114c781a63616e6f6e6963616c5f6d65616e696e675f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d781b6170706c69636174696f6e5f736368656d615f6964656e74697479582017ff6f37fcc8b1f81e58cc3ef5c0d94a4c3f96fb955bfb9cae7ff0bac29cb249781d6170706c69636174696f6e5f736368656d615f636f6f7264696e6174656b6170702e4578706f727473746f7065726174696f6e5f636f6f7264696e6174656c6170702e757064617465403175696e7075745f736368656d615f6964656e746974795820d277705b5ab03fcb58590bad62fb8058e075c3b4c65efc21282bb531aa22b3ad76726573756c745f736368656d615f6964656e746974795820de5c0c4f0c640354c40bb4f9d4886cd820353a548caea4953269eba15fe902bc777461726765745f70726f66696c655f6964656e746974795820eaca21c05664241efd1e1c5b5636c7132fe8854f6bcaddb2fd4f014e1bd4a479781a617574686f726974795f70726f66696c655f6964656e7469747958208eca01fcc45de0eb065bfd04fb21d2c7881c346ca07dfc1536df5ca2fe0812dc781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781b666f6f747072696e745f636f6e74726163745f6964656e746974795820362b20fb18488ce180f6c1096b5189994354604fd1b1b126a3ea4ef839e5232b781b6f62737472756374696f6e5f736368656d615f6964656e746974795820b1f96fdf3ec65e84d500702951393a931b4fdd843ad02723ea3725f90a671418781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c781e726573756c745f696e746572707265746174696f6e5f6964656e74697479582042734d5e73435acc0fdd5a66b97e5bfd94278e64fce2b4890ec609f054dc969e78206576616c756174696f6e5f62617369735f736368656d615f6964656e74697479582020b6239db569aa86ae1c27596b374df90caa5ea41e8e005fd4fe00bfe5c3b38178216170706c69636174696f6e5f62617369735f736368656d615f6964656e7469747958208b294c69120a2e749c56266691b0974e7e0323c198fc42a467fd3790790f791078236f62737472756374696f6e5f696e746572707265746174696f6e5f6964656e746974795820924462071f3992b34c629f4788cc666bd14a87a0b9d0c398feba929ff4dd25f9 diff --git a/crates/echo-wesley-gen/tests/provider_effect_schema.rs b/crates/echo-wesley-gen/tests/provider_effect_schema.rs new file mode 100644 index 000000000..cdbde5794 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_effect_schema.rs @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Public configuration selection for the current authored-effect runtime kind. +use echo_wesley_gen::provider_artifacts::generate_provider_primary_artifacts_v1; +use echo_wesley_gen::provider_canonical::{encode_canonical_cbor_v1, CanonicalValueV1 as V}; +use echo_wesley_gen::provider_contract_pack::admit_provider_contract_pack_v1; +use echo_wesley_gen::provider_generation::build_provider_generation_input_v1; +fn text(value: &str) -> V { + V::Text(value.into()) +} +fn map(fields: [(&str, V); N]) -> V { + V::Map(fields.into_iter().map(|(k, v)| (text(k), v)).collect()) +} +fn configuration() -> V { + map([ + ( + "apiVersion", + text("echo.operation-lowering-configuration/v1"), + ), + ("programKind", text("edict-effect-program/v1")), + ("identityLaw", V::Integer(1)), + ("intrinsicCatalog", V::Bytes(vec![0xa0])), + ( + "graphSchema", + V::Bytes( + encode_canonical_cbor_v1(&map([ + ("node_types", V::Map(vec![])), + ("edge_types", V::Map(vec![])), + ])) + .expect("empty executable schema"), + ), + ), + ( + "footprintLaw", + V::Bytes( + encode_canonical_cbor_v1(&map([ + ("read_node_types", V::Array(vec![])), + ("write_node_types", V::Array(vec![])), + ("atom_types", V::Array(vec![])), + ("range_edge_types", V::Array(vec![])), + ("write_edge_types", V::Array(vec![])), + ("portal_owner_types", V::Array(vec![])), + ("max_reads", V::Integer(0)), + ("max_writes", V::Integer(0)), + ("max_range_edges", V::Integer(0)), + ])) + .expect("empty executable footprint"), + ), + ), + ("maxInputBytes", V::Integer(65536)), + ("maxResultBytes", V::Integer(65536)), + ("maxAllocatedBytes", V::Integer(16777216)), + ( + "budgetCeiling", + map([ + ("steps", V::Integer(10000)), + ("readBytes", V::Integer(4096)), + ("writeBytes", V::Integer(4096)), + ]), + ), + ("authorityProfile", text("app.effect-writer@1")), + ]) +} +fn set(value: &mut V, name: &str, replacement: V) { + let V::Map(fields) = value else { panic!("map") }; + fields + .iter_mut() + .find(|(key, _)| key == &text(name)) + .expect("field") + .1 = replacement; +} +fn generated() -> echo_wesley_gen::provider_artifacts::ProviderPrimaryArtifactsV1 { + let pack = admit_provider_contract_pack_v1( + include_bytes!("../assets/v1/edict-provider/contracts/v1/edict-provider-contracts.cddl"), + include_bytes!("../assets/v1/edict-provider/contracts/v1/manifest.json"), + ) + .expect("pack"); + let input = build_provider_generation_input_v1( + include_bytes!("../assets/v1/edict-provider/echo-provider-semantics-v1.json"), + &pack, + include_bytes!("../assets/v1/edict-provider/generation-settings-v1.json"), + ) + .expect("input"); + generate_provider_primary_artifacts_v1(&input, &pack).expect("primary") +} + +#[test] +fn authored_effect_configuration_is_selected_with_explicit_resource_and_law_fields() { + let generated = generated(); + let schema = generated.schema(); + let accepts = |value: &V| { + schema.validate_root_bytes( + "echo-operation-lowering-configuration", + &encode_canonical_cbor_v1(value).expect("canonical"), + ) + }; + accepts(&configuration()) + .expect("current authored-effect configuration must reach the lowerer"); + for (field, value) in [ + ("maxInputBytes", 0), + ("maxResultBytes", 0), + ("maxAllocatedBytes", 0), + ] { + let mut config = configuration(); + set(&mut config, field, V::Integer(value)); + accepts(&config).expect("zero declaration is explicit"); + } + for (field, value) in [ + ("maxInputBytes", 65537), + ("maxResultBytes", 65537), + ("maxAllocatedBytes", 16777217), + ("identityLaw", 0), + ] { + let mut config = configuration(); + set(&mut config, field, V::Integer(value)); + assert!(accepts(&config).is_err(), "{field} outside current profile"); + } + for field in ["graphSchema", "footprintLaw", "intrinsicCatalog"] { + let mut config = configuration(); + set(&mut config, field, V::Bytes(vec![])); + assert!(accepts(&config).is_err(), "{field} must carry bytes"); + let mut config = configuration(); + set(&mut config, field, map([])); + assert!( + accepts(&config).is_err(), + "{field} is canonical encoded data" + ); + } + for (steps, read, write, valid) in [ + (1, 0, 0, true), + ( + i128::from(u64::MAX), + i128::from(u64::MAX), + i128::from(u64::MAX), + true, + ), + (0, 0, 0, false), + (1, -1, 0, false), + (1, 0, -1, false), + ] { + let mut config = configuration(); + set( + &mut config, + "budgetCeiling", + map([ + ("steps", V::Integer(steps)), + ("readBytes", V::Integer(read)), + ("writeBytes", V::Integer(write)), + ]), + ); + assert_eq!( + accepts(&config).is_ok(), + valid, + "budget {steps}/{read}/{write}" + ); + } + let V::Map(fields) = configuration() else { + panic!("map") + }; + for (name, _) in &fields { + let config = V::Map( + fields + .iter() + .filter(|(key, _)| key != name) + .cloned() + .collect(), + ); + assert!(accepts(&config).is_err(), "missing {name:?}"); + } + let mut extra = fields; + extra.push((text("operation"), text("consumer-specific"))); + assert!(accepts(&V::Map(extra)).is_err()); + let mut config = configuration(); + set( + &mut config, + "apiVersion", + text("echo.operation-lowering-configuration/v2"), + ); + assert!(accepts(&config).is_err()); + for profile in [text(""), V::Integer(1), V::Bytes(vec![1; 32])] { + let mut config = configuration(); + set(&mut config, "authorityProfile", profile); + assert!( + accepts(&config).is_err(), + "the requested authority profile is nonempty text" + ); + } +} + +fn publication_control() -> Vec { + hex::decode(include_str!("fixtures/current-effect-publication/package.hex").trim()) + .expect("synthetic current publication") +} +#[test] +fn current_runtime_effect_publication_reaches_the_owning_output_root() { + let bytes = publication_control(); + let expected = warp_core::echo_operation_package_id_v1(&bytes).as_hash(); + let prepared = warp_core::ExecutableOperationPackage::prepare(&bytes, expected) + .expect("the current runtime accepts the synthetic wire control"); + assert_eq!( + prepared.to_canonical_bytes().expect("runtime round trip"), + bytes + ); + generated() + .schema() + .validate_root_bytes("echo-operation-package", &bytes) + .expect("owning provider schema must admit the current runtime effect variant"); +} + +#[test] +fn current_effect_output_fields_are_required_typed_and_closed() { + use echo_wesley_gen::provider_canonical::decode_canonical_cbor_v1 as decode; + let generated = generated(); + let schema = generated.schema(); + let accepts = |value: &V| { + schema + .validate_root_bytes( + "echo-operation-package", + &encode_canonical_cbor_v1(value).expect("canonical control"), + ) + .is_ok() + }; + let value = decode(&publication_control()).expect("current proposal"); + let V::Map(fields) = &value else { + panic!("package") + }; + for (name, _) in fields { + let missing = V::Map(fields.iter().filter(|(k, _)| k != name).cloned().collect()); + assert!(!accepts(&missing), "missing {name:?}"); + } + for (key, _) in fields { + let V::Text(key) = key else { + panic!("text field") + }; + if key.ends_with("_identity") { + let mut malformed = value.clone(); + set(&mut malformed, key, V::Bytes(vec![0; 31])); + assert!(!accepts(&malformed), "short {key}"); + } + } + for (name, replacement) in [ + ("schema", text("echo.operation-package/v2")), + ("operation_coordinate", text("")), + ("program", V::Null), + ( + "budget_ceiling", + map([ + ("steps", V::Integer(0)), + ("read_bytes", V::Integer(0)), + ("write_bytes", V::Integer(0)), + ]), + ), + ] { + let mut malformed = value.clone(); + set(&mut malformed, name, replacement); + assert!(!accepts(&malformed), "invalid {name}"); + } + let mut extra = fields.clone(); + extra.push((text("package_kind"), text("edict-effect-program/v1"))); + assert!(!accepts(&V::Map(extra))); + // An effect package declares its obstructions in its embedded exports; the + // single fixed-profile header is not part of its shape. + assert!(!fields + .iter() + .any(|(key, _)| key == &text("obstruction_coordinate"))); + let mut header = fields.clone(); + header.push((text("obstruction_coordinate"), text("app.Refusal"))); + assert!( + !accepts(&V::Map(header)), + "an effect package carrying the header" + ); + let mut malformed = value.clone(); + let V::Map(fields) = &mut malformed else { + panic!("package") + }; + let (_, V::Map(closure)) = fields + .iter_mut() + .find(|(key, _)| key == &text("semantic_closure")) + .expect("closure") + else { + panic!("closure") + }; + closure.push((text("lawpack_coordinate"), text("unretained.law@1"))); + closure.push((text("lawpack_identity"), V::Bytes(vec![1; 32]))); + assert!( + !accepts(&malformed), + "effect closure does not duplicate a lawpack" + ); +} + +#[test] +fn output_schema_admission_does_not_verify_the_opaque_program() { + use echo_wesley_gen::provider_canonical::decode_canonical_cbor_v1 as decode; + let mut value = decode(&publication_control()).expect("proposal"); + set(&mut value, "program", V::Bytes(vec![0xa0])); + let bytes = encode_canonical_cbor_v1(&value).expect("canonical metadata"); + generated() + .schema() + .validate_root_bytes("echo-operation-package", &bytes) + .expect("the metadata schema only declares an opaque program byte string"); + let expected = warp_core::echo_operation_package_id_v1(&bytes).as_hash(); + assert!( + warp_core::ExecutableOperationPackage::prepare(&bytes, expected).is_err(), + "a fresh package pin and schema admission do not authenticate a program" + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_package.rs b/crates/echo-wesley-gen/tests/provider_package.rs index c982a9fa7..0acdd0fc5 100644 --- a/crates/echo-wesley-gen/tests/provider_package.rs +++ b/crates/echo-wesley-gen/tests/provider_package.rs @@ -65,7 +65,7 @@ const SEMANTIC_DIGEST: &str = const RELEASE_DIGEST: &str = "sha256:c39449495281b51f978468d08c21e93bcfa423176063b41675da61e4674b0066"; const PACKAGE_ARTIFACT_SHA256: &str = - "e82a973ba7b0031a75958e3fa01480e2aeeaad1989a914214232896f3d7a246f"; + "17cca54e166fe42c6dfbcb76f4a0d9c8ff3bc873e8dd2ac1d15bcf48b3ac183e"; const OTHER_PACKAGE_ARTIFACT_SHA256: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; diff --git a/crates/echo-wesley-gen/tests/provider_package_corpus.rs b/crates/echo-wesley-gen/tests/provider_package_corpus.rs index 2bc7ec994..d634e93a7 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:e82a973ba7b0031a75958e3fa01480e2aeeaad1989a914214232896f3d7a246f"; + "sha256:17cca54e166fe42c6dfbcb76f4a0d9c8ff3bc873e8dd2ac1d15bcf48b3ac183e"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "15b370fd6c56adc59d1e189dee0c7e213db05a58279d630eed013a54de112b6d"; + "6e0154bfc7612c486deef1db8e031fbab042ca5cc5421438bf4d8afa4d225aef"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 3d7aea444..89c53f2e7 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -179,6 +179,18 @@ required-features = ["trusted_runtime"] name = "edict_node_read_tests" required-features = ["trusted_runtime"] +[[test]] +name = "edict_effect_range_reads" +required-features = ["trusted_runtime"] + +[[test]] +name = "edict_effect_frame_tests" +required-features = ["trusted_runtime"] + +[[test]] +name = "edict_effect_read_image_tests" +required-features = ["trusted_runtime"] + [[test]] name = "edict_byte_concat_tests" required-features = ["trusted_runtime"] @@ -235,3 +247,20 @@ required-features = ["delta_validate"] [[test]] name = "parallel_stress_multiwarp" required-features = ["delta_validate"] + +[[test]] +name = "edict_effect_graph_schema_tests" +path = "tests/edict_effect_graph_schema_tests.rs" +required-features = ["trusted_runtime"] + +[[test]] +name = "edict_effect_package_tests" +required-features = ["trusted_runtime"] + +[[test]] +name = "edict_effect_admission_tests" +required-features = ["native_rule_bootstrap", "trusted_runtime"] + +[[test]] +name = "current_operation_publication_tests" +required-features = ["trusted_runtime"] diff --git a/crates/warp-core/benches/support/action_scene.rs b/crates/warp-core/benches/support/action_scene.rs index 62be19731..294383174 100644 --- a/crates/warp-core/benches/support/action_scene.rs +++ b/crates/warp-core/benches/support/action_scene.rs @@ -24,11 +24,11 @@ use warp_core::{ echo_operation_action_envelope_v1, echo_operation_anchored_node_application_basis_v1, echo_operation_atom_value_digest_v1, echo_operation_package_id_v1, make_edge_id, make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, - EchoOperationAdmissionPolicyV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EdgeRecord, EngineBuilder, - GraphStore, InboxPolicy, IngressEnvelope, IngressTarget, NodeId, NodeKey, NodeRecord, - PlaybackMode, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeWalConfig, TypeId, WorldlineId, - WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, + EchoOperationAdmissionPolicy, EchoOperationBudgetV1, EchoOperationInvocation, + EchoOperationInvocationAdmissionPolicy, EdgeRecord, EngineBuilder, GraphStore, InboxPolicy, + IngressEnvelope, IngressTarget, NodeId, NodeKey, NodeRecord, PlaybackMode, SchedulerKind, + TrustedRuntimeHost, TrustedRuntimeWalConfig, TypeId, WorldlineId, WorldlineRuntime, + WorldlineState, WriterHead, WriterHeadKey, }; const OPERATION: &str = "examples.cas_echo@1.updateCell"; @@ -226,9 +226,9 @@ impl Scene { }; let package = package(); let admitted = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&package), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&package).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), @@ -236,10 +236,10 @@ impl Scene { package.clone(), ) .expect("the compiler package is admitted"); - host.install_admitted_echo_operation_package_v1(admitted) + host.install_admitted_echo_operation_package(admitted) .expect("the package is installed"); host.install_echo_operation_action_admission_policy_v1( - EchoOperationInvocationAdmissionPolicyV1::new( + EchoOperationInvocationAdmissionPolicy::new( profile("cas.echo.authority.local-demo/v1"), GRANT, budget(), @@ -286,7 +286,7 @@ impl Scene { ])) .expect("application input encodes"); let invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set_with_application_input( + EchoOperationInvocation::anchored_node_attachment_compare_and_set_with_application_input( echo_operation_package_id_v1(&self.package), OPERATION, basis, diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 932e3bcb3..9cfb9f992 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -13,9 +13,8 @@ use thiserror::Error; use crate::clock::{GlobalTick, WorldlineTick}; #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] use crate::echo_operation::{ - AdmittedEchoOperationInvocationV1, EchoOperationActionOutcomeV1, - EchoOperationApplicationBasisV1, EchoOperationCommitErrorV1, - EchoOperationEvaluationAuthorityV1, EchoOperationEvaluationBasisV1, + AdmittedEchoOperationInvocation, EchoOperationActionOutcomeV1, EchoOperationApplicationBasisV1, + EchoOperationCommitErrorV1, EchoOperationEvaluationAuthorityV1, EchoOperationEvaluationBasisV1, SchedulerEchoOperationCandidateV1, }; use crate::engine_impl::{CommitOutcome, Engine, EngineError}; @@ -5665,7 +5664,7 @@ impl SchedulerCoordinator { runtime: &mut WorldlineRuntime, provenance: &mut ProvenanceService, engine: &mut Engine, - operation_actions: &BTreeMap, + operation_actions: &BTreeMap, evaluation_authority: &EchoOperationEvaluationAuthorityV1, ) -> Result< ( @@ -5690,7 +5689,7 @@ impl SchedulerCoordinator { engine: &mut Engine, #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] operation_actions: Option<( - &BTreeMap, + &BTreeMap, &EchoOperationEvaluationAuthorityV1, )>, #[cfg(not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")))] @@ -5841,8 +5840,7 @@ impl SchedulerCoordinator { .ok_or(RuntimeError::UnknownWorldline(key.worldline_id))? .state(); let preparation = crate::echo_operation::prepare_operation_v1( - engine - .installed_echo_operation_package_v1(admitted_action.package_id()), + engine.installed_echo_operation_package(admitted_action.package_id()), admitted_action.clone(), current_basis, state, @@ -5869,7 +5867,11 @@ impl SchedulerCoordinator { candidates.push(SchedulerEchoOperationCandidateV1 { submission_id, ingress_id, - scope: admitted_action.scope(), + scope: admitted_action.scope().ok_or(RuntimeError::Strand( + StrandError::InvariantViolation( + "admitted fixed Action lacks its anchor", + ), + ))?, rule_id: admitted_action.installed_operation_id().as_hash(), preparation, }); diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index b5da765c5..248a500fd 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -53,8 +53,6 @@ use crate::{ const PACKAGE_SCHEMA: &str = "echo.operation-package/v1"; const PROGRAM_SCHEMA: &str = "echo.operation-program/v1"; const INVOCATION_SCHEMA: &str = "echo.operation-invocation/v1"; -const PROJECTED_CAS_INVOCATION_SCHEMA: &str = - "echo.operation-invocation.anchored-node-alpha-cas-projected/v1"; const PROGRAM_KIND: &str = "anchored-node-attachment-compare-and-set/v1"; const FOOTPRINT_CONTRACT: &str = "anchored-node-alpha-exact/v1"; const INPUT_SCHEMA: &str = "echo.operation.input.anchored-node-alpha-cas/v1"; @@ -67,10 +65,6 @@ const OBSTRUCTION_INTERPRETATION: &str = const BASIS_SCHEMA: &str = "echo.operation.evaluation-basis/v1"; const APPLICATION_BASIS_SCHEMA: &str = "echo.operation.basis.anchored-node-alpha/v1"; const TARGET_PROFILE: &str = "echo.operation-target.anchored-node-alpha-cas/v1"; -const CREATE_INVOCATION_SCHEMA: &str = - "echo.operation-invocation.anchored-node-alpha-create-if-absent/v1"; -const PROJECTED_CREATE_INVOCATION_SCHEMA: &str = - "echo.operation-invocation.anchored-node-alpha-create-if-absent-projected/v1"; const CREATE_PROGRAM_KIND: &str = "anchored-node-attachment-create-if-absent/v1"; const CREATE_FOOTPRINT_CONTRACT: &str = "anchored-node-alpha-create-if-absent-exact/v1"; const CREATE_INPUT_SCHEMA: &str = "echo.operation.input.anchored-node-alpha-create-if-absent/v1"; @@ -151,7 +145,7 @@ impl EchoOperationEvaluationAuthorityV1 { Self(Arc::new(())) } - fn same_owner(&self, other: &Self) -> bool { + pub(crate) fn same_owner(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) } } @@ -169,6 +163,12 @@ impl EchoOperationPackageIdV1 { } } +impl From for EchoOperationPackageIdV1 { + fn from(value: Hash) -> Self { + Self(value) + } +} + /// Stable content identity of exact Echo operation-program bytes. #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -325,7 +325,7 @@ pub fn echo_operation_action_envelope_v1( target: IngressTarget, canonical_invocation_bytes: Vec, ) -> Result { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(&canonical_invocation_bytes)?; + let invocation = EchoOperationInvocation::from_canonical_bytes(&canonical_invocation_bytes)?; if invocation.to_canonical_bytes()? != canonical_invocation_bytes { return Err(artifact_error( EchoOperationArtifactErrorKindV1::NonCanonical, @@ -506,7 +506,7 @@ impl EchoOperationBudgetV1 { && self.write_bytes <= ceiling.write_bytes } - fn to_value(self) -> CanonicalValueV1 { + pub(crate) fn to_value(self) -> CanonicalValueV1 { map_value([ ("read_bytes", uint_value(self.read_bytes)), ("steps", uint_value(self.steps)), @@ -514,7 +514,9 @@ impl EchoOperationBudgetV1 { ]) } - fn from_value(value: CanonicalValueV1) -> Result { + pub(crate) fn from_value( + value: CanonicalValueV1, + ) -> Result { let mut fields = exact_text_map(value, &["read_bytes", "steps", "write_bytes"])?; Ok(Self { steps: take_u64(&mut fields, "steps")?, @@ -525,20 +527,20 @@ impl EchoOperationBudgetV1 { } #[derive(Clone, Copy, Debug)] -struct EchoOperationBudgetMeterV1 { +pub(crate) struct EchoOperationBudgetMeterV1 { delegated: EchoOperationBudgetV1, consumed: EchoOperationBudgetV1, } impl EchoOperationBudgetMeterV1 { - const fn new(delegated: EchoOperationBudgetV1) -> Self { + pub(crate) const fn new(delegated: EchoOperationBudgetV1) -> Self { Self { delegated, consumed: EchoOperationBudgetV1::new(0, 0, 0), } } - fn charge(&mut self, steps: u64, read_bytes: u64, write_bytes: u64) -> bool { + pub(crate) fn charge(&mut self, steps: u64, read_bytes: u64, write_bytes: u64) -> bool { let Some(next_steps) = self.consumed.steps.checked_add(steps) else { return false; }; @@ -556,7 +558,11 @@ impl EchoOperationBudgetMeterV1 { true } - const fn consumed(self) -> EchoOperationBudgetV1 { + pub(crate) const fn delegated(self) -> EchoOperationBudgetV1 { + self.delegated + } + + pub(crate) const fn consumed(self) -> EchoOperationBudgetV1 { self.consumed } } @@ -570,18 +576,12 @@ pub enum EchoOperationFootprintContractV1 { AnchoredNodeAlphaCreateIfAbsentExact, } -impl EchoOperationFootprintContractV1 { - const fn coordinate(self) -> &'static str { - match self { - Self::AnchoredNodeAlphaExact => FOOTPRINT_CONTRACT, - Self::AnchoredNodeAlphaCreateIfAbsentExact => CREATE_FOOTPRINT_CONTRACT, - } - } -} - /// Data-only executable meaning interpreted by Echo. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum EchoOperationProgramV1 { +pub enum EchoOperationProgram { + /// An authored program with executable schema, footprint law and resource declarations. + EdictEffect(Arc), + /// Compare and replace one anchored typed node attachment. AnchoredNodeAttachmentCompareAndSet { /// Required skeleton node type. @@ -602,7 +602,29 @@ pub enum EchoOperationProgramV1 { }, } -impl EchoOperationProgramV1 { +impl EchoOperationProgram { + /// Returns the authored-effect payload when this is that program kind. + #[must_use] + pub fn as_effect_program(&self) -> Option<&crate::edict_effects::EffectProgram> { + match self { + Self::EdictEffect(program) => Some(program), + _ => None, + } + } + + const fn interpreter_profile(&self) -> &'static str { + match self { + Self::EdictEffect(_) => "echo.operation-interpreter.edict-effect/v1", + _ => INTERPRETER_PROFILE, + } + } + const fn intrinsic_profile(&self) -> &'static str { + match self { + Self::EdictEffect(_) => "echo.operation-intrinsics.edict-effect/v1", + _ => INTRINSIC_PROFILE, + } + } + /// Creates the first bounded, unique-match operation program. #[must_use] pub const fn anchored_node_attachment_compare_and_set( @@ -633,8 +655,7 @@ impl EchoOperationProgramV1 { /// Returns the exact program identity. pub fn identity(&self) -> Result { - Ok(EchoOperationProgramIdV1(domain_hash( - PROGRAM_ID_DOMAIN, + Ok(EchoOperationProgramIdV1(program_identity_bytes( &self.to_canonical_bytes()?, ))) } @@ -645,19 +666,19 @@ impl EchoOperationProgramV1 { encode_canonical_cbor_v1(&self.to_value()).map_err(canonical_error) } - fn footprint_contract(&self) -> EchoOperationFootprintContractV1 { + fn footprint_contract_identity(&self) -> Hash { match self { - Self::AnchoredNodeAttachmentCompareAndSet { .. } => { - EchoOperationFootprintContractV1::AnchoredNodeAlphaExact - } + Self::AnchoredNodeAttachmentCompareAndSet { .. } => profile_digest(FOOTPRINT_CONTRACT), Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => { - EchoOperationFootprintContractV1::AnchoredNodeAlphaCreateIfAbsentExact + profile_digest(CREATE_FOOTPRINT_CONTRACT) } + Self::EdictEffect(program) => program.footprint_identity(), } } const fn minimum_budget(&self) -> EchoOperationBudgetV1 { match self { + Self::EdictEffect(_) => EchoOperationBudgetV1::new(1, 0, 0), Self::AnchoredNodeAttachmentCompareAndSet { .. } => { EchoOperationBudgetV1::new(4, 64, 32) } @@ -673,6 +694,7 @@ impl EchoOperationProgramV1 { const fn input_schema(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.input.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => INPUT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_INPUT_SCHEMA, } @@ -680,6 +702,7 @@ impl EchoOperationProgramV1 { const fn result_schema(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.result.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_SCHEMA, } @@ -687,6 +710,7 @@ impl EchoOperationProgramV1 { const fn obstruction_schema(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.obstruction.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_SCHEMA, } @@ -694,6 +718,7 @@ impl EchoOperationProgramV1 { const fn result_interpretation(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.result-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_INTERPRETATION, } @@ -701,6 +726,7 @@ impl EchoOperationProgramV1 { const fn obstruction_interpretation(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.obstruction-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_INTERPRETATION, } @@ -708,6 +734,7 @@ impl EchoOperationProgramV1 { const fn application_basis_schema(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation.basis.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => APPLICATION_BASIS_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_APPLICATION_BASIS_SCHEMA, } @@ -715,20 +742,31 @@ impl EchoOperationProgramV1 { const fn target_profile(&self) -> &'static str { match self { + Self::EdictEffect(_) => "echo.operation-target.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => TARGET_PROFILE, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_TARGET_PROFILE, } } - const fn accepts_invocation_kind(&self, kind: EchoOperationInvocationKindV1) -> bool { + const fn accepts_invocation_kind(&self, kind: EchoOperationInvocationKind) -> bool { + #[cfg(feature = "trusted_runtime")] + if matches!( + (self, kind), + ( + Self::EdictEffect(_), + EchoOperationInvocationKind::EdictEffect + ) + ) { + return true; + } matches!( (self, kind), ( Self::AnchoredNodeAttachmentCompareAndSet { .. }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { .. } + EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { .. } ) | ( Self::AnchoredNodeAttachmentCreateIfAbsent { .. }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent + EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent ) ) } @@ -746,7 +784,8 @@ impl EchoOperationProgramV1 { EchoOperationArtifactErrorKindV1::UnsupportedProgram, "program replacement bound must be nonzero", )), - Self::AnchoredNodeAttachmentCompareAndSet { .. } + Self::EdictEffect(_) + | Self::AnchoredNodeAttachmentCompareAndSet { .. } | Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => Ok(()), } } @@ -754,6 +793,7 @@ impl EchoOperationProgramV1 { fn to_value(&self) -> CanonicalValueV1 { let (kind, required_node_type, required_attachment_type, max_replacement_bytes) = match self { + Self::EdictEffect(program) => return program.to_value(), Self::AnchoredNodeAttachmentCompareAndSet { required_node_type, required_attachment_type, @@ -797,6 +837,11 @@ impl EchoOperationProgramV1 { fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; + if text_map(value.clone())?.get("kind") == Some(&text_value("edict-effect-program/v1")) { + return crate::edict_effects::EffectProgram::from_value(&value) + .map(|program| Self::EdictEffect(Arc::new(program))) + .map_err(|_| invalid_structure("invalid authored-effect program")); + } let mut fields = exact_text_map( value, &[ @@ -852,18 +897,18 @@ impl EchoOperationProgramV1 { /// Echo treats these as opaque substitution evidence. It does not infer source, /// Core, Target IR, application schema, or lawpack identity from program bytes. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct EchoOperationSemanticClosureV1 { +pub struct EchoOperationSemanticClosure { edict_source_identity: Hash, canonical_meaning_identity: Hash, core_identity: Hash, target_ir_identity: Hash, application_schema_coordinate: String, application_schema_identity: Hash, - lawpack_coordinate: String, - lawpack_identity: Hash, + lawpack_coordinate: Option, + lawpack_identity: Option, } -impl EchoOperationSemanticClosureV1 { +impl EchoOperationSemanticClosure { /// Creates the exact upstream semantic closure bound by one package. #[allow(clippy::too_many_arguments)] #[must_use] @@ -884,8 +929,8 @@ impl EchoOperationSemanticClosureV1 { target_ir_identity, application_schema_coordinate: application_schema_coordinate.into(), application_schema_identity, - lawpack_coordinate: lawpack_coordinate.into(), - lawpack_identity, + lawpack_coordinate: Some(lawpack_coordinate.into()), + lawpack_identity: Some(lawpack_identity), } } @@ -897,12 +942,18 @@ impl EchoOperationSemanticClosureV1 { /// Returns the application-owned lawpack identity. #[must_use] - pub const fn lawpack_identity(&self) -> Hash { + pub const fn lawpack_identity(&self) -> Option { self.lawpack_identity } fn validate(&self) -> Result<(), EchoOperationArtifactErrorV1> { - if self.application_schema_coordinate.is_empty() || self.lawpack_coordinate.is_empty() { + if self.application_schema_coordinate.is_empty() + || self + .lawpack_coordinate + .as_ref() + .is_some_and(String::is_empty) + || self.lawpack_coordinate.is_some() != self.lawpack_identity.is_some() + { return Err(invalid_structure( "application schema and lawpack coordinates must not be empty", )); @@ -911,7 +962,7 @@ impl EchoOperationSemanticClosureV1 { } fn to_value(&self) -> CanonicalValueV1 { - map_value([ + let mut fields = vec![ ( "application_schema_coordinate", text_value(&self.application_schema_coordinate), @@ -929,26 +980,36 @@ impl EchoOperationSemanticClosureV1 { "edict_source_identity", hash_value(self.edict_source_identity), ), - ("lawpack_coordinate", text_value(&self.lawpack_coordinate)), - ("lawpack_identity", hash_value(self.lawpack_identity)), ("target_ir_identity", hash_value(self.target_ir_identity)), - ]) + ]; + if let (Some(coordinate), Some(identity)) = + (&self.lawpack_coordinate, self.lawpack_identity) + { + fields.push(("lawpack_coordinate", text_value(coordinate))); + fields.push(("lawpack_identity", hash_value(identity))); + } + CanonicalValueV1::Map( + fields + .into_iter() + .map(|(key, value)| (text_value(key), value)) + .collect(), + ) } fn from_value(value: CanonicalValueV1) -> Result { - let mut fields = exact_text_map( - value, - &[ - "application_schema_coordinate", - "application_schema_identity", - "canonical_meaning_identity", - "core_identity", - "edict_source_identity", - "lawpack_coordinate", - "lawpack_identity", - "target_ir_identity", - ], - )?; + let has_lawpack = text_map(value.clone())?.contains_key("lawpack_coordinate"); + let mut names = vec![ + "application_schema_coordinate", + "application_schema_identity", + "canonical_meaning_identity", + "core_identity", + "edict_source_identity", + "target_ir_identity", + ]; + if has_lawpack { + names.extend(["lawpack_coordinate", "lawpack_identity"]); + } + let mut fields = exact_text_map(value, &names)?; let closure = Self { edict_source_identity: take_hash(&mut fields, "edict_source_identity")?, canonical_meaning_identity: take_hash(&mut fields, "canonical_meaning_identity")?, @@ -956,8 +1017,16 @@ impl EchoOperationSemanticClosureV1 { target_ir_identity: take_hash(&mut fields, "target_ir_identity")?, application_schema_coordinate: take_text(&mut fields, "application_schema_coordinate")?, application_schema_identity: take_hash(&mut fields, "application_schema_identity")?, - lawpack_coordinate: take_text(&mut fields, "lawpack_coordinate")?, - lawpack_identity: take_hash(&mut fields, "lawpack_identity")?, + lawpack_coordinate: if has_lawpack { + Some(take_text(&mut fields, "lawpack_coordinate")?) + } else { + None + }, + lawpack_identity: if has_lawpack { + Some(take_hash(&mut fields, "lawpack_identity")?) + } else { + None + }, }; closure.validate()?; Ok(closure) @@ -1322,7 +1391,7 @@ impl EchoOperationApplicationResultProjectionV1 { canonical_application_input_bytes: &[u8], node: NodeKey, replacement_bytes: &[u8], - kind: EchoOperationInvocationKindV1, + kind: EchoOperationInvocationKind, ) -> Result<(), EchoOperationArtifactErrorV1> { let application_input = decode_canonical_cbor_v1(canonical_application_input_bytes).map_err(canonical_error)?; @@ -1368,13 +1437,13 @@ impl EchoOperationApplicationResultProjectionV1 { fn validate_expected_digest_binding( &self, application_input: &CanonicalValueV1, - kind: EchoOperationInvocationKindV1, + kind: EchoOperationInvocationKind, ) -> Result<(), EchoOperationArtifactErrorV1> { match (&self.application_input_expected_value_digest_path, kind) { - (None, EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent) => Ok(()), + (None, EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent) => Ok(()), ( Some(path), - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest, }, ) => { @@ -1785,10 +1854,12 @@ fn require_field_names( /// Exact executable-operation publication material and provenance. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ExecutableOperationPackageV1 { +pub struct ExecutableOperationPackage { operation_coordinate: String, - obstruction_coordinate: String, - semantic_closure: EchoOperationSemanticClosureV1, + /// Present for fixed-profile programs and absent for authored effect + /// programs, whose embedded exports declare every obstruction. + obstruction_coordinate: Option, + semantic_closure: EchoOperationSemanticClosure, target_profile_identity: Hash, interpreter_profile_identity: Hash, intrinsic_profile_identity: Hash, @@ -1802,21 +1873,40 @@ pub struct ExecutableOperationPackageV1 { evaluation_basis_schema_identity: Hash, footprint_contract_identity: Hash, budget_ceiling: EchoOperationBudgetV1, - program: EchoOperationProgramV1, + program: EchoOperationProgram, application_result_projection: Option, } -impl ExecutableOperationPackageV1 { +impl ExecutableOperationPackage { + /// Prepares exact pinned current publication bytes, without admission authority. + /// + /// This data-only compiler/verifier check is available without trusted-host + /// features. It checks the current package and embedded program contract; + /// it does not authenticate source-to-Core compilation, select an authorized + /// release pin, install a package, or grant invocation rights. + /// # Errors + /// Refuses a different pin or an unsupported canonical publication contract. + pub fn prepare( + bytes: &[u8], + expected: Hash, + ) -> Result { + use crate::edict_effects::EffectPackageError; + if echo_operation_package_id_v1(bytes).as_hash() != expected { + return Err(EffectPackageError::IdentityMismatch); + } + Self::from_canonical_bytes(bytes).map_err(|_| EffectPackageError::InvalidPackage) + } + /// Creates publication material for the supported bounded operation profile. #[must_use] pub fn new( operation_coordinate: impl Into, obstruction_coordinate: impl Into, - semantic_closure: EchoOperationSemanticClosureV1, + semantic_closure: EchoOperationSemanticClosure, target_profile_identity: Hash, authority_profile_identity: Hash, budget_ceiling: EchoOperationBudgetV1, - program: EchoOperationProgramV1, + program: EchoOperationProgram, ) -> Self { let input_schema_identity = profile_digest(program.input_schema()); let result_schema_identity = profile_digest(program.result_schema()); @@ -1827,11 +1917,11 @@ impl ExecutableOperationPackageV1 { let application_basis_schema_identity = profile_digest(program.application_basis_schema()); Self { operation_coordinate: operation_coordinate.into(), - obstruction_coordinate: obstruction_coordinate.into(), + obstruction_coordinate: Some(obstruction_coordinate.into()), semantic_closure, target_profile_identity, - interpreter_profile_identity: profile_digest(INTERPRETER_PROFILE), - intrinsic_profile_identity: profile_digest(INTRINSIC_PROFILE), + interpreter_profile_identity: profile_digest(program.interpreter_profile()), + intrinsic_profile_identity: profile_digest(program.intrinsic_profile()), authority_profile_identity, input_schema_identity, result_schema_identity, @@ -1840,7 +1930,7 @@ impl ExecutableOperationPackageV1 { obstruction_interpretation_identity, application_basis_schema_identity, evaluation_basis_schema_identity: profile_digest(BASIS_SCHEMA), - footprint_contract_identity: profile_digest(program.footprint_contract().coordinate()), + footprint_contract_identity: program.footprint_contract_identity(), budget_ceiling, program, application_result_projection: None, @@ -1955,10 +2045,12 @@ impl ExecutableOperationPackageV1 { &self.operation_coordinate } - /// Returns the application-authored obstruction coordinate. + /// Returns the application-authored obstruction coordinate of a + /// fixed-profile package. An authored effect package has none: its + /// embedded exports declare every obstruction its program can return. #[must_use] - pub fn obstruction_coordinate(&self) -> &str { - &self.obstruction_coordinate + pub fn obstruction_coordinate(&self) -> Option<&str> { + self.obstruction_coordinate.as_deref() } /// Returns the Edict semantic identity bound by this package. @@ -1969,7 +2061,7 @@ impl ExecutableOperationPackageV1 { /// Returns the application-owned lawpack identity bound by this package. #[must_use] - pub const fn lawpack_identity(&self) -> Hash { + pub const fn lawpack_identity(&self) -> Option { self.semantic_closure.lawpack_identity } @@ -1993,7 +2085,7 @@ impl ExecutableOperationPackageV1 { /// Returns the executable program. #[must_use] - pub const fn program(&self) -> &EchoOperationProgramV1 { + pub const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2013,12 +2105,7 @@ impl ExecutableOperationPackageV1 { "operation coordinate must not be empty", )); } - if self.obstruction_coordinate.is_empty() { - return Err(artifact_error( - EchoOperationArtifactErrorKindV1::EmptyObstructionCoordinate, - "obstruction coordinate must not be empty", - )); - } + self.validate_obstruction_header()?; if !self.budget_ceiling.is_nonzero() { return Err(artifact_error( EchoOperationArtifactErrorKindV1::InvalidBudget, @@ -2075,10 +2162,6 @@ impl ExecutableOperationPackageV1 { "obstruction_interpretation_identity", hash_value(self.obstruction_interpretation_identity), ), - ( - "obstruction_coordinate", - text_value(&self.obstruction_coordinate), - ), ( "operation_coordinate", text_value(&self.operation_coordinate), @@ -2099,6 +2182,9 @@ impl ExecutableOperationPackageV1 { hash_value(self.target_profile_identity), ), ]; + if let Some(obstruction) = &self.obstruction_coordinate { + fields.push(("obstruction_coordinate", text_value(obstruction))); + } if let Some(projection) = &self.application_result_projection { fields.push(("application_result_projection", projection.to_value())); } @@ -2113,12 +2199,14 @@ impl ExecutableOperationPackageV1 { fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; - let has_application_result_projection = match &value { - CanonicalValueV1::Map(entries) => entries.iter().any(|(key, _)| { - key == &CanonicalValueV1::Text("application_result_projection".to_owned()) - }), + let has_key = |name: &str| match &value { + CanonicalValueV1::Map(entries) => entries + .iter() + .any(|(key, _)| key == &CanonicalValueV1::Text(name.to_owned())), _ => false, }; + let has_application_result_projection = has_key("application_result_projection"); + let has_obstruction_coordinate = has_key("obstruction_coordinate"); let mut expected_fields = vec![ "authority_profile_identity", "application_basis_schema_identity", @@ -2130,7 +2218,6 @@ impl ExecutableOperationPackageV1 { "intrinsic_profile_identity", "obstruction_schema_identity", "obstruction_interpretation_identity", - "obstruction_coordinate", "operation_coordinate", "program", "result_schema_identity", @@ -2142,6 +2229,9 @@ impl ExecutableOperationPackageV1 { if has_application_result_projection { expected_fields.push("application_result_projection"); } + if has_obstruction_coordinate { + expected_fields.push("obstruction_coordinate"); + } let mut fields = exact_text_map(value, &expected_fields)?; require_text(&mut fields, "schema", PACKAGE_SCHEMA)?; let operation_coordinate = take_text(&mut fields, "operation_coordinate")?; @@ -2151,15 +2241,13 @@ impl ExecutableOperationPackageV1 { "operation coordinate must not be empty", )); } - let obstruction_coordinate = take_text(&mut fields, "obstruction_coordinate")?; - if obstruction_coordinate.is_empty() { - return Err(artifact_error( - EchoOperationArtifactErrorKindV1::EmptyObstructionCoordinate, - "obstruction coordinate must not be empty", - )); - } + let obstruction_coordinate = if has_obstruction_coordinate { + Some(take_text(&mut fields, "obstruction_coordinate")?) + } else { + None + }; let program_bytes = take_bytes(&mut fields, "program")?; - let program = EchoOperationProgramV1::from_canonical_bytes(&program_bytes)?; + let program = EchoOperationProgram::from_canonical_bytes(&program_bytes)?; let application_result_projection = if has_application_result_projection { Some(EchoOperationApplicationResultProjectionV1::from_value( take_field(&mut fields, "application_result_projection")?, @@ -2170,7 +2258,7 @@ impl ExecutableOperationPackageV1 { let package = Self { operation_coordinate, obstruction_coordinate, - semantic_closure: EchoOperationSemanticClosureV1::from_value(take_field( + semantic_closure: EchoOperationSemanticClosure::from_value(take_field( &mut fields, "semantic_closure", )?)?, @@ -2215,13 +2303,62 @@ impl ExecutableOperationPackageV1 { Ok(package) } + /// A fixed-profile package names one nonempty obstruction coordinate; an + /// authored effect package names none. + fn validate_obstruction_header(&self) -> Result<(), EchoOperationArtifactErrorV1> { + let effect = matches!(&self.program, EchoOperationProgram::EdictEffect(_)); + match (&self.obstruction_coordinate, effect) { + (Some(coordinate), false) if !coordinate.is_empty() => Ok(()), + (None, true) => Ok(()), + (Some(_), false) => Err(artifact_error( + EchoOperationArtifactErrorKindV1::EmptyObstructionCoordinate, + "obstruction coordinate must not be empty", + )), + (None, false) => Err(invalid_structure( + "a fixed-profile package requires its obstruction coordinate", + )), + (Some(_), true) => Err(invalid_structure( + "an authored effect package declares its obstructions in its exports", + )), + } + } + fn self_validate_supported_profile(&self) -> Result<(), EchoOperationArtifactErrorV1> { + self.validate_obstruction_header()?; if !self.budget_ceiling.is_nonzero() { return Err(artifact_error( EchoOperationArtifactErrorKindV1::InvalidBudget, "package step budget must be nonzero", )); } + self.semantic_closure.validate()?; + let requires_lawpack = !matches!(&self.program, EchoOperationProgram::EdictEffect(_)); + if requires_lawpack != self.semantic_closure.lawpack_identity.is_some() { + return Err(invalid_structure( + "lawpack binding does not match the program kind", + )); + } + if let Some(effect) = self.program.as_effect_program() { + effect + .validate_compiled_binding( + &self.operation_coordinate, + self.semantic_closure.core_identity, + self.semantic_closure.application_schema_identity, + ) + .map_err(|_| { + invalid_structure( + "compiled Core, selected operation and semantic closure disagree", + ) + })?; + + if self.semantic_closure.target_ir_identity != effect.target_ir_identity() + || self.application_result_projection.is_some() + { + return Err(invalid_structure( + "effect closure, program and projection disagree", + )); + } + } let expected = [ ( "input schema", @@ -2261,7 +2398,7 @@ impl ExecutableOperationPackageV1 { ( "footprint contract", self.footprint_contract_identity, - profile_digest(self.program.footprint_contract().coordinate()), + self.program.footprint_contract_identity(), ), ]; if let Some((label, _, _)) = expected @@ -2279,8 +2416,8 @@ impl ExecutableOperationPackageV1 { "unsupported Echo target profile identity", )); } - if self.interpreter_profile_identity != profile_digest(INTERPRETER_PROFILE) - || self.intrinsic_profile_identity != profile_digest(INTRINSIC_PROFILE) + if self.interpreter_profile_identity != profile_digest(self.program.interpreter_profile()) + || self.intrinsic_profile_identity != profile_digest(self.program.intrinsic_profile()) { return Err(artifact_error( EchoOperationArtifactErrorKindV1::UnsupportedTargetProfile, @@ -2290,7 +2427,7 @@ impl ExecutableOperationPackageV1 { if let Some(projection) = &self.application_result_projection { let requires_digest = matches!( self.program, - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } ); if projection .application_input_expected_value_digest_path @@ -2362,18 +2499,18 @@ impl EchoOperationArtifactErrorV1 { /// Independently pinned policy for the package-admission crossing. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct EchoOperationAdmissionPolicyV1 { - expected_package_id: EchoOperationPackageIdV1, +pub struct EchoOperationAdmissionPolicy { + expected_package_id: Hash, expected_operation_coordinate: String, expected_authority_profile_identity: Hash, maximum_budget: EchoOperationBudgetV1, } -impl EchoOperationAdmissionPolicyV1 { +impl EchoOperationAdmissionPolicy { /// Pins the exact package, public operation, authority profile, and budget ceiling. #[must_use] pub fn exact( - expected_package_id: EchoOperationPackageIdV1, + expected_package_id: Hash, expected_operation_coordinate: impl Into, expected_authority_profile_identity: Hash, maximum_budget: EchoOperationBudgetV1, @@ -2386,17 +2523,46 @@ impl EchoOperationAdmissionPolicyV1 { } } - fn identity(&self) -> Hash { + /// Identity of the independently selected pins and resource ceiling. + #[must_use] + pub fn identity(&self) -> Hash { let mut hasher = Hasher::new(); hasher.update(b"echo:operation-admission-policy:v1\0"); - hasher.update(&self.expected_package_id.as_hash()); + hasher.update(&self.expected_package_id); hash_len_bytes(&mut hasher, self.expected_operation_coordinate.as_bytes()); hasher.update(&self.expected_authority_profile_identity); hash_budget(&mut hasher, self.maximum_budget); hasher.finalize().into() } - fn to_value(&self) -> CanonicalValueV1 { + pub(crate) fn validate_contract( + &self, + operation_coordinate: &str, + authority_profile_identity: Hash, + budget: EchoOperationBudgetV1, + ) -> Result<(), EchoOperationAdmissionError> { + if operation_coordinate != self.expected_operation_coordinate { + return Err(admission_error( + EchoOperationAdmissionErrorKind::OperationCoordinateMismatch, + "package operation coordinate differs from admission policy", + )); + } + if authority_profile_identity != self.expected_authority_profile_identity { + return Err(admission_error( + EchoOperationAdmissionErrorKind::AuthorityProfileMismatch, + "package authority profile differs from admission policy", + )); + } + if !budget.fits_within(self.maximum_budget) { + return Err(admission_error( + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy, + "package budget ceiling exceeds admission policy", + )); + } + Ok(()) + } + + pub(crate) fn to_value(&self) -> CanonicalValueV1 { map_value([ ( "expected_authority_profile_identity", @@ -2406,15 +2572,14 @@ impl EchoOperationAdmissionPolicyV1 { "expected_operation_coordinate", text_value(&self.expected_operation_coordinate), ), - ( - "expected_package_id", - hash_value(self.expected_package_id.as_hash()), - ), + ("expected_package_id", hash_value(self.expected_package_id)), ("maximum_budget", self.maximum_budget.to_value()), ]) } - fn from_value(value: CanonicalValueV1) -> Result { + pub(crate) fn from_value( + value: CanonicalValueV1, + ) -> Result { let mut fields = exact_text_map( value, &[ @@ -2425,10 +2590,7 @@ impl EchoOperationAdmissionPolicyV1 { ], )?; let policy = Self { - expected_package_id: EchoOperationPackageIdV1(take_hash( - &mut fields, - "expected_package_id", - )?), + expected_package_id: take_hash(&mut fields, "expected_package_id")?, expected_operation_coordinate: take_text(&mut fields, "expected_operation_coordinate")?, expected_authority_profile_identity: take_hash( &mut fields, @@ -2450,7 +2612,7 @@ impl EchoOperationAdmissionPolicyV1 { /// Stable package-admission refusal categories. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EchoOperationAdmissionErrorKindV1 { +pub enum EchoOperationAdmissionErrorKind { /// Exact package bytes were malformed or unsupported. ArtifactInvalid, /// Exact package bytes did not match the independently pinned package id. @@ -2466,16 +2628,16 @@ pub enum EchoOperationAdmissionErrorKindV1 { /// One structured package-admission refusal. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("{kind:?}: {detail}")] -pub struct EchoOperationAdmissionErrorV1 { - kind: EchoOperationAdmissionErrorKindV1, +pub struct EchoOperationAdmissionError { + kind: EchoOperationAdmissionErrorKind, detail: String, artifact: Option, } -impl EchoOperationAdmissionErrorV1 { +impl EchoOperationAdmissionError { /// Returns the stable refusal category. #[must_use] - pub const fn kind(&self) -> EchoOperationAdmissionErrorKindV1 { + pub const fn kind(&self) -> EchoOperationAdmissionErrorKind { self.kind } @@ -2487,26 +2649,65 @@ impl EchoOperationAdmissionErrorV1 { } /// Opaque evidence that Echo admitted exact package bytes under separate policy. -#[derive(Clone, Debug)] -pub struct AdmittedExecutableOperationPackageV1 { - package: ExecutableOperationPackageV1, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdmittedExecutableOperationPackage { + package: ExecutableOperationPackage, canonical_package_bytes: Vec, package_id: EchoOperationPackageIdV1, + program_id: EchoOperationProgramIdV1, admission_policy_id: Hash, admission_id: EchoOperationPackageAdmissionIdV1, - admission_policy: EchoOperationAdmissionPolicyV1, + admission_policy: EchoOperationAdmissionPolicy, +} + +impl AdmittedExecutableOperationPackage { + /// Exact publication admitted by the runtime owner. + #[must_use] + pub const fn package(&self) -> &ExecutableOperationPackage { + &self.package + } + /// Exact canonical publication retained at admission. + #[must_use] + pub fn canonical_package_bytes(&self) -> &[u8] { + &self.canonical_package_bytes + } + /// Identity of the exact current publication. + #[must_use] + pub const fn package_identity(&self) -> Hash { + self.package_id.as_hash() + } + /// Identity of the exact current program. + #[must_use] + pub const fn program_identity(&self) -> Hash { + self.program_id.as_hash() + } + /// The independently selected owner policy. + #[must_use] + pub const fn admission_policy(&self) -> &EchoOperationAdmissionPolicy { + &self.admission_policy + } + /// Identity of that independent policy. + #[must_use] + pub const fn admission_policy_identity(&self) -> Hash { + self.admission_policy_id + } + /// Evidence identity under the sole current admission recipe. + #[must_use] + pub const fn identity(&self) -> Hash { + self.admission_id.as_hash() + } } /// Installed executable meaning. Installation does not itself authorize an invocation. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct InstalledEchoOperationV1 { +pub struct InstalledEchoOperation { installed_operation_id: InstalledEchoOperationIdV1, package_id: EchoOperationPackageIdV1, package_admission_id: EchoOperationPackageAdmissionIdV1, // Shared, not copied, by every prepared Action (#903). operation_coordinate: std::sync::Arc, semantic_identity: Hash, - lawpack_identity: Hash, + lawpack_identity: Option, target_profile_identity: Hash, interpreter_profile_identity: Hash, intrinsic_profile_identity: Hash, @@ -2514,15 +2715,42 @@ pub struct InstalledEchoOperationV1 { application_basis_schema_identity: Hash, budget_ceiling: EchoOperationBudgetV1, program_id: EchoOperationProgramIdV1, - program: EchoOperationProgramV1, + program: EchoOperationProgram, application_result_projection: Option>, canonical_package_bytes: std::sync::Arc<[u8]>, admission_policy_id: Hash, - admission_policy: std::sync::Arc, + admission_policy: std::sync::Arc, } -impl InstalledEchoOperationV1 { +impl InstalledEchoOperation { + /// Raw identity of the current installation evidence. + #[must_use] + pub const fn identity(&self) -> Hash { + self.installed_operation_id.as_hash() + } + /// Raw identity of the exact current publication. + #[must_use] + pub const fn package_identity(&self) -> Hash { + self.package_id.as_hash() + } + /// Raw identity of the exact current program. + #[must_use] + pub const fn program_identity(&self) -> Hash { + self.program_id.as_hash() + } + /// Canonical retained evidence; this does not grant invocation authority. + /// # Errors + /// Refuses invalid canonical material. + pub fn retained_bytes(&self) -> Result, EchoOperationArtifactErrorV1> { + retain_operation_installation(self) + } + /// Reconstructs retained evidence without inserting it into a registry. + /// # Errors + /// Refuses unsupported records, policy substitutions or inconsistent identities. + pub fn from_retained_bytes(bytes: &[u8]) -> Result { + recover_operation_installation(bytes) + } /// Returns Echo's installed-operation identity. #[must_use] pub const fn installed_operation_id(&self) -> InstalledEchoOperationIdV1 { @@ -2553,7 +2781,9 @@ impl InstalledEchoOperationV1 { self.program_id } - pub(crate) const fn program(&self) -> &EchoOperationProgramV1 { + /// Returns the installed current program kind and metadata. + #[must_use] + pub const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2573,14 +2803,14 @@ impl InstalledEchoOperationV1 { /// Returns the lawpack identity bound by the admitted package. #[must_use] - pub const fn lawpack_identity(&self) -> Hash { + pub const fn lawpack_identity(&self) -> Option { self.lawpack_identity } /// Returns the admission policy installed with the package. Every /// prepared Action shares it with the registry. #[must_use] - pub fn admission_policy(&self) -> &EchoOperationAdmissionPolicyV1 { + pub fn admission_policy(&self) -> &EchoOperationAdmissionPolicy { &self.admission_policy } @@ -2616,44 +2846,41 @@ impl EchoOperationInstallationErrorV1 { } } -pub(crate) fn admit_package_v1( - policy: &EchoOperationAdmissionPolicyV1, +pub(crate) fn admit_operation_package( + policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, -) -> Result { +) -> Result { let package_id = echo_operation_package_id_v1(&canonical_package_bytes); - if package_id != policy.expected_package_id { + if package_id.as_hash() != policy.expected_package_id { return Err(admission_error( - EchoOperationAdmissionErrorKindV1::PackageIdentityMismatch, + EchoOperationAdmissionErrorKind::PackageIdentityMismatch, "exact package bytes differ from the independently pinned package id", )); } - let package = ExecutableOperationPackageV1::from_canonical_bytes(&canonical_package_bytes) - .map_err(|artifact| EchoOperationAdmissionErrorV1 { - kind: EchoOperationAdmissionErrorKindV1::ArtifactInvalid, + let package = ExecutableOperationPackage::from_canonical_bytes(&canonical_package_bytes) + .map_err(|artifact| EchoOperationAdmissionError { + kind: EchoOperationAdmissionErrorKind::ArtifactInvalid, detail: artifact.to_string(), artifact: Some(artifact), })?; - if package.operation_coordinate != policy.expected_operation_coordinate { - return Err(admission_error( - EchoOperationAdmissionErrorKindV1::OperationCoordinateMismatch, - "package operation coordinate differs from admission policy", - )); - } - if package.authority_profile_identity != policy.expected_authority_profile_identity { - return Err(admission_error( - EchoOperationAdmissionErrorKindV1::AuthorityProfileMismatch, - "package authority profile differs from admission policy", - )); - } - if !package.budget_ceiling.fits_within(policy.maximum_budget) { - return Err(admission_error( - EchoOperationAdmissionErrorKindV1::BudgetExceedsPolicy, - "package budget ceiling exceeds admission policy", - )); - } + policy.validate_contract( + &package.operation_coordinate, + package.authority_profile_identity, + package.budget_ceiling, + )?; + let program_id = + package + .program + .identity() + .map_err(|artifact| EchoOperationAdmissionError { + kind: EchoOperationAdmissionErrorKind::ArtifactInvalid, + detail: artifact.to_string(), + artifact: Some(artifact), + })?; let admission_policy_id = policy.identity(); - Ok(AdmittedExecutableOperationPackageV1 { + Ok(AdmittedExecutableOperationPackage { package, + program_id, canonical_package_bytes, package_id, admission_policy_id, @@ -2663,10 +2890,10 @@ pub(crate) fn admit_package_v1( } pub(crate) fn installed_from_admitted( - admitted: AdmittedExecutableOperationPackageV1, -) -> Result { + admitted: AdmittedExecutableOperationPackage, +) -> InstalledEchoOperation { let package = admitted.package; - let mut installed = InstalledEchoOperationV1 { + let mut installed = InstalledEchoOperation { installed_operation_id: InstalledEchoOperationIdV1([0; 32]), package_id: admitted.package_id, package_admission_id: admitted.admission_id, @@ -2679,7 +2906,7 @@ pub(crate) fn installed_from_admitted( authority_profile_identity: package.authority_profile_identity, application_basis_schema_identity: package.application_basis_schema_identity, budget_ceiling: package.budget_ceiling, - program_id: package.program.identity()?, + program_id: admitted.program_id, program: package.program, application_result_projection: package .application_result_projection @@ -2689,11 +2916,11 @@ pub(crate) fn installed_from_admitted( admission_policy: std::sync::Arc::new(admitted.admission_policy), }; installed.installed_operation_id = installed_operation_id(&installed); - Ok(installed) + installed } -pub(crate) fn retain_installation_v1( - installed: &InstalledEchoOperationV1, +pub(crate) fn retain_operation_installation( + installed: &InstalledEchoOperation, ) -> Result, EchoOperationArtifactErrorV1> { encode_canonical_cbor_v1(&map_value([ ("admission_policy", installed.admission_policy.to_value()), @@ -2719,9 +2946,9 @@ pub(crate) fn retain_installation_v1( .map_err(canonical_error) } -pub(crate) fn recover_installation_v1( +pub(crate) fn recover_operation_installation( bytes: &[u8], -) -> Result { +) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; let mut fields = exact_text_map( value, @@ -2744,7 +2971,7 @@ pub(crate) fn recover_installation_v1( )); } let admission_policy = - EchoOperationAdmissionPolicyV1::from_value(take_field(&mut fields, "admission_policy")?)?; + EchoOperationAdmissionPolicy::from_value(take_field(&mut fields, "admission_policy")?)?; let admission_policy_id = take_hash(&mut fields, "admission_policy_id")?; if admission_policy.identity() != admission_policy_id { return Err(invalid_structure( @@ -2760,7 +2987,7 @@ pub(crate) fn recover_installation_v1( )); } let admitted = - admit_package_v1(&admission_policy, canonical_package_bytes).map_err(|error| { + admit_operation_package(&admission_policy, canonical_package_bytes).map_err(|error| { invalid_structure(format!( "retained executable-operation package no longer admits: {error}" )) @@ -2770,7 +2997,7 @@ pub(crate) fn recover_installation_v1( "retained package-admission evidence does not match re-admission", )); } - let installed = installed_from_admitted(admitted)?; + let installed = installed_from_admitted(admitted); let retained_installed_operation_id = InstalledEchoOperationIdV1(take_hash(&mut fields, "installed_operation_id")?); if retained_installed_operation_id != installed.installed_operation_id { @@ -2778,7 +3005,7 @@ pub(crate) fn recover_installation_v1( "retained installed-operation identity does not match exact installation", )); } - if retain_installation_v1(&installed)? != bytes { + if retain_operation_installation(&installed)? != bytes { return Err(artifact_error( EchoOperationArtifactErrorKindV1::NonCanonical, "installation did not reproduce the exact retained bytes", @@ -2787,11 +3014,11 @@ pub(crate) fn recover_installation_v1( Ok(installed) } -pub(crate) fn install_recovered_v1( - packages: &mut BTreeMap, +pub(crate) fn restore_operation_catalog_entry( + packages: &mut BTreeMap, operations: &mut BTreeMap, - installed: InstalledEchoOperationV1, -) -> Result { + installed: InstalledEchoOperation, +) -> Result { if let Some(existing) = packages.get(&installed.package_id) { if existing == &installed { return Ok(existing.clone()); @@ -2964,7 +3191,9 @@ impl EchoOperationEvaluationBasisV1 { ]) } - fn from_value(value: CanonicalValueV1) -> Result { + pub(crate) fn from_value( + value: CanonicalValueV1, + ) -> Result { let mut fields = exact_text_map( value, &[ @@ -3004,8 +3233,12 @@ impl EchoOperationEvaluationBasisV1 { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum EchoOperationInvocationKindV1 { - AnchoredNodeAttachmentCompareAndSet { expected_value_digest: Hash }, +enum EchoOperationInvocationKind { + #[cfg(feature = "trusted_runtime")] + EdictEffect, + AnchoredNodeAttachmentCompareAndSet { + expected_value_digest: Hash, + }, AnchoredNodeAttachmentCreateIfAbsent, } @@ -3017,20 +3250,20 @@ enum AnchoredNodeOperationModeV1 { /// Canonical basis-bearing invocation emitted by a generated client/helper. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct EchoOperationInvocationV1 { +pub struct EchoOperationInvocation { observation: Option, package_id: EchoOperationPackageIdV1, operation_coordinate: String, evaluation_basis: EchoOperationEvaluationBasisV1, authority_grant_identity: Hash, delegated_budget: EchoOperationBudgetV1, - node: NodeKey, - kind: EchoOperationInvocationKindV1, + node: Option, + kind: EchoOperationInvocationKind, replacement_bytes: Vec, application_input_bytes: Option>, } -impl EchoOperationInvocationV1 { +impl EchoOperationInvocation { /// Creates an invocation for the update-only compare-and-set program. #[allow(clippy::too_many_arguments)] #[must_use] @@ -3050,8 +3283,8 @@ impl EchoOperationInvocationV1 { evaluation_basis, authority_grant_identity, delegated_budget, - node, - kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + node: Some(node), + kind: EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest, }, replacement_bytes, @@ -3084,8 +3317,8 @@ impl EchoOperationInvocationV1 { evaluation_basis, authority_grant_identity, delegated_budget, - node, - kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + node: Some(node), + kind: EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest, }, replacement_bytes, @@ -3112,8 +3345,8 @@ impl EchoOperationInvocationV1 { evaluation_basis, authority_grant_identity, delegated_budget, - node, - kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, + node: Some(node), + kind: EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent, replacement_bytes, application_input_bytes: None, observation: None, @@ -3140,14 +3373,121 @@ impl EchoOperationInvocationV1 { evaluation_basis, authority_grant_identity, delegated_budget, - node, - kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, + node: Some(node), + kind: EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent, replacement_bytes, application_input_bytes: Some(canonical_application_input_bytes), observation: None, } } + /// The fixed-operation anchor, absent for authored effects. + #[must_use] + pub const fn anchor_node(&self) -> Option { + self.node + } + /// Exact canonical application input, when required by the program. + #[must_use] + pub fn application_input_bytes(&self) -> Option<&[u8]> { + self.application_input_bytes.as_deref() + } + /// Claimed exact parent, corroborated only by owner admission. + #[must_use] + pub const fn evaluation_basis(&self) -> EchoOperationEvaluationBasisV1 { + self.evaluation_basis + } + /// Claimed grant, corroborated only by owner admission. + #[must_use] + pub const fn authority_grant_identity(&self) -> Hash { + self.authority_grant_identity + } + /// Requested resource budget. + #[must_use] + pub const fn delegated_budget(&self) -> EchoOperationBudgetV1 { + self.delegated_budget + } + + /// Prepares current invocation bytes against the selected publication. + /// This establishes static binding only, not a grant or runtime freshness. + /// # Errors + /// Refuses malformed bytes, inconsistent input or an exceeded package budget. + #[cfg(feature = "trusted_runtime")] + pub fn prepare( + bytes: &[u8], + package: &ExecutableOperationPackage, + ) -> Result { + let invocation = Self::from_canonical_bytes(bytes)?; + if invocation.package_id != echo_operation_package_id_v1(&package.to_canonical_bytes()?) + || invocation.operation_coordinate != package.operation_coordinate() + || !package.program().accepts_invocation_kind(invocation.kind) + || !invocation + .delegated_budget + .fits_within(package.budget_ceiling()) + || !package + .program() + .minimum_budget() + .fits_within(invocation.delegated_budget) + { + return Err(invalid_structure( + "invocation differs from the selected package contract", + )); + } + invocation.validate_input_contract( + package.program(), + package.application_result_projection.as_ref(), + )?; + Ok(invocation) + } + + fn validate_input_contract( + &self, + program: &EchoOperationProgram, + projection: Option<&EchoOperationApplicationResultProjectionV1>, + ) -> Result<(), EchoOperationArtifactErrorV1> { + #[cfg(feature = "trusted_runtime")] + if let EchoOperationProgram::EdictEffect(effect) = program { + let input = self + .application_input_bytes + .as_deref() + .ok_or_else(|| invalid_structure("effect invocation requires canonical input"))?; + if u64::try_from(input.len()).map_err(|_| invalid_structure("input length overflow"))? + > effect.max_input_bytes() + || self.evaluation_basis.application_basis != self.effect_application_basis()? + { + return Err(invalid_structure( + "effect input exceeds its package ceiling or differs from its basis", + )); + } + return Ok(()); + } + #[cfg(not(feature = "trusted_runtime"))] + let _ = program; + match (projection, self.application_input_bytes.as_deref()) { + (Some(projection), Some(input)) => projection.validate_application_input_binding( + input, + self.node + .ok_or_else(|| invalid_structure("projected invocation requires an anchor"))?, + &self.replacement_bytes, + self.kind, + ), + (None, None) => Ok(()), + _ => Err(invalid_structure( + "application input does not match the installed result projection", + )), + } + } + + #[cfg(feature = "trusted_runtime")] + fn effect_application_basis( + &self, + ) -> Result { + let input = self + .application_input_bytes + .as_deref() + .ok_or_else(|| invalid_structure("effect input is absent"))?; + Ok(effect_input_basis(input)) + } + /// Returns the canonical invocation identity. pub fn identity(&self) -> Result { Ok(EchoOperationInvocationIdV1(domain_hash( @@ -3183,67 +3523,74 @@ impl EchoOperationInvocationV1 { ])) .map_err(canonical_error); } - let common = |schema| { - [ - ( - "authority_grant_identity", - hash_value(self.authority_grant_identity), - ), - ("delegated_budget", self.delegated_budget.to_value()), - ("evaluation_basis", self.evaluation_basis.to_value()), - ("node_id", hash_value(self.node.local_id.0)), - ( - "operation_coordinate", - text_value(&self.operation_coordinate), - ), - ("package_id", hash_value(self.package_id.as_hash())), - ( - "replacement_bytes", - CanonicalValueV1::Bytes(self.replacement_bytes.clone()), - ), - ("schema", text_value(schema)), - ("warp_id", hash_value(self.node.warp_id.0)), - ] - }; - let value = match self.kind { - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { - expected_value_digest, - } => { - let schema = if self.application_input_bytes.is_some() { - PROJECTED_CAS_INVOCATION_SCHEMA - } else { - INVOCATION_SCHEMA - }; - let mut fields = Vec::from(common(schema)); - fields.push(("expected_value_digest", hash_value(expected_value_digest))); - self.append_application_input(&mut fields)?; - CanonicalValueV1::Map( - fields - .into_iter() - .map(|(key, value)| (text_value(key), value)) - .collect(), - ) + let mut fields = vec![ + ("schema", text_value(INVOCATION_SCHEMA)), + ( + "authority_grant_identity", + hash_value(self.authority_grant_identity), + ), + ("delegated_budget", self.delegated_budget.to_value()), + ("evaluation_basis", self.evaluation_basis.to_value()), + ( + "operation_coordinate", + text_value(&self.operation_coordinate), + ), + ("package_id", hash_value(self.package_id.as_hash())), + ]; + match self.kind { + #[cfg(feature = "trusted_runtime")] + EchoOperationInvocationKind::EdictEffect => { + if self.node.is_some() + || !self.replacement_bytes.is_empty() + || self.application_input_bytes.is_none() + || self.observation.is_some() + { + return Err(invalid_structure( + "effect invocation requires input and has no fixed anchor", + )); + } + fields.push(("kind", text_value("edict-effect-program/v1"))); } - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent => { - let schema = if self.application_input_bytes.is_some() { - PROJECTED_CREATE_INVOCATION_SCHEMA - } else { - CREATE_INVOCATION_SCHEMA - }; - let mut fields = Vec::from(common(schema)); - fields.push(( - "absence_precondition", - text_value(CREATE_ABSENCE_PRECONDITION), - )); - self.append_application_input(&mut fields)?; - CanonicalValueV1::Map( - fields - .into_iter() - .map(|(key, value)| (text_value(key), value)) - .collect(), - ) + kind => { + let node = self + .node + .ok_or_else(|| invalid_structure("fixed invocation requires an anchor"))?; + fields.extend([ + ("node_id", hash_value(node.local_id.0)), + ("warp_id", hash_value(node.warp_id.0)), + ( + "replacement_bytes", + CanonicalValueV1::Bytes(self.replacement_bytes.clone()), + ), + ]); + match kind { + EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { + expected_value_digest, + } => { + fields.push(("kind", text_value(PROGRAM_KIND))); + fields.push(("expected_value_digest", hash_value(expected_value_digest))); + } + EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent => { + fields.push(("kind", text_value(CREATE_PROGRAM_KIND))); + fields.push(( + "absence_precondition", + text_value(CREATE_ABSENCE_PRECONDITION), + )); + } + #[cfg(feature = "trusted_runtime")] + EchoOperationInvocationKind::EdictEffect => { + return Err(invalid_structure("effect invocation is not anchored")) + } + } } - }; + } + self.append_application_input(&mut fields)?; + let value = CanonicalValueV1::Map( + fields + .into_iter() + .map(|(key, value)| (text_value(key), value)) + .collect(), + ); encode_canonical_cbor_v1(&value).map_err(canonical_error) } @@ -3275,7 +3622,8 @@ impl EchoOperationInvocationV1 { Ok(()) } - pub(crate) fn from_canonical_bytes(bytes: &[u8]) -> Result { + /// Decodes the sole current invocation format and rejects extra fields. + pub fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; let schema = match &value { CanonicalValueV1::Map(entries) => entries @@ -3302,6 +3650,11 @@ impl EchoOperationInvocationV1 { } } let mut invocation = Self::from_canonical_bytes(&inner_bytes)?; + if invocation.node.is_none() { + return Err(invalid_structure( + "effect observation wrappers are not executable", + )); + } invocation.observation = Some(EchoOperationObservationV1::from_value(take_field( &mut fields, "observation", @@ -3311,100 +3664,67 @@ impl EchoOperationInvocationV1 { } return Ok(invocation); } - let (expected_fields, create_if_absent, projected) = match schema { - INVOCATION_SCHEMA => ( - &[ - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "expected_value_digest", - "node_id", - "operation_coordinate", - "package_id", - "replacement_bytes", - "schema", - "warp_id", - ][..], - false, - false, - ), - CREATE_INVOCATION_SCHEMA => ( - &[ - "absence_precondition", - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "node_id", - "operation_coordinate", - "package_id", - "replacement_bytes", - "schema", - "warp_id", - ][..], - true, - false, - ), - PROJECTED_CAS_INVOCATION_SCHEMA => ( - &[ - "application_input_bytes", - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "expected_value_digest", - "node_id", - "operation_coordinate", - "package_id", - "replacement_bytes", - "schema", - "warp_id", - ][..], - false, - true, - ), - PROJECTED_CREATE_INVOCATION_SCHEMA => ( - &[ - "absence_precondition", - "application_input_bytes", - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "node_id", - "operation_coordinate", - "package_id", - "replacement_bytes", - "schema", - "warp_id", - ][..], - true, - true, - ), - _ => return Err(invalid_structure("unsupported invocation schema")), + if schema != INVOCATION_SCHEMA { + return Err(invalid_structure("unsupported invocation schema")); + } + let CanonicalValueV1::Map(entries) = &value else { + return Err(invalid_structure("invocation must be a map")); }; - let mut fields = exact_text_map(value, expected_fields)?; - require_text( - &mut fields, + let projected = entries + .iter() + .any(|(key, _)| key == &text_value("application_input_bytes")); + let kind_text = entries + .iter() + .find_map(|(key, value)| (key == &text_value("kind")).then_some(value)); + let mut expected_fields = vec![ "schema", - if projected && create_if_absent { - PROJECTED_CREATE_INVOCATION_SCHEMA - } else if projected { - PROJECTED_CAS_INVOCATION_SCHEMA - } else if create_if_absent { - CREATE_INVOCATION_SCHEMA - } else { - INVOCATION_SCHEMA - }, - )?; - let kind = if create_if_absent { - require_text( - &mut fields, - "absence_precondition", - CREATE_ABSENCE_PRECONDITION, - )?; - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent - } else { - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + "kind", + "authority_grant_identity", + "delegated_budget", + "evaluation_basis", + "operation_coordinate", + "package_id", + ]; + let anchored = match kind_text { + Some(CanonicalValueV1::Text(kind)) if kind == PROGRAM_KIND => { + expected_fields.push("expected_value_digest"); + true + } + Some(CanonicalValueV1::Text(kind)) if kind == CREATE_PROGRAM_KIND => { + expected_fields.push("absence_precondition"); + true + } + #[cfg(feature = "trusted_runtime")] + Some(CanonicalValueV1::Text(kind)) + if kind == "edict-effect-program/v1" && projected => + { + false + } + _ => return Err(invalid_structure("unsupported invocation kind")), + }; + if anchored { + expected_fields.extend(["node_id", "warp_id", "replacement_bytes"]); + } + if projected { + expected_fields.push("application_input_bytes"); + } + let mut fields = exact_text_map(value, &expected_fields)?; + require_text(&mut fields, "schema", INVOCATION_SCHEMA)?; + let kind = match take_text(&mut fields, "kind")?.as_str() { + PROGRAM_KIND => EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: take_hash(&mut fields, "expected_value_digest")?, + }, + CREATE_PROGRAM_KIND => { + require_text( + &mut fields, + "absence_precondition", + CREATE_ABSENCE_PRECONDITION, + )?; + EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent } + #[cfg(feature = "trusted_runtime")] + "edict-effect-program/v1" => EchoOperationInvocationKind::EdictEffect, + _ => return Err(invalid_structure("unsupported invocation kind")), }; let invocation = Self { observation: None, @@ -3419,12 +3739,20 @@ impl EchoOperationInvocationV1 { &mut fields, "delegated_budget", )?)?, - node: NodeKey { - warp_id: crate::WarpId(take_hash(&mut fields, "warp_id")?), - local_id: crate::NodeId(take_hash(&mut fields, "node_id")?), + node: if anchored { + Some(NodeKey { + warp_id: crate::WarpId(take_hash(&mut fields, "warp_id")?), + local_id: crate::NodeId(take_hash(&mut fields, "node_id")?), + }) + } else { + None }, kind, - replacement_bytes: take_bytes(&mut fields, "replacement_bytes")?, + replacement_bytes: if anchored { + take_bytes(&mut fields, "replacement_bytes")? + } else { + Vec::new() + }, application_input_bytes: if projected { Some(take_bytes(&mut fields, "application_input_bytes")?) } else { @@ -3448,13 +3776,13 @@ impl EchoOperationInvocationV1 { /// Runtime-owner invocation policy, separate from authored invocation bytes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct EchoOperationInvocationAdmissionPolicyV1 { +pub struct EchoOperationInvocationAdmissionPolicy { authority_profile_identity: Hash, authority_grant_identity: Hash, maximum_delegated_budget: EchoOperationBudgetV1, } -impl EchoOperationInvocationAdmissionPolicyV1 { +impl EchoOperationInvocationAdmissionPolicy { /// Creates one independently supplied invocation policy. #[must_use] pub const fn new( @@ -3481,11 +3809,13 @@ impl EchoOperationInvocationAdmissionPolicyV1 { /// Stable invocation-admission refusal categories. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EchoOperationInvocationAdmissionErrorKindV1 { +pub enum EchoOperationInvocationAdmissionErrorKind { /// Invocation bytes were malformed or noncanonical. MalformedInvocation, /// No admitted executable operation package is installed under the claimed id. OperationUnavailable, + /// The selected program has no scheduler execution implementation yet. + ExecutionUnavailable, /// The invocation's public operation coordinate disagrees with the package. OperationCoordinateMismatch, /// The invocation schema does not match the installed program profile. @@ -3505,33 +3835,42 @@ pub enum EchoOperationInvocationAdmissionErrorKindV1 { /// One structured invocation-admission refusal. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("{kind:?}: {detail}")] -pub struct EchoOperationInvocationAdmissionErrorV1 { - kind: EchoOperationInvocationAdmissionErrorKindV1, +pub struct EchoOperationInvocationAdmissionError { + kind: EchoOperationInvocationAdmissionErrorKind, detail: String, } -impl EchoOperationInvocationAdmissionErrorV1 { +impl EchoOperationInvocationAdmissionError { /// Returns the stable refusal category. #[must_use] - pub const fn kind(&self) -> EchoOperationInvocationAdmissionErrorKindV1 { + pub const fn kind(&self) -> EchoOperationInvocationAdmissionErrorKind { self.kind } } /// Opaque evidence that Echo admitted an installed, basis-bearing invocation. #[derive(Clone, Debug)] -pub struct AdmittedEchoOperationInvocationV1 { - invocation: EchoOperationInvocationV1, +pub struct AdmittedEchoOperationInvocation { + invocation: EchoOperationInvocation, invocation_id: EchoOperationInvocationIdV1, canonical_invocation_bytes: Vec, admission_policy_id: Hash, admission_id: EchoOperationInvocationAdmissionIdV1, installed_operation_id: InstalledEchoOperationIdV1, evaluation_authority: EchoOperationEvaluationAuthorityV1, - admission_policy: EchoOperationInvocationAdmissionPolicyV1, + admission_policy: EchoOperationInvocationAdmissionPolicy, } -impl AdmittedEchoOperationInvocationV1 { +impl AdmittedEchoOperationInvocation { + pub(crate) fn is_owned_by(&self, authority: &EchoOperationEvaluationAuthorityV1) -> bool { + self.evaluation_authority.same_owner(authority) + } + /// The invocation bound to this runtime owner's admission evidence. + #[must_use] + pub const fn invocation(&self) -> &EchoOperationInvocation { + &self.invocation + } + pub(crate) const fn package_id(&self) -> EchoOperationPackageIdV1 { self.invocation.package_id } @@ -3540,7 +3879,7 @@ impl AdmittedEchoOperationInvocationV1 { self.invocation.evaluation_basis } - pub(crate) const fn scope(&self) -> NodeKey { + pub(crate) const fn scope(&self) -> Option { self.invocation.node } @@ -3554,18 +3893,18 @@ impl AdmittedEchoOperationInvocationV1 { } pub(crate) fn admit_invocation_v1( - installed: Option<&InstalledEchoOperationV1>, - policy: EchoOperationInvocationAdmissionPolicyV1, + installed: Option<&InstalledEchoOperation>, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], current_basis: EchoOperationEvaluationBasisV1, current_state: &WorldlineState, evaluation_authority: EchoOperationEvaluationAuthorityV1, -) -> Result { +) -> Result { let (invocation, installed) = admit_invocation_static_v1(installed, policy, canonical_invocation_bytes)?; if invocation.evaluation_basis != current_basis { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, "invocation does not name the current exact parent basis", )); } @@ -3576,7 +3915,7 @@ pub(crate) fn admit_invocation_v1( != installed.application_basis_schema_identity { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, "invocation application-basis schema differs from installed package", )); } @@ -3584,7 +3923,7 @@ pub(crate) fn admit_invocation_v1( current_application_basis(installed, &invocation, current_state)?; if invocation.evaluation_basis.application_basis != current_application_basis { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, "invocation application basis differs from Echo's current graph proposition", )); } @@ -3605,13 +3944,19 @@ pub(crate) fn admit_invocation_v1( /// earlier basis reach the evaluator and receive a typed `BasisChanged` /// obstruction instead of failing the host admission loop. pub(crate) fn admit_action_invocation_v1( - installed: Option<&InstalledEchoOperationV1>, - policy: EchoOperationInvocationAdmissionPolicyV1, + installed: Option<&InstalledEchoOperation>, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], evaluation_authority: EchoOperationEvaluationAuthorityV1, -) -> Result { +) -> Result { let (invocation, installed) = admit_invocation_static_v1(installed, policy, canonical_invocation_bytes)?; + if invocation.node.is_none() { + return Err(invocation_admission_error( + EchoOperationInvocationAdmissionErrorKind::ExecutionUnavailable, + "authored-effect Action execution is not implemented", + )); + } if invocation .evaluation_basis .application_basis @@ -3619,7 +3964,7 @@ pub(crate) fn admit_action_invocation_v1( != installed.application_basis_schema_identity { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, "invocation application-basis schema differs from installed package", )); } @@ -3633,72 +3978,58 @@ pub(crate) fn admit_action_invocation_v1( } fn admit_invocation_static_v1<'a>( - installed: Option<&'a InstalledEchoOperationV1>, - policy: EchoOperationInvocationAdmissionPolicyV1, + installed: Option<&'a InstalledEchoOperation>, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], ) -> Result< - (EchoOperationInvocationV1, &'a InstalledEchoOperationV1), - EchoOperationInvocationAdmissionErrorV1, + (EchoOperationInvocation, &'a InstalledEchoOperation), + EchoOperationInvocationAdmissionError, > { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) + let invocation = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes) .map_err(|error| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation, + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation, error.to_string(), ) })?; let installed = installed.ok_or_else(|| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::OperationUnavailable, + EchoOperationInvocationAdmissionErrorKind::OperationUnavailable, "claimed executable operation package is not installed", ) })?; if invocation.operation_coordinate != *installed.operation_coordinate { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::OperationCoordinateMismatch, + EchoOperationInvocationAdmissionErrorKind::OperationCoordinateMismatch, "invocation operation coordinate differs from installed package", )); } if !installed.program.accepts_invocation_kind(invocation.kind) { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::OperationProfileMismatch, + EchoOperationInvocationAdmissionErrorKind::OperationProfileMismatch, "invocation schema differs from the installed program profile", )); } - match ( - installed.application_result_projection.as_deref(), - invocation.application_input_bytes.as_deref(), - ) { - (Some(projection), Some(application_input_bytes)) => projection - .validate_application_input_binding( - application_input_bytes, - invocation.node, - &invocation.replacement_bytes, - invocation.kind, + invocation + .validate_input_contract( + &installed.program, + installed.application_result_projection.as_deref(), + ) + .map_err(|error| { + invocation_admission_error( + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch, + error.to_string(), ) - .map_err(|error| { - invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch, - error.to_string(), - ) - })?, - (None, None) => {} - _ => { - return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch, - "projected packages require projected invocations and legacy packages forbid them", - )) - } - } + })?; if policy.authority_profile_identity != installed.authority_profile_identity { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::AuthorityProfileMismatch, + EchoOperationInvocationAdmissionErrorKind::AuthorityProfileMismatch, "runtime authority profile differs from installed package", )); } if invocation.authority_grant_identity != policy.authority_grant_identity { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::AuthorityGrantMismatch, + EchoOperationInvocationAdmissionErrorKind::AuthorityGrantMismatch, "invocation authority grant was not admitted by runtime policy", )); } @@ -3714,7 +4045,7 @@ fn admit_invocation_static_v1<'a>( .fits_within(invocation.delegated_budget) { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded, + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded, "delegated budget is below the program minimum or exceeds an admitted ceiling", )); } @@ -3722,12 +4053,12 @@ fn admit_invocation_static_v1<'a>( } fn build_admitted_invocation_v1( - invocation: EchoOperationInvocationV1, - installed: &InstalledEchoOperationV1, - policy: EchoOperationInvocationAdmissionPolicyV1, + invocation: EchoOperationInvocation, + installed: &InstalledEchoOperation, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], evaluation_authority: EchoOperationEvaluationAuthorityV1, -) -> AdmittedEchoOperationInvocationV1 { +) -> AdmittedEchoOperationInvocation { let invocation_id = EchoOperationInvocationIdV1(domain_hash( INVOCATION_ID_DOMAIN, canonical_invocation_bytes, @@ -3735,7 +4066,7 @@ fn build_admitted_invocation_v1( let admission_policy_id = policy.identity(); let installed_operation_id = installed.installed_operation_id; let evaluation_basis_id = invocation.evaluation_basis.identity(); - AdmittedEchoOperationInvocationV1 { + AdmittedEchoOperationInvocation { invocation, invocation_id, canonical_invocation_bytes: canonical_invocation_bytes.to_vec(), @@ -3753,26 +4084,38 @@ fn build_admitted_invocation_v1( } fn current_application_basis( - installed: &InstalledEchoOperationV1, - invocation: &EchoOperationInvocationV1, + installed: &InstalledEchoOperation, + invocation: &EchoOperationInvocation, state: &WorldlineState, -) -> Result { +) -> Result { let basis_mismatch = |detail| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, detail, ) }; match installed.program { - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } => { + #[cfg(feature = "trusted_runtime")] + EchoOperationProgram::EdictEffect(_) => invocation + .effect_application_basis() + .map_err(|_| basis_mismatch("effect application input is absent")), + #[cfg(not(feature = "trusted_runtime"))] + EchoOperationProgram::EdictEffect(_) => Err(invocation_admission_error( + EchoOperationInvocationAdmissionErrorKind::ExecutionUnavailable, + "effect publication validation grants no invocation authority", + )), + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } => { + let node = invocation + .node + .ok_or_else(|| basis_mismatch("fixed invocation has no anchor"))?; let store = state - .store(&invocation.node.warp_id) + .store(&node.warp_id) .ok_or_else(|| basis_mismatch("application-basis warp is unavailable"))?; store - .node(&invocation.node.local_id) + .node(&node.local_id) .ok_or_else(|| basis_mismatch("application-basis node is unavailable"))?; let attachment = store - .node_attachment(&invocation.node.local_id) + .node_attachment(&node.local_id) .ok_or_else(|| basis_mismatch("application-basis attachment is unavailable"))?; let AttachmentValue::Atom(atom) = attachment else { return Err(basis_mismatch( @@ -3781,39 +4124,41 @@ fn current_application_basis( }; let atom_len = u64::try_from(atom.bytes.len()).map_err(|_| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded, + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded, "application-basis attachment length exceeds the v1 budget domain", ) })?; let required_read_bytes = 64_u64.checked_add(atom_len).ok_or_else(|| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded, + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded, "application-basis read requirement overflowed", ) })?; if required_read_bytes > invocation.delegated_budget.read_bytes { return Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded, + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded, "application-basis corroboration exceeds the delegated read budget", )); } Ok(echo_operation_anchored_node_application_basis_v1( - invocation.node, + node, atom.type_id, &atom.bytes, )) } - EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + let node = invocation + .node + .ok_or_else(|| basis_mismatch("fixed invocation has no anchor"))?; let store = state - .store(&invocation.node.warp_id) + .store(&node.warp_id) .ok_or_else(|| basis_mismatch("application-basis warp is unavailable"))?; let occupancy = EchoOperationAnchoredNodeOccupancyV1::from_presence( - store.node(&invocation.node.local_id).is_some(), - store.node_attachment(&invocation.node.local_id).is_some(), + store.node(&node.local_id).is_some(), + store.node_attachment(&node.local_id).is_some(), ); Ok(echo_operation_anchored_node_creation_application_basis_v1( - invocation.node, - occupancy, + node, occupancy, )) } } @@ -3843,22 +4188,25 @@ fn current_application_basis( /// Anything else keeps the whole-frontier rule, and the caller obstructs it /// with `BasisChanged`. `state` must be the state the Action commits onto. pub(crate) fn scoped_action_basis_holds_v1( - installed: &InstalledEchoOperationV1, - invocation: &EchoOperationInvocationV1, + installed: &InstalledEchoOperation, + invocation: &EchoOperationInvocation, current_head: WriterHeadKey, current_tick: WorldlineTick, state: &WorldlineState, ) -> bool { + let Some(node) = invocation.node else { + return false; + }; let origin = &invocation.evaluation_basis; let supported = matches!( installed.program, - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } - | EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } + | EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } ); supported && origin.writer_head == current_head && origin.worldline_tick < current_tick - && invocation.node.warp_id == state.root().warp_id + && node.warp_id == state.root().warp_id && invocation.observation.as_ref().is_none_or(|observation| { observation.basis().worldline_tick() <= origin.worldline_tick && observation @@ -3937,7 +4285,7 @@ pub(crate) fn extend_echo_operation_written_slots_v1( /// history per Action. pub(crate) fn echo_operation_origin_corroborated_v1( provenance: &crate::provenance_store::ProvenanceService, - invocation: &EchoOperationInvocationV1, + invocation: &EchoOperationInvocation, written: Option<&BTreeSet>, ) -> bool { use crate::provenance_store::ProvenanceStore as _; @@ -3970,7 +4318,7 @@ pub(crate) fn echo_operation_origin_corroborated_v1( )) }; retained - && untouched(invocation.node) + && invocation.node.is_some_and(untouched) && invocation .observation .as_ref() @@ -3980,7 +4328,7 @@ pub(crate) fn echo_operation_origin_corroborated_v1( /// [`echo_operation_origin_corroborated_v1`] for an admitted Action. pub(crate) fn admitted_origin_corroborated_v1( provenance: &crate::provenance_store::ProvenanceService, - admitted: &AdmittedEchoOperationInvocationV1, + admitted: &AdmittedEchoOperationInvocation, written: Option<&BTreeSet>, ) -> bool { echo_operation_origin_corroborated_v1(provenance, &admitted.invocation, written) @@ -3993,20 +4341,20 @@ pub(crate) fn recovered_origin_corroborated_v1( canonical_invocation_bytes: &[u8], written: Option<&BTreeSet>, ) -> bool { - EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes).is_ok_and( + EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes).is_ok_and( |invocation| echo_operation_origin_corroborated_v1(provenance, &invocation, written), ) } /// [`scoped_action_basis_holds_v1`] for a recovered canonical invocation. pub(crate) fn scoped_action_basis_holds_for_bytes_v1( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], current_head: WriterHeadKey, current_tick: WorldlineTick, state: &WorldlineState, ) -> bool { - EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes).is_ok_and( + EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes).is_ok_and( |invocation| { scoped_action_basis_holds_v1(installed, &invocation, current_head, current_tick, state) }, @@ -4014,14 +4362,14 @@ pub(crate) fn scoped_action_basis_holds_for_bytes_v1( } pub(crate) fn action_application_basis_matches_state_v1( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], state: &WorldlineState, -) -> Result { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) +) -> Result { + let invocation = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes) .map_err(|error| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation, + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation, error.to_string(), ) })?; @@ -4030,18 +4378,17 @@ pub(crate) fn action_application_basis_matches_state_v1( } pub(crate) fn action_admission_evidence_matches_v1( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], maximum_budget: EchoOperationBudgetV1, expected_policy_id: Hash, expected_admission_id: EchoOperationInvocationAdmissionIdV1, ) -> bool { - let Ok(invocation) = - EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) + let Ok(invocation) = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes) else { return false; }; - let policy = EchoOperationInvocationAdmissionPolicyV1::new( + let policy = EchoOperationInvocationAdmissionPolicy::new( installed.authority_profile_identity, invocation.authority_grant_identity, maximum_budget, @@ -4073,7 +4420,7 @@ pub(crate) fn action_preparation_identity_matches_v1( } pub(crate) fn reconstruct_action_evaluation_v1( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], maximum_budget: EchoOperationBudgetV1, expected_policy_id: Hash, @@ -4091,8 +4438,8 @@ pub(crate) fn reconstruct_action_evaluation_v1( return None; } let invocation = - EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes).ok()?; - let policy = EchoOperationInvocationAdmissionPolicyV1::new( + EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes).ok()?; + let policy = EchoOperationInvocationAdmissionPolicy::new( installed.authority_profile_identity, invocation.authority_grant_identity, maximum_budget, @@ -4120,7 +4467,7 @@ pub(crate) fn reconstruct_action_evaluation_v1( } pub(crate) fn reconstruct_action_preparation_v1( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], maximum_budget: EchoOperationBudgetV1, expected_policy_id: Hash, @@ -4146,12 +4493,12 @@ pub(crate) fn decode_invocation_route_v1( canonical_invocation_bytes: &[u8], ) -> Result< (EchoOperationPackageIdV1, EchoOperationEvaluationBasisV1), - EchoOperationInvocationAdmissionErrorV1, + EchoOperationInvocationAdmissionError, > { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) + let invocation = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes) .map_err(|error| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation, + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation, error.to_string(), ) })?; @@ -4168,17 +4515,22 @@ pub(crate) struct EchoOperationActionInvocationEvidenceV1 { pub(crate) fn inspect_action_invocation_v1( canonical_invocation_bytes: &[u8], -) -> Result { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) +) -> Result { + let invocation = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes) .map_err(|error| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation, + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation, error.to_string(), ) })?; Ok(EchoOperationActionInvocationEvidenceV1 { package_id: invocation.package_id, - scope: invocation.node, + scope: invocation.node.ok_or_else(|| { + invocation_admission_error( + EchoOperationInvocationAdmissionErrorKind::ExecutionUnavailable, + "authored-effect Action execution is not implemented", + ) + })?, evaluation_basis: invocation.evaluation_basis, invocation_id: EchoOperationInvocationIdV1(domain_hash( INVOCATION_ID_DOMAIN, @@ -4193,15 +4545,15 @@ pub(crate) fn inspect_action_invocation_v1( pub(crate) fn invocation_runtime_error( detail: impl Into, -) -> EchoOperationInvocationAdmissionErrorV1 { +) -> EchoOperationInvocationAdmissionError { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, detail, ) } pub(crate) fn runtime_basis_obstruction( - admitted: AdmittedEchoOperationInvocationV1, + admitted: AdmittedEchoOperationInvocation, ) -> EchoOperationPreparationV1 { EchoOperationPreparationV1::Obstructed(EchoOperationObstructionV1 { kind: EchoOperationObstructionKindV1::BasisChanged, @@ -4946,8 +5298,9 @@ pub enum EchoOperationPreparationV1 { /// Complete private evaluation, bound to all substitution and resource evidence. #[derive(Clone, Debug)] pub struct PreparedEchoOperationV1 { - installed: InstalledEchoOperationV1, - invocation: EchoOperationInvocationV1, + scope: NodeKey, + installed: InstalledEchoOperation, + invocation: EchoOperationInvocation, invocation_id: EchoOperationInvocationIdV1, canonical_invocation_bytes: Vec, invocation_admission_policy_id: Hash, @@ -4971,7 +5324,7 @@ impl PreparedEchoOperationV1 { /// Returns the installed operation this preparation evaluated. It shares /// the installed package's bytes and projection with the registry. #[must_use] - pub const fn installed(&self) -> &InstalledEchoOperationV1 { + pub const fn installed(&self) -> &InstalledEchoOperation { &self.installed } @@ -5083,8 +5436,8 @@ impl PreparedEchoOperationV1 { } pub(crate) fn prepare_operation_v1( - installed: Option<&InstalledEchoOperationV1>, - admitted: AdmittedEchoOperationInvocationV1, + installed: Option<&InstalledEchoOperation>, + admitted: AdmittedEchoOperationInvocation, current_basis: EchoOperationEvaluationBasisV1, state: &WorldlineState, policy_id: u32, @@ -5139,12 +5492,12 @@ pub(crate) fn prepare_operation_v1( let (required_node_type, required_attachment_type, max_replacement_bytes, mode) = match (&installed.program, admitted.invocation.kind) { ( - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { required_node_type, required_attachment_type, max_replacement_bytes, }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest, }, ) => ( @@ -5156,12 +5509,12 @@ pub(crate) fn prepare_operation_v1( }, ), ( - EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { required_node_type, required_attachment_type, max_replacement_bytes, }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, + EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent, ) => ( *required_node_type, *required_attachment_type, @@ -5176,7 +5529,9 @@ pub(crate) fn prepare_operation_v1( if replacement_len > max_replacement_bytes { return obstruction(EchoOperationObstructionKindV1::ReplacementTooLarge); } - let node = admitted.invocation.node; + let Some(node) = admitted.invocation.node else { + return obstruction(EchoOperationObstructionKindV1::OperationUnavailable); + }; let mut actual_footprint = Footprint::default(); let mut budget_meter = EchoOperationBudgetMeterV1::new(admitted.invocation.delegated_budget); if let Some(observation) = &admitted.invocation.observation { @@ -5354,7 +5709,7 @@ pub(crate) fn prepare_operation_v1( }; let result_id = operation_result_id( installed, - &admitted.invocation, + node, mode, replacement_value_digest, patch.digest(), @@ -5383,6 +5738,7 @@ pub(crate) fn prepare_operation_v1( // receipt records that parent in its Tick fields. let origin_basis = admitted.invocation.evaluation_basis; EchoOperationPreparationV1::Prepared(Box::new(PreparedEchoOperationV1 { + scope: node, installed: installed.clone(), invocation: admitted.invocation, invocation_id, @@ -5426,7 +5782,7 @@ pub struct EchoOperationReceiptV1 { installed_operation_id: InstalledEchoOperationIdV1, operation_coordinate: String, semantic_identity: Hash, - lawpack_identity: Hash, + lawpack_identity: Option, target_profile_identity: Hash, interpreter_profile_identity: Hash, intrinsic_profile_identity: Hash, @@ -5745,7 +6101,11 @@ impl EchoOperationReceiptV1 { hash_value(self.invocation_bytes_digest), ), ("invocation_id", hash_value(self.invocation_id.as_hash())), - ("lawpack_identity", hash_value(self.lawpack_identity)), + ( + "lawpack_identity", + self.lawpack_identity + .map_or(CanonicalValueV1::Null, hash_value), + ), ( "operation_coordinate", text_value(&self.operation_coordinate), @@ -5939,7 +6299,7 @@ impl EchoOperationReceiptV1 { )?), operation_coordinate: take_text(&mut fields, "operation_coordinate")?, semantic_identity: take_hash(&mut fields, "semantic_identity")?, - lawpack_identity: take_hash(&mut fields, "lawpack_identity")?, + lawpack_identity: take_optional_hash(&mut fields, "lawpack_identity")?, target_profile_identity: take_hash(&mut fields, "target_profile_identity")?, interpreter_profile_identity: take_hash(&mut fields, "interpreter_profile_identity")?, intrinsic_profile_identity: take_hash(&mut fields, "intrinsic_profile_identity")?, @@ -6055,7 +6415,7 @@ impl EchoOperationReceiptV1 { "operation receipt invocation-admission identity mismatch", )); } - let retained_invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let retained_invocation_policy = EchoOperationInvocationAdmissionPolicy::new( receipt.authority_profile_identity, receipt.authority_grant_identity, receipt.invocation_admission_maximum_budget, @@ -6250,7 +6610,7 @@ pub(crate) fn recover_committed_execution_receipt_v1( pub(crate) fn validate_receipt_installation_v1( receipt: &EchoOperationReceiptV1, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, ) -> Result<(), EchoOperationArtifactErrorV1> { let package_evidence_matches = receipt.package_id == installed.package_id && receipt.package_admission_id == installed.package_admission_id @@ -6305,10 +6665,10 @@ pub(crate) fn validate_receipt_installation_v1( pub(crate) fn validate_receipt_application_result_v1( receipt: &EchoOperationReceiptV1, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, canonical_invocation_bytes: &[u8], ) -> Result<(), EchoOperationArtifactErrorV1> { - let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes)?; + let invocation = EchoOperationInvocation::from_canonical_bytes(canonical_invocation_bytes)?; let result_matches = match ( installed.application_result_projection.as_deref(), receipt.prepared_application_result.as_ref(), @@ -6955,7 +7315,7 @@ pub(crate) fn commit_prepared_to_state( .checked_add(1) .ok_or(EchoOperationCommitErrorV1::TransactionCoordinateOverflow)?; let tx = TxId::from_raw(tx_raw); - let scope = prepared.invocation.node; + let scope = prepared.scope; let rule_id = prepared.installed.installed_operation_id.as_hash(); let tick_receipt = TickReceipt::new( tx, @@ -7111,7 +7471,15 @@ fn receipt_digest(receipt: &EchoOperationReceiptV1) -> Hash { hasher.update(&receipt.installed_operation_id.as_hash()); hash_len_bytes(&mut hasher, receipt.operation_coordinate.as_bytes()); hasher.update(&receipt.semantic_identity); - hasher.update(&receipt.lawpack_identity); + match receipt.lawpack_identity { + Some(identity) => { + hasher.update(&[1]); + hasher.update(&identity); + } + None => { + hasher.update(&[0]); + } + } hasher.update(&receipt.target_profile_identity); hasher.update(&receipt.interpreter_profile_identity); hasher.update(&receipt.intrinsic_profile_identity); @@ -7290,7 +7658,7 @@ fn package_admission_id( EchoOperationPackageAdmissionIdV1(hasher.finalize().into()) } -fn installed_operation_id(installed: &InstalledEchoOperationV1) -> InstalledEchoOperationIdV1 { +fn installed_operation_id(installed: &InstalledEchoOperation) -> InstalledEchoOperationIdV1 { let mut hasher = Hasher::new(); hasher.update(INSTALLATION_ID_DOMAIN); hasher.update(&installed.package_id.as_hash()); @@ -7317,8 +7685,8 @@ fn invocation_admission_id( } fn operation_result_id( - installed: &InstalledEchoOperationV1, - invocation: &EchoOperationInvocationV1, + installed: &InstalledEchoOperation, + node: NodeKey, mode: AnchoredNodeOperationModeV1, replacement_value_digest: Hash, patch_digest: Hash, @@ -7334,8 +7702,8 @@ fn operation_result_id( hasher.update(RESULT_ID_DOMAIN); hasher.update(&installed.installed_operation_id.as_hash()); hasher.update(&profile_digest(RESULT_SCHEMA)); - hasher.update(invocation.node.warp_id.as_bytes()); - hasher.update(invocation.node.local_id.as_bytes()); + hasher.update(node.warp_id.as_bytes()); + hasher.update(node.local_id.as_bytes()); hasher.update(&expected_value_digest); } AnchoredNodeOperationModeV1::CreateIfAbsent => { @@ -7346,8 +7714,8 @@ fn operation_result_id( }); hasher.update(&installed.installed_operation_id.as_hash()); hasher.update(&profile_digest(CREATE_RESULT_SCHEMA)); - hasher.update(invocation.node.warp_id.as_bytes()); - hasher.update(invocation.node.local_id.as_bytes()); + hasher.update(node.warp_id.as_bytes()); + hasher.update(node.local_id.as_bytes()); hasher.update(CREATE_RESULT_ABSENCE_PROPOSITION_DOMAIN); } } @@ -7658,6 +8026,15 @@ fn hash_port_set<'a>( } } +pub(crate) fn program_identity_bytes(bytes: &[u8]) -> Hash { + domain_hash(PROGRAM_ID_DOMAIN, bytes) +} +pub(crate) fn effect_input_basis(input: &[u8]) -> EchoOperationApplicationBasisV1 { + EchoOperationApplicationBasisV1::new( + profile_digest("echo.operation.basis.edict-effect/v1"), + domain_hash(b"echo:operation-edict-effect-basis:v1\0", input), + ) +} fn domain_hash(domain: &[u8], bytes: &[u8]) -> Hash { let mut hasher = Hasher::new(); hasher.update(domain); @@ -7681,7 +8058,7 @@ fn hash_budget(hasher: &mut Hasher, budget: EchoOperationBudgetV1) { hasher.update(&budget.write_bytes.to_le_bytes()); } -fn map_value(fields: [(&str, CanonicalValueV1); N]) -> CanonicalValueV1 { +pub(crate) fn map_value(fields: [(&str, CanonicalValueV1); N]) -> CanonicalValueV1 { CanonicalValueV1::Map( fields .into_iter() @@ -7836,15 +8213,15 @@ fn artifact_error( } } -fn invalid_structure(detail: impl Into) -> EchoOperationArtifactErrorV1 { +pub(crate) fn invalid_structure(detail: impl Into) -> EchoOperationArtifactErrorV1 { artifact_error(EchoOperationArtifactErrorKindV1::InvalidStructure, detail) } -fn admission_error( - kind: EchoOperationAdmissionErrorKindV1, +pub(crate) fn admission_error( + kind: EchoOperationAdmissionErrorKind, detail: impl Into, -) -> EchoOperationAdmissionErrorV1 { - EchoOperationAdmissionErrorV1 { +) -> EchoOperationAdmissionError { + EchoOperationAdmissionError { kind, detail: detail.into(), artifact: None, @@ -7852,16 +8229,16 @@ fn admission_error( } fn invocation_admission_error( - kind: EchoOperationInvocationAdmissionErrorKindV1, + kind: EchoOperationInvocationAdmissionErrorKind, detail: impl Into, -) -> EchoOperationInvocationAdmissionErrorV1 { - EchoOperationInvocationAdmissionErrorV1 { +) -> EchoOperationInvocationAdmissionError { + EchoOperationInvocationAdmissionError { kind, detail: detail.into(), } } -fn installation_error( +pub(crate) fn installation_error( kind: EchoOperationInstallationErrorKindV1, detail: impl Into, ) -> EchoOperationInstallationErrorV1 { @@ -7908,11 +8285,11 @@ mod tests { ]) } - fn projection_test_package(operation_coordinate: &str) -> ExecutableOperationPackageV1 { - ExecutableOperationPackageV1::new( + fn projection_test_package(operation_coordinate: &str) -> ExecutableOperationPackage { + ExecutableOperationPackage::new( operation_coordinate, "echo.fixture.ProjectionBinding.Obstruction/v1", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( digest(171), digest(172), digest(173), @@ -7925,7 +8302,7 @@ mod tests { echo_operation_create_if_absent_target_profile_identity_v1(), digest(177), EchoOperationBudgetV1::new(8, 1_024, 1_024), - EchoOperationProgramV1::anchored_node_attachment_create_if_absent( + EchoOperationProgram::anchored_node_attachment_create_if_absent( crate::make_type_id("projection-binding-node"), crate::make_type_id("projection-binding-atom"), 1_024, @@ -7985,11 +8362,11 @@ mod tests { pub(super) fn projected_create_fixture( max_output_bytes: u64, ) -> ( - InstalledEchoOperationV1, + InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, - EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, + EchoOperationInvocationAdmissionPolicy, + EchoOperationInvocation, Vec, ) { projected_create_fixture_with_key(max_output_bytes, "fixture-key", None) @@ -8000,11 +8377,11 @@ mod tests { key: &str, key_bound: Option, ) -> ( - InstalledEchoOperationV1, + InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, - EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, + EchoOperationInvocationAdmissionPolicy, + EchoOperationInvocation, Vec, ) { let operation_coordinate = "echo.fixture.ProjectedCreate.v1"; @@ -8053,10 +8430,10 @@ mod tests { let runtime_expression_bytes = encode_canonical_cbor_v1(&runtime_expression).expect("runtime expression encodes"); let authority_profile = digest(201); - let mut package = ExecutableOperationPackageV1::new( + let mut package = ExecutableOperationPackage::new( operation_coordinate, "echo.fixture.ProjectedCreate.Obstruction/v1", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( digest(202), digest(203), digest(204), @@ -8069,7 +8446,7 @@ mod tests { echo_operation_create_if_absent_target_profile_identity_v1(), authority_profile, EchoOperationBudgetV1::new(8, 1_024, 1_024), - EchoOperationProgramV1::anchored_node_attachment_create_if_absent( + EchoOperationProgram::anchored_node_attachment_create_if_absent( crate::make_type_id("projected-created-node"), crate::make_type_id("projected-created-atom"), 1_024, @@ -8091,9 +8468,9 @@ mod tests { let package_bytes = package.to_canonical_bytes().expect("package encodes"); let package_id = echo_operation_package_id_v1(&package_bytes); let installed = installed_from_admitted( - admit_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + admit_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile, EchoOperationBudgetV1::new(8, 1_024, 1_024), @@ -8101,8 +8478,7 @@ mod tests { package_bytes, ) .expect("package admits"), - ) - .expect("package installs"); + ); let warp_id = crate::make_warp_id("projected-create-warp"); let root_node = crate::make_node_id("projected-create-root"); @@ -8153,7 +8529,7 @@ mod tests { ); let authority_grant = digest(211); let invocation = - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( package_id, operation_coordinate, basis, @@ -8163,7 +8539,7 @@ mod tests { b"fixture-message".to_vec(), application_input_bytes.clone(), ); - let policy = EchoOperationInvocationAdmissionPolicyV1::new( + let policy = EchoOperationInvocationAdmissionPolicy::new( authority_profile, authority_grant, EchoOperationBudgetV1::new(8, 1_024, 1_024), @@ -8182,18 +8558,21 @@ mod tests { fn package_admission_rejects_projection_digest_binding_for_the_wrong_program() { let (installed, _, _, _, _, _) = projected_create_fixture(1_024); let create = - ExecutableOperationPackageV1::from_canonical_bytes(installed.canonical_package_bytes()) + ExecutableOperationPackage::from_canonical_bytes(installed.canonical_package_bytes()) .expect("valid create package"); for cas in [false, true] { let mut package = if cas { - ExecutableOperationPackageV1::new( + ExecutableOperationPackage::new( create.operation_coordinate.clone(), - create.obstruction_coordinate.clone(), + create + .obstruction_coordinate() + .expect("a fixed-profile package names its obstruction") + .to_owned(), create.semantic_closure.clone(), echo_operation_target_profile_identity_v1(), create.authority_profile_identity, create.budget_ceiling, - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("projected-created-node"), crate::make_type_id("projected-created-atom"), 1_024, @@ -8211,14 +8590,14 @@ mod tests { package.application_result_projection = Some(projection); let valid = package.to_canonical_bytes().expect("valid package encodes"); let policy_for = |bytes: &[u8]| { - EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(bytes), + EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(bytes).as_hash(), package.operation_coordinate.clone(), package.authority_profile_identity, package.budget_ceiling, ) }; - admit_package_v1(&policy_for(&valid), valid.clone()) + admit_operation_package(&policy_for(&valid), valid.clone()) .expect("matching projection admits"); let CanonicalValueV1::Map(mut fields) = decode_canonical_cbor_v1(&valid).expect("package") @@ -8240,11 +8619,11 @@ mod tests { } let invalid = encode_canonical_cbor_v1(&CanonicalValueV1::Map(fields)) .expect("invalid package encodes"); - let error = admit_package_v1(&policy_for(&invalid), invalid) + let error = admit_operation_package(&policy_for(&invalid), invalid) .expect_err("program-incompatible projection must refuse before installation"); assert_eq!( error.kind(), - EchoOperationAdmissionErrorKindV1::ArtifactInvalid + EchoOperationAdmissionErrorKind::ArtifactInvalid ); assert_eq!( error.artifact().expect("artifact failure").kind(), @@ -8257,16 +8636,19 @@ mod tests { fn package_admission_rejects_empty_or_overlapping_expected_digest_paths() { let (installed, _, _, _, _, _) = projected_create_fixture(1_024); let create = - ExecutableOperationPackageV1::from_canonical_bytes(installed.canonical_package_bytes()) + ExecutableOperationPackage::from_canonical_bytes(installed.canonical_package_bytes()) .expect("valid create package"); - let mut package = ExecutableOperationPackageV1::new( + let mut package = ExecutableOperationPackage::new( create.operation_coordinate.clone(), - create.obstruction_coordinate.clone(), + create + .obstruction_coordinate() + .expect("a fixed-profile package names its obstruction") + .to_owned(), create.semantic_closure.clone(), echo_operation_target_profile_identity_v1(), create.authority_profile_identity, create.budget_ceiling, - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("projected-created-node"), crate::make_type_id("projected-created-atom"), 1_024, @@ -8279,14 +8661,15 @@ mod tests { package.application_result_projection = Some(projection); let valid = package.to_canonical_bytes().expect("valid package encodes"); let policy_for = |bytes: &[u8]| { - EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(bytes), + EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(bytes).as_hash(), package.operation_coordinate.clone(), package.authority_profile_identity, package.budget_ceiling, ) }; - admit_package_v1(&policy_for(&valid), valid.clone()).expect("disjoint digest path admits"); + admit_operation_package(&policy_for(&valid), valid.clone()) + .expect("disjoint digest path admits"); let inside = |path: &[String]| { let mut extended = path.to_vec(); extended.push("inner".to_owned()); @@ -8313,11 +8696,11 @@ mod tests { } let invalid = encode_canonical_cbor_v1(&CanonicalValueV1::Map(fields)) .expect("invalid package encodes"); - let error = admit_package_v1(&policy_for(&invalid), invalid) + let error = admit_operation_package(&policy_for(&invalid), invalid) .expect_err("an unusable digest path must refuse before installation"); assert_eq!( error.kind(), - EchoOperationAdmissionErrorKindV1::ArtifactInvalid, + EchoOperationAdmissionErrorKind::ArtifactInvalid, "{path:?}" ); assert_eq!( @@ -8331,12 +8714,12 @@ mod tests { fn projected_cas_admission_requires_an_expected_digest_binding() { let (mut installed, _, _, policy, mut invocation, _) = projected_create_fixture(1_024); invocation.delegated_budget = EchoOperationBudgetV1::new(8, 1_024, 1_024); - installed.program = EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + installed.program = EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("projected-created-node"), crate::make_type_id("projected-created-atom"), 1_024, ); - invocation.kind = EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + invocation.kind = EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: digest(212), }; let bytes = invocation.to_canonical_bytes().expect("CAS encodes"); @@ -8344,7 +8727,7 @@ mod tests { .expect_err("projected CAS must bind its expected digest"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -8352,7 +8735,7 @@ mod tests { fn projected_cas_admission_rejects_substituted_expected_digest() { let (mut installed, _, _, policy, mut invocation, _) = projected_create_fixture(1_024); invocation.delegated_budget = EchoOperationBudgetV1::new(8, 1_024, 1_024); - installed.program = EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + installed.program = EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("projected-created-node"), crate::make_type_id("projected-created-atom"), 1_024, @@ -8381,7 +8764,7 @@ mod tests { ])) .expect("input encodes"), ); - invocation.kind = EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + invocation.kind = EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: digest(212), }; admit_invocation_static_v1( @@ -8390,7 +8773,7 @@ mod tests { &invocation.to_canonical_bytes().expect("CAS encodes"), ) .expect("exact expected digest admits at the static boundary"); - invocation.kind = EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + invocation.kind = EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: digest(213), }; let error = admit_invocation_static_v1( @@ -8401,21 +8784,21 @@ mod tests { .expect_err("substituted expected digest refuses"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } #[test] fn projected_cas_invocation_preserves_application_input_and_identity() { let (_, _, _, _, mut invocation, application_input) = projected_create_fixture(1_024); - invocation.kind = EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + invocation.kind = EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: digest(212), }; let encoded = invocation .to_canonical_bytes() .expect("projected CAS encodes"); - let decoded = EchoOperationInvocationV1::from_canonical_bytes(&encoded) - .expect("projected CAS decodes"); + let decoded = + EchoOperationInvocation::from_canonical_bytes(&encoded).expect("projected CAS decodes"); assert_eq!(decoded.application_input_bytes, Some(application_input)); assert_eq!(decoded.kind, invocation.kind); let projected_id = invocation.identity().expect("projected identity"); @@ -8429,13 +8812,13 @@ mod tests { #[test] fn projected_invocation_rejects_missing_or_rebound_application_input() { let (installed, state, basis, policy, invocation, _) = projected_create_fixture(1_024); - let legacy = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let legacy = EchoOperationInvocation::anchored_node_attachment_create_if_absent( invocation.package_id, invocation.operation_coordinate.clone(), basis, invocation.authority_grant_identity, invocation.delegated_budget, - invocation.node, + invocation.node.expect("anchored fixture"), invocation.replacement_bytes.clone(), ); let authority = EchoOperationEvaluationAuthorityV1::new(); @@ -8452,7 +8835,7 @@ mod tests { .expect_err("a projected package must reject an invocation without application input"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); let rebound_input = encode_canonical_cbor_v1(&map_value([ @@ -8461,13 +8844,13 @@ mod tests { ])) .expect("rebound input encodes"); let rebound = - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( invocation.package_id, invocation.operation_coordinate, basis, invocation.authority_grant_identity, invocation.delegated_budget, - invocation.node, + invocation.node.expect("anchored fixture"), invocation.replacement_bytes, rebound_input, ); @@ -8484,7 +8867,7 @@ mod tests { .expect_err("application input cannot retarget the graph consequence"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -8940,10 +9323,10 @@ mod tests { let operation_coordinate = "echo.fixture.DescendedCreateIfAbsent.v1"; let authority_profile = digest(40); - let package = ExecutableOperationPackageV1::new( + let package = ExecutableOperationPackage::new( operation_coordinate, "echo.fixture.DescendedCreateIfAbsent.AlreadyExists/v1", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( digest(41), digest(42), digest(43), @@ -8956,7 +9339,7 @@ mod tests { echo_operation_create_if_absent_target_profile_identity_v1(), authority_profile, EchoOperationBudgetV1::new(16, 4_096, 4_096), - EchoOperationProgramV1::anchored_node_attachment_create_if_absent( + EchoOperationProgram::anchored_node_attachment_create_if_absent( crate::make_type_id("operation-descended-created-node"), crate::make_type_id("operation-descended-created-atom"), 1_024, @@ -8965,9 +9348,9 @@ mod tests { let package_bytes = package.to_canonical_bytes().expect("package encodes"); let package_id = echo_operation_package_id_v1(&package_bytes); let installed = installed_from_admitted( - admit_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + admit_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile, EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -8975,8 +9358,7 @@ mod tests { package_bytes, ) .expect("package admits"), - ) - .expect("package installs"); + ); let writer_head = WriterHeadKey { worldline_id: crate::WorldlineId::from_bytes(digest(47)), @@ -8991,7 +9373,7 @@ mod tests { echo_operation_anchored_node_absent_application_basis_v1(target), ); let authority_grant = digest(50); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id, operation_coordinate, evaluation_basis, @@ -9011,7 +9393,7 @@ mod tests { &state, )); let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( authority_profile, authority_grant, EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -9081,7 +9463,7 @@ mod tests { echo_operation_anchored_node_absent_application_basis_v1(target), ); let budget_limited_invocation = - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id, operation_coordinate, budget_limited_basis, @@ -9092,7 +9474,7 @@ mod tests { ); let admitted = admit_invocation_v1( Some(&installed), - EchoOperationInvocationAdmissionPolicyV1::new( + EchoOperationInvocationAdmissionPolicy::new( authority_profile, authority_grant, EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -9125,7 +9507,7 @@ mod tests { #[test] fn public_artifact_encoders_refuse_values_their_decoders_refuse() { - let invalid_program = EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + let invalid_program = EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("invalid-program-node"), crate::make_type_id("invalid-program-atom"), 0, @@ -9153,17 +9535,16 @@ mod tests { digest(14), EchoOperationApplicationBasisV1::new(digest(15), digest(16)), ); - let invalid_invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( - EchoOperationPackageIdV1(digest(17)), - "echo.fixture.InvalidBudget.v1", - basis, - digest(18), - EchoOperationBudgetV1::new(0, 64, 32), - node, - digest(19), - Vec::new(), - ); + let invalid_invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( + EchoOperationPackageIdV1(digest(17)), + "echo.fixture.InvalidBudget.v1", + basis, + digest(18), + EchoOperationBudgetV1::new(0, 64, 32), + node, + digest(19), + Vec::new(), + ); let error = invalid_invocation .to_canonical_bytes() .expect_err("a zero-step budget must not produce invocation bytes"); @@ -9176,11 +9557,11 @@ mod tests { .expect_err("an invalid invocation must not mint a stable identity"); } - fn retained_fixture_installation() -> InstalledEchoOperationV1 { - let package = ExecutableOperationPackageV1::new( + fn retained_fixture_installation() -> InstalledEchoOperation { + let package = ExecutableOperationPackage::new( "echo.fixture.Retention.v1", "echo.fixture.Retention.Obstruction/v1", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( digest(1), digest(2), digest(3), @@ -9193,7 +9574,7 @@ mod tests { echo_operation_target_profile_identity_v1(), digest(7), EchoOperationBudgetV1::new(8, 512, 512), - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("retention-node"), crate::make_type_id("retention-atom"), 128, @@ -9201,14 +9582,13 @@ mod tests { ); let bytes = package.to_canonical_bytes().expect("package encodes"); let package_id = echo_operation_package_id_v1(&bytes); - let policy = EchoOperationAdmissionPolicyV1::exact( - package_id, + let policy = EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), "echo.fixture.Retention.v1", digest(7), EchoOperationBudgetV1::new(8, 512, 512), ); - installed_from_admitted(admit_package_v1(&policy, bytes).expect("package admits")) - .expect("an admitted program remains canonically encodable") + installed_from_admitted(admit_operation_package(&policy, bytes).expect("package admits")) } fn replace_map_field(bytes: &[u8], field_name: &str, replacement: CanonicalValueV1) -> Vec { @@ -9269,7 +9649,7 @@ mod tests { #[test] fn retained_installation_rejects_identity_substitution() { let installed = retained_fixture_installation(); - let bytes = retain_installation_v1(&installed).expect("installation retains"); + let bytes = retain_operation_installation(&installed).expect("installation retains"); for field_name in [ "admission_policy_id", "package_id", @@ -9281,7 +9661,7 @@ mod tests { field_name, CanonicalValueV1::Bytes(digest(99).to_vec()), ); - let error = recover_installation_v1(&substituted) + let error = recover_operation_installation(&substituted) .expect_err("retained installation identities cannot be substituted"); assert_eq!( error.kind(), @@ -9289,15 +9669,15 @@ mod tests { "field {field_name}" ); } - let substituted_policy = EchoOperationAdmissionPolicyV1::exact( - installed.package_id, + let substituted_policy = EchoOperationAdmissionPolicy::exact( + installed.package_id.as_hash(), installed.operation_coordinate.to_string(), installed.authority_profile_identity, EchoOperationBudgetV1::new(9, 512, 512), ); let substituted = replace_map_field(&bytes, "admission_policy", substituted_policy.to_value()); - let error = recover_installation_v1(&substituted) + let error = recover_operation_installation(&substituted) .expect_err("recovery must revalidate retained package-admission policy material"); assert_eq!( error.kind(), @@ -9324,7 +9704,7 @@ mod tests { ); let invocation_id = EchoOperationInvocationIdV1(digest(14)); let invocation_admission_maximum_budget = EchoOperationBudgetV1::new(8, 512, 512); - let invocation_admission_policy_id = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_admission_policy_id = EchoOperationInvocationAdmissionPolicy::new( installed.authority_profile_identity, digest(19), invocation_admission_maximum_budget, @@ -9415,6 +9795,26 @@ mod tests { receipt.receipt_digest = receipt_digest(&receipt); validate_receipt_installation_v1(&receipt, &installed) .expect("the retained receipt fixture matches its installation"); + let mut absent_lawpack = receipt.clone(); + absent_lawpack.lawpack_identity = None; + absent_lawpack.receipt_digest = receipt_digest(&absent_lawpack); + let mut zero_lawpack = receipt.clone(); + zero_lawpack.lawpack_identity = Some([0; 32]); + assert_ne!( + receipt_digest(&absent_lawpack), + receipt_digest(&zero_lawpack) + ); + let absent_bytes = absent_lawpack + .to_canonical_bytes() + .expect("nullable lawpack record"); + let decoded_absence = recover_committed_execution_receipt_v1(&absent_bytes) + .expect("nullable evidence is structurally valid"); + assert_eq!(decoded_absence.lawpack_identity, None); + assert!( + validate_receipt_installation_v1(&decoded_absence, &installed).is_err(), + "a self-consistent absent claim cannot replace the installed fixed lawpack" + ); + let mut mismatched_policy_receipt = receipt.clone(); mismatched_policy_receipt.invocation_admission_maximum_budget = EchoOperationBudgetV1::new(9, 512, 512); @@ -9555,7 +9955,7 @@ mod tests { // layout: a raw 32-byte previous digest with no variant tag. let installed = retained_fixture_installation(); let node = node_key_from_bytes([20; 64]); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), EchoOperationEvaluationBasisV1::new( @@ -9583,8 +9983,20 @@ mod tests { expected.update(RESULT_ID_DOMAIN); expected.update(&installed.installed_operation_id.as_hash()); expected.update(&profile_digest(RESULT_SCHEMA)); - expected.update(invocation.node.warp_id.as_bytes()); - expected.update(invocation.node.local_id.as_bytes()); + expected.update( + invocation + .node + .expect("anchored fixture") + .warp_id + .as_bytes(), + ); + expected.update( + invocation + .node + .expect("anchored fixture") + .local_id + .as_bytes(), + ); expected.update(&previous); expected.update(&replacement_value_digest); expected.update(&patch_digest); @@ -9593,7 +10005,7 @@ mod tests { assert_eq!( operation_result_id( &installed, - &invocation, + invocation.node.expect("anchored fixture"), AnchoredNodeOperationModeV1::CompareAndSet { expected_value_digest: previous, }, @@ -9607,10 +10019,10 @@ mod tests { } #[test] - fn legacy_compare_and_set_program_and_invocation_bytes_remain_fixed() { + fn current_compare_and_set_invocation_requires_its_program_kind() { let required_node_type = TypeId(digest(31)); let required_attachment_type = TypeId(digest(32)); - let program = EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + let program = EchoOperationProgram::anchored_node_attachment_compare_and_set( required_node_type, required_attachment_type, 1_024, @@ -9662,7 +10074,7 @@ mod tests { digest(37), EchoOperationApplicationBasisV1::new(digest(38), digest(39)), ); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( EchoOperationPackageIdV1(digest(40)), "echo.fixture.LegacyCompareAndSet.v1", basis, @@ -9673,6 +10085,7 @@ mod tests { b"after".to_vec(), ); let expected_invocation = encode_canonical_cbor_v1(&map_value([ + ("kind", text_value(PROGRAM_KIND)), ("authority_grant_identity", hash_value(digest(41))), ( "delegated_budget", @@ -9697,9 +10110,21 @@ mod tests { assert_eq!( invocation.to_canonical_bytes().expect("invocation encodes"), expected_invocation, - "the legacy update invocation bytes are a compatibility fixture" + "the current invocation carries its explicit program kind" ); + let mut historical = decode_canonical_cbor_v1(&expected_invocation).expect("fixture"); + let CanonicalValueV1::Map(ref mut entries) = historical else { + panic!("map"); + }; + entries.retain(|(key, _)| key != &text_value("kind")); + assert!( + EchoOperationInvocation::from_canonical_bytes( + &encode_canonical_cbor_v1(&historical).expect("old bytes") + ) + .is_err(), + "an old invocation cannot enter the current runtime" + ); let mut widened_value = decode_canonical_cbor_v1(&expected_invocation).expect("fixture decodes"); let CanonicalValueV1::Map(fields) = &mut widened_value else { @@ -9713,7 +10138,7 @@ mod tests { let widened_bytes = encode_canonical_cbor_v1(&widened_value).expect("mutated fixture re-encodes"); assert!( - EchoOperationInvocationV1::from_canonical_bytes(&widened_bytes).is_err(), + EchoOperationInvocation::from_canonical_bytes(&widened_bytes).is_err(), "null must never gain creation meaning under the legacy invocation schema" ); } @@ -9728,7 +10153,8 @@ mod tests { "🦀".repeat(65), ] { let mut invocation = original.clone(); - invocation.node.local_id = crate::NodeId(Sha256::digest(key.as_bytes()).into()); + invocation.node.as_mut().expect("anchored fixture").local_id = + crate::NodeId(Sha256::digest(key.as_bytes()).into()); invocation.application_input_bytes = Some( encode_canonical_cbor_v1(&map_value([ ("key", text_value(&key)), @@ -9774,7 +10200,7 @@ mod tests { ("🦀".repeat(65), false), (String::new(), false), ] { - let mut node = invocation.node; + let mut node = invocation.node.expect("anchored fixture"); node.local_id = crate::NodeId(Sha256::digest(key.as_bytes()).into()); let input = encode_canonical_cbor_v1(&map_value([ ("key", text_value(&key)), @@ -9878,9 +10304,9 @@ mod tests { let initial_root = state.state_root(); let initial_tick = state.current_tick(); assert!(state - .store(&invocation.node.warp_id) + .store(&invocation.node.expect("anchored fixture").warp_id) .expect("store exists") - .node(&invocation.node.local_id) + .node(&invocation.node.expect("anchored fixture").local_id) .is_none()); let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); let authority = EchoOperationEvaluationAuthorityV1::new(); @@ -9897,13 +10323,19 @@ mod tests { admitted .expect_err("out-of-bounds key refuses at admission") .kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); assert_eq!(state.state_root(), initial_root); assert_eq!(state.current_tick(), initial_tick); - let store = state.store(&invocation.node.warp_id).expect("store exists"); - assert!(store.node(&invocation.node.local_id).is_none()); - assert!(store.node_attachment(&invocation.node.local_id).is_none()); + let store = state + .store(&invocation.node.expect("anchored fixture").warp_id) + .expect("store exists"); + assert!(store + .node(&invocation.node.expect("anchored fixture").local_id) + .is_none()); + assert!(store + .node_attachment(&invocation.node.expect("anchored fixture").local_id) + .is_none()); continue; } let EchoOperationPreparationV1::Prepared(prepared) = prepare_operation_v1( @@ -9927,17 +10359,17 @@ mod tests { .expect("boundary key commits"); assert_eq!(state.current_tick().as_u64(), initial_tick.as_u64() + 1); let store = state - .store(&invocation.node.warp_id) + .store(&invocation.node.expect("anchored fixture").warp_id) .expect("committed store exists"); assert_eq!( store - .node(&invocation.node.local_id) + .node(&invocation.node.expect("anchored fixture").local_id) .expect("node created") .ty, crate::make_type_id("projected-created-node") ); let Some(AttachmentValue::Atom(atom)) = - store.node_attachment(&invocation.node.local_id) + store.node_attachment(&invocation.node.expect("anchored fixture").local_id) else { panic!("created attachment is an atom"); }; @@ -10003,12 +10435,18 @@ mod tests { .expect_err("malformed or substituted input refuses"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); assert_eq!(state.current_tick(), WorldlineTick::ZERO); - let store = state.store(&invocation.node.warp_id).expect("store exists"); - assert!(store.node(&invocation.node.local_id).is_none()); - assert!(store.node_attachment(&invocation.node.local_id).is_none()); + let store = state + .store(&invocation.node.expect("anchored fixture").warp_id) + .expect("store exists"); + assert!(store + .node(&invocation.node.expect("anchored fixture").local_id) + .is_none()); + assert!(store + .node_attachment(&invocation.node.expect("anchored fixture").local_id) + .is_none()); } } @@ -10058,12 +10496,18 @@ mod tests { .expect_err("wire decoder rejects invalid UTF8"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation ); assert_eq!(state.current_tick(), WorldlineTick::ZERO); - let store = state.store(&invocation.node.warp_id).expect("store exists"); - assert!(store.node(&invocation.node.local_id).is_none()); - assert!(store.node_attachment(&invocation.node.local_id).is_none()); + let store = state + .store(&invocation.node.expect("anchored fixture").warp_id) + .expect("store exists"); + assert!(store + .node(&invocation.node.expect("anchored fixture").local_id) + .is_none()); + assert!(store + .node_attachment(&invocation.node.expect("anchored fixture").local_id) + .is_none()); } #[test] diff --git a/crates/warp-core/src/echo_operation/observed.rs b/crates/warp-core/src/echo_operation/observed.rs index a7f27d1ab..82fecf645 100644 --- a/crates/warp-core/src/echo_operation/observed.rs +++ b/crates/warp-core/src/echo_operation/observed.rs @@ -245,7 +245,7 @@ impl EchoOperationObservationV1 { } } -impl EchoOperationInvocationV1 { +impl EchoOperationInvocation { pub(crate) fn has_observation(&self, observation: &EchoOperationObservationV1) -> bool { self.observation.as_ref() == Some(observation) } @@ -448,7 +448,7 @@ mod tests { let mut retry = invocation.clone(); retry.evaluation_basis.application_basis = echo_operation_anchored_node_creation_application_basis_v1( - invocation.node, + invocation.node.expect("anchored fixture"), EchoOperationAnchoredNodeOccupancyV1::NodeAndAttachment, ); retry.evaluation_basis.worldline_tick = WorldlineTick::from_raw(100); diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs new file mode 100644 index 000000000..3072660a0 --- /dev/null +++ b/crates/warp-core/src/edict_effects.rs @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Authored-effect preparation, private recording reads and current effect program preparation. +//! +//! Current publications and effect invocations can be prepared, and the runtime +//! owner can admit package policy and retain/install its catalog evidence. None +//! of those package crossings grants invocation rights or enables effect execution. +//! The common owner invocation API separately checks grants and exact parents. +//! Private frames bind immutable runtime frontiers, typed read laws and shared +//! logical meters. Private ordered read/let/require evaluation uses those frames. +//! Five mutation instructions buffer intents without modifying the frontier. +//! The runtime owner can validate mutations and derive a counterfactual patch at +//! one exact full parent. Physical arena guarantees, scheduler integration and +//! Action/receipt/WAL decision recovery remain unfinished. +mod compiled_program; +mod execution; +mod failure_names; +mod imported_types; +mod instruction_relation; +mod intrinsic_contract; +mod lowering_relation; +mod mutations; +pub use mutations::EffectMutation; +mod package; +mod runtime_abi; +pub use package::{EffectPackageError, EffectProgram}; +mod completion; +#[cfg(feature = "native_rule_bootstrap")] +pub(crate) mod derive; +mod value_digest; +#[cfg(feature = "native_rule_bootstrap")] +pub use derive::{DerivedEffectPatch, EffectDerivationError, EffectDerivationKind}; +mod frame; +pub use completion::{CompletedEffectEvaluation, EffectCompletionError}; +mod read_image; +pub use read_image::{ + EffectEdgePairRow, EffectReadImageError, EffectReadRow, EffectReadSetImage, EffectReadSetView, + EffectSlotRow, +}; +mod schema; +pub use schema::{EffectGraphSchema, EffectGraphSchemaError, EffectNodeAddressing}; +mod law; +mod typed_reads; +pub use law::{EffectFootprintLaw, EffectLawError}; +pub use typed_reads::TypedNodeRead; +mod node_reads; +pub use frame::{EffectEvaluationFrame, EffectFrameError, EffectFrameSetupError}; +pub(crate) use node_reads::{NodeAperture, RecordedNodeAccess}; + +use crate::edict_pure::evaluate::Meter; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::echo_operation::EchoOperationBudgetMeterV1; +use crate::edict_read::ReadBasis; +use crate::{ + EchoOperationBudgetV1, EdgeRecord, Footprint, OutgoingEdgeRange, SlotId, SlotReading, TypeId, + WorldlineFrontier, +}; + +/// Maximum declared value-storage reservation for this read primitive. +/// +/// This is a deterministic logical storage ceiling, not an OS memory limit. +pub const EFFECT_READ_STORAGE_CEILING_V1: u64 = 16 * 1024 * 1024; +/// Maximum emitted effects per whole authored operation, whether or not they write. +pub const EFFECT_COUNT_CEILING_V1: usize = 1024; +const OBSERVATION_STORAGE_BYTES: u64 = 128; +const EDGE_RECORD_STORAGE_BYTES: u64 = 128; + +/// Invalid trusted-host preparation, before any read is evaluated. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectReadSetupError { + /// The declared reservation exceeds the fixed read-storage ceiling. + #[error("declared read storage exceeds the fixed ceiling")] + AllocationLimitTooLarge, +} + +/// A read failure, without an Action outcome or any mutation authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectRangeReadError { + /// A resolved type or distinct read count is outside the selected read law. + #[error("range edge type is outside the footprint law")] + FootprintLawViolation, + /// No authorized root-to-target portal chain reaches the requested warp. + #[error("requested warp is unreachable from the bound root")] + UnreachableWarp, + /// Read work, I/O or storage cannot fit within the remaining limits. + #[error("range read exceeds its budget")] + BudgetExceeded, + /// Host storage could not be provisioned despite a successful reservation. + /// + /// This is a host fault. It must never become an Action obstruction or a + /// retained decision; an integrating host must stop the evaluation. + #[error("host could not provision reserved read storage")] + HostAllocationFailed, +} + +/// Immutable frontier plus an owned, runtime-recorded dependency transcript. +/// +/// Reads resolve by full WARP/source/type keys, including unreachable records. +/// The caller cannot supply or alter the transcript. Values are host-owned +/// materializations; this API does not claim a bounded interpreter arena. +#[derive(Debug)] +pub struct EffectRangeReadSession<'a> { + frontier: &'a WorldlineFrontier, + basis: ReadBasis, + allowed_types: BTreeSet, + max_range_edges: u64, + meter: EchoOperationBudgetMeterV1, + max_allocated_bytes: u64, + allocated_bytes: u64, + readings: BTreeMap, + footprint: Footprint, + law: Option, + authorized_reads: BTreeSet<(SlotId, TypeId)>, + validated_warps: BTreeSet, +} + +impl<'a> EffectRangeReadSession<'a> { + /// Prepares a trusted-host view, hashing the real frontier and indexing policy. + /// + /// This setup cost precedes the read budget; the immutable borrow binds all + /// subsequent observations to the same state. This is not package admission. + #[must_use] + pub fn new( + frontier: &'a WorldlineFrontier, + allowed_types: &[TypeId], + max_range_edges: u64, + budget: EchoOperationBudgetV1, + ) -> Self { + Self::prepare( + frontier, + allowed_types, + max_range_edges, + budget, + EFFECT_READ_STORAGE_CEILING_V1, + ) + } + + /// Prepares a view with an explicit value-storage reservation. + /// + /// # Errors + /// + /// Refuses a declaration above the fixed ceiling before preparing the basis. + pub fn with_allocation_limit( + frontier: &'a WorldlineFrontier, + allowed_types: &[TypeId], + max_range_edges: u64, + budget: EchoOperationBudgetV1, + max_allocated_bytes: u64, + ) -> Result { + if max_allocated_bytes > EFFECT_READ_STORAGE_CEILING_V1 { + return Err(EffectReadSetupError::AllocationLimitTooLarge); + } + Ok(Self::prepare( + frontier, + allowed_types, + max_range_edges, + budget, + max_allocated_bytes, + )) + } + + fn prepare( + frontier: &'a WorldlineFrontier, + allowed_types: &[TypeId], + max_range_edges: u64, + budget: EchoOperationBudgetV1, + max_allocated_bytes: u64, + ) -> Self { + Self { + frontier, + basis: ReadBasis::at(frontier), + allowed_types: allowed_types.iter().copied().collect(), + max_range_edges, + meter: EchoOperationBudgetMeterV1::new(budget), + max_allocated_bytes, + allocated_bytes: 0, + readings: BTreeMap::new(), + footprint: Footprint::default(), + law: None, + authorized_reads: BTreeSet::new(), + validated_warps: BTreeSet::new(), + } + } + + /// Resolves a typed outgoing range after authorizing and precharging its scan. + /// + /// Source presence costs one step and 64 read bytes, present or absent. + /// Every edge in the entire source bucket then costs one step and 64 read + /// bytes, including nonmatching types. A failed charge is never added. + /// Source absence is distinct from a present source with no matching edges. + /// + /// # Errors + /// + /// A forbidden type is refused before state lookup or charging. A scan that + /// exceeds the whole-bucket limit or remaining budget is refused before any + /// edge is inspected; its already-resolved node dependency remains visible. + /// Storage and I/O reservations are atomic. Host allocation failure is + /// separately reported as a host fault, never as budget exhaustion. + pub fn read( + &mut self, + range: OutgoingEdgeRange, + ) -> Result>, EffectRangeReadError> { + self.read_with_meter(range, None) + } + + fn read_with_meter( + &mut self, + range: OutgoingEdgeRange, + shared: Option<&mut Meter>, + ) -> Result>, EffectRangeReadError> { + self.read_range_inner(range, shared, None) + } + + fn read_range_inner( + &mut self, + range: OutgoingEdgeRange, + mut shared: Option<&mut Meter>, + absent_type: Option, + ) -> Result>, EffectRangeReadError> { + if !self.allowed_types.contains(&range.edge_type) { + return Err(EffectRangeReadError::FootprintLawViolation); + } + if absent_type.is_some() { + let law = self + .law + .as_ref() + .ok_or(EffectRangeReadError::FootprintLawViolation)?; + if !law.ranges.contains(&range.edge_type) { + return Err(EffectRangeReadError::FootprintLawViolation); + } + self.reachable( + range.source.warp_id, + shared + .as_deref_mut() + .ok_or(EffectRangeReadError::FootprintLawViolation)?, + )?; + } + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, shared.as_deref_mut()) { + return Err(EffectRangeReadError::BudgetExceeded); + } + let frontier = self.frontier; + let store = frontier.state().store(&range.source.warp_id); + let node = store.and_then(|store| store.node(&range.source.local_id)); + if let Some(absent_type) = absent_type { + self.authorize( + SlotId::Node(range.source), + node.map_or(absent_type, |node| node.ty), + typed_reads::ReadPermission::Node, + shared + .as_deref_mut() + .ok_or(EffectRangeReadError::FootprintLawViolation)?, + )?; + } + self.readings + .insert(SlotId::Node(range.source), SlotReading::Node(node.cloned())); + self.footprint.n_read.insert(range.source); + let bucket = store + .filter(|_| node.is_some()) + .map(|store| store.edges_from(&range.source.local_id)); + let capacity = bucket.as_ref().map_or(0, ExactSizeIterator::len); + let count = u64::try_from(capacity).map_err(|_| EffectRangeReadError::BudgetExceeded)?; + let bytes = count + .checked_mul(64) + .ok_or(EffectRangeReadError::BudgetExceeded)?; + let storage = count + .checked_mul(2 * EDGE_RECORD_STORAGE_BYTES) + .and_then(|rows| rows.checked_add(OBSERVATION_STORAGE_BYTES)) + .ok_or(EffectRangeReadError::BudgetExceeded)?; + let limit = self + .law + .as_ref() + .filter(|_| absent_type.is_some()) + .map_or(self.max_range_edges, |law| { + self.max_range_edges.min(law.max_range_edges) + }); + if count > limit || !self.charge(count, bytes, storage, shared.as_deref_mut()) { + return Err(EffectRangeReadError::BudgetExceeded); + } + if absent_type.is_some() { + self.retain_authorization( + SlotId::OutgoingEdges(range), + TypeId([0; 32]), + shared.ok_or(EffectRangeReadError::FootprintLawViolation)?, + )?; + } + let (value, observed) = if let Some(bucket) = bucket { + // Provision both worst-case row buffers before inspecting any edge. + // Host inability to provide these bytes is not a semantic refusal. + let mut rows = Vec::new(); + rows.try_reserve_exact(capacity) + .map_err(|_| EffectRangeReadError::HostAllocationFailed)?; + let mut observed = Vec::new(); + observed + .try_reserve_exact(capacity) + .map_err(|_| EffectRangeReadError::HostAllocationFailed)?; + rows.extend(bucket.filter(|edge| edge.ty == range.edge_type).cloned()); + // Edge IDs are unique; unstable sorting is canonical and allocates + // no additional temporary buffer. + rows.sort_unstable_by_key(|edge| edge.id); + observed.extend_from_slice(&rows); + (Some(rows), Some(observed)) + } else { + (None, None) + }; + self.readings.insert( + SlotId::OutgoingEdges(range), + SlotReading::OutgoingEdges(observed), + ); + self.footprint.r_read.insert(range); + Ok(value) + } + + fn charge( + &mut self, + steps: u64, + read_bytes: u64, + storage: u64, + shared: Option<&mut Meter>, + ) -> bool { + let Some(allocated) = self.allocated_bytes.checked_add(storage) else { + return false; + }; + let mut meter = self.meter; + if allocated > self.max_allocated_bytes || !meter.charge(steps, read_bytes, 0) { + return false; + } + if let Some(shared) = shared { + let mut candidate = *shared; + if candidate.charge_steps(steps).is_err() || candidate.allocate(storage).is_err() { + return false; + } + *shared = candidate; + } + self.meter = meter; + self.allocated_bytes = allocated; + true + } + + /// Cumulative reserved value storage, including both worst-case row buffers. + /// + /// Each node/range observation reserves 128 logical bytes. Each bucket edge + /// reserves 128 bytes in each of the returned and recorded row buffers, + /// including nonmatching edges. Index/allocator overhead and trusted-host + /// preparation are separate; this is not a complete interpreter arena. + #[must_use] + pub const fn allocated_bytes(&self) -> u64 { + self.allocated_bytes + } + + /// Reading coordinate computed from the borrowed frontier during preparation. + /// + /// This is not the complete commit-parent basis: its state root covers the + /// reachable graph, while by-key reads may observe unreachable records. + /// Admission must separately bind the writer head and commit identity. + #[must_use] + pub const fn basis(&self) -> ReadBasis { + self.basis + } + + /// Successful charges, including reads resolved before a later refusal. + #[must_use] + pub const fn consumed(&self) -> EchoOperationBudgetV1 { + self.meter.consumed() + } + + /// Real values observed at this basis, including absence and empty ranges. + #[must_use] + pub const fn readings(&self) -> &BTreeMap { + &self.readings + } + + /// Runtime-derived read claims. No writes or application claims are inserted. + #[must_use] + pub const fn footprint(&self) -> &Footprint { + &self.footprint + } +} diff --git a/crates/warp-core/src/edict_effects/compiled_program.rs b/crates/warp-core/src/edict_effects/compiled_program.rs new file mode 100644 index 000000000..97044bff3 --- /dev/null +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Current compiler-artifact binding and ordered instruction correspondence. +use crate::edict_pure::{ + values::{array, exact_fields, field, map, number, require_text, text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::{ + decode_canonical_cbor_v1 as decode, digest_canonical_value_bytes_v1 as digest, + CanonicalValueV1 as Value, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct CompiledProgram { + module: Value, + identity: [u8; 32], + target: Value, + target_identity: [u8; 32], + exports_identity: [u8; 32], + intent: String, + exports: Value, + lowering: Value, +} +impl CompiledProgram { + pub(super) fn parse(bytes: &[u8]) -> Result { + let bundle = decode(bytes).map_err(|_| Error::InvalidArtifact)?; + exact_fields( + &bundle, + &[ + "schema", + "intent", + "core_artifact", + "target_ir_artifact", + "lawpack_exports_artifact", + "lowering_artifacts", + ], + )?; + require_text( + &bundle, + "schema", + "echo.compiler-produced-effect-program/v1", + )?; + let intent = text_field(&bundle, "intent")?.to_owned(); + if intent.is_empty() { + return Err(Error::InvalidArtifact); + } + let embedded = |key| { + decode(crate::edict_pure::values::bytes(field(&bundle, key)?)?) + .map_err(|_| Error::InvalidArtifact) + }; + let module = embedded("core_artifact")?; + let target = embedded("target_ir_artifact")?; + let exports = embedded("lawpack_exports_artifact")?; + exact_fields( + &target, + &[ + "domain", + "kind", + "intents", + "targetProfile", + "semanticClosure", + "sourceCoreCoordinate", + ], + )?; + require_text(&target, "domain", "echo.span-ir/v2")?; + require_text(&target, "kind", "orderedTargetIrArtifact")?; + exact_fields( + &exports, + &[ + "types", + "effects", + "constants", + "obstructions", + "pureFunctions", + "operationProfiles", + ], + )?; + array(field(&exports, "types")?)?; + map(field(&exports, "operationProfiles")?)?; + for key in ["effects", "constants", "obstructions", "pureFunctions"] { + array(field(&exports, key)?)?; + } + let target_identity = + digest("edict.target-ir.artifact/v1", &target).map_err(|_| Error::InvalidArtifact)?; + let exports_identity = + digest("edict.lawpack-exports/v1", &exports).map_err(|_| Error::InvalidArtifact)?; + let mut fields = vec![ + "apiVersion", + "coordinate", + "imports", + "types", + "intents", + "requiredCoreCapabilities", + ]; + if field(&module, "functions").is_ok() { + fields.push("functions"); + } + exact_fields(&module, &fields)?; + require_text(&module, "apiVersion", "edict.core/v1")?; + if text_field(&module, "coordinate")?.is_empty() + || map(field(&module, "intents")?)?.is_empty() + { + return Err(Error::InvalidArtifact); + } + array(field(&module, "imports")?)?; + map(field(&module, "types")?)?; + if let Ok(functions) = field(&module, "functions") { + if map(functions)?.is_empty() { + return Err(Error::InvalidArtifact); + } + } + for capability in array(field(&module, "requiredCoreCapabilities")?)? { + text(capability)?; + } + let identity = + digest("edict.core.module/v1", &module).map_err(|_| Error::InvalidArtifact)?; + require_text( + &target, + "sourceCoreCoordinate", + text_field(&module, "coordinate")?, + )?; + let source = field(field(&target, "semanticClosure")?, "sourceCore")?; + exact_fields(source, &["id", "digest"])?; + require_text(source, "id", text_field(&module, "coordinate")?)?; + let [algorithm, claimed] = array(field(source, "digest")?)? else { + return Err(Error::InvalidArtifact); + }; + if text(algorithm)? != "sha256" || crate::edict_pure::values::bytes(claimed)? != identity { + return Err(Error::InvalidArtifact); + } + Ok(Self { + module, + identity, + target, + target_identity, + exports_identity, + intent, + exports, + lowering: field(&bundle, "lowering_artifacts")?.clone(), + }) + } + pub(super) fn execution( + &self, + limits: crate::edict_pure::EvaluationLimits, + ) -> Result { + super::execution::Program::prepare( + &self.module, + field(field(&self.module, "intents")?, &self.intent)?, + field(field(&self.target, "intents")?, &self.intent)?, + &self.exports, + limits, + ) + } + pub(super) fn validate_binding( + &self, + operation: &str, + expected_core: [u8; 32], + expected_target: [u8; 32], + expected_exports: [u8; 32], + output_type: &str, + ) -> Result<(), Error> { + if self.identity != expected_core + || self.target_identity != expected_target + || self.exports_identity != expected_exports + { + return Err(Error::InvalidArtifact); + } + let module = text_field(&self.module, "coordinate")?; + let name = operation + .strip_prefix(module) + .and_then(|suffix| suffix.strip_prefix('.')) + .filter(|name| !name.is_empty()) + .ok_or(Error::InvalidArtifact)?; + if name != self.intent { + return Err(Error::InvalidArtifact); + } + let intent = field(field(&self.module, "intents")?, name)?; + let target = field(field(&self.target, "intents")?, name)?; + for key in ["inputConstraints", "coreEvaluationBudget"] { + if field(target, key)? != field(intent, key)? { + return Err(Error::InvalidArtifact); + } + } + require_text( + target, + "operationProfile", + text_field(intent, "requiredOperationProfile")?, + )?; + match (field(target, "basis"), field(intent, "basis")) { + (Ok(a), Ok(b)) if a == b => {} + (Err(_), Err(_)) => {} + _ => return Err(Error::InvalidArtifact), + } + let mut fields = vec![ + "input", + "output", + "requiredOperationProfile", + "inputConstraints", + "coreEvaluationBudget", + "body", + ]; + if field(intent, "basis").is_ok() { + fields.push("basis"); + } + if field(intent, "optic").is_ok() { + fields.push("optic"); + } + exact_fields(intent, &fields)?; + if text_field(intent, "input")?.is_empty() + || text_field(intent, "requiredOperationProfile")?.is_empty() + || text_field(intent, "output")? != output_type + { + return Err(Error::InvalidArtifact); + } + array(field(intent, "inputConstraints")?)?; + let budget = field(intent, "coreEvaluationBudget")?; + exact_fields(budget, &["maxSteps", "maxAllocatedBytes", "maxOutputBytes"])?; + if number(field(budget, "maxSteps")?)? == 0 { + return Err(Error::InvalidArtifact); + } + number(field(budget, "maxAllocatedBytes")?)?; + number(field(budget, "maxOutputBytes")?)?; + let body = field(intent, "body")?; + exact_fields(body, &["locals", "nodes", "result"])?; + array(field(body, "locals")?)?; + array(field(body, "nodes")?)?; + map(field(body, "result")?)?; + if field(target, "result")? != field(body, "result")? { + return Err(Error::InvalidArtifact); + } + let signatures = super::lowering_relation::validate( + &self.module, + intent, + &self.target, + target, + &self.exports, + &self.lowering, + )?; + super::instruction_relation::validate( + &self.module, + intent, + target, + &self.exports, + &signatures, + )?; + Ok(()) + } +} diff --git a/crates/warp-core/src/edict_effects/completion.rs b/crates/warp-core/src/edict_effects/completion.rs new file mode 100644 index 000000000..b37017fb9 --- /dev/null +++ b/crates/warp-core/src/edict_effects/completion.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! One runtime-owned completed authored execution, without proposal or Action authority. +use super::{EffectMutation, EffectReadImageError, EffectReadSetImage}; +use crate::{EchoOperationApplicationBasisV1, EchoOperationEvaluationBasisV1, Hash}; +#[derive(Debug)] +pub(super) struct AuthoredExecution { + pub(super) program: Hash, + pub(super) input_basis: EchoOperationApplicationBasisV1, + pub(super) output: Vec, + pub(super) mutations: Vec, +} +/// Refusal while consuming a frame into completed authored evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectCompletionError { + /// The frame has no successful whole authored operation. + #[error("no completed authored operation")] + NoCompletedOperation, + /// The exact canonical input does not bind the runtime parent's application basis. + #[error("authored input does not match the bound application basis")] + InputBasisMismatch, + /// Bounded read-image export failed; no completed evidence is delivered. + #[error(transparent)] + ReadImage(#[from] EffectReadImageError), +} +/// Owned evidence from one successful program on one runtime-selected parent. +/// +/// There is no public constructor. This binds completed private evaluation, +/// not installed meaning, validated mutation preconditions, a patch or an Action. +/// The process-local owner binding prevents importing another runtime's work +/// under equal reachable-state parent coordinates. Structural data getters +/// grant no invocation or mutation authority. +#[derive(Debug)] +pub struct CompletedEffectEvaluation { + execution: AuthoredExecution, + reads: EffectReadSetImage, + #[cfg(feature = "native_rule_bootstrap")] + authority: crate::echo_operation::EchoOperationEvaluationAuthorityV1, +} +impl CompletedEffectEvaluation { + pub(super) fn new( + execution: AuthoredExecution, + reads: EffectReadSetImage, + authority: crate::echo_operation::EchoOperationEvaluationAuthorityV1, + ) -> Self { + // Without the native owner feature there is no runtime-bound frame + // constructor or derivation crossing. Retaining owner authority is + // required precisely when that owner crossing is compiled. + #[cfg(not(feature = "native_rule_bootstrap"))] + drop(authority); + Self { + execution, + reads, + #[cfg(feature = "native_rule_bootstrap")] + authority, + } + } + #[cfg(feature = "native_rule_bootstrap")] + pub(crate) fn is_owned_by( + &self, + authority: &crate::echo_operation::EchoOperationEvaluationAuthorityV1, + ) -> bool { + self.authority.same_owner(authority) + } + /// Identity of the exact producing current operation program. + #[must_use] + pub const fn program_identity(&self) -> Hash { + self.execution.program + } + /// Input binding using the same recipe as current invocation admission. + #[must_use] + pub const fn input_basis(&self) -> EchoOperationApplicationBasisV1 { + self.execution.input_basis + } + /// Runtime-selected full parent retained by the recorded reads. + #[must_use] + pub const fn evaluation_basis(&self) -> EchoOperationEvaluationBasisV1 { + self.reads.evaluation_basis() + } + /// Actual successful canonical program result, retained without a second copy. + #[must_use] + pub fn output(&self) -> &[u8] { + &self.execution.output + } + /// Emitted intents in execution order; still unvalidated and not a patch. + #[must_use] + pub fn mutations(&self) -> &[EffectMutation] { + &self.execution.mutations + } + /// Bounded canonical actual observations with the same parent and usage. + #[must_use] + pub const fn read_set_image(&self) -> &EffectReadSetImage { + &self.reads + } +} diff --git a/crates/warp-core/src/edict_effects/derive.rs b/crates/warp-core/src/edict_effects/derive.rs new file mode 100644 index 000000000..4ed06d4c9 --- /dev/null +++ b/crates/warp-core/src/edict_effects/derive.rs @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owned whole-parent mutation validation and counterfactual patch derivation. +use super::CompletedEffectEvaluation; +use crate::{EchoOperationBudgetV1, Footprint, Hash, WarpTickPatchV1}; +/// First failed validation rule, before publication or scheduler composition. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EffectDerivationKind { + /// Admission or completed evaluation belongs to another runtime owner. + AuthorityMismatch, + /// The installed meaning is absent or has a different program kind. + OperationUnavailable, + /// Producing program/input/parent or evaluated resource bounds do not match. + OriginMismatch, + /// The runtime parent has changed since evaluation/admission. + BasisChanged, + /// Remaining delegated work or I/O cannot cover resolution. + BudgetExceeded, + /// An effect key has no lawful root-to-target WARP chain. + UnreachableWarp, + /// Effects share a target record or attachment slot. + DuplicateEffectTarget, + /// A carried/resolved type or endpoint pair violates the program schema. + SchemaViolation, + /// A content/slot identity or mutability rule does not hold. + IdentityLawViolation, + /// A typed permission or resolved write maximum is exceeded. + FootprintLawViolation, + /// Atom replacement has no owner node at the parent. + NodeMissing, + /// A required absence or comparison does not hold. + PreconditionMismatch, + /// Ensure finds different content at its target. + ContentIdentityConflict, + /// An introduced/retargeted edge has a missing endpoint. + DanglingReference, + /// Derived operations differ from their resolved write set. + FootprintMismatch, +} +/// Failed derivation with actual admitted costs; no patch is published. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("effect derivation refused: {kind:?}")] +pub struct EffectDerivationError { + kind: EffectDerivationKind, + consumed: EchoOperationBudgetV1, +} +impl EffectDerivationError { + pub(crate) const fn new(kind: EffectDerivationKind, consumed: EchoOperationBudgetV1) -> Self { + Self { kind, consumed } + } + /// First failed rule. + #[must_use] + pub const fn kind(&self) -> EffectDerivationKind { + self.kind + } + /// Successful charges after origin authentication, including private evaluation. + /// Authority, installation and origin refusals report zero unauthenticated usage. + #[must_use] + pub const fn consumed(&self) -> EchoOperationBudgetV1 { + self.consumed + } +} +/// Counterfactual patch derived by the runtime owner at one exact full parent. +/// +/// No public constructor or commit API accepts this as scheduler authority. +/// Same-Tick composition, scoped history and receipt/WAL publication remain separate. +#[derive(Debug)] +pub struct DerivedEffectPatch { + completed: CompletedEffectEvaluation, + patch: WarpTickPatchV1, + footprint: Footprint, + consumed: EchoOperationBudgetV1, + invocation: Hash, +} +impl DerivedEffectPatch { + /// Canonical derived operations, without applying them. + #[must_use] + pub const fn patch(&self) -> &WarpTickPatchV1 { + &self.patch + } + /// Runtime-recorded reads and derived writes, including outgoing ranges. + #[must_use] + pub const fn footprint(&self) -> &Footprint { + &self.footprint + } + /// Evaluation and validation charges together. + #[must_use] + pub const fn consumed(&self) -> EchoOperationBudgetV1 { + self.consumed + } + /// Exact admitted invocation identity. + #[must_use] + pub const fn invocation_identity(&self) -> Hash { + self.invocation + } + /// Producing evaluation origin and result. + #[must_use] + pub const fn completed(&self) -> &CompletedEffectEvaluation { + &self.completed + } +} + +mod checks; +mod context; +use crate::{ + AttachmentKey, AttachmentValue, EdgeKey, EdgeRecord, NodeId, NodeKey, NodeRecord, + OutgoingEdgeRange, SlotId, SlotReading, TypeId, WarpId, +}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(crate) fn derive( + state: &crate::WorldlineState, + program: &super::EffectProgram, + completed: CompletedEffectEvaluation, + budget: EchoOperationBudgetV1, + policy: u32, + installed: Hash, + invocation: Hash, +) -> Result { + let mut ctx = context::Context::new(state, program, &completed, budget)?; + let mut effects = completed.mutations().iter().collect::>(); + effects.sort_by(|a, b| checks::compare(a, b)); + // Whole-parent derivation refuses any changed basis at the owner crossing. + // Dependency-scoped history and candidate composition are not enabled here. + for effect in &effects { + ctx.reachable(checks::key(effect).1)?; + } + let prior = ordered_read_slots(&ctx.read_slots) + .map_err(|_| ctx.error(EffectDerivationKind::OriginMismatch))?; + for slot in prior { + let warp = match slot { + SlotId::Node(key) => key.warp_id, + SlotId::Edge(key) => key.warp_id, + SlotId::OutgoingEdges(key) => key.source.warp_id, + SlotId::Attachment(key) => match key.owner { + crate::AttachmentOwner::Node(n) => n.warp_id, + crate::AttachmentOwner::Edge(e) => e.warp_id, + }, + SlotId::Port(_) => return Err(ctx.error(EffectDerivationKind::OriginMismatch)), + }; + ctx.reachable(warp)?; + } + let resolved = checks::resolve(&mut ctx, &effects)?; + let mut targets = BTreeSet::new(); + for effect in &effects { + for slot in checks::targets(effect) { + if !targets.insert(slot) { + return Err(ctx.error(EffectDerivationKind::DuplicateEffectTarget)); + } + } + } + checks::schema(&ctx, &resolved)?; + checks::identities(&ctx, &resolved, &effects)?; + let expected = checks::writes(&resolved); + checks::law(&ctx, &resolved, &expected)?; + checks::preconditions(&ctx, &resolved)?; + let ops = checks::operations(&resolved); + let mut actual = BTreeSet::new(); + for op in &ops { + match op { + crate::WarpOp::UpsertNode { node, .. } => { + actual.insert(SlotId::Node(*node)); + ctx.footprint.n_write.insert(*node); + } + crate::WarpOp::SetAttachment { key, .. } => { + actual.insert(SlotId::Attachment(*key)); + ctx.footprint.a_write.insert(*key); + } + crate::WarpOp::UpsertEdge { warp_id, record } => { + let key = EdgeKey { + warp_id: *warp_id, + local_id: record.id, + }; + let range = OutgoingEdgeRange { + source: NodeKey { + warp_id: *warp_id, + local_id: record.from, + }, + edge_type: record.ty, + }; + actual.insert(SlotId::Edge(key)); + actual.insert(SlotId::OutgoingEdges(range)); + ctx.footprint.e_write.insert(key); + ctx.footprint.r_write.insert(range); + } + _ => return Err(ctx.error(EffectDerivationKind::FootprintMismatch)), + } + } + if actual != expected { + return Err(ctx.error(EffectDerivationKind::FootprintMismatch)); + } + let count = ops.len(); + let records = |slot: &SlotId| !matches!(slot, SlotId::OutgoingEdges(_)); + let patch = WarpTickPatchV1::new( + policy, + installed, + crate::TickCommitStatus::Committed, + ctx.read_slots + .iter() + .filter(|s| records(s)) + .copied() + .collect(), + expected.iter().filter(|s| records(s)).copied().collect(), + ops, + ); + if patch.ops().len() != count { + return Err(ctx.error(EffectDerivationKind::FootprintMismatch)); + } + Ok(DerivedEffectPatch { + completed, + patch, + footprint: ctx.footprint, + consumed: ctx.meter.consumed(), + invocation, + }) +} + +fn ordered_read_slots( + slots: &BTreeSet, +) -> Result, super::EffectReadImageError> { + let mut ordered = slots + .iter() + .map(|slot| super::EffectSlotRow::from_slot(*slot).map(|row| (row, *slot))) + .collect::, _>>()?; + ordered.sort_unstable_by(|a, b| a.0.compare(&b.0)); + Ok(ordered.into_iter().map(|(_, slot)| slot).collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Native SlotId orders edges before attachments. The effect contract does + // the opposite; changing the visited order changes the first refusal. + #[test] + fn prior_read_reachability_uses_effect_slot_order() { + let attachment = SlotId::Attachment(AttachmentKey::node_alpha(NodeKey { + warp_id: WarpId([2; 32]), + local_id: NodeId([4; 32]), + })); + let edge = SlotId::Edge(EdgeKey { + warp_id: WarpId([3; 32]), + local_id: crate::EdgeId([5; 32]), + }); + assert_eq!( + ordered_read_slots(&BTreeSet::from([edge, attachment])), + Ok(vec![attachment, edge]) + ); + } +} diff --git a/crates/warp-core/src/edict_effects/derive/checks.rs b/crates/warp-core/src/edict_effects/derive/checks.rs new file mode 100644 index 000000000..de9a219ba --- /dev/null +++ b/crates/warp-core/src/edict_effects/derive/checks.rs @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Ordered resolution, law checks and operation derivation for all five effects. +use super::super::{EffectMutation as M, EffectNodeAddressing as Addressing}; +use super::context::Context; +use super::{ + AttachmentKey, AttachmentValue, BTreeSet, EdgeRecord, EffectDerivationError, + EffectDerivationKind, Hash, NodeId, NodeKey, NodeRecord, OutgoingEdgeRange, SlotId, + SlotReading, TypeId, WarpId, +}; +pub(super) struct Resolved<'a> { + effect: &'a M, + node: Option, + atom: Option, + edge: Option, + source: Option, + target: Option, + alias: bool, + noop: bool, +} +pub(super) fn key(effect: &M) -> (u8, WarpId, Hash) { + match effect { + M::EnsureNode { node, .. } | M::CreateNode { node, .. } => { + (0, node.warp_id, node.local_id.0) + } + M::ReplaceAtom { node, .. } => (1, node.warp_id, node.local_id.0), + M::EnsureEdge { edge, .. } | M::ReplaceEdge { edge, .. } => { + (2, edge.warp_id, edge.local_id.0) + } + } +} +fn row(effect: &M) -> ([u8; 280], &[u8]) { + let mut row = [0; 280]; + let (_, warp, id) = key(effect); + row[8..40].copy_from_slice(&warp.0); + row[40..72].copy_from_slice(&id); + let payload = match effect { + M::EnsureNode { ty, atom, .. } | M::CreateNode { ty, atom, .. } => { + row[0] = u8::from(matches!(effect, M::CreateNode { .. })); + row[72..104].copy_from_slice(&ty.0); + row[104..136].copy_from_slice(&atom.type_id.0); + atom.bytes.as_ref() + } + M::ReplaceAtom { expected, atom, .. } => { + row[0] = 2; + row[104..136].copy_from_slice(&atom.type_id.0); + row[152..184].copy_from_slice(expected); + atom.bytes.as_ref() + } + M::EnsureEdge { from, to, ty, .. } => { + row[0] = 3; + row[72..104].copy_from_slice(&ty.0); + row[184..216].copy_from_slice(&from.0); + row[216..248].copy_from_slice(&to.0); + &[] + } + M::ReplaceEdge { + expected_to, to, .. + } => { + row[0] = 4; + row[216..248].copy_from_slice(&to.0); + row[248..280].copy_from_slice(&expected_to.0); + &[] + } + }; + (row, payload) +} +pub(super) fn compare(a: &M, b: &M) -> std::cmp::Ordering { + key(a).cmp(&key(b)).then_with(|| { + let a = row(a); + let b = row(b); + a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)) + }) +} +pub(super) fn targets(effect: &M) -> Vec { + match effect { + M::EnsureNode { node, .. } | M::CreateNode { node, .. } => vec![ + SlotId::Node(*node), + SlotId::Attachment(AttachmentKey::node_alpha(*node)), + ], + M::ReplaceAtom { node, .. } => vec![SlotId::Attachment(AttachmentKey::node_alpha(*node))], + M::EnsureEdge { edge, .. } | M::ReplaceEdge { edge, .. } => vec![SlotId::Edge(*edge)], + } +} +pub(super) fn resolve<'e>( + ctx: &mut Context<'_>, + effects: &[&'e M], +) -> Result>, EffectDerivationError> { + let mut resolved = Vec::new(); + for effect in effects { + let mut r = Resolved { + effect, + node: None, + atom: None, + edge: None, + source: None, + target: None, + alias: false, + noop: false, + }; + match effect { + M::EnsureNode { node, ty, atom } | M::CreateNode { node, ty, atom } => { + r.node = ctx.node(*node)?; + r.atom = ctx.alpha(*node)?; + r.noop = matches!(&r.node,Some(record) if record.ty==*ty) + && r.atom == Some(AttachmentValue::Atom(atom.clone())); + if matches!(effect, M::EnsureNode { .. }) + && ctx.program.graph_schema().node_addressing(*ty) == Some(Addressing::Content) + && r.node.is_some() + { + if !r.noop { + return Err(ctx.error(EffectDerivationKind::ContentIdentityConflict)); + } + let mut actual = ctx.bucket(*node, None)?; + let mut planned = edges_for(node, effects); + actual.sort_by_key(|e| (e.ty, e.to, e.id)); + planned.sort_by_key(|e| (e.ty, e.to, e.id)); + if actual != planned { + return Err(ctx.error(EffectDerivationKind::ContentIdentityConflict)); + } + } + if r.node.is_none() && r.atom.is_none() { + ctx.empty_bucket(*node)?; + ctx.created.insert(*node); + ctx.own_nodes.insert(*node, NodeRecord { ty: *ty }); + } + } + M::ReplaceAtom { node, .. } => { + r.node = ctx.node(*node)?; + if r.node.is_none() { + return Err(ctx.error(EffectDerivationKind::NodeMissing)); + } + r.atom = ctx.alpha(*node)?; + } + M::EnsureEdge { edge, from, to, ty } => { + r.edge = ctx.edge(*edge)?; + r.noop = r + .edge + .as_ref() + .is_some_and(|old| old.from == *from && old.to == *to && old.ty == *ty); + let source = r.edge.as_ref().map_or(*from, |old| old.from); + let edge_type = r.edge.as_ref().map_or(*ty, |old| old.ty); + let source_key = NodeKey { + warp_id: edge.warp_id, + local_id: source, + }; + r.source = ctx.endpoint(source_key)?; + if r.edge.is_none() { + r.target = ctx.endpoint(NodeKey { + warp_id: edge.warp_id, + local_id: *to, + })?; + } + if r.source.as_ref().is_some_and(|n| { + ctx.program.graph_schema().node_addressing(n.ty) == Some(Addressing::Mutable) + }) { + r.alias = ctx + .bucket(source_key, Some(edge_type))? + .iter() + .any(|old| old.ty == edge_type && old.id != edge.local_id); + } + } + M::ReplaceEdge { edge, to, .. } => { + let old = ctx + .edge(*edge)? + .ok_or_else(|| ctx.error(EffectDerivationKind::PreconditionMismatch))?; + let source = NodeKey { + warp_id: edge.warp_id, + local_id: old.from, + }; + r.source = ctx.endpoint(source)?; + r.target = ctx.endpoint(NodeKey { + warp_id: edge.warp_id, + local_id: *to, + })?; + if r.source.as_ref().is_some_and(|n| { + ctx.program.graph_schema().node_addressing(n.ty) == Some(Addressing::Mutable) + }) { + r.alias = ctx + .bucket(source, Some(old.ty))? + .iter() + .any(|other| other.ty == old.ty && other.id != old.id); + } + r.edge = Some(old); + } + } + resolved.push(r); + } + Ok(resolved) +} +fn edges_for(node: &NodeKey, effects: &[&M]) -> Vec { + effects + .iter() + .filter_map(|e| match e { + M::EnsureEdge { edge, from, to, ty } + if edge.warp_id == node.warp_id && *from == node.local_id => + { + Some(EdgeRecord { + id: edge.local_id, + from: *from, + to: *to, + ty: *ty, + }) + } + _ => None, + }) + .collect() +} +pub(super) fn schema( + ctx: &Context<'_>, + resolved: &[Resolved<'_>], +) -> Result<(), EffectDerivationError> { + let schema = ctx.program.graph_schema(); + let bad = || ctx.error(EffectDerivationKind::SchemaViolation); + for key in &ctx.normal_nodes { + if let Some(SlotReading::Node(Some(node))) = ctx.records.get(&SlotId::Node(*key)) { + if schema.node_addressing(node.ty).is_none() { + return Err(bad()); + } + } + } + for key in &ctx.implied_atoms { + if let ( + crate::AttachmentOwner::Node(owner), + Some(SlotReading::Attachment(Some(AttachmentValue::Atom(atom)))), + ) = (key.owner, ctx.records.get(&SlotId::Attachment(*key))) + { + if let Some(SlotReading::Node(Some(node))) = ctx.records.get(&SlotId::Node(owner)) { + if !schema.allows_atom(node.ty, atom.type_id) { + return Err(bad()); + } + } + } + } + for key in &ctx.implied_edges { + if let Some(SlotReading::Edge(Some(edge))) = ctx.records.get(&SlotId::Edge(*key)) { + if !schema.has_edge_type(edge.ty) { + return Err(bad()); + } + } + } + for r in resolved { + match r.effect { + M::EnsureNode { ty, atom, .. } | M::CreateNode { ty, atom, .. } => { + if !schema.allows_atom(*ty, atom.type_id) { + return Err(bad()); + } + } + M::ReplaceAtom { atom, .. } => { + if !r + .node + .as_ref() + .is_some_and(|n| schema.allows_atom(n.ty, atom.type_id)) + { + return Err(bad()); + } + } + M::EnsureEdge { ty, .. } => { + if !schema.has_edge_type(*ty) { + return Err(bad()); + } + if r.edge.is_none() { + if let (Some(from), Some(to)) = (&r.source, &r.target) { + if !schema.allows_edge(*ty, from.ty, to.ty) { + return Err(bad()); + } + } + } + } + M::ReplaceEdge { .. } => { + if let (Some(edge), Some(from), Some(to)) = (&r.edge, &r.source, &r.target) { + if !schema.allows_edge(edge.ty, from.ty, to.ty) { + return Err(bad()); + } + } + } + } + } + Ok(()) +} +pub(super) fn identities( + ctx: &Context<'_>, + resolved: &[Resolved<'_>], + effects: &[&M], +) -> Result<(), EffectDerivationError> { + let bad = || ctx.error(EffectDerivationKind::IdentityLawViolation); + let schema = ctx.program.graph_schema(); + for r in resolved { + match r.effect { + M::EnsureNode { node, ty, atom } => { + if schema.node_addressing(*ty) != Some(Addressing::Content) { + return Err(bad()); + } + let mut edges = edges_for(node, effects); + edges.sort_by_key(|e| (e.ty, e.to)); + let mut hash = blake3::Hasher::new(); + hash.update(b"echo.bridge.node-id.v1\0"); + hash.update(&node.warp_id.0); + hash.update(&ty.0); + hash.update(&super::super::value_digest::atom(atom)); + hash.update(&(edges.len() as u64).to_le_bytes()); + for edge in edges { + hash.update(&edge.ty.0); + hash.update(&edge.to.0); + } + if hash.finalize().as_bytes() != &node.local_id.0 { + return Err(bad()); + } + } + M::CreateNode { ty, .. } => { + if schema.node_addressing(*ty) != Some(Addressing::Mutable) { + return Err(bad()); + } + } + M::ReplaceAtom { .. } => { + if !r + .node + .as_ref() + .is_some_and(|n| schema.node_addressing(n.ty) == Some(Addressing::Mutable)) + { + return Err(bad()); + } + } + M::EnsureEdge { edge, from, to, ty } => { + if let Some(source) = &r.source { + let (from, to, ty) = r + .edge + .as_ref() + .map_or((*from, *to, *ty), |old| (old.from, old.to, old.ty)); + let address = schema.node_addressing(source.ty).ok_or_else(bad)?; + if address == Addressing::Content + && r.edge.is_none() + && !ctx.created.contains(&NodeKey { + warp_id: edge.warp_id, + local_id: from, + }) + { + return Err(bad()); + } + if r.alias || edge_id(edge.warp_id, from, ty, to, address) != edge.local_id.0 { + return Err(bad()); + } + } + } + M::ReplaceEdge { edge, .. } => { + if let (Some(source), Some(old)) = (&r.source, &r.edge) { + if schema.node_addressing(source.ty) != Some(Addressing::Mutable) + || r.alias + || edge_id(edge.warp_id, old.from, old.ty, old.to, Addressing::Mutable) + != edge.local_id.0 + { + return Err(bad()); + } + } + } + } + } + Ok(()) +} +fn edge_id(warp: WarpId, from: NodeId, ty: TypeId, to: NodeId, kind: Addressing) -> Hash { + let mut hash = blake3::Hasher::new(); + hash.update(if kind == Addressing::Content { + b"echo.bridge.edge-id.v1\0" + } else { + b"echo.bridge.slot-edge-id.v1\0" + }); + hash.update(&warp.0); + hash.update(&from.0); + hash.update(&ty.0); + if kind == Addressing::Content { + hash.update(&to.0); + } + hash.finalize().into() +} +pub(super) fn writes(resolved: &[Resolved<'_>]) -> BTreeSet { + let mut slots = BTreeSet::new(); + for r in resolved { + match r.effect { + M::EnsureNode { .. } | M::EnsureEdge { .. } if r.noop => {} + M::EnsureEdge { edge, from, ty, .. } => { + slots.insert(SlotId::Edge(*edge)); + if r.edge.is_none() { + slots.insert(SlotId::OutgoingEdges(OutgoingEdgeRange { + source: NodeKey { + warp_id: edge.warp_id, + local_id: *from, + }, + edge_type: *ty, + })); + } + } + M::ReplaceEdge { edge, .. } => { + if let Some(old) = &r.edge { + slots.insert(SlotId::Edge(*edge)); + slots.insert(SlotId::OutgoingEdges(OutgoingEdgeRange { + source: NodeKey { + warp_id: edge.warp_id, + local_id: old.from, + }, + edge_type: old.ty, + })); + } + } + other => slots.extend(targets(other)), + } + } + slots +} +pub(super) fn law( + ctx: &Context<'_>, + resolved: &[Resolved<'_>], + writes: &BTreeSet, +) -> Result<(), EffectDerivationError> { + let law = ctx.program.footprint_law(); + let bad = || ctx.error(EffectDerivationKind::FootprintLawViolation); + if writes.len() as u64 > law.max_writes { + return Err(bad()); + } + for key in &ctx.normal_nodes { + if let Some(SlotReading::Node(Some(n))) = ctx.records.get(&SlotId::Node(*key)) { + if !law.nodes.contains(&n.ty) { + return Err(bad()); + } + } + } + for key in &ctx.implied_atoms { + if let Some(SlotReading::Attachment(Some(AttachmentValue::Atom(atom)))) = + ctx.records.get(&SlotId::Attachment(*key)) + { + if !law.atoms.contains(&atom.type_id) { + return Err(bad()); + } + } + } + for key in &ctx.implied_edges { + if let Some(SlotReading::Edge(Some(edge))) = ctx.records.get(&SlotId::Edge(*key)) { + if !law.write_edges.contains(&edge.ty) && !law.ranges.contains(&edge.ty) { + return Err(bad()); + } + } + } + for r in resolved { + match r.effect { + M::EnsureNode { ty, atom, .. } | M::CreateNode { ty, atom, .. } => { + if !law.write_nodes.contains(ty) || !law.atoms.contains(&atom.type_id) { + return Err(bad()); + } + } + M::ReplaceAtom { atom, .. } => { + if !law.atoms.contains(&atom.type_id) { + return Err(bad()); + } + } + M::EnsureEdge { ty, .. } => { + if !law.write_edges.contains(ty) { + return Err(bad()); + } + } + M::ReplaceEdge { .. } => { + if !r + .edge + .as_ref() + .is_some_and(|e| law.write_edges.contains(&e.ty)) + { + return Err(bad()); + } + } + } + } + Ok(()) +} +pub(super) fn preconditions( + ctx: &Context<'_>, + resolved: &[Resolved<'_>], +) -> Result<(), EffectDerivationError> { + for r in resolved { + let kind = match r.effect { + M::CreateNode { .. } if r.node.is_some() || r.atom.is_some() => { + Some(EffectDerivationKind::PreconditionMismatch) + } + M::EnsureNode { ty, atom, .. } + if !(r.node.is_none() && r.atom.is_none() + || r.node.as_ref().is_some_and(|n| n.ty == *ty) + && r.atom == Some(AttachmentValue::Atom(atom.clone()))) => + { + Some(EffectDerivationKind::ContentIdentityConflict) + } + M::ReplaceAtom { expected, .. } if !matches!(&r.atom,Some(AttachmentValue::Atom(atom)) if super::super::value_digest::atom(atom)==*expected) => { + Some(EffectDerivationKind::PreconditionMismatch) + } + M::EnsureEdge { .. } if r.edge.is_some() && !r.noop => { + Some(EffectDerivationKind::ContentIdentityConflict) + } + M::ReplaceEdge { expected_to, .. } + if r.edge.as_ref().is_none_or(|e| e.to != *expected_to) => + { + Some(EffectDerivationKind::PreconditionMismatch) + } + M::EnsureEdge { .. } + if r.source.is_none() || (r.edge.is_none() && r.target.is_none()) => + { + Some(EffectDerivationKind::DanglingReference) + } + M::ReplaceEdge { .. } if r.source.is_none() || r.target.is_none() => { + Some(EffectDerivationKind::DanglingReference) + } + _ => None, + }; + if let Some(kind) = kind { + return Err(ctx.error(kind)); + } + } + Ok(()) +} +pub(super) fn operations(resolved: &[Resolved<'_>]) -> Vec { + let mut ops = Vec::new(); + for r in resolved { + match r.effect { + M::EnsureNode { .. } | M::EnsureEdge { .. } if r.noop => {} + M::EnsureNode { node, ty, atom } | M::CreateNode { node, ty, atom } => { + ops.push(crate::WarpOp::UpsertNode { + node: *node, + record: NodeRecord { ty: *ty }, + }); + ops.push(crate::WarpOp::SetAttachment { + key: AttachmentKey::node_alpha(*node), + value: Some(AttachmentValue::Atom(atom.clone())), + }); + } + M::ReplaceAtom { node, atom, .. } => ops.push(crate::WarpOp::SetAttachment { + key: AttachmentKey::node_alpha(*node), + value: Some(AttachmentValue::Atom(atom.clone())), + }), + M::EnsureEdge { edge, from, to, ty } => ops.push(crate::WarpOp::UpsertEdge { + warp_id: edge.warp_id, + record: EdgeRecord { + id: edge.local_id, + from: *from, + to: *to, + ty: *ty, + }, + }), + M::ReplaceEdge { edge, to, .. } => { + if let Some(old) = &r.edge { + ops.push(crate::WarpOp::UpsertEdge { + warp_id: edge.warp_id, + record: EdgeRecord { + to: *to, + ..old.clone() + }, + }); + } + } + } + } + ops +} diff --git a/crates/warp-core/src/edict_effects/derive/context.rs b/crates/warp-core/src/edict_effects/derive/context.rs new file mode 100644 index 000000000..3d5ae9e19 --- /dev/null +++ b/crates/warp-core/src/edict_effects/derive/context.rs @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Charged immutable-parent resolutions, separate from program-view observations. +use super::{ + AttachmentKey, AttachmentValue, BTreeMap, BTreeSet, CompletedEffectEvaluation, + EchoOperationBudgetV1, EdgeKey, EdgeRecord, EffectDerivationError, EffectDerivationKind, + Footprint, NodeId, NodeKey, NodeRecord, OutgoingEdgeRange, SlotId, SlotReading, TypeId, WarpId, +}; +use crate::{AttachmentOwner, AttachmentPlane}; +pub(super) struct Context<'a> { + pub state: &'a crate::WorldlineState, + pub program: &'a super::super::EffectProgram, + pub meter: crate::echo_operation::EchoOperationBudgetMeterV1, + pub records: BTreeMap, + pub footprint: Footprint, + pub read_slots: BTreeSet, + pub normal_nodes: BTreeSet, + pub implied_atoms: BTreeSet, + pub implied_edges: BTreeSet, + pub own_nodes: BTreeMap, + pub created: BTreeSet, + known: BTreeSet, + reached: BTreeSet, + scanned: BTreeMap>, + lengths: BTreeMap, +} +impl<'a> Context<'a> { + pub fn new( + state: &'a crate::WorldlineState, + program: &'a super::super::EffectProgram, + completed: &CompletedEffectEvaluation, + budget: EchoOperationBudgetV1, + ) -> Result { + let mut meter = crate::echo_operation::EchoOperationBudgetMeterV1::new(budget); + let used = completed.read_set_image().consumed(); + if !meter.charge(used.steps(), used.read_bytes(), used.write_bytes()) { + return Err(EffectDerivationError::new( + EffectDerivationKind::OriginMismatch, + used, + )); + } + let view = super::super::EffectReadSetView::parse(completed.read_set_image().as_bytes()) + .map_err(|_| EffectDerivationError::new(EffectDerivationKind::OriginMismatch, used))?; + let mut known = BTreeSet::new(); + let mut footprint = Footprint::default(); + for row in view.rows() { + let key = NodeKey { + warp_id: WarpId(row.slot.warp_id), + local_id: NodeId(row.slot.local_id), + }; + let slot = match row.slot.class { + 0 => SlotId::Node(key), + 1 => SlotId::Attachment(if row.slot.owner == 0 { + AttachmentKey::node_alpha(key) + } else { + AttachmentKey::edge_beta(EdgeKey { + warp_id: key.warp_id, + local_id: crate::EdgeId(key.local_id.0), + }) + }), + 2 => SlotId::Edge(EdgeKey { + warp_id: key.warp_id, + local_id: crate::EdgeId(key.local_id.0), + }), + 3 => SlotId::OutgoingEdges(OutgoingEdgeRange { + source: key, + edge_type: TypeId(row.slot.edge_type), + }), + _ => { + return Err(EffectDerivationError::new( + EffectDerivationKind::OriginMismatch, + used, + )) + } + }; + mark_read(&mut footprint, slot); + known.insert(slot); + } + Ok(Self { + state, + program, + meter, + records: BTreeMap::new(), + footprint, + read_slots: known.clone(), + known, + normal_nodes: BTreeSet::new(), + implied_atoms: BTreeSet::new(), + implied_edges: BTreeSet::new(), + own_nodes: BTreeMap::new(), + created: BTreeSet::new(), + reached: BTreeSet::new(), + scanned: BTreeMap::new(), + lengths: BTreeMap::new(), + }) + } + pub fn error(&self, kind: EffectDerivationKind) -> EffectDerivationError { + EffectDerivationError::new(kind, self.meter.consumed()) + } + pub fn charge(&mut self, steps: u64, bytes: u64) -> Result<(), EffectDerivationError> { + if self.meter.charge(steps, bytes, 0) { + Ok(()) + } else { + Err(self.error(EffectDerivationKind::BudgetExceeded)) + } + } + fn read(&mut self, slot: SlotId) -> Result { + if let Some(value) = self.records.get(&slot) { + return Ok(value.clone()); + } + let cost = if matches!(slot, SlotId::Edge(_)) { + 128 + } else { + 64 + }; + let fresh = !self.known.contains(&slot); + if fresh { + self.charge(1, cost)?; + } + // Length/type metadata is inspected before any payload is retained or hashed. + let value = match slot { + SlotId::Node(key) => SlotReading::Node( + self.state + .store(&key.warp_id) + .and_then(|s| s.node(&key.local_id)) + .cloned(), + ), + SlotId::Edge(key) => SlotReading::Edge( + self.state + .store(&key.warp_id) + .and_then(|s| s.edge_record(&key.local_id)), + ), + SlotId::Attachment(key) => { + let value = match key.owner { + AttachmentOwner::Node(node) => self + .state + .store(&node.warp_id) + .and_then(|s| s.node_attachment(&node.local_id)), + AttachmentOwner::Edge(edge) => self + .state + .store(&edge.warp_id) + .and_then(|s| s.edge_attachment(&edge.local_id)), + }; + if fresh { + if let Some(AttachmentValue::Atom(atom)) = value { + self.charge(0, atom.bytes.len() as u64)?; + } + } + let result = SlotReading::Attachment(value.cloned()); + self.records.insert(slot, result.clone()); + self.read_slots.insert(slot); + mark_read(&mut self.footprint, slot); + return Ok(result); + } + _ => return Err(self.error(EffectDerivationKind::OriginMismatch)), + }; + self.records.insert(slot, value.clone()); + self.read_slots.insert(slot); + mark_read(&mut self.footprint, slot); + Ok(value) + } + pub fn node(&mut self, key: NodeKey) -> Result, EffectDerivationError> { + self.normal_nodes.insert(key); + match self.read(SlotId::Node(key))? { + SlotReading::Node(value) => Ok(value), + _ => Err(self.error(EffectDerivationKind::OriginMismatch)), + } + } + pub fn alpha( + &mut self, + key: NodeKey, + ) -> Result, EffectDerivationError> { + let slot = AttachmentKey::node_alpha(key); + self.implied_atoms.insert(slot); + match self.read(SlotId::Attachment(slot))? { + SlotReading::Attachment(value) => Ok(value), + _ => Err(self.error(EffectDerivationKind::OriginMismatch)), + } + } + pub fn edge(&mut self, key: EdgeKey) -> Result, EffectDerivationError> { + self.implied_edges.insert(key); + match self.read(SlotId::Edge(key))? { + SlotReading::Edge(value) => Ok(value), + _ => Err(self.error(EffectDerivationKind::OriginMismatch)), + } + } + pub fn endpoint(&mut self, key: NodeKey) -> Result, EffectDerivationError> { + if let Some(record) = self.own_nodes.get(&key) { + return Ok(Some(record.clone())); + } + self.node(key) + } + pub fn empty_bucket(&mut self, key: NodeKey) -> Result<(), EffectDerivationError> { + if !self.lengths.contains_key(&key) { + self.charge(1, 64)?; + let size = self + .state + .store(&key.warp_id) + .map_or(0, |s| s.edges_from(&key.local_id).len()); + self.lengths.insert(key, size); + } + if self.lengths[&key] != 0 { + return Err(self.error(EffectDerivationKind::PreconditionMismatch)); + } + Ok(()) + } + pub fn bucket( + &mut self, + key: NodeKey, + ty: Option, + ) -> Result, EffectDerivationError> { + if let Some(ty) = ty { + self.footprint.r_read.insert(OutgoingEdgeRange { + source: key, + edge_type: ty, + }); + } + if let Some(edges) = self.scanned.get(&key) { + return Ok(edges.clone()); + } + let total = self + .state + .store(&key.warp_id) + .map_or(0, |s| s.edges_from(&key.local_id).len()); + // Known record IDs are resolved through the index, without inspecting + // any unknown bucket record before its whole scan reservation. + let mut cached = 0usize; + let mut known = self + .known + .iter() + .filter_map(|slot| { + if let SlotId::Edge(edge) = slot { + Some(*edge) + } else { + None + } + }) + .collect::>(); + known.extend(self.records.keys().filter_map(|slot| { + if let SlotId::Edge(edge) = slot { + Some(*edge) + } else { + None + } + })); + for edge in known { + if edge.warp_id == key.warp_id + && self + .state + .store(&key.warp_id) + .and_then(|s| s.edge_record(&edge.local_id)) + .is_some_and(|r| r.from == key.local_id) + { + cached += 1; + } + } + let unknown = total + .checked_sub(cached) + .ok_or_else(|| self.error(EffectDerivationKind::OriginMismatch))? + as u64; + self.charge( + unknown, + unknown + .checked_mul(128) + .ok_or_else(|| self.error(EffectDerivationKind::BudgetExceeded))?, + )?; + let edges = self.state.store(&key.warp_id).map_or_else(Vec::new, |s| { + s.edges_from(&key.local_id).cloned().collect::>() + }); + for edge in &edges { + let slot = SlotId::Edge(EdgeKey { + warp_id: key.warp_id, + local_id: edge.id, + }); + self.implied_edges.insert(EdgeKey { + warp_id: key.warp_id, + local_id: edge.id, + }); + self.read_slots.insert(slot); + mark_read(&mut self.footprint, slot); + self.records + .insert(slot, SlotReading::Edge(Some(edge.clone()))); + self.footprint.r_read.insert(OutgoingEdgeRange { + source: key, + edge_type: edge.ty, + }); + } + self.scanned.insert(key, edges.clone()); + Ok(edges) + } + pub fn reachable(&mut self, target: WarpId) -> Result<(), EffectDerivationError> { + let root = self.state.root().warp_id; + if target == root || self.reached.contains(&target) { + return Ok(()); + } + let mut cursor = target; + let mut seen = BTreeSet::new(); + let mut chain = Vec::new(); + while cursor != root && !self.reached.contains(&cursor) { + if !seen.insert(cursor) { + return Err(self.error(EffectDerivationKind::UnreachableWarp)); + } + let parent = self + .state + .warp_state() + .instance(&cursor) + .and_then(|i| i.parent) + .ok_or_else(|| self.error(EffectDerivationKind::UnreachableWarp))?; + let AttachmentOwner::Node(owner) = parent.owner else { + return Err(self.error(EffectDerivationKind::FootprintLawViolation)); + }; + if parent.plane != AttachmentPlane::Alpha { + return Err(self.error(EffectDerivationKind::FootprintLawViolation)); + } + chain.push((cursor, owner)); + cursor = owner.warp_id; + } + for (child, owner) in chain.into_iter().rev() { + let SlotReading::Node(Some(record)) = self.read(SlotId::Node(owner))? else { + return Err(self.error(EffectDerivationKind::UnreachableWarp)); + }; + if !self.program.footprint_law().portals.contains(&record.ty) { + return Err(self.error(EffectDerivationKind::FootprintLawViolation)); + } + if self.read(SlotId::Attachment(AttachmentKey::node_alpha(owner)))? + != SlotReading::Attachment(Some(AttachmentValue::Descend(child))) + { + return Err(self.error(EffectDerivationKind::UnreachableWarp)); + } + self.reached.insert(child); + } + Ok(()) + } +} +fn mark_read(footprint: &mut Footprint, slot: SlotId) { + match slot { + SlotId::Node(key) => { + footprint.n_read.insert(key); + } + SlotId::Edge(key) => { + footprint.e_read.insert(key); + } + SlotId::Attachment(key) => { + footprint.a_read.insert(key); + } + SlotId::OutgoingEdges(key) => { + footprint.r_read.insert(key); + } + SlotId::Port(_) => {} + } +} diff --git a/crates/warp-core/src/edict_effects/execution.rs b/crates/warp-core/src/edict_effects/execution.rs new file mode 100644 index 000000000..f998e3aa2 --- /dev/null +++ b/crates/warp-core/src/edict_effects/execution.rs @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Private ordered authored execution over the existing expression evaluator and recorded reads. +use super::mutations::{EffectMutation, MutationKind}; +use super::{EffectFrameError, EffectGraphSchema, RecordedNodeAccess}; +use crate::edict_pure::{ + evaluate::{expression, predicate, validate, Meter, VALUE_CELL_BYTES}, + functions, + model::{Expr, Helper, Predicate, RuntimeType, MAX_PROGRAM_NODES}, + syntax::Parser, + values::{array, bytes, field, map, number, text, text_field}, + EvaluationError as Error, EvaluationLimits, +}; +use crate::edict_read::{NodeAtomAccess, ReadError, ReadObstruction}; +use crate::{NodeId, NodeKey, SlotId, SlotReading, TypeId, WarpId}; +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as Value}; +use std::collections::BTreeMap; + +struct Binding { + name: String, + ty: RuntimeType, + value: Expr, +} +struct Reason { + name: String, + payload: Expr, +} +enum Instruction { + Let(Binding), + Mutation { + kind: MutationKind, + binding: Binding, + }, + Read { + binding: Binding, + failures: BTreeMap, + max_bytes: u64, + }, + Require { + condition: Predicate, + reason: Reason, + }, +} +pub(super) struct Evaluation { + pub(super) output: Vec, + pub(super) mutations: Vec, +} +pub(super) struct Program { + input: RuntimeType, + output: RuntimeType, + basis: Option, + constraints: Vec<(String, Predicate)>, + instructions: Vec, + helpers: BTreeMap, + result: Expr, + limits: EvaluationLimits, +} +impl Program { + // The existing ordered relation and lowering/ABI validator establish authority. + // This decodes that already checked body into the shared evaluator's types. + pub(super) fn prepare( + module: &Value, + intent: &Value, + target: &Value, + exports: &Value, + mut limits: EvaluationLimits, + ) -> Result { + let mut parser = Parser { + types: field(module, "types")?, + coordinate: text_field(module, "coordinate")?, + remaining: MAX_PROGRAM_NODES, + }; + let helpers = functions::decode(module, exports, &mut parser)?; + let budget = field(intent, "coreEvaluationBudget")?; + limits.max_steps = limits.max_steps.min(number(field(budget, "maxSteps")?)?); + limits.max_allocated_bytes = limits + .max_allocated_bytes + .min(number(field(budget, "maxAllocatedBytes")?)?); + limits.max_output_bytes = limits + .max_output_bytes + .min(number(field(budget, "maxOutputBytes")?)?); + let mut constraints = Vec::new(); + for constraint in array(field(intent, "inputConstraints")?)? { + constraints.push(( + text_field(constraint, "coordinate")?.to_owned(), + parser.predicate(field(constraint, "predicate")?, 0)?, + )); + } + let mut lowered = BTreeMap::new(); + for step in array(field(target, "steps")?)? { + lowered.insert(text_field(step, "id")?, step); + } + let body = field(intent, "body")?; + let mut instructions = Vec::new(); + for (node, id) in array(field(body, "nodes")?)? + .iter() + .zip(array(field(target, "executionOrder")?)?) + { + parser.enter(0)?; + let instruction = match text_field(node, "kind")? { + "let" => Instruction::Let(binding(node, "value", &mut parser)?), + "effect" => { + let step = lowered.get(text(id)?).ok_or(Error::InvalidArtifact)?; + let binding = binding(node, "input", &mut parser)?; + if let Some(kind) = + MutationKind::from_intrinsic(text_field(step, "targetIntrinsic")?) + { + instructions.push(Instruction::Mutation { kind, binding }); + continue; + } + let RuntimeType::Bytes { max, .. } = binding.ty else { + return Err(Error::UnsupportedProgram); + }; + // Arms are keyed by catalog name; the read raises runtime tags. + let mut arms = BTreeMap::new(); + for (name, arm) in map(field(step, "obstructionArms")?)? { + arms.insert( + text(name)?.to_owned(), + text_field(field(arm, "value")?, "callee")?.to_owned(), + ); + } + let failures = super::failure_names::runtime_arms( + text_field(step, "targetIntrinsic")?, + arms, + )?; + Instruction::Read { + binding, + failures, + max_bytes: max, + } + } + "require" => { + let reason = field(field(node, "onFailure")?, "reason")?; + let mut payload = Vec::new(); + for (key, value) in map(field(reason, "payload")?)? { + payload.push((text(key)?.to_owned(), parser.expr(value, 0)?)); + } + Instruction::Require { + condition: parser.predicate(field(node, "predicate")?, 0)?, + reason: Reason { + name: text_field(reason, "reasonKind")?.to_owned(), + payload: Expr::Record(payload), + }, + } + } + _ => return Err(Error::UnsupportedProgram), + }; + instructions.push(instruction); + } + Ok(Self { + input: parser.ty(text_field(intent, "input")?, 0)?, + output: parser.ty(text_field(intent, "output")?, 0)?, + basis: field(intent, "basis") + .ok() + .map(|v| parser.expr(v, 0)) + .transpose()?, + constraints, + instructions, + helpers, + result: parser.expr(field(body, "result")?, 0)?, + limits, + }) + } + pub(super) fn run( + &self, + input: Value, + access: &mut RecordedNodeAccess<'_, '_>, + schema: &EffectGraphSchema, + meter: &mut Meter, + ) -> Result { + meter.with_component(self.limits, |meter| { + self.run_body(input, access, schema, meter) + }) + } + fn run_body( + &self, + input: Value, + access: &mut RecordedNodeAccess<'_, '_>, + schema: &EffectGraphSchema, + meter: &mut Meter, + ) -> Result { + validate(&input, &self.input, meter, 0).map_err(|e| { + if e == Error::InvalidArtifact { + Error::InvalidInput + } else { + e + } + })?; + meter.charge(&input, 0)?; + let mut locals = BTreeMap::from([("arg.0", input)]); + let mut mutations = Vec::new(); + let helpers = &self.helpers; + if let Some(basis) = &self.basis { + let basis = expression(basis, &locals, helpers, meter, 0)?; + if bytes(&basis)?.len() != 32 { + return Err(Error::InvalidInput.into()); + } + } + for (name, condition) in &self.constraints { + if !predicate(condition, &locals, helpers, meter, 0)? { + return Err(Error::InputConstraintFailed(name.clone()).into()); + } + } + for instruction in &self.instructions { + // Mutation's fixed step is admitted atomically with its write bytes + // and row reservation after evaluating its argument. + if !matches!(instruction, Instruction::Mutation { .. }) { + meter.step(0)?; + } + match instruction { + Instruction::Mutation { kind, binding } => { + let value = expression(&binding.value, &locals, helpers, meter, 0)?; + if mutations.len() >= super::EFFECT_COUNT_CEILING_V1 { + return Err(EffectFrameError::BudgetExceeded); + } + let mutation = kind.emit(value)?; + let write_bytes = mutation + .emission_write_bytes() + .ok_or(EffectFrameError::BudgetExceeded)?; + access.reads.charge_emission(meter, write_bytes)?; + mutations + .try_reserve(1) + .map_err(|_| Error::HostAllocationFailed)?; + mutations.push(mutation); + let result = Value::Map(Vec::new()); + meter.charge(&result, 0)?; + validate(&result, &binding.ty, meter, 0)?; + locals.insert(binding.name.as_str(), result); + } + Instruction::Let(binding) => { + let value = expression(&binding.value, &locals, helpers, meter, 0)?; + validate(&value, &binding.ty, meter, 0)?; + locals.insert(binding.name.as_str(), value); + } + Instruction::Read { + binding, + failures, + max_bytes, + } => { + let address = expression(&binding.value, &locals, helpers, meter, 0)?; + let (node, expected) = address_value(&address)?; + if !access.contains(node) { + return Err(ReadError::OutsideAperture(node).into()); + } + let value = match access.atom(node, expected, *max_bytes, meter)? { + Ok(value) => { + let count = value.len() as u64; + meter.charge_steps(count)?; + meter.allocate( + VALUE_CELL_BYTES + .checked_add(count) + .ok_or(Error::AllocationBudgetExceeded)?, + )?; + meter.copy_precharged_read_bytes(value)? + } + Err(cause) => { + let tag = match cause { + ReadObstruction::Missing => "echo.atom-missing/v1", + ReadObstruction::AtomRequired => "echo.atom-required/v1", + ReadObstruction::TypeMismatch => "echo.atom-type-mismatch/v1", + ReadObstruction::AtomTooLarge => "echo.atom-byte-bound-exceeded/v1", + ReadObstruction::Guard => return Err(Error::InvalidArtifact.into()), + }; + return Err(self.obstruct( + failures.get(tag).ok_or(Error::InvalidArtifact)?, + Value::Map(Vec::new()), + meter, + )?); + } + }; + if !matches!(access.reads.readings.get(&SlotId::Node(node)), Some(SlotReading::Node(Some(record))) if schema.allows_atom(record.ty, expected)) + { + return Err(EffectFrameError::GraphSchemaViolation); + } + validate(&value, &binding.ty, meter, 0)?; + locals.insert(binding.name.as_str(), value); + } + Instruction::Require { condition, reason } => { + if !predicate(condition, &locals, helpers, meter, 0)? { + let payload = expression(&reason.payload, &locals, helpers, meter, 0)?; + return Err(self.obstruct(&reason.name, payload, meter)?); + } + } + } + } + let value = expression(&self.result, &locals, helpers, meter, 0)?; + validate(&value, &self.output, meter, 0)?; + Ok(Evaluation { + output: self.output_bytes(&value, meter)?, + mutations, + }) + } + fn output_bytes(&self, value: &Value, meter: &mut Meter) -> Result, EffectFrameError> { + meter.charge(value, 0)?; + let output = encode(value).map_err(|_| Error::InvalidArtifact)?; + if output.len() as u64 > self.limits.max_output_bytes { + return Err(Error::OutputBudgetExceeded.into()); + } + Ok(output) + } + fn obstruct( + &self, + name: &str, + payload: Value, + meter: &mut Meter, + ) -> Result { + Ok(EffectFrameError::AuthoredObstruction { + reason_kind: name.to_owned(), + payload: self.output_bytes(&payload, meter)?, + }) + } +} +fn binding(node: &Value, key: &str, parser: &mut Parser<'_>) -> Result { + let local = field(node, "binding")?; + Ok(Binding { + name: text_field(local, "id")?.to_owned(), + ty: parser.ty(text_field(local, "type")?, 0)?, + value: parser.expr(field(node, key)?, 0)?, + }) +} +fn address_value(value: &Value) -> Result<(NodeKey, TypeId), Error> { + let id = |key| { + bytes(field(value, key)?)? + .try_into() + .map_err(|_| Error::InvalidInput) + }; + Ok(( + NodeKey { + warp_id: WarpId(id("warpId")?), + local_id: NodeId(id("nodeId")?), + }, + TypeId(id("typeId")?), + )) +} diff --git a/crates/warp-core/src/edict_effects/failure_names.rs b/crates/warp-core/src/edict_effects/failure_names.rs new file mode 100644 index 000000000..177e82fd7 --- /dev/null +++ b/crates/warp-core/src/edict_effects/failure_names.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! The closed, per-intrinsic translation from catalog failure names to the +//! runtime tags Echo records. +//! +//! Edict's ABI names an intrinsic's failures with identifiers. Echo's runtime +//! records versioned tags such as `echo.atom-missing/v1`. Each runtime +//! intrinsic pins its own pairs here: the key is the intrinsic and the catalog +//! name together, never a name alone, and there is no fallback that forwards an +//! unfamiliar string. Application aliases (an adapter's own failure names) map +//! onto catalog names in the application's adapter and never appear here. +use std::collections::BTreeMap; + +use crate::edict_pure::EvaluationError as Error; + +/// One failure of one runtime intrinsic. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct FailureName { + /// Identifier the pinned catalog declares. + pub(super) catalog: &'static str, + /// Tag Echo's runtime records for it. + pub(super) runtime: &'static str, + /// Authority class the catalog must give it. + pub(super) authority: &'static str, +} + +const NODE_ATOM_READ_V1: [FailureName; 4] = [ + FailureName { + catalog: "atomMissing", + runtime: "echo.atom-missing/v1", + authority: "resourceFault", + }, + FailureName { + catalog: "atomRequired", + runtime: "echo.atom-required/v1", + authority: "integrityFault", + }, + FailureName { + catalog: "atomTypeMismatch", + runtime: "echo.atom-type-mismatch/v1", + authority: "integrityFault", + }, + FailureName { + catalog: "atomByteBoundExceeded", + runtime: "echo.atom-byte-bound-exceeded/v1", + authority: "integrityFault", + }, +]; + +/// The complete failure set of an exact runtime intrinsic identity, or `None` +/// for an identity that is not a runtime intrinsic. Mutations declare none. +pub(super) fn failures(intrinsic: &str) -> Option<&'static [FailureName]> { + match intrinsic { + "echo.dpo@1.node-atom-read" => Some(&NODE_ATOM_READ_V1), + "echo.dpo@1.ensure-node" + | "echo.dpo@1.create-node" + | "echo.dpo@1.replace-atom" + | "echo.dpo@1.ensure-edge" + | "echo.dpo@1.replace-edge" => Some(&[]), + _ => None, + } +} + +/// Translates a lowered step's obstruction arms, keyed by catalog name, into +/// arms keyed by runtime tag. Refuses an unknown, missing or repeated name. +pub(super) fn runtime_arms( + intrinsic: &str, + arms: BTreeMap, +) -> Result, Error> { + let declared = failures(intrinsic).ok_or(Error::InvalidArtifact)?; + if arms.len() != declared.len() { + return Err(Error::InvalidArtifact); + } + let mut runtime = BTreeMap::new(); + for (name, callee) in arms { + let failure = declared + .iter() + .find(|failure| failure.catalog == name) + .ok_or(Error::InvalidArtifact)?; + if runtime.insert(failure.runtime.to_owned(), callee).is_some() { + return Err(Error::InvalidArtifact); + } + } + Ok(runtime) +} + +#[cfg(test)] +#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] +mod tests { + use super::*; + + const READ: &str = "echo.dpo@1.node-atom-read"; + + fn arms(names: &[&str]) -> BTreeMap { + names + .iter() + .map(|name| { + ( + (*name).to_owned(), + format!("jedit.text@1.{name}Obstruction"), + ) + }) + .collect() + } + + #[test] + fn every_read_failure_has_exactly_one_runtime_meaning() { + let read = failures(READ).expect("read"); + let mut catalog = read.iter().map(|f| f.catalog).collect::>(); + let mut runtime = read.iter().map(|f| f.runtime).collect::>(); + catalog.sort_unstable(); + catalog.dedup(); + runtime.sort_unstable(); + runtime.dedup(); + assert_eq!(catalog.len(), 4); + assert_eq!(runtime.len(), 4); + // The runtime's four read obstructions are exactly these tags. + assert_eq!( + runtime, + [ + "echo.atom-byte-bound-exceeded/v1", + "echo.atom-missing/v1", + "echo.atom-required/v1", + "echo.atom-type-mismatch/v1", + ] + ); + } + + #[test] + fn the_table_is_scoped_by_intrinsic() { + // The read's names mean nothing on a mutation or an unknown identity. + assert_eq!(failures("echo.dpo@1.ensure-node"), Some(&[][..])); + assert_eq!(failures("echo.dpo@1.delete-node"), None); + assert!(runtime_arms("echo.dpo@1.ensure-node", arms(&["atomMissing"])).is_err()); + assert!(runtime_arms("echo.dpo@1.delete-node", arms(&[])).is_err()); + } + + #[test] + fn unknown_missing_and_tag_spelled_arms_are_refused() { + let all = [ + "atomMissing", + "atomRequired", + "atomTypeMismatch", + "atomByteBoundExceeded", + ]; + assert!(runtime_arms(READ, arms(&all)).is_ok()); + assert!(runtime_arms(READ, arms(&all[..3])).is_err()); + assert!(runtime_arms( + READ, + arms(&[ + "atomMissing", + "atomRequired", + "atomTypeMismatch", + "atomAbsent" + ]) + ) + .is_err()); + assert!(runtime_arms( + READ, + arms(&[ + "echo.atom-missing/v1", + "atomRequired", + "atomTypeMismatch", + "atomByteBoundExceeded" + ]) + ) + .is_err()); + } + + /// Jim's own name, the provider's catalog name and the recorded runtime + /// tag are three distinct layers; only the last two meet in Echo's table. + #[test] + fn jims_missing_alias_traces_to_the_recorded_runtime_tag() { + // Jim's adapter (the authored route's spelling): `missing` → `atomMissing`. + let jim_local = "missing"; + let catalog_name = "atomMissing"; + // The Target IR arm Edict emits is keyed by the adapter's target. + let lowered = [ + ("atomMissing", "jedit.text@1.MissingFact"), + ("atomRequired", "jedit.text@1.NotAtom"), + ("atomTypeMismatch", "jedit.text@1.TypeMismatch"), + ("atomByteBoundExceeded", "jedit.text@1.TooLarge"), + ] + .map(|(name, callee)| (name.to_owned(), callee.to_owned())) + .into_iter() + .collect(); + let runtime = runtime_arms(READ, lowered).expect("closed translation"); + assert_eq!( + runtime.get("echo.atom-missing/v1").map(String::as_str), + Some("jedit.text@1.MissingFact"), + "{jim_local} → {catalog_name} → echo.atom-missing/v1" + ); + // Jim's alias is not an Echo name. + assert!(failures(READ) + .expect("read") + .iter() + .all(|failure| failure.catalog != jim_local)); + } +} diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs new file mode 100644 index 000000000..a78c7f811 --- /dev/null +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -0,0 +1,574 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! One logical work/storage envelope for computation and recorded reads. +use super::{EffectRangeReadError, EffectRangeReadSession, EFFECT_READ_STORAGE_CEILING_V1}; +use crate::edict_pure::evaluate::Meter; +use crate::edict_pure::{self, EvaluationError, EvaluationLimits}; +use crate::{ + EchoOperationBudgetV1, EchoOperationEvaluationBasisV1, EdgeRecord, Footprint, + OutgoingEdgeRange, SlotId, SlotReading, +}; +use std::collections::BTreeMap; + +/// Refusal before constructing a shared evaluation frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectFrameSetupError { + /// The requested full runtime parent differs from the current parent. + #[error("effect frame parent basis does not match the runtime parent")] + BasisMismatch, + /// Already-observed resources cannot be imported without accounting for them. + #[error("read session has already been used")] + ReadSessionAlreadyUsed, + /// A typed law requires an unused runtime-bound frame. + #[error("typed law requires an unused runtime-bound frame")] + InvalidLawBinding, + /// Read storage or the operation budget exceeds the fixed effect ceilings. + #[error("frame resource declaration exceeds the fixed ceiling")] + ResourceCeilingExceeded, +} + +/// Terminal frame failure, without any mutation or admission authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EffectFrameError { + /// A caller-frontier frame cannot run recorded compiled reads as runtime-bound work. + UnboundParent, + /// Failure from the existing bounded-read interpreter or aperture validation. + BoundedRead(crate::edict_read::ReadError), + /// Failure from the unchanged compiler-package interpreter. + Evaluation(EvaluationError), + /// Read refusal or host fault; host allocation faults must not become Actions. + Read(EffectRangeReadError), + /// Failure or completed whole authored execution ended this frame. + Terminated, + /// Whole authored evaluation cannot adopt another private component's work. + UsedFrame, + /// The fixed effect-count ceiling or delegated emission budget is exhausted. + BudgetExceeded, + /// A resolved node/atom pair violates the exact prepared graph schema. + GraphSchemaViolation, + /// An authored terminal reason or mapped read failure, without an Action decision. + AuthoredObstruction { + /// Authored reason name in the bound source context. + reason_kind: String, + /// Canonical payload bytes. + payload: Vec, + }, +} + +impl EffectRangeReadSession<'_> { + pub(super) fn charge_emission( + &mut self, + shared: &mut Meter, + write_bytes: u64, + ) -> Result<(), EffectFrameError> { + let mut io = self.meter; + let mut candidate = *shared; + if !io.charge(1, 0, write_bytes) { + return Err(EffectFrameError::BudgetExceeded); + } + candidate.charge_steps(1)?; + candidate.allocate(280)?; + self.meter = io; + *shared = candidate; + Ok(()) + } +} + +impl From for EffectFrameError { + fn from(error: EffectRangeReadError) -> Self { + Self::Read(error) + } +} + +impl From for EffectFrameError { + fn from(error: crate::edict_read::ReadError) -> Self { + Self::BoundedRead(error) + } +} +impl From for EffectFrameError { + fn from(error: EvaluationError) -> Self { + Self::BoundedRead(error.into()) + } +} + +/// Trusted-host composition with one cumulative interpreter/read resource meter. +/// +/// This reuses the real pure interpreter and independently pinned packages. It +/// is not a graph-effect program, installed profile, physical arena or admission +/// capability. The caller must select verified package pins independently. +#[derive(Debug)] +pub struct EffectEvaluationFrame<'a> { + reads: EffectRangeReadSession<'a>, + meter: Meter, + limits: EvaluationLimits, + terminated: bool, + completed: Option, + evaluation_basis: Option, + evaluation_authority: Option, +} + +impl<'a> EffectEvaluationFrame<'a> { + /// Starts an unused session under intersected computation/read ceilings. + /// + /// # Errors + /// + /// Refuses used sessions or declarations above the fixed effect ceilings. + pub fn new( + reads: EffectRangeReadSession<'a>, + mut limits: EvaluationLimits, + ) -> Result { + if reads.allocated_bytes != 0 + || !reads.readings.is_empty() + || reads.consumed() != EchoOperationBudgetV1::new(0, 0, 0) + { + return Err(EffectFrameSetupError::ReadSessionAlreadyUsed); + } + let budget = reads.meter.delegated(); + if limits.max_allocated_bytes > EFFECT_READ_STORAGE_CEILING_V1 + || budget.steps() > 1_048_576 + || budget.read_bytes() > 67_108_864 + || budget.write_bytes() > 16_777_216 + { + return Err(EffectFrameSetupError::ResourceCeilingExceeded); + } + limits.max_steps = limits.max_steps.min(budget.steps()); + limits.max_allocated_bytes = limits.max_allocated_bytes.min(reads.max_allocated_bytes); + Ok(Self { + reads, + meter: Meter::new_atomic(limits), + limits, + terminated: false, + completed: None, + evaluation_basis: None, + evaluation_authority: None, + }) + } + + // Only the runtime owner may pair a frame with its resolved parent. + // A caller-provided frontier cannot acquire this coordinate through `new`. + #[cfg(feature = "native_rule_bootstrap")] + pub(crate) fn at_parent( + reads: EffectRangeReadSession<'a>, + limits: EvaluationLimits, + basis: EchoOperationEvaluationBasisV1, + authority: crate::echo_operation::EchoOperationEvaluationAuthorityV1, + ) -> Result { + let reading = reads.basis(); + if reading.worldline_id() != basis.writer_head().worldline_id + || reading.tick() != basis.worldline_tick() + || reading.state_root() != basis.state_root() + { + return Err(EffectFrameSetupError::BasisMismatch); + } + let mut frame = Self::new(reads, limits)?; + frame.evaluation_basis = Some(basis); + frame.evaluation_authority = Some(authority); + Ok(frame) + } + + /// Binds host-selected typed read permissions before any evaluation work. + /// + /// This does not admit a package or bind the law into package identity. + /// # Errors + /// Refuses unbound, used, terminated or already law-bound frames. + pub fn with_footprint_law( + mut self, + law: super::EffectFootprintLaw, + ) -> Result { + if self.evaluation_basis.is_none() + || self.terminated + || self.reads.law.is_some() + || self.consumed() != EchoOperationBudgetV1::new(0, 0, 0) + || self.allocated_bytes() != 0 + { + return Err(EffectFrameSetupError::InvalidLawBinding); + } + self.reads.law = Some(law); + Ok(self) + } + + /// Resolves a node using its actual type, or the named type for absence. + /// # Errors + /// Refuses a missing law, unreachable warp, denied type or exhausted limits. + /// Any refusal terminates the frame without delivering a node value. + pub fn read_typed_node( + &mut self, + node: crate::NodeKey, + absent_type: crate::TypeId, + ) -> Result, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.reads.law.is_none() { + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + let result = self + .reads + .typed_node(node, absent_type, &mut self.meter) + .map_err(EffectFrameError::Read); + self.terminated = result.is_err(); + result + } + + /// Distinct authorized slot/type pairs, including separately named absence. + /// + /// This is host-private authorization evidence, not the canonical persisted + /// ReadRow image. Permission is rechecked even when a pair was seen before. + #[must_use] + pub const fn authorized_reads(&self) -> &std::collections::BTreeSet<(SlotId, crate::TypeId)> { + &self.reads.authorized_reads + } + + /// Runtime-selected parent, present only for a host-created bound frame. + /// + /// Low-level [`Self::new`] frames remain unbound. This coordinate does not + /// authorize an invocation or establish an installed effect profile. + #[must_use] + pub const fn evaluation_basis(&self) -> Option { + self.evaluation_basis + } + + /// Reads a range through the recording view under the shared meter. + /// + /// # Errors + /// + /// A refusal or host fault ends the frame. Subsequent calls do no work. + pub fn read_range( + &mut self, + range: OutgoingEdgeRange, + ) -> Result>, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.reads.law.is_some() { + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + let result = self + .reads + .read_with_meter(range, Some(&mut self.meter)) + .map_err(EffectFrameError::Read); + self.terminated = result.is_err(); + result + } + + /// Reads a range under both host and typed-law source/edge permissions. + /// + /// The law's bucket bound intersects the host limit. Matching and + /// nonmatching edges consume the same precharged scan budget. + /// # Errors + /// Refuses missing law, unreachability, denied types or exhausted limits. + /// Every refusal terminates the frame before delivering a range value. + pub fn read_typed_range( + &mut self, + range: OutgoingEdgeRange, + absent_source_type: crate::TypeId, + ) -> Result>, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.reads.law.is_none() { + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + let result = self + .reads + .read_range_inner(range, Some(&mut self.meter), Some(absent_source_type)) + .map_err(EffectFrameError::Read); + self.terminated = result.is_err(); + result + } + + /// Privately evaluates the prepared current authored program against its bound parent. + /// Reads are recorded and mutations are buffered as unvalidated intents. + /// This grants no invocation or mutation authority. + /// # Errors + /// Requires an unused frame. Failure or successful whole authored execution + /// ends evaluation; only retention/inspection remains afterward. + pub fn evaluate_effect( + &mut self, + program: &super::EffectProgram, + input: &[u8], + aperture: &[super::TypedNodeRead], + ) -> Result<&[u8], EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.evaluation_basis.is_none() { + return Err(EffectFrameError::UnboundParent); + } + if self.consumed() != EchoOperationBudgetV1::new(0, 0, 0) || self.allocated_bytes() != 0 { + return Err(EffectFrameError::UsedFrame); + } + if self.reads.law.as_ref() != Some(program.footprint_law()) { + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + let aperture = super::NodeAperture::Typed(aperture); + aperture.validate()?; + if program.compiled_program().len() > self.limits.max_package_bytes { + return Err(EvaluationError::PackageTooLarge.into()); + } + if input.len() as u64 > (self.limits.max_input_bytes as u64).min(program.max_input_bytes()) + { + return Err(EvaluationError::InputTooLarge.into()); + } + let executable = program.execution(self.limits)?; + let input_basis = crate::echo_operation::effect_input_basis(input); + let input = echo_edict_canonical::decode_canonical_cbor_v1(input) + .map_err(|_| EffectFrameError::Evaluation(EvaluationError::InvalidInput))?; + let mut access = super::RecordedNodeAccess { + reads: &mut self.reads, + aperture, + }; + let result = executable.run(input, &mut access, program.graph_schema(), &mut self.meter)?; + let completed = self.completed.insert(super::completion::AuthoredExecution { + program: program.identity(), + input_basis, + output: result.output, + mutations: result.mutations, + }); + Ok(&completed.output) + } + + /// Intents from the completed whole authored operation, in execution order. + /// These have not passed proposal validation and are not writes or a patch. + /// Failure never publishes a partial intent buffer. + #[must_use] + pub fn buffered_mutations(&self) -> &[super::EffectMutation] { + self.completed + .as_ref() + .map_or(&[], |value| value.mutations.as_slice()) + } + + /// Consumes one completed authored operation into its exact origin evidence. + /// Failed, unused and mixed-component frames cannot produce this value. + /// This is bounded host retention, not proposal validation or Action admission. + /// # Errors + /// Refuses missing completion, input-basis mismatch or read-image export failure. + pub fn into_effect_evaluation( + mut self, + max_read_bytes: usize, + ) -> Result { + let execution = self + .completed + .take() + .ok_or(super::EffectCompletionError::NoCompletedOperation)?; + let parent = self + .evaluation_basis + .ok_or(super::EffectCompletionError::NoCompletedOperation)?; + if parent.application_basis() != execution.input_basis { + return Err(super::EffectCompletionError::InputBasisMismatch); + } + let authority = self + .evaluation_authority + .take() + .ok_or(super::EffectCompletionError::NoCompletedOperation)?; + let reads = self.into_read_set_image(max_read_bytes)?; + Ok(super::CompletedEffectEvaluation::new( + execution, reads, authority, + )) + } + + /// Evaluates exact compiler-package bytes using the same cumulative meter. + /// + /// Package/input decoding is bounded host preparation. This is private + /// computation; matching a caller-selected pin does not establish approval. + /// + /// # Errors + /// + /// Any decode, identity, execution or resource failure ends this frame. + /// Host allocation failure must stop the host without deciding an Action. + pub fn evaluate_pure( + &mut self, + package: &[u8], + pin: [u8; 32], + input: &[u8], + ) -> Result, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + let result = edict_pure::prepare(package, pin, input, self.limits) + .and_then(|(program, input)| { + edict_pure::evaluate::run_with_meter(&program, input, &mut self.meter) + }) + .map(|result| result.output) + .map_err(EffectFrameError::Evaluation); + self.terminated = result.is_err(); + result + } + + /// Interprets a pinned bounded-read package over the bound runtime frontier. + /// + /// The trusted host selects a sorted node aperture. Actual node and alpha + /// attachment resolutions are charged and retained, including absence and + /// semantic failures. Expression work, value storage and previous components + /// remain under the shared frame meter. This does not implement the complete + /// node-type/portal footprint law, installation or admission of effect programs. + /// + /// # Errors + /// + /// Refuses an unbound frame, malformed aperture/package/input, authored + /// obstruction or exhausted limits. Every failure terminates the frame. + /// Host allocation faults must stop the host rather than decide an Action. + pub fn evaluate_read( + &mut self, + package: &[u8], + pin: [u8; 32], + input: &[u8], + aperture: &[crate::NodeKey], + limits: crate::edict_read::ReadLimits, + ) -> Result, EffectFrameError> { + if self.reads.law.is_some() { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + self.evaluate_read_aperture( + package, + pin, + input, + super::NodeAperture::Untyped(aperture), + limits, + ) + } + + /// Evaluates a pinned bounded-read package under typed host permissions. + /// + /// Present records authorize their actual types; named node types authorize + /// absence only. Portal chains are authorized before target resolution. + /// # Errors + /// Refuses missing law, invalid aperture, denied types/reachability, authored + /// obstructions and resource failures. Every refusal terminates the frame. + pub fn evaluate_typed_read( + &mut self, + package: &[u8], + pin: [u8; 32], + input: &[u8], + aperture: &[super::TypedNodeRead], + limits: crate::edict_read::ReadLimits, + ) -> Result, EffectFrameError> { + if self.reads.law.is_none() { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + return Err(EffectRangeReadError::FootprintLawViolation.into()); + } + self.evaluate_read_aperture( + package, + pin, + input, + super::NodeAperture::Typed(aperture), + limits, + ) + } + + fn evaluate_read_aperture( + &mut self, + package: &[u8], + pin: [u8; 32], + input: &[u8], + aperture: super::NodeAperture<'_>, + mut limits: crate::edict_read::ReadLimits, + ) -> Result, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.evaluation_basis.is_none() { + return Err(EffectFrameError::UnboundParent); + } + limits.evaluation.max_package_bytes = limits + .evaluation + .max_package_bytes + .min(self.limits.max_package_bytes); + limits.evaluation.max_input_bytes = limits + .evaluation + .max_input_bytes + .min(self.limits.max_input_bytes); + limits.evaluation.max_output_bytes = limits + .evaluation + .max_output_bytes + .min(self.limits.max_output_bytes); + limits.evaluation.max_steps = limits.evaluation.max_steps.min(self.limits.max_steps); + limits.evaluation.max_allocated_bytes = limits + .evaluation + .max_allocated_bytes + .min(self.limits.max_allocated_bytes); + let result = crate::edict_read::evaluate_recorded( + package, + pin, + input, + &mut self.reads, + aperture, + &mut self.meter, + limits, + ) + .map(|result| result.output); + self.terminated = result.is_err(); + result + } + + /// Consumes this frame into bounded canonical completed-read evidence. + /// + /// Failed typed frames may export their completed observation prefix. A + /// permission reservation without a completed value resolution is omitted. + /// This is bounded host retention work outside the evaluation meter, not a + /// physical interpreter arena, installed capability or historical decision. + /// The output limit intersects the fixed 16 MiB read-storage ceiling. + /// # Errors + /// Refuses unbound/untyped frames, inconsistent observations, an output + /// exceeding the host limit, or host allocation failure. No image is + /// published on failure; a retention host must not turn it into an Action. + pub fn into_read_set_image( + self, + max_bytes: usize, + ) -> Result { + let basis = self + .evaluation_basis + .ok_or(super::EffectReadImageError::MissingTypedParent)?; + if self.reads.law.is_none() { + return Err(super::EffectReadImageError::MissingTypedParent); + } + super::read_image::export( + &self.reads, + basis, + self.consumed(), + self.allocated_bytes(), + max_bytes, + ) + } + + /// Global computation/read/emission steps and all admitted I/O charges. + #[must_use] + pub fn consumed(&self) -> EchoOperationBudgetV1 { + EchoOperationBudgetV1::new( + self.meter.usage().0, + self.reads.consumed().read_bytes(), + self.reads.consumed().write_bytes(), + ) + } + + /// Global cumulative expression and read-value storage reservations. + #[must_use] + pub fn allocated_bytes(&self) -> u64 { + self.meter.usage().1 + } + + /// Values observed at the immutable source frontier, including any prefix. + #[must_use] + pub const fn readings(&self) -> &BTreeMap { + self.reads.readings() + } + + /// Runtime-recorded graph dependencies; pure computation contributes none. + #[must_use] + pub const fn footprint(&self) -> &Footprint { + self.reads.footprint() + } +} diff --git a/crates/warp-core/src/edict_effects/imported_types.rs b/crates/warp-core/src/edict_effects/imported_types.rs new file mode 100644 index 000000000..3e627a6c3 --- /dev/null +++ b/crates/warp-core/src/edict_effects/imported_types.rs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Authenticate imported Core declarations against pinned canonical export definitions. +use crate::edict_pure::{ + model::{MAX_DEPTH, MAX_PROGRAM_NODES}, + values::{array, exact_fields, field, map, number, text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::CanonicalValueV1 as Value; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn validate(module: &Value, exports: &Value, lawpack: &str) -> Result<(), Error> { + let mut definitions = BTreeMap::new(); + let mut remaining = MAX_PROGRAM_NODES; + for item in array(field(exports, "types")?)? { + spend(&mut remaining)?; + exact_fields(item, &["coordinate", "definition"])?; + let name = text_field(item, "coordinate")?; + let definition = text_field(item, "definition")?; + if name.is_empty() + || definition.is_empty() + || definitions.insert(name, definition).is_some() + { + return Err(Error::InvalidArtifact); + } + } + let prefix = format!("{lawpack}."); + for (name, value) in map(field(module, "types")?)? { + spend(&mut remaining)?; + let name = text(name)?; + if !name.starts_with(&prefix) { + continue; + } + let mut expected = *definitions.get(name).ok_or(Error::InvalidArtifact)?; + let mut seen = BTreeSet::new(); + while let Some(next) = definitions.get(expected) { + spend(&mut remaining)?; + if seen.len() >= MAX_DEPTH || !seen.insert(expected) { + return Err(Error::InvalidArtifact); + } + expected = next; + } + if render(name, value, &mut remaining)? != expected { + return Err(Error::InvalidArtifact); + } + } + Ok(()) +} +fn spend(remaining: &mut usize) -> Result<(), Error> { + *remaining = remaining.checked_sub(1).ok_or(Error::UnsupportedProgram)?; + Ok(()) +} +fn reference<'a>(value: &'a Value, remaining: &mut usize) -> Result<&'a str, Error> { + spend(remaining)?; + let value = text(value)?; + if value.is_empty() || value.len() > 1024 { + return Err(Error::InvalidArtifact); + } + let mut depth = 0usize; + for byte in value.bytes() { + match byte { + b'<' => { + depth += 1; + if depth > MAX_DEPTH { + return Err(Error::UnsupportedProgram); + } + } + b'>' => { + depth = depth.checked_sub(1).ok_or(Error::InvalidArtifact)?; + } + _ => {} + } + } + if depth != 0 { + return Err(Error::InvalidArtifact); + } + Ok(value) +} +fn render(name: &str, value: &Value, remaining: &mut usize) -> Result { + spend(remaining)?; + let kind = text_field(value, "kind")?; + match kind { + "Bool" | "Unit" | "I8" | "I16" | "I32" | "I64" | "U8" | "U16" | "U32" | "U64" => { + exact_fields(value, &["kind"])?; + Ok(kind.to_owned()) + } + "Bytes" => { + let max = number(field(value, "max")?)?; + if let Ok(min) = field(value, "min") { + exact_fields(value, &["kind", "min", "max"])?; + let min = number(min)?; + if min > max { + return Err(Error::InvalidArtifact); + } + if min == max { + Ok(format!("Bytes")) + } else { + Ok(format!("Bytes")) + } + } else { + exact_fields(value, &["kind", "max"])?; + Ok(format!("Bytes")) + } + } + "String" => { + exact_fields(value, &["kind", "max", "canonical"])?; + let max = number(field(value, "max")?)?; + let canonical = text_field(value, "canonical")?; + if !["raw-utf8", "unicode-scalar-nfc"].contains(&canonical) { + return Err(Error::InvalidArtifact); + } + Ok(format!("String")) + } + "Record" => { + exact_fields(value, &["kind", "fields"])?; + let mut fields = BTreeMap::new(); + for (key, ty) in map(field(value, "fields")?)? { + let key = text(key)?; + if key.is_empty() + || !key.bytes().enumerate().all(|(i, c)| { + c == b'_' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit()) + }) + { + return Err(Error::InvalidArtifact); + } + if fields.insert(key, reference(ty, remaining)?).is_some() { + return Err(Error::InvalidArtifact); + } + } + let fields = fields + .into_iter() + .map(|(name, ty)| format!("{name}:{ty}")) + .collect::>() + .join(","); + Ok(format!("Record<{fields}>")) + } + "Nominal" => { + exact_fields(value, &["kind", "contract", "representation"])?; + if text_field(value, "contract")? != name { + return Err(Error::InvalidArtifact); + } + let representation = reference(field(value, "representation")?, remaining)?; + Ok(format!("Nominal<{representation}>")) + } + "Option" | "CapabilityRef" => { + exact_fields(value, &["kind", "item"])?; + let item = reference(field(value, "item")?, remaining)?; + Ok(format!("{kind}<{item}>")) + } + "List" => { + exact_fields(value, &["kind", "item", "max"])?; + let item = reference(field(value, "item")?, remaining)?; + let max = number(field(value, "max")?)?; + Ok(format!("List<{item},max={max}>")) + } + "Map" => { + exact_fields(value, &["kind", "key", "value", "max"])?; + let key = reference(field(value, "key")?, remaining)?; + let item = reference(field(value, "value")?, remaining)?; + let max = number(field(value, "max")?)?; + Ok(format!("Map<{key},{item},max={max}>")) + } + "ExternalActionRequest" => { + exact_fields(value, &["kind", "settlement"])?; + let settlement = reference(field(value, "settlement")?, remaining)?; + Ok(format!("edict.external-action.request/v1<{settlement}>")) + } + _ => Err(Error::UnsupportedProgram), + } +} diff --git a/crates/warp-core/src/edict_effects/instruction_relation.rs b/crates/warp-core/src/edict_effects/instruction_relation.rs new file mode 100644 index 000000000..c14053a8e --- /dev/null +++ b/crates/warp-core/src/edict_effects/instruction_relation.rs @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Ordered Core/Target correspondence and exact lexical local identities. +use crate::edict_pure::{ + functions, + model::{Expr, Helper, Predicate, RuntimeType, MAX_DEPTH, MAX_PROGRAM_NODES}, + syntax::Parser, + values::{array, exact_fields, field, map, require_text, text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as Value}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn validate( + module: &Value, + intent: &Value, + target: &Value, + exports: &Value, + signatures: &BTreeMap<&str, &Value>, +) -> Result<(), Error> { + let mut fields = vec![ + "operationProfile", + "inputConstraints", + "coreEvaluationBudget", + "steps", + "requirements", + "result", + "executionOrder", + ]; + for key in ["basis", "pureBindings"] { + if field(target, key).is_ok() { + fields.push(key); + } + } + exact_fields(target, &fields)?; + if field(intent, "optic").is_ok() { + return Err(Error::UnsupportedProgram); + } + let body = field(intent, "body")?; + let mut scope = Scope::new( + module, + exports, + array(field(body, "locals")?)?, + text_field(intent, "input")?, + )?; + scope.parser.ty(text_field(intent, "output")?, 0)?; + if let Ok(basis) = field(intent, "basis") { + scope.expression(basis, None)?; + scope.data_type(basis)?; + } + for constraint in array(field(intent, "inputConstraints")?)? { + scope.predicate(field(constraint, "predicate")?)?; + } + let mut lowered = BTreeMap::new(); + for (collection, kind) in [ + ("steps", "effect"), + ("requirements", "require"), + ("pureBindings", "let"), + ] { + if collection == "pureBindings" && field(target, collection).is_err() { + continue; + } + for node in array(field(target, collection)?)? { + let id = text_field(node, "id")?; + if id.is_empty() || lowered.insert(id, (kind, node)).is_some() { + return Err(Error::InvalidArtifact); + } + } + } + let nodes = array(field(body, "nodes")?)?; + let order = array(field(target, "executionOrder")?)?; + if nodes.len() != order.len() || nodes.len() != lowered.len() { + return Err(Error::InvalidArtifact); + } + for (node, id) in nodes.iter().zip(order) { + scope.parser.enter(0)?; + let (kind, target) = lowered.remove(text(id)?).ok_or(Error::InvalidArtifact)?; + require_text(node, "kind", kind)?; + match kind { + "let" => { + exact_fields(node, &["kind", "binding", "value"])?; + exact_fields(target, &["id", "binding", "value"])?; + equal(node, target, "binding")?; + equal(node, target, "value")?; + scope.typed_expression( + field(node, "value")?, + text_field(field(node, "binding")?, "type")?, + )?; + scope.introduce(field(node, "binding")?)?; + } + "require" => { + exact_fields(node, &["kind", "predicate", "onFailure"])?; + exact_fields(target, &["id", "predicate", "onFailure"])?; + equal(node, target, "predicate")?; + equal(node, target, "onFailure")?; + scope.predicate(field(node, "predicate")?)?; + let failure = field(node, "onFailure")?; + exact_fields(failure, &["kind", "reason"])?; + require_text(failure, "kind", "terminal")?; + let reason = field(failure, "reason")?; + exact_fields(reason, &["reasonKind", "payload"])?; + if text_field(reason, "reasonKind")?.is_empty() { + return Err(Error::InvalidArtifact); + } + for (_, value) in map(field(reason, "payload")?)? { + scope.expression(value, None)?; + scope.data_type(value)?; + } + } + "effect" => effect( + node, + target, + &mut scope, + signatures + .get(text_field(target, "id")?) + .copied() + .ok_or(Error::InvalidArtifact)?, + )?, + _ => return Err(Error::UnsupportedProgram), + } + } + scope.typed_expression(field(body, "result")?, text_field(intent, "output")?)?; + if scope.used.len() != scope.inventory.len() { + return Err(Error::InvalidArtifact); + } + functions::check_roots( + &scope.helpers, + scope.expressions.iter(), + scope.predicates.iter(), + )?; + Ok(()) +} +fn equal(core: &Value, target: &Value, key: &str) -> Result<(), Error> { + if field(core, key)? != field(target, key)? { + return Err(Error::InvalidArtifact); + } + Ok(()) +} +fn effect( + core: &Value, + target: &Value, + scope: &mut Scope<'_>, + signature: &Value, +) -> Result<(), Error> { + exact_fields( + core, + &["kind", "binding", "effect", "input", "obstructionMap"], + )?; + exact_fields( + target, + &[ + "id", + "binding", + "effect", + "input", + "targetIntrinsic", + "obstructionArms", + "obstructionFailures", + ], + )?; + for key in ["binding", "effect", "input"] { + equal(core, target, key)?; + } + if text_field(target, "targetIntrinsic")?.is_empty() || text_field(core, "effect")?.is_empty() { + return Err(Error::InvalidArtifact); + } + scope.typed_expression(field(core, "input")?, text_field(signature, "inputType")?)?; + let mut arms = BTreeSet::new(); + for (_, arm) in map(field(core, "obstructionMap")?)? { + exact_fields(arm, &["binder", "value"])?; + let binder = scope.claim(field(arm, "binder")?)?; + scope.expression(field(arm, "value")?, Some(binder))?; + if !arms.insert(encode(arm).map_err(|_| Error::InvalidArtifact)?) { + return Err(Error::InvalidArtifact); + } + } + let mut failures = BTreeSet::new(); + for failure in array(field(target, "obstructionFailures")?)? { + let failure = text(failure)?; + if failure.is_empty() || !failures.insert(failure) { + return Err(Error::InvalidArtifact); + } + } + for (name, arm) in map(field(target, "obstructionArms")?)? { + if !failures.remove(text(name)?) + || !arms.remove(&encode(arm).map_err(|_| Error::InvalidArtifact)?) + { + return Err(Error::InvalidArtifact); + } + } + if !arms.is_empty() || !failures.is_empty() { + return Err(Error::InvalidArtifact); + } + scope.introduce(field(core, "binding")?)?; + Ok(()) +} +struct Scope<'a> { + inventory: BTreeMap<&'a str, &'a Value>, + available: BTreeMap<&'a str, &'a Value>, + used: BTreeSet<&'a str>, + parser: Parser<'a>, + remaining_references: usize, + definitions: BTreeMap>, + helpers: BTreeMap, + expressions: Vec, + predicates: Vec, +} +impl<'a> Scope<'a> { + fn new( + module: &'a Value, + exports: &'a Value, + locals: &'a [Value], + input_type: &str, + ) -> Result { + let mut parser = Parser { + types: field(module, "types")?, + coordinate: text_field(module, "coordinate")?, + remaining: MAX_PROGRAM_NODES, + }; + let helpers = functions::decode(module, exports, &mut parser)?; + let definitions = functions::definitions(module, exports, parser.coordinate)?; + let mut inventory = BTreeMap::new(); + for local in locals { + functions::local(local)?; + parser.ty(text_field(local, "type")?, 0)?; + if inventory.insert(text_field(local, "id")?, local).is_some() { + return Err(Error::InvalidArtifact); + } + } + let input = *inventory.get("arg.0").ok_or(Error::InvalidArtifact)?; + if text_field(input, "type")? != input_type { + return Err(Error::InvalidArtifact); + } + Ok(Self { + inventory, + available: BTreeMap::from([("arg.0", input)]), + used: BTreeSet::from(["arg.0"]), + parser, + remaining_references: MAX_PROGRAM_NODES, + definitions, + helpers, + expressions: Vec::new(), + predicates: Vec::new(), + }) + } + fn claim(&mut self, reference: &Value) -> Result<&'a Value, Error> { + let id = text_field(reference, "id")?; + let declared = *self.inventory.get(id).ok_or(Error::InvalidArtifact)?; + let id = text_field(declared, "id")?; + if declared != reference || self.available.contains_key(id) || !self.used.insert(id) { + return Err(Error::InvalidArtifact); + } + Ok(declared) + } + fn introduce(&mut self, reference: &Value) -> Result<(), Error> { + let declared = self.claim(reference)?; + self.available.insert(text_field(declared, "id")?, declared); + Ok(()) + } + fn expression(&mut self, value: &Value, binder: Option<&Value>) -> Result<(), Error> { + self.parser.expr(value, 0)?; + self.references(value, binder, 0) + } + fn data_type(&mut self, value: &Value) -> Result { + // Effect programs carry no proof yet, so runtime slices and + // subtractions need literal operands. + let caps = functions::Caps::new(); + let actual = functions::infer( + value, + &self.available, + functions::Proof::new(&caps, &[]), + &self.definitions, + true, + &mut self.parser, + 0, + )?; + self.expressions.push(self.parser.expr(value, 0)?); + Ok(actual) + } + fn typed_expression(&mut self, value: &Value, expected: &str) -> Result<(), Error> { + self.expression(value, None)?; + let actual = self.data_type(value)?; + let expected = self.parser.ty(expected, 0)?; + if !functions::accepts(&expected, &actual) { + return Err(Error::InvalidArtifact); + } + Ok(()) + } + fn predicate(&mut self, value: &Value) -> Result<(), Error> { + let caps = functions::Caps::new(); + functions::check_predicate( + value, + &self.available, + functions::Proof::new(&caps, &[]), + &self.definitions, + true, + &mut self.parser, + 0, + )?; + self.predicates.push(self.parser.predicate(value, 0)?); + self.references(value, None, 0) + } + fn references( + &mut self, + value: &Value, + binder: Option<&Value>, + depth: usize, + ) -> Result<(), Error> { + if depth > MAX_DEPTH { + return Err(Error::UnsupportedProgram); + } + self.remaining_references = self + .remaining_references + .checked_sub(1) + .ok_or(Error::UnsupportedProgram)?; + match value { + Value::Map(fields) => { + if text_field(value, "kind") == Ok("local") { + let reference = field(value, "ref")?; + let id = text_field(reference, "id")?; + let declared = binder + .filter(|binder| text_field(binder, "id") == Ok(id)) + .or_else(|| self.available.get(id).copied()); + if declared != Some(reference) { + return Err(Error::InvalidArtifact); + } + } + for (_, value) in fields { + self.references(value, binder, depth + 1)?; + } + } + Value::Array(values) => { + for value in values { + self.references(value, binder, depth + 1)?; + } + } + _ => {} + } + Ok(()) + } +} diff --git a/crates/warp-core/src/edict_effects/intrinsic_contract.rs b/crates/warp-core/src/edict_effects/intrinsic_contract.rs new file mode 100644 index 000000000..76bcfdd01 --- /dev/null +++ b/crates/warp-core/src/edict_effects/intrinsic_contract.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Concrete declared signature/fault agreement across the pinned catalog and lawpack. +//! +//! For an application intrinsic the catalog and the exported effect name the +//! same types. For a shared runtime intrinsic the catalog names Echo's ABI +//! coordinates instead, and the application's own types are checked for +//! representation conformance by `runtime_abi`. +use crate::edict_pure::{ + values::{array, exact_fields, field, map, require_text, text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::CanonicalValueV1 as Value; + +pub(super) fn validate( + intrinsic_name: &str, + intrinsic: &Value, + effect: &Value, + implementation: &Value, +) -> Result<(), Error> { + let abi = super::runtime_abi::contract(intrinsic_name); + exact_fields( + intrinsic, + &[ + "intrinsicClass", + "argumentTypes", + "returnType", + "typeParameters", + "effectKind", + "writeClass", + "guardSupport", + "canParticipateInAtomicGuard", + "costTemplate", + "footprintTemplate", + "effectFailures", + ], + )?; + require_text(intrinsic, "intrinsicClass", "effect")?; + require_text(effect, "executionClass", "runtime")?; + let [argument] = array(field(intrinsic, "argumentTypes")?)? else { + return Err(Error::UnsupportedProgram); + }; + let input = text(argument)?; + let output = text_field(intrinsic, "returnType")?; + let (expected_input, expected_output) = match abi { + Some(abi) => (abi.input, abi.result), + None => ( + text_field(effect, "inputType")?, + text_field(effect, "outputType")?, + ), + }; + if input != expected_input || output != expected_output { + return Err(Error::InvalidArtifact); + } + if !array(field(intrinsic, "typeParameters")?)?.is_empty() + || !array(field(effect, "typeParameters")?)?.is_empty() + { + return Err(Error::UnsupportedProgram); + } + require_text( + intrinsic, + "writeClass", + text_field(implementation, "writeClass")?, + )?; + let boolean = |value: &Value| { + if let Value::Bool(value) = value { + Ok(*value) + } else { + Err(Error::InvalidArtifact) + } + }; + if boolean(field(effect, "guardSupport")?)? && !boolean(field(intrinsic, "guardSupport")?)? { + return Err(Error::InvalidArtifact); + } + boolean(field(intrinsic, "guardSupport")?)?; + boolean(field(intrinsic, "canParticipateInAtomicGuard")?)?; + for name in ["effectKind", "costTemplate", "footprintTemplate"] { + if text_field(intrinsic, name)?.is_empty() { + return Err(Error::InvalidArtifact); + } + } + let failures = field(intrinsic, "effectFailures")?; + let exported = field(effect, "effectFailures")?; + let mappings = map(field(implementation, "failureMappings")?)?; + if map(failures)?.len() != mappings.len() || map(exported)?.len() != mappings.len() { + return Err(Error::InvalidArtifact); + } + for (source, target) in mappings { + let actual = field(failures, text(target)?)?; + let expected = field(exported, text(source)?)?; + exact_fields(actual, &["authorityClass", "payloadType"])?; + exact_fields(expected, &["authorityClass", "payloadType"])?; + if text_field(actual, "authorityClass")? != text_field(expected, "authorityClass")? { + return Err(Error::InvalidArtifact); + } + // A shared intrinsic's failure carries its ABI input; the exported + // failure must carry this effect's own declared input type. + let (catalog_payload, exported_payload) = match abi { + Some(abi) => (abi.input, text_field(effect, "inputType")?), + None => ( + text_field(actual, "payloadType")?, + text_field(actual, "payloadType")?, + ), + }; + if text_field(actual, "payloadType")? != catalog_payload + || text_field(expected, "payloadType")? != exported_payload + { + return Err(Error::InvalidArtifact); + } + } + Ok(()) +} diff --git a/crates/warp-core/src/edict_effects/law.rs b/crates/warp-core/src/edict_effects/law.rs new file mode 100644 index 000000000..858cd96f8 --- /dev/null +++ b/crates/warp-core/src/edict_effects/law.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Strict host-side decoding of the proposed footprint-law shape. +use crate::edict_pure::{ + values::{array, bytes, exact_fields, field, number}, + EvaluationError, +}; +use crate::TypeId; +use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; +use std::collections::BTreeSet; + +/// Invalid canonical law shape, set ordering, type identifier or profile count. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectLawError { + /// The bounded input is not an exact canonical footprint-law value. + #[error("invalid canonical footprint law")] + InvalidLaw, +} + +/// Host-selected typed footprint permissions, not an admitted package capability. +/// +/// Decoding validates all nine fields. Private derivation enforces write permissions. +/// Package identity and installation must separately bind these exact bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectFootprintLaw { + pub(super) nodes: BTreeSet, + pub(super) atoms: BTreeSet, + pub(super) portals: BTreeSet, + pub(super) ranges: BTreeSet, + pub(super) max_reads: u64, + pub(super) max_range_edges: u64, + pub(super) write_nodes: BTreeSet, + pub(super) write_edges: BTreeSet, + pub(super) max_writes: u64, +} + +impl EffectFootprintLaw { + /// Decodes canonical bytes without sorting or deduplicating set arrays. + /// + /// Setup is bounded to 16 MiB and precedes evaluation metering. Counts may + /// be zero; ceilings are 4096 reads, 256 writes and 65536 bucket edges. + /// + /// # Errors + /// Refuses noncanonical CBOR, missing/extra fields, malformed or unordered + /// sets, and counts outside the effect-profile ceilings. + pub fn from_canonical_bytes(bytes: &[u8]) -> Result { + if u64::try_from(bytes.len()).map_err(|_| EffectLawError::InvalidLaw)? + > super::EFFECT_READ_STORAGE_CEILING_V1 + { + return Err(EffectLawError::InvalidLaw); + } + let value = decode_canonical_cbor_v1(bytes).map_err(|_| EffectLawError::InvalidLaw)?; + Self::parse(&value).map_err(|_| EffectLawError::InvalidLaw) + } + + fn parse(value: &Value) -> Result { + exact_fields( + value, + &[ + "read_node_types", + "write_node_types", + "atom_types", + "range_edge_types", + "write_edge_types", + "portal_owner_types", + "max_reads", + "max_writes", + "max_range_edges", + ], + )?; + let max_reads = number(field(value, "max_reads")?)?; + let max_writes = number(field(value, "max_writes")?)?; + let max_range_edges = number(field(value, "max_range_edges")?)?; + if max_reads > 4096 || max_writes > 256 || max_range_edges > 65536 { + return Err(EvaluationError::InvalidArtifact); + } + Ok(Self { + nodes: ids(value, "read_node_types")?, + atoms: ids(value, "atom_types")?, + portals: ids(value, "portal_owner_types")?, + ranges: ids(value, "range_edge_types")?, + max_reads, + max_range_edges, + write_nodes: ids(value, "write_node_types")?, + write_edges: ids(value, "write_edge_types")?, + max_writes, + }) + } +} + +fn ids(value: &Value, name: &str) -> Result, EvaluationError> { + let mut result = BTreeSet::new(); + let mut previous = None; + for value in array(field(value, name)?)? { + let id = TypeId( + bytes(value)? + .try_into() + .map_err(|_| EvaluationError::InvalidArtifact)?, + ); + if previous.is_some_and(|previous| previous >= id) { + return Err(EvaluationError::InvalidArtifact); + } + previous = Some(id); + result.insert(id); + } + Ok(result) +} diff --git a/crates/warp-core/src/edict_effects/lowering_relation.rs b/crates/warp-core/src/edict_effects/lowering_relation.rs new file mode 100644 index 000000000..d4b633122 --- /dev/null +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pinned lawpack adapter correspondence, independent of instruction ordering. +use crate::edict_pure::{ + values::{array, bytes, exact_fields, field, map, require_text, text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::{ + decode_canonical_cbor_v1 as decode, digest_canonical_value_bytes_v1 as digest, + CanonicalValueV1 as Value, +}; +use std::collections::{BTreeMap, BTreeSet}; + +pub(super) fn validate<'a>( + module: &Value, + intent: &Value, + target_artifact: &Value, + target: &'a Value, + exports: &'a Value, + artifacts: &Value, +) -> Result, Error> { + let steps = array(field(target, "steps")?)?; + if steps.is_empty() { + return if artifacts == &Value::Null { + Ok(BTreeMap::new()) + } else { + Err(Error::InvalidArtifact) + }; + } + exact_fields( + artifacts, + &[ + "source_artifact", + "lawpack_artifact", + "adapter_artifact", + "target_profile_artifact", + "intrinsics_artifact", + ], + )?; + let embedded = |key| decode(bytes(field(artifacts, key)?)?).map_err(|_| Error::InvalidArtifact); + let source = embedded("source_artifact")?; + let source = std::str::from_utf8(bytes(&source)?).map_err(|_| Error::InvalidArtifact)?; + let lawpack = embedded("lawpack_artifact")?; + let adapter = embedded("adapter_artifact")?; + let target_profile = embedded("target_profile_artifact")?; + let catalog = embedded("intrinsics_artifact")?; + require_text(&target_profile, "apiVersion", "edict.target-profile/v1")?; + let profile_coordinate = format!( + "{}@{}", + text_field(&target_profile, "id")?, + text_field(&target_profile, "version")? + ); + let profile_reference = field(target_artifact, "targetProfile")?; + require_text(profile_reference, "id", &profile_coordinate)?; + pinned( + profile_reference, + digest("edict.target-profile/v1", &target_profile).map_err(|_| Error::InvalidArtifact)?, + )?; + if !array(field(&target_profile, "acceptedCoreAbi")?)? + .iter() + .any(|value| text(value) == text_field(module, "apiVersion")) + { + return Err(Error::UnsupportedProgram); + } + require_text(&target_profile, "intrinsicNamespace", &profile_coordinate)?; + let catalog_reference = field(&target_profile, "intrinsics")?; + require_text(catalog_reference, "id", "echo.dpo.intrinsics/v1")?; + pinned( + catalog_reference, + digest("echo.dpo.intrinsics/v1", &catalog).map_err(|_| Error::InvalidArtifact)?, + )?; + exact_fields(&catalog, &["apiVersion", "intrinsics"])?; + require_text(&catalog, "apiVersion", "edict.target-profile.intrinsics/v1")?; + let catalog = field(&catalog, "intrinsics")?; + map(catalog)?; + require_text(&lawpack, "apiVersion", "edict.lawpack/v1")?; + require_text(&adapter, "apiVersion", "edict.lawpack-adapter/v1")?; + require_text(&adapter, "class", "declarative")?; + let coordinate = format!( + "{}@{}", + text_field(&lawpack, "id")?, + text_field(&lawpack, "version")? + ); + let identity = digest("edict.lawpack/v1", &lawpack).map_err(|_| Error::InvalidArtifact)?; + let closure = field(target_artifact, "semanticClosure")?; + let [reference] = array(field(closure, "lawpacks")?)? else { + return Err(Error::UnsupportedProgram); + }; + require_text(reference, "id", &coordinate)?; + pinned(reference, identity)?; + let mut imports = array(field(module, "imports")?)? + .iter() + .filter(|entry| text_field(entry, "kind") == Ok("lawpack")); + let import = imports.next().ok_or(Error::InvalidArtifact)?; + if imports.next().is_some() || field(import, "ref")? != reference { + return Err(Error::UnsupportedProgram); + } + pinned( + field(&lawpack, "exports")?, + digest("edict.lawpack-exports/v1", exports).map_err(|_| Error::InvalidArtifact)?, + )?; + let mut matching = array(field(&lawpack, "targetAdapters")?)? + .iter() + .filter(|entry| { + field(entry, "acceptedTargetProfile") == field(target_artifact, "targetProfile") + }); + let entry = matching.next().ok_or(Error::InvalidArtifact)?; + if field(&target_profile, "targetIr")? != field(entry, "acceptedTargetIr")? { + return Err(Error::InvalidArtifact); + } + if matching.next().is_some() { + return Err(Error::InvalidArtifact); + } + require_text( + field(entry, "acceptedTargetIr")?, + "id", + text_field(target_artifact, "domain")?, + )?; + pinned( + field(entry, "adapter")?, + digest("edict.lawpack-adapter/v1", &adapter).map_err(|_| Error::InvalidArtifact)?, + )?; + let alias = alias( + source, + &coordinate, + identity, + text_field(module, "coordinate")?, + )?; + super::imported_types::validate(module, exports, &coordinate)?; + let profile = unique_profile(&adapter, text_field(intent, "requiredOperationProfile")?)?; + let authorized = array(field(profile, "semanticEffects")?)?; + let nodes = array(field(field(intent, "body")?, "nodes")?)?; + let mut signatures = BTreeMap::new(); + let steps = steps + .iter() + .map(|step| Ok((text_field(step, "id")?, step))) + .collect::, Error>>()?; + for (core, id) in nodes.iter().zip(array(field(target, "executionOrder")?)?) { + if text_field(core, "kind")? != "effect" { + continue; + } + let lowered = *steps.get(text(id)?).ok_or(Error::InvalidArtifact)?; + let name = resolve(text_field(core, "effect")?, &alias, &coordinate)?; + if !authorized + .iter() + .any(|value| text(value) == Ok(name.as_str())) + { + return Err(Error::InvalidArtifact); + } + let implementation = field(field(&adapter, "effectImplementations")?, &name)?; + let intrinsic_name = text_field(implementation, "targetIntrinsic")?; + if !intrinsic_name + .strip_prefix(&profile_coordinate) + .is_some_and(|suffix| suffix.starts_with('.') && suffix.len() > 1) + { + return Err(Error::InvalidArtifact); + } + let intrinsic = field(catalog, intrinsic_name)?; + require_text(intrinsic, "intrinsicClass", "effect")?; + require_text( + lowered, + "targetIntrinsic", + text_field(implementation, "targetIntrinsic")?, + )?; + let mut declarations = array(field(exports, "effects")?)? + .iter() + .filter(|effect| text_field(effect, "coordinate") == Ok(name.as_str())); + let effect = declarations.next().ok_or(Error::InvalidArtifact)?; + if declarations.next().is_some() { + return Err(Error::InvalidArtifact); + } + super::intrinsic_contract::validate(intrinsic_name, intrinsic, effect, implementation)?; + super::runtime_abi::validate(intrinsic_name, module, effect, intrinsic)?; + if super::mutations::MutationKind::from_intrinsic(intrinsic_name).is_some() { + require_text( + intent, + "requiredOperationProfile", + "continuum.profile.graph-effects/v1", + )?; + } + require_text( + field(core, "binding")?, + "type", + text_field(effect, "outputType")?, + )?; + signatures.insert(text(id)?, effect); + let failures = field(effect, "effectFailures")?; + let mappings = map(field(implementation, "failureMappings")?)?; + let source_arms = field(core, "obstructionMap")?; + let target_arms = field(lowered, "obstructionArms")?; + if mappings.len() != map(failures)?.len() + || mappings.len() != map(source_arms)?.len() + || mappings.len() != map(target_arms)?.len() + { + return Err(Error::InvalidArtifact); + } + let mut seen = BTreeSet::new(); + for (failure, tag) in mappings { + let tag = text(tag)?; + if tag.is_empty() || !seen.insert(tag) { + return Err(Error::InvalidArtifact); + } + let failure = text(failure)?; + let arm = field(source_arms, failure)?; + if arm != field(target_arms, tag)? { + return Err(Error::InvalidArtifact); + } + require_text( + field(arm, "binder")?, + "type", + text_field(field(failures, failure)?, "payloadType")?, + )?; + let reason = field(arm, "value")?; + exact_fields(reason, &["kind", "callee", "args", "typeArgs"])?; + require_text(reason, "kind", "call")?; + if !array(field(reason, "args")?)?.is_empty() + || !array(field(reason, "typeArgs")?)?.is_empty() + { + return Err(Error::UnsupportedProgram); + } + let coordinate = resolve(text_field(reason, "callee")?, &alias, &coordinate)?; + let mut obstructions = array(field(exports, "obstructions")?)? + .iter() + .filter(|item| text_field(item, "coordinate") == Ok(coordinate.as_str())); + let obstruction = obstructions.next().ok_or(Error::InvalidArtifact)?; + if obstructions.next().is_some() { + return Err(Error::InvalidArtifact); + } + let authority = text_field(field(failures, failure)?, "authorityClass")?; + if !["domainMappable", "resourceFault", "integrityFault"].contains(&authority) { + return Err(Error::InvalidArtifact); + } + require_text(obstruction, "authorityClass", authority)?; + } + } + Ok(signatures) +} +fn pinned(reference: &Value, expected: [u8; 32]) -> Result<(), Error> { + exact_fields(reference, &["id", "digest"])?; + if text_field(reference, "id")?.is_empty() { + return Err(Error::InvalidArtifact); + } + let [algorithm, value] = array(field(reference, "digest")?)? else { + return Err(Error::InvalidArtifact); + }; + if text(algorithm)? != "sha256" || bytes(value)? != expected { + return Err(Error::InvalidArtifact); + } + Ok(()) +} +fn resolve(name: &str, alias: &str, coordinate: &str) -> Result { + if name + .strip_prefix(coordinate) + .is_some_and(|rest| rest.starts_with('.') && rest.len() > 1) + { + return Ok(name.to_owned()); + } + let member = name + .strip_prefix(alias) + .and_then(|rest| rest.strip_prefix('.')) + .filter(|rest| !rest.is_empty()) + .ok_or(Error::InvalidArtifact)?; + Ok(format!("{coordinate}.{member}")) +} +fn unique_profile<'a>(adapter: &'a Value, core: &str) -> Result<&'a Value, Error> { + let mut selected = None; + for (_, profile) in map(field(adapter, "operationProfiles")?)? { + if text_field(profile, "core")? == core && selected.replace(profile).is_some() { + return Err(Error::InvalidArtifact); + } + } + // The Core profile is already the lowered profile coordinate. Source aliases + // are used only for effect names, never to select a different Core profile. + selected.ok_or(Error::InvalidArtifact) +} +fn alias( + source: &str, + coordinate: &str, + identity: [u8; 32], + module: &str, +) -> Result { + // Accept a bounded, explicit declaration preamble, not an import-looking + // string inside a body or comment. A full source/Core proof remains separate. + let mut lines = source + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()); + let package = format!("package {module};"); + if lines.next() != Some(package.as_str()) { + return Err(Error::UnsupportedProgram); + } + let expected = format!("\"sha256:{}\"", hex::encode(identity)); + let mut result = None; + for line in lines { + if !line.starts_with("use ") { + break; + } + let tokens = line.split_whitespace().collect::>(); + let ["use", "lawpack", name, "digest", value, "as", alias] = tokens.as_slice() else { + return Err(Error::UnsupportedProgram); + }; + let alias = alias.strip_suffix(';').ok_or(Error::InvalidArtifact)?; + if *name != coordinate + || *value != expected + || alias.is_empty() + || !alias + .bytes() + .enumerate() + .all(|(i, c)| c == b'_' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit())) + || result.replace(alias.to_owned()).is_some() + { + return Err(Error::InvalidArtifact); + } + } + result.ok_or(Error::InvalidArtifact) +} diff --git a/crates/warp-core/src/edict_effects/mutations.rs b/crates/warp-core/src/edict_effects/mutations.rs new file mode 100644 index 000000000..d9307efdd --- /dev/null +++ b/crates/warp-core/src/edict_effects/mutations.rs @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owned mutation emitter ABI. Emission is not validation or a write. +use crate::edict_pure::{model::RuntimeType, EvaluationError as Error}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum MutationKind { + EnsureNode, + CreateNode, + ReplaceAtom, + EnsureEdge, + ReplaceEdge, +} +impl MutationKind { + pub(super) fn from_intrinsic(name: &str) -> Option { + match name { + "echo.dpo@1.ensure-node" => Some(Self::EnsureNode), + "echo.dpo@1.create-node" => Some(Self::CreateNode), + "echo.dpo@1.replace-atom" => Some(Self::ReplaceAtom), + "echo.dpo@1.ensure-edge" => Some(Self::EnsureEdge), + "echo.dpo@1.replace-edge" => Some(Self::ReplaceEdge), + _ => None, + } + } + /// The provider effect kind the catalog must declare for this mutation. + pub(super) const fn effect_kind(self) -> &'static str { + match self { + Self::CreateNode => "create", + Self::EnsureNode | Self::EnsureEdge => "ensure", + Self::ReplaceAtom | Self::ReplaceEdge => "replace", + } + } + pub(super) fn fields(self) -> &'static [&'static str] { + match self { + Self::EnsureNode | Self::CreateNode => { + &["warpId", "nodeId", "nodeType", "atomType", "atomBytes"] + } + Self::ReplaceAtom => &[ + "warpId", + "nodeId", + "expectedAtomDigest", + "atomType", + "atomBytes", + ], + Self::EnsureEdge => &["warpId", "edgeId", "fromId", "toId", "edgeType"], + Self::ReplaceEdge => &["warpId", "edgeId", "expectedToId", "toId"], + } + } + pub(super) fn validate_type( + self, + input: &RuntimeType, + output: &RuntimeType, + ) -> Result<(), Error> { + let RuntimeType::Record(fields) = input.representation() else { + return Err(Error::InvalidArtifact); + }; + if fields.len()!=self.fields().len() || !self.fields().iter().all(|name|fields.iter().any(|(key,ty)| key==name && if *name=="atomBytes" { + matches!(ty.representation(), RuntimeType::Bytes {min:0,max} if *max>0 && *max<=super::EFFECT_READ_STORAGE_CEILING_V1) + } else {ty.representation()==&RuntimeType::Bytes {min:32,max:32}})) { + return Err(Error::InvalidArtifact); + } + if !matches!(output,RuntimeType::Record(fields) if fields.is_empty()) { + return Err(Error::UnsupportedProgram); + } + Ok(()) + } +} + +use crate::edict_pure::values::{bytes, exact_fields, field}; +use crate::{AtomPayload, EdgeKey, Hash, NodeId, NodeKey, TypeId, WarpId}; +use echo_edict_canonical::CanonicalValueV1 as Value; +use std::collections::BTreeMap; +/// A compiler-emitted mutation intent, not a validated write or patch. +/// +/// Values are inspectable data. No public API accepts caller-constructed values +/// as evaluation evidence or grants mutation authority from this enum. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EffectMutation { + /// Create content, or later require the identical complete content identity. + EnsureNode { + /// Instance-scoped target. + node: NodeKey, + /// Declared node type. + ty: TypeId, + /// Both atom type and bytes. + atom: AtomPayload, + }, + /// Later require a vacant mutable node/atom target. + CreateNode { + /// Instance-scoped target. + node: NodeKey, + /// Declared node type. + ty: TypeId, + /// Both atom type and bytes. + atom: AtomPayload, + }, + /// Later compare the current typed atom digest before replacement. + ReplaceAtom { + /// Instance-scoped owner. + node: NodeKey, + /// Expected digest of both current atom type and bytes. + expected: Hash, + /// Replacement type and bytes. + atom: AtomPayload, + }, + /// Create an edge, or later require the identical edge and lawful identity. + EnsureEdge { + /// Instance-scoped edge target. + edge: EdgeKey, + /// Source local id in the edge's warp. + from: NodeId, + /// Target local id in the edge's warp. + to: NodeId, + /// Edge type, distinct from node types. + ty: TypeId, + }, + /// Later compare an existing edge's target before retargeting it. + ReplaceEdge { + /// Instance-scoped edge target. + edge: EdgeKey, + /// Expected current target local id. + expected_to: NodeId, + /// New target local id. + to: NodeId, + }, +} +impl EffectMutation { + pub(super) fn emission_write_bytes(&self) -> Option { + match self { + Self::EnsureNode { atom, .. } + | Self::CreateNode { atom, .. } + | Self::ReplaceAtom { atom, .. } => { + u64::try_from(atom.bytes.len()).ok()?.checked_add(64) + } + Self::EnsureEdge { .. } | Self::ReplaceEdge { .. } => Some(128), + } + } +} +impl MutationKind { + pub(super) fn emit(self, value: Value) -> Result { + exact_fields(&value, self.fields())?; + for name in self.fields() { + let value = bytes(field(&value, name)?)?; + if *name == "atomBytes" { + if value.len() as u64 > super::EFFECT_READ_STORAGE_CEILING_V1 { + return Err(Error::InvalidArtifact); + } + } else if value.len() != 32 { + return Err(Error::InvalidArtifact); + } + } + let Value::Map(fields) = value else { + return Err(Error::InvalidArtifact); + }; + let mut fields = fields + .into_iter() + .map(|(key, value)| match key { + Value::Text(key) => Ok((key, value)), + _ => Err(Error::InvalidArtifact), + }) + .collect::, _>>()?; + let warp_id = WarpId(id(&mut fields, "warpId")?); + Ok(match self { + Self::EnsureNode | Self::CreateNode => { + let node = NodeKey { + warp_id, + local_id: NodeId(id(&mut fields, "nodeId")?), + }; + let ty = TypeId(id(&mut fields, "nodeType")?); + let atom = atom(&mut fields)?; + if self == Self::EnsureNode { + EffectMutation::EnsureNode { node, ty, atom } + } else { + EffectMutation::CreateNode { node, ty, atom } + } + } + Self::ReplaceAtom => { + let node = NodeKey { + warp_id, + local_id: NodeId(id(&mut fields, "nodeId")?), + }; + let expected = id(&mut fields, "expectedAtomDigest")?; + EffectMutation::ReplaceAtom { + node, + expected, + atom: atom(&mut fields)?, + } + } + Self::EnsureEdge => { + let edge = EdgeKey { + warp_id, + local_id: crate::EdgeId(id(&mut fields, "edgeId")?), + }; + EffectMutation::EnsureEdge { + edge, + from: NodeId(id(&mut fields, "fromId")?), + to: NodeId(id(&mut fields, "toId")?), + ty: TypeId(id(&mut fields, "edgeType")?), + } + } + Self::ReplaceEdge => { + let edge = EdgeKey { + warp_id, + local_id: crate::EdgeId(id(&mut fields, "edgeId")?), + }; + EffectMutation::ReplaceEdge { + edge, + expected_to: NodeId(id(&mut fields, "expectedToId")?), + to: NodeId(id(&mut fields, "toId")?), + } + } + }) + } +} +fn take(fields: &mut BTreeMap, name: &str) -> Result, Error> { + match fields.remove(name) { + Some(Value::Bytes(value)) => Ok(value), + _ => Err(Error::InvalidArtifact), + } +} +fn id(fields: &mut BTreeMap, name: &str) -> Result<[u8; 32], Error> { + take(fields, name)? + .try_into() + .map_err(|_| Error::InvalidArtifact) +} +fn atom(fields: &mut BTreeMap) -> Result { + Ok(AtomPayload { + type_id: TypeId(id(fields, "atomType")?), + bytes: take(fields, "atomBytes")?.into(), + }) +} diff --git a/crates/warp-core/src/edict_effects/node_reads.rs b/crates/warp-core/src/edict_effects/node_reads.rs new file mode 100644 index 000000000..d47e35b2d --- /dev/null +++ b/crates/warp-core/src/edict_effects/node_reads.rs @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Recording adapter for the existing compiler-produced bounded-read interpreter. +use super::{ + EffectFrameError, EffectRangeReadError, EffectRangeReadSession, OBSERVATION_STORAGE_BYTES, +}; +use crate::edict_pure::evaluate::Meter; +use crate::edict_read::{NodeAtomAccess, ReadBasis, ReadObstruction}; +use crate::{AttachmentKey, AttachmentValue, NodeKey, SlotId, SlotReading, TypeId}; + +pub(crate) struct RecordedNodeAccess<'r, 'f> { + pub(crate) reads: &'r mut EffectRangeReadSession<'f>, + pub(crate) aperture: NodeAperture<'r>, +} + +impl NodeAtomAccess for RecordedNodeAccess<'_, '_> { + type Error = EffectFrameError; + fn basis(&self) -> ReadBasis { + self.reads.basis() + } + fn contains(&self, node: NodeKey) -> bool { + self.aperture.absent_type(node).is_some() + } + fn atom( + &mut self, + node: NodeKey, + expected: TypeId, + max_bytes: u64, + meter: &mut Meter, + ) -> Result, Self::Error> { + match self.aperture { + NodeAperture::Untyped(_) => self.reads.record_atom(node, expected, max_bytes, meter), + NodeAperture::Typed(_) => { + let absent_type = self + .aperture + .absent_type(node) + .ok_or(EffectRangeReadError::FootprintLawViolation)?; + self.reads + .record_typed_atom(node, absent_type, expected, max_bytes, meter) + } + } + } +} + +impl EffectRangeReadSession<'_> { + fn record_atom( + &mut self, + key: NodeKey, + expected: TypeId, + max_bytes: u64, + meter: &mut Meter, + ) -> Result, EffectFrameError> { + let frontier = self.frontier; + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)); + } + let store = frontier.state().store(&key.warp_id); + let node = store.and_then(|store| store.node(&key.local_id)); + self.readings + .insert(SlotId::Node(key), SlotReading::Node(node.cloned())); + self.footprint.n_read.insert(key); + if node.is_none() { + return Ok(Err(ReadObstruction::Missing)); + } + let value = store.and_then(|store| store.node_attachment(&key.local_id)); + // Reading a length is bounded metadata access. Reserve the full value + // before retaining it or inspecting its payload/type for the program. + let payload = match value { + Some(AttachmentValue::Atom(atom)) => u64::try_from(atom.bytes.len()) + .map_err(|_| EffectFrameError::Read(EffectRangeReadError::BudgetExceeded))?, + _ => 0, + }; + let read_bytes = 64_u64 + .checked_add(payload) + .ok_or(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded))?; + let storage = OBSERVATION_STORAGE_BYTES + .checked_add(payload) + .ok_or(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded))?; + if !self.charge(1, read_bytes, storage, Some(meter)) { + return Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)); + } + let attachment = AttachmentKey::node_alpha(key); + self.readings.insert( + SlotId::Attachment(attachment), + SlotReading::Attachment(value.cloned()), + ); + self.footprint.a_read.insert(attachment); + Ok(match value { + None => Err(ReadObstruction::Missing), + Some(AttachmentValue::Descend(_)) => Err(ReadObstruction::AtomRequired), + Some(AttachmentValue::Atom(atom)) if atom.type_id != expected => { + Err(ReadObstruction::TypeMismatch) + } + Some(AttachmentValue::Atom(atom)) if payload > max_bytes => { + Err(ReadObstruction::AtomTooLarge) + } + Some(AttachmentValue::Atom(atom)) => Ok(&atom.bytes), + }) + } +} + +pub(crate) enum NodeAperture<'a> { + Untyped(&'a [NodeKey]), + Typed(&'a [super::TypedNodeRead]), +} +impl NodeAperture<'_> { + pub(crate) fn validate(&self) -> Result<(), crate::edict_read::ReadError> { + match self { + Self::Untyped(nodes) => crate::edict_read::ReadView::validate_aperture(nodes), + Self::Typed(nodes) + if nodes.len() <= 65536 + && nodes.windows(2).all(|pair| pair[0].node < pair[1].node) => + { + Ok(()) + } + Self::Typed(_) => Err(crate::edict_read::ReadError::InvalidAperture), + } + } + fn absent_type(&self, key: NodeKey) -> Option { + match self { + Self::Untyped(nodes) => nodes.binary_search(&key).ok().map(|_| TypeId([0; 32])), + Self::Typed(nodes) => nodes + .binary_search_by_key(&key, |node| node.node) + .ok() + .map(|index| nodes[index].absent_node_type), + } + } +} + +impl EffectRangeReadSession<'_> { + fn record_typed_atom( + &mut self, + key: NodeKey, + absent_type: TypeId, + expected: TypeId, + max_bytes: u64, + meter: &mut Meter, + ) -> Result, EffectFrameError> { + use super::typed_reads::ReadPermission; + self.reachable(key.warp_id, meter)?; + let frontier = self.frontier; + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(EffectRangeReadError::BudgetExceeded.into()); + } + let store = frontier.state().store(&key.warp_id); + let node = store.and_then(|store| store.node(&key.local_id)); + let Some(node) = node else { + self.authorize(SlotId::Node(key), absent_type, ReadPermission::Node, meter)?; + self.readings + .insert(SlotId::Node(key), SlotReading::Node(None)); + self.footprint.n_read.insert(key); + return Ok(Err(ReadObstruction::Missing)); + }; + // Attachment kind/type/length are bounded metadata, never payload data. + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(EffectRangeReadError::BudgetExceeded.into()); + } + let value = store.and_then(|store| store.node_attachment(&key.local_id)); + let portal = matches!(value, Some(AttachmentValue::Descend(_))); + let permission = if portal { + ReadPermission::Portal + } else { + ReadPermission::Node + }; + self.authorize(SlotId::Node(key), node.ty, permission, meter)?; + self.readings + .insert(SlotId::Node(key), SlotReading::Node(Some(node.clone()))); + self.footprint.n_read.insert(key); + let attachment = AttachmentKey::node_alpha(key); + let (ty, permission) = match value { + Some(AttachmentValue::Descend(_)) => (node.ty, ReadPermission::Portal), + Some(AttachmentValue::Atom(atom)) => (atom.type_id, ReadPermission::Atom), + None => (expected, ReadPermission::Atom), + }; + self.authorize(SlotId::Attachment(attachment), ty, permission, meter)?; + let payload = match value { + Some(AttachmentValue::Atom(atom)) => { + u64::try_from(atom.bytes.len()).map_err(|_| EffectRangeReadError::BudgetExceeded)? + } + _ => 0, + }; + if !self.charge(0, payload, payload, Some(meter)) { + return Err(EffectRangeReadError::BudgetExceeded.into()); + } + self.readings.insert( + SlotId::Attachment(attachment), + SlotReading::Attachment(value.cloned()), + ); + self.footprint.a_read.insert(attachment); + Ok(match value { + None => Err(ReadObstruction::Missing), + Some(AttachmentValue::Descend(_)) => Err(ReadObstruction::AtomRequired), + Some(AttachmentValue::Atom(atom)) if atom.type_id != expected => { + Err(ReadObstruction::TypeMismatch) + } + Some(AttachmentValue::Atom(_)) if payload > max_bytes => { + Err(ReadObstruction::AtomTooLarge) + } + Some(AttachmentValue::Atom(atom)) => Ok(&atom.bytes), + }) + } +} diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs new file mode 100644 index 000000000..edc916875 --- /dev/null +++ b/crates/warp-core/src/edict_effects/package.rs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! The authored-effect payload of the single current operation program. +use super::{EffectFootprintLaw, EffectGraphSchema, EFFECT_READ_STORAGE_CEILING_V1}; +use crate::edict_pure::{ + values::{bytes, exact_fields, field, number, require_text, text}, + EvaluationError, +}; +use echo_edict_canonical::CanonicalValueV1 as Value; + +/// Refusal while preparing a pinned effect publication. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectPackageError { + /// The exact bytes differ from the requested current package identity. + #[error("effect package identity mismatch")] + IdentityMismatch, + /// The publication has malformed or inconsistent fields or profiles. + #[error("invalid effect package")] + InvalidPackage, +} +/// Prepared authored-effect program payload, without execution authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectProgram { + value: Value, + identity: crate::Hash, + compiled: Vec, + compiled_contract: super::compiled_program::CompiledProgram, + schema: EffectGraphSchema, + law: EffectFootprintLaw, + target_ir: [u8; 32], + footprint: [u8; 32], + output_type: String, + max_input_bytes: u64, + max_result_bytes: u64, + max_allocated_bytes: u64, +} +impl EffectProgram { + pub(crate) fn from_value(value: &Value) -> Result { + exact_fields( + value, + &[ + "schema", + "kind", + "interpreter_profile_identity", + "intrinsic_profile_identity", + "compiled_program", + "target_ir_identity", + "identity_law", + "graph_schema", + "footprint_law", + "max_input_bytes", + "max_result_bytes", + "max_allocated_bytes", + "output_type", + ], + )?; + require_text(value, "schema", "echo.operation-program/v1")?; + require_text(value, "kind", "edict-effect-program/v1")?; + if number(field(value, "identity_law")?)? != 1 { + return Err(EvaluationError::InvalidArtifact); + } + for (key, label) in [ + ( + "interpreter_profile_identity", + "echo.operation-interpreter.edict-effect/v1", + ), + ( + "intrinsic_profile_identity", + "echo.operation-intrinsics.edict-effect/v1", + ), + ] { + if hash(field(value, key)?)? + != domain_hash(b"echo:operation-profile:v1\0", label.as_bytes()) + { + return Err(EvaluationError::InvalidArtifact); + } + } + let schema = EffectGraphSchema::from_canonical_bytes(bytes(field(value, "graph_schema")?)?) + .map_err(|_| EvaluationError::InvalidArtifact)?; + let law_bytes = bytes(field(value, "footprint_law")?)?; + let law = EffectFootprintLaw::from_canonical_bytes(law_bytes) + .map_err(|_| EvaluationError::InvalidArtifact)?; + let mut footprint = blake3::Hasher::new(); + footprint.update(b"echo:operation-footprint-law:v1\0"); + footprint.update(law_bytes); + let max_input_bytes = number(field(value, "max_input_bytes")?)?; + let max_result_bytes = number(field(value, "max_result_bytes")?)?; + let max_allocated_bytes = number(field(value, "max_allocated_bytes")?)?; + let output_type = coordinate(field(value, "output_type")?)?; + if max_input_bytes > 65536 + || max_result_bytes > 65536 + || max_allocated_bytes > EFFECT_READ_STORAGE_CEILING_V1 + || output_type.len() > 1024 + { + return Err(EvaluationError::InvalidArtifact); + } + Ok(Self { + value: value.clone(), + identity: crate::echo_operation::program_identity_bytes( + &echo_edict_canonical::encode_canonical_cbor_v1(value) + .map_err(|_| EvaluationError::InvalidArtifact)?, + ), + compiled: bytes(field(value, "compiled_program")?)?.to_vec(), + compiled_contract: super::compiled_program::CompiledProgram::parse(bytes(field( + value, + "compiled_program", + )?)?)?, + schema, + law, + target_ir: hash(field(value, "target_ir_identity")?)?, + footprint: footprint.finalize().into(), + output_type: output_type.to_owned(), + max_input_bytes, + max_result_bytes, + max_allocated_bytes, + }) + } + pub(crate) fn validate_compiled_binding( + &self, + operation: &str, + core_identity: [u8; 32], + exports_identity: [u8; 32], + ) -> Result<(), EvaluationError> { + self.compiled_contract.validate_binding( + operation, + core_identity, + self.target_ir, + exports_identity, + &self.output_type, + ) + } + pub(super) const fn identity(&self) -> crate::Hash { + self.identity + } + pub(super) fn execution( + &self, + mut limits: crate::edict_pure::EvaluationLimits, + ) -> Result { + limits.max_allocated_bytes = limits.max_allocated_bytes.min(self.max_allocated_bytes); + limits.max_output_bytes = limits.max_output_bytes.min(self.max_result_bytes); + self.compiled_contract.execution(limits) + } + pub(crate) fn to_value(&self) -> Value { + self.value.clone() + } + pub(crate) const fn target_ir_identity(&self) -> [u8; 32] { + self.target_ir + } + pub(crate) const fn footprint_identity(&self) -> [u8; 32] { + self.footprint + } + /// Canonical Core/Target IR/exports bundle bound to the selected intent. + /// Private read/let/require evaluation and mutation emission use a bound frame. + /// Mutation validation and installed Action execution remain unfinished. + #[must_use] + pub fn compiled_program(&self) -> &[u8] { + &self.compiled + } + /// Executable graph schema from the exact program. + #[must_use] + pub const fn graph_schema(&self) -> &EffectGraphSchema { + &self.schema + } + /// Exact footprint law paired with the executable schema. + #[must_use] + pub const fn footprint_law(&self) -> &EffectFootprintLaw { + &self.law + } + /// Compiler-declared output type coordinate. + #[must_use] + pub fn output_type(&self) -> &str { + &self.output_type + } + /// Maximum canonical input bytes, including framing. + #[must_use] + pub const fn max_input_bytes(&self) -> u64 { + self.max_input_bytes + } + /// Maximum canonical result bytes. + #[must_use] + pub const fn max_result_bytes(&self) -> u64 { + self.max_result_bytes + } + /// Maximum declared evaluation allocation bytes. + #[must_use] + pub const fn max_allocated_bytes(&self) -> u64 { + self.max_allocated_bytes + } +} +pub(super) fn hash(value: &Value) -> Result<[u8; 32], EvaluationError> { + bytes(value)? + .try_into() + .map_err(|_| EvaluationError::InvalidArtifact) +} +fn coordinate(value: &Value) -> Result<&str, EvaluationError> { + let value = text(value)?; + if value.is_empty() { + return Err(EvaluationError::InvalidArtifact); + } + Ok(value) +} +pub(super) fn domain_hash(domain: &[u8], bytes: &[u8]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(domain); + h.update(&(bytes.len() as u64).to_le_bytes()); + h.update(bytes); + h.finalize().into() +} diff --git a/crates/warp-core/src/edict_effects/read_image.rs b/crates/warp-core/src/edict_effects/read_image.rs new file mode 100644 index 000000000..c64a323cc --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Bounded host export and allocation-free structural validation of read evidence. +mod encode; +mod rows; +mod validate; +use crate::EchoOperationEvaluationBasisV1; +pub use rows::{EffectEdgePairRow, EffectReadRow, EffectSlotRow}; +pub use validate::EffectReadSetView; + +const DOMAIN: &[u8] = b"echo.bridge.read-set.v1\0"; + +#[repr(C, align(8))] +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct ImageWord([u8; 8]); +const _: () = + assert!(std::mem::size_of::() == 8 && std::mem::align_of::() == 8); + +/// Host export or read-image validation failure, never an Action decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectReadImageError { + /// Export requires a runtime-selected parent and a bound typed read law. + #[error("read image requires a typed runtime-bound frame")] + MissingTypedParent, + /// The complete image cannot fit the selected host export ceiling. + #[error("read image exceeds the host export limit")] + ExportLimitExceeded, + /// Host storage could not be provisioned; no evidence may be published. + #[error("host could not provision read image storage")] + HostAllocationFailed, + /// The image violates its framing, tags, fields, ordering or packed ranges. + #[error("invalid canonical read-set image")] + InvalidImage, + /// Zero-copy row access requires an eight-byte-aligned input buffer. + #[error("read-set image buffer is not eight-byte aligned")] + UnalignedImage, + /// A recorded slot/value/type combination is inconsistent. + #[error("recorded typed observation is inconsistent")] + InconsistentObservation, +} + +/// Owned, aligned export of a runtime frame's completed typed observations. +/// +/// The bytes are the read-set digest preimage after its domain: row count, +/// 192-byte rows, pair count, then 64-byte edge pairs. This carries the actual +/// runtime parent separately; neither the image nor a structural view is an +/// installation, policy-authentication or historical-decision capability. +#[derive(Debug)] +pub struct EffectReadSetImage { + words: Vec, + basis: EchoOperationEvaluationBasisV1, + consumed: crate::EchoOperationBudgetV1, + allocated_bytes: u64, +} +impl EffectReadSetImage { + /// Canonical read-set bytes in an eight-byte-aligned owned buffer. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + bytemuck::cast_slice(&self.words) + } + /// Runtime-selected full parent retained by the consuming frame export. + #[must_use] + pub const fn evaluation_basis(&self) -> EchoOperationEvaluationBasisV1 { + self.basis + } + /// Evaluation work and I/O preserved before the consuming host export. + #[must_use] + pub const fn consumed(&self) -> crate::EchoOperationBudgetV1 { + self.consumed + } + /// Evaluation storage preserved separately from host export buffer storage. + #[must_use] + pub const fn allocated_bytes(&self) -> u64 { + self.allocated_bytes + } + /// BLAKE3 over the read-set domain and exact canonical image bytes. + #[must_use] + pub fn digest(&self) -> crate::Hash { + digest(self.as_bytes()) + } +} +fn digest(bytes: &[u8]) -> crate::Hash { + let mut hash = blake3::Hasher::new(); + hash.update(DOMAIN); + hash.update(bytes); + hash.finalize().into() +} +pub(super) use encode::export; diff --git a/crates/warp-core/src/edict_effects/read_image/encode.rs b/crates/warp-core/src/edict_effects/read_image/encode.rs new file mode 100644 index 000000000..e21528f9e --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image/encode.rs @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +use super::super::EffectRangeReadSession; +use super::{ + EffectEdgePairRow, EffectReadImageError as Error, EffectReadRow, EffectReadSetImage, + EffectReadSetView, EffectSlotRow, ImageWord, +}; +use crate::{ + AttachmentKey, AttachmentOwner, AttachmentValue, EchoOperationEvaluationBasisV1, EdgeRecord, + SlotId, SlotReading, TypeId, +}; +use bytemuck::Zeroable; + +pub(in crate::edict_effects) fn export( + source: &EffectRangeReadSession<'_>, + basis: EchoOperationEvaluationBasisV1, + consumed: crate::EchoOperationBudgetV1, + allocated_bytes: u64, + max_bytes: usize, +) -> Result { + let ceiling = usize::try_from(super::super::EFFECT_READ_STORAGE_CEILING_V1) + .map_err(|_| Error::ExportLimitExceeded)?; + let limit = max_bytes.min(ceiling); + if limit < 16 { + return Err(Error::ExportLimitExceeded); + } + let mut rows = Vec::new(); + rows.try_reserve_exact(source.authorized_reads.len()) + .map_err(|_| Error::HostAllocationFailed)?; + for (slot, ty) in &source.authorized_reads { + // A permission reservation can precede a failed payload charge. Only + // completed observations belong in this failed-frame export. + let Some(value) = source.readings.get(slot) else { + continue; + }; + rows.push(prepare(source, *slot, *ty, value)?); + } + rows.sort_unstable_by(|a, b| a.0.compare(&b.0)); + let pair_count = rows.iter().try_fold(0_usize, |count, (_, pairs)| { + count + .checked_add(pairs.map_or(0, <[EdgeRecord]>::len)) + .ok_or(Error::ExportLimitExceeded) + })?; + let size = rows + .len() + .checked_mul(192) + .and_then(|n| { + pair_count + .checked_mul(64) + .and_then(|pairs| n.checked_add(pairs)) + }) + .and_then(|n| n.checked_add(16)) + .filter(|size| *size <= limit) + .ok_or(Error::ExportLimitExceeded)?; + let mut words = Vec::new(); + words + .try_reserve_exact(size / 8) + .map_err(|_| Error::HostAllocationFailed)?; + words.push(ImageWord( + u64::try_from(rows.len()) + .map_err(|_| Error::ExportLimitExceeded)? + .to_le_bytes(), + )); + let mut cursor = 0_u64; + for (row, pairs) in &mut rows { + if let Some(pairs) = pairs { + row.pairs.start_le = cursor.to_le(); + let count = u64::try_from(pairs.len()).map_err(|_| Error::ExportLimitExceeded)?; + row.pairs.len_le = count.to_le(); + cursor = cursor + .checked_add(count) + .ok_or(Error::ExportLimitExceeded)?; + } + append(&mut words, row)?; + } + words.push(ImageWord(cursor.to_le_bytes())); + for (_, pairs) in rows { + for edge in pairs.into_iter().flatten() { + append( + &mut words, + &EffectEdgePairRow { + edge_local_id: edge.id.0, + target_local_id: edge.to.0, + }, + )?; + } + } + EffectReadSetView::parse(bytemuck::cast_slice(&words))?; + Ok(EffectReadSetImage { + words, + basis, + consumed, + allocated_bytes, + }) +} +fn append(words: &mut Vec, row: &T) -> Result<(), Error> { + words.extend_from_slice( + bytemuck::try_cast_slice::(std::slice::from_ref(row)) + .map_err(|_| Error::InvalidImage)?, + ); + Ok(()) +} +fn prepare<'a>( + source: &EffectRangeReadSession<'_>, + slot: SlotId, + ty: TypeId, + value: &'a SlotReading, +) -> Result<(EffectReadRow, Option<&'a [EdgeRecord]>), Error> { + let mut row = EffectReadRow::zeroed(); + row.slot = EffectSlotRow::from_slot(slot)?; + row.read_type = ty.0; + match (slot, value) { + (SlotId::Node(_), SlotReading::Node(Some(record))) if record.ty == ty => { + let mut hash = blake3::Hasher::new(); + hash.update(b"echo.bridge.node-value.v1\0"); + hash.update(&record.ty.0); + row.subject = hash.finalize().into(); + } + (SlotId::Node(_), SlotReading::Node(None)) + | (SlotId::Edge(_), SlotReading::Edge(None)) + | (SlotId::Attachment(_), SlotReading::Attachment(None)) => row.outcome = 1, + (SlotId::Edge(key), SlotReading::Edge(Some(record))) + if key.local_id == record.id && record.ty == ty => + { + let mut hash = blake3::Hasher::new(); + hash.update(b"echo.bridge.edge-value.v1\0"); + hash.update(&record.ty.0); + hash.update(&record.from.0); + hash.update(&record.to.0); + row.subject = hash.finalize().into(); + } + (SlotId::Attachment(_), SlotReading::Attachment(Some(AttachmentValue::Atom(atom)))) + if atom.type_id == ty => + { + row.subject = super::super::value_digest::atom(atom); + } + ( + SlotId::Attachment(key), + SlotReading::Attachment(Some(AttachmentValue::Descend(warp))), + ) => { + let AttachmentOwner::Node(owner) = key.owner else { + return Err(Error::InconsistentObservation); + }; + if key != AttachmentKey::node_alpha(owner) + || !matches!(source.readings.get(&SlotId::Node(owner)),Some(SlotReading::Node(Some(record))) if record.ty==ty) + { + return Err(Error::InconsistentObservation); + } + row.outcome = 3; + let mut hash = blake3::Hasher::new(); + hash.update(b"echo.bridge.attachment-value.v1\0"); + hash.update(&[1]); + hash.update(&warp.0); + row.subject = hash.finalize().into(); + } + (SlotId::OutgoingEdges(range), SlotReading::OutgoingEdges(edges)) if ty.0 == [0; 32] => { + let owner = source.readings.get(&SlotId::Node(range.source)); + if !matches!( + (owner, edges), + (Some(SlotReading::Node(Some(_))), Some(_)) | (Some(SlotReading::Node(None)), None) + ) { + return Err(Error::InconsistentObservation); + } + let edges = edges.as_deref().unwrap_or(&[]); + if edges + .iter() + .any(|edge| edge.ty != range.edge_type || edge.from != range.source.local_id) + { + return Err(Error::InconsistentObservation); + } + row.outcome = 2; + return Ok((row, Some(edges))); + } + _ => return Err(Error::InconsistentObservation), + } + Ok((row, None)) +} diff --git a/crates/warp-core/src/edict_effects/read_image/rows.rs b/crates/warp-core/src/edict_effects/read_image/rows.rs new file mode 100644 index 000000000..173868e67 --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image/rows.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +use super::EffectReadImageError as Error; +use crate::{AttachmentOwner, AttachmentPlane, SlotId}; +use bytemuck::{Pod, Zeroable}; + +/// Fixed canonical slot descriptor, 104 bytes, distinct from native SlotId order. +#[repr(C, align(8))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)] +pub struct EffectSlotRow { + /// Node 0, attachment 1, edge 2, outgoing range 3. + pub class: u8, + /// Attachment owner: node 0, edge 1; zero for every other class. + pub owner: u8, + /// Attachment plane: alpha 0, beta 1; zero for every other class. + pub plane: u8, + /// Canonical zero padding. + pub padding: [u8; 5], + /// Exact WARP identifier. + pub warp_id: crate::Hash, + /// Node, edge, attachment-owner or range-source local identifier. + pub local_id: crate::Hash, + /// Range edge type only; zero for every other class. + pub edge_type: crate::Hash, +} +/// Fixed canonical typed observation, 192 bytes. +#[repr(C, align(8))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)] +pub struct EffectReadRow { + /// Canonical slot descriptor. + pub slot: EffectSlotRow, + /// Present 0, absent 1, range 2, portal 3. + pub outcome: u8, + /// Canonical zero padding. + pub padding: [u8; 7], + /// Actual/named/portal-owner type, or zero for a range. + pub read_type: crate::Hash, + /// Present value or portal digest; zero for absent/range outcomes. + pub subject: crate::Hash, + /// Contiguous edge-pair run for a range; zero for all other outcomes. + pub pairs: crate::wsc::types::Range, +} +/// One outgoing-range result, 64 bytes, in edge-local-ID order. +#[repr(C, align(8))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)] +pub struct EffectEdgePairRow { + /// Exact edge local identifier, unique within its range. + pub edge_local_id: crate::Hash, + /// Exact target local identifier in the range's WARP. + pub target_local_id: crate::Hash, +} +const _: () = assert!(std::mem::size_of::() == 104); +const _: () = assert!(std::mem::size_of::() == 192); +const _: () = assert!(std::mem::size_of::() == 64); + +impl EffectSlotRow { + pub(in crate::edict_effects) fn from_slot(slot: SlotId) -> Result { + let mut row = Self::zeroed(); + match slot { + SlotId::Node(key) => { + row.warp_id = key.warp_id.0; + row.local_id = key.local_id.0; + } + SlotId::Edge(key) => { + row.class = 2; + row.warp_id = key.warp_id.0; + row.local_id = key.local_id.0; + } + SlotId::Attachment(key) => { + row.class = 1; + match (key.owner, key.plane) { + (AttachmentOwner::Node(node), AttachmentPlane::Alpha) => { + row.warp_id = node.warp_id.0; + row.local_id = node.local_id.0; + } + (AttachmentOwner::Edge(edge), AttachmentPlane::Beta) => { + row.owner = 1; + row.plane = 1; + row.warp_id = edge.warp_id.0; + row.local_id = edge.local_id.0; + } + _ => return Err(Error::InconsistentObservation), + } + } + SlotId::OutgoingEdges(range) => { + row.class = 3; + row.warp_id = range.source.warp_id.0; + row.local_id = range.source.local_id.0; + row.edge_type = range.edge_type.0; + } + SlotId::Port(_) => return Err(Error::InconsistentObservation), + } + Ok(row) + } + pub(in crate::edict_effects) fn compare(&self, other: &Self) -> std::cmp::Ordering { + ( + self.class, + self.warp_id, + self.local_id, + self.edge_type, + self.owner, + self.plane, + ) + .cmp(&( + other.class, + other.warp_id, + other.local_id, + other.edge_type, + other.owner, + other.plane, + )) + } + pub(super) fn valid(&self) -> bool { + self.padding == [0; 5] + && self.class <= 3 + && (if self.class == 1 { + (self.owner, self.plane) == (0, 0) || (self.owner, self.plane) == (1, 1) + } else { + self.owner == 0 && self.plane == 0 + }) + && (self.class == 3 || self.edge_type == [0; 32]) + } +} +impl EffectReadRow { + pub(super) fn compare(&self, other: &Self) -> std::cmp::Ordering { + self.slot + .compare(&other.slot) + .then_with(|| self.read_type.cmp(&other.read_type)) + } + pub(super) fn valid(&self) -> bool { + if !self.slot.valid() || self.padding != [0; 7] { + return false; + } + let no_pairs = self.pairs.start() == 0 && self.pairs.is_empty(); + match self.outcome { + 0 => self.slot.class != 3 && no_pairs, + 1 => self.slot.class != 3 && no_pairs && self.subject == [0; 32], + 2 => self.slot.class == 3 && self.read_type == [0; 32] && self.subject == [0; 32], + 3 => self.slot.class == 1 && self.slot.owner == 0 && self.slot.plane == 0 && no_pairs, + _ => false, + } + } +} diff --git a/crates/warp-core/src/edict_effects/read_image/validate.rs b/crates/warp-core/src/edict_effects/read_image/validate.rs new file mode 100644 index 000000000..00d70a0ca --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image/validate.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +use super::{EffectEdgePairRow, EffectReadImageError as Error, EffectReadRow}; + +/// Allocation-free borrowed view after canonical structural validation. +/// +/// Validation proves framing and canonical form, not provenance, type-law +/// authorization, dependency completeness or a historical Action decision. +#[derive(Debug, Clone, Copy)] +pub struct EffectReadSetView<'a> { + bytes: &'a [u8], + rows: &'a [EffectReadRow], + pairs: &'a [EffectEdgePairRow], +} +impl<'a> EffectReadSetView<'a> { + /// Checks counts, exact sizes, alignment, tags, unused fields, sorting and packing. + /// + /// Input must be eight-byte aligned. Validation walks borrowed rows without + /// allocating, copying the image, or hydrating owned collections. + /// # Errors + /// Refuses invalid structure, more than 4096 rows, ranges above 65536 pairs, + /// an image above the fixed read-storage ceiling, or an unaligned buffer. + pub fn parse(bytes: &'a [u8]) -> Result { + if bytes.len() < 16 + || u64::try_from(bytes.len()).map_err(|_| Error::InvalidImage)? + > super::super::EFFECT_READ_STORAGE_CEILING_V1 + { + return Err(Error::InvalidImage); + } + if bytes.as_ptr().align_offset(8) != 0 { + return Err(Error::UnalignedImage); + } + let count = word(&bytes[..8])?; + if count > 4096 { + return Err(Error::InvalidImage); + } + let row_end = usize::try_from(count) + .ok() + .and_then(|n| n.checked_mul(192)) + .and_then(|n| n.checked_add(8)) + .ok_or(Error::InvalidImage)?; + let pair_start = row_end + .checked_add(8) + .filter(|end| *end <= bytes.len()) + .ok_or(Error::InvalidImage)?; + let pair_count = word(&bytes[row_end..pair_start])?; + let end = usize::try_from(pair_count) + .ok() + .and_then(|n| n.checked_mul(64)) + .and_then(|n| n.checked_add(pair_start)) + .ok_or(Error::InvalidImage)?; + if end != bytes.len() { + return Err(Error::InvalidImage); + } + let rows: &[EffectReadRow] = + bytemuck::try_cast_slice(&bytes[8..row_end]).map_err(|_| Error::UnalignedImage)?; + let pairs: &[EffectEdgePairRow] = + bytemuck::try_cast_slice(&bytes[pair_start..]).map_err(|_| Error::UnalignedImage)?; + validate_rows(rows, pairs)?; + Ok(Self { bytes, rows, pairs }) + } + /// Rows borrowed directly from the validated image. + #[must_use] + pub const fn rows(&self) -> &'a [EffectReadRow] { + self.rows + } + /// Packed pairs borrowed directly from the validated image. + #[must_use] + pub const fn pairs(&self) -> &'a [EffectEdgePairRow] { + self.pairs + } + /// Domain-separated digest of these exact validated bytes. + #[must_use] + pub fn digest(&self) -> crate::Hash { + super::digest(self.bytes) + } +} +fn word(bytes: &[u8]) -> Result { + Ok(u64::from_le_bytes( + bytes.try_into().map_err(|_| Error::InvalidImage)?, + )) +} +fn validate_rows(rows: &[EffectReadRow], pairs: &[EffectEdgePairRow]) -> Result<(), Error> { + let mut previous: Option<&EffectReadRow> = None; + let mut cursor = 0_usize; + for row in rows { + if !row.valid() { + return Err(Error::InvalidImage); + } + if let Some(previous) = previous { + if previous.compare(row) != std::cmp::Ordering::Less + || (previous.slot == row.slot && (previous.outcome != 1 || row.outcome != 1)) + { + return Err(Error::InvalidImage); + } + } + if row.outcome == 2 { + let start = usize::try_from(row.pairs.start()).map_err(|_| Error::InvalidImage)?; + let len = usize::try_from(row.pairs.len()).map_err(|_| Error::InvalidImage)?; + if start != cursor || len > 65536 { + return Err(Error::InvalidImage); + } + cursor = cursor + .checked_add(len) + .filter(|end| *end <= pairs.len()) + .ok_or(Error::InvalidImage)?; + if pairs[start..cursor] + .windows(2) + .any(|pair| pair[0].edge_local_id >= pair[1].edge_local_id) + { + return Err(Error::InvalidImage); + } + } + previous = Some(row); + } + if cursor != pairs.len() { + return Err(Error::InvalidImage); + } + Ok(()) +} diff --git a/crates/warp-core/src/edict_effects/runtime_abi.rs b/crates/warp-core/src/edict_effects/runtime_abi.rs new file mode 100644 index 000000000..07da11296 --- /dev/null +++ b/crates/warp-core/src/edict_effects/runtime_abi.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owned recording/emitter ABIs; declarations cannot redefine them. +//! +//! The provider's pinned catalog names each shared intrinsic's input and result +//! by Echo-owned ABI coordinate (`.input`, `.result`). +//! An application keeps its own nominal types; the effect its adapter +//! explicitly binds to one of these intrinsics must declare representations +//! that conform to that intrinsic's ABI. Conformance applies only to the exact +//! intrinsic identities below and never selects an intrinsic by shape. +use crate::edict_pure::{ + model::{RuntimeType, MAX_PROGRAM_NODES}, + syntax::Parser, + values::{field, map, require_text, text_field}, + EvaluationError as Error, +}; +use echo_edict_canonical::CanonicalValueV1 as Value; + +/// The Echo-owned ABI coordinates of one shared runtime intrinsic. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct AbiContract { + /// Coordinate the catalog must give as the single argument type. + pub(super) input: &'static str, + /// Coordinate the catalog must give as the return type. + pub(super) result: &'static str, +} + +/// The ABI contract of an exact runtime intrinsic identity, if it is one. +pub(super) fn contract(intrinsic: &str) -> Option { + Some(match intrinsic { + "echo.dpo@1.node-atom-read" => AbiContract { + input: "echo.dpo@1.node-atom-read.input", + result: "echo.dpo@1.node-atom-read.result", + }, + "echo.dpo@1.ensure-node" => AbiContract { + input: "echo.dpo@1.ensure-node.input", + result: "echo.dpo@1.ensure-node.result", + }, + "echo.dpo@1.create-node" => AbiContract { + input: "echo.dpo@1.create-node.input", + result: "echo.dpo@1.create-node.result", + }, + "echo.dpo@1.replace-atom" => AbiContract { + input: "echo.dpo@1.replace-atom.input", + result: "echo.dpo@1.replace-atom.result", + }, + "echo.dpo@1.ensure-edge" => AbiContract { + input: "echo.dpo@1.ensure-edge.input", + result: "echo.dpo@1.ensure-edge.result", + }, + "echo.dpo@1.replace-edge" => AbiContract { + input: "echo.dpo@1.replace-edge.input", + result: "echo.dpo@1.replace-edge.result", + }, + _ => return None, + }) +} + +pub(super) fn validate( + name: &str, + module: &Value, + effect: &Value, + intrinsic: &Value, +) -> Result<(), Error> { + let mutation = super::mutations::MutationKind::from_intrinsic(name); + let Some(abi) = contract(name) else { + return Err(Error::UnsupportedProgram); + }; + + let mut parser = Parser { + types: field(module, "types")?, + coordinate: text_field(module, "coordinate")?, + remaining: MAX_PROGRAM_NODES, + }; + let input_name = text_field(effect, "inputType")?; + let input = parser.ty(input_name, 0)?; + let output = parser.ty(text_field(effect, "outputType")?, 0)?; + if let Some(kind) = mutation { + require_text(intrinsic, "effectKind", kind.effect_kind())?; + require_text(intrinsic, "writeClass", kind.effect_kind())?; + if !map(field(intrinsic, "effectFailures")?)?.is_empty() { + return Err(Error::InvalidArtifact); + } + return kind.validate_type(&input, &output); + } + require_text(intrinsic, "effectKind", "read")?; + require_text(intrinsic, "writeClass", "read")?; + let RuntimeType::Record(fields) = input.representation() else { + return Err(Error::InvalidArtifact); + }; + if fields.len() != 3 + || !["nodeId", "typeId", "warpId"].iter().all(|key| { + fields.iter().any(|(name, ty)| { + name == key && ty.representation() == &RuntimeType::Bytes { min: 32, max: 32 } + }) + }) + { + return Err(Error::InvalidArtifact); + } + if !matches!(output, RuntimeType::Bytes { min: 0, max } if max > 0 && max <= super::EFFECT_READ_STORAGE_CEILING_V1) + { + return Err(Error::UnsupportedProgram); + } + // The catalog names each failure by the identifier Echo pins for this + // intrinsic, and carries the call's ABI input; the contract checker binds + // the exported failure to this effect's own input type. + let failures = field(intrinsic, "effectFailures")?; + let declared = super::failure_names::failures(name).ok_or(Error::InvalidArtifact)?; + if map(failures)?.len() != declared.len() { + return Err(Error::InvalidArtifact); + } + for expected in declared { + let failure = field(failures, expected.catalog)?; + require_text(failure, "authorityClass", expected.authority)?; + require_text(failure, "payloadType", abi.input)?; + } + Ok(()) +} diff --git a/crates/warp-core/src/edict_effects/schema.rs b/crates/warp-core/src/edict_effects/schema.rs new file mode 100644 index 000000000..df01ac14d --- /dev/null +++ b/crates/warp-core/src/edict_effects/schema.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Executable graph-schema preparation, separate from Action admission. +use crate::edict_pure::{ + values::{array, bytes, exact_fields, field, map, number}, + EvaluationError, +}; +use crate::TypeId; +use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; +use std::collections::{BTreeMap, BTreeSet}; + +/// Addressing classification declared by a node type's executable schema. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EffectNodeAddressing { + /// Immutable content identity, enforced later by effect validation. + Content, + /// Program-chosen identity for a mutable record. + Mutable, +} +/// Invalid canonical graph-schema shape or set encoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectGraphSchemaError { + /// The input is not an exact canonical graph schema. + #[error("invalid canonical effect graph schema")] + InvalidSchema, +} +#[derive(Debug, Clone, PartialEq, Eq)] +struct NodeType { + addressing: EffectNodeAddressing, + atoms: BTreeSet, +} +/// Decoded node/atom and edge-endpoint relations, without admission authority. +/// +/// Preparation bounds input to 16 MiB and uses the canonical decoder's combined +/// node/depth ceilings. It precedes evaluation metering and is not an arena, +/// graph-state validation or authorization to read or mutate graph records. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectGraphSchema { + nodes: BTreeMap, + edges: BTreeMap>, +} +impl EffectGraphSchema { + /// Decodes the executable schema's canonical maps and ordered set arrays. + /// # Errors + /// Refuses noncanonical bytes, malformed fields, identifiers and sets. + pub fn from_canonical_bytes(bytes: &[u8]) -> Result { + if u64::try_from(bytes.len()).map_err(|_| EffectGraphSchemaError::InvalidSchema)? + > super::EFFECT_READ_STORAGE_CEILING_V1 + { + return Err(EffectGraphSchemaError::InvalidSchema); + } + let value = + decode_canonical_cbor_v1(bytes).map_err(|_| EffectGraphSchemaError::InvalidSchema)?; + Self::parse(&value).map_err(|_| EffectGraphSchemaError::InvalidSchema) + } + /// Addressing classification for a declared node type, if present. + #[must_use] + pub fn node_addressing(&self, node_type: TypeId) -> Option { + self.nodes.get(&node_type).map(|node| node.addressing) + } + /// Whether a node type declares this atom type. + #[must_use] + pub fn allows_atom(&self, node_type: TypeId, atom_type: TypeId) -> bool { + self.nodes + .get(&node_type) + .is_some_and(|node| node.atoms.contains(&atom_type)) + } + /// Whether an edge type declares this source/target node-type pair. + /// + /// This is relation membership only; it does not resolve endpoint records. + #[must_use] + pub fn allows_edge(&self, edge_type: TypeId, source_type: TypeId, target_type: TypeId) -> bool { + self.edges + .get(&edge_type) + .is_some_and(|pairs| pairs.contains(&(source_type, target_type))) + } + #[cfg(feature = "native_rule_bootstrap")] + pub(super) fn has_edge_type(&self, ty: TypeId) -> bool { + self.edges.contains_key(&ty) + } + fn parse(value: &Value) -> Result { + exact_fields(value, &["node_types", "edge_types"])?; + let mut nodes = BTreeMap::new(); + let mut previous = None; + for (key, value) in map(field(value, "node_types")?)? { + let ty = id(key)?; + ascending(&mut previous, ty)?; + exact_fields(value, &["addressing", "atom_types"])?; + let addressing = match number(field(value, "addressing")?)? { + 0 => EffectNodeAddressing::Content, + 1 => EffectNodeAddressing::Mutable, + _ => return Err(EvaluationError::InvalidArtifact), + }; + let mut atoms = BTreeSet::new(); + let mut previous_atom = None; + for atom in array(field(value, "atom_types")?)? { + let atom = id(atom)?; + ascending(&mut previous_atom, atom)?; + atoms.insert(atom); + } + nodes.insert(ty, NodeType { addressing, atoms }); + } + let mut edges = BTreeMap::new(); + let mut previous = None; + for (key, value) in map(field(value, "edge_types")?)? { + let ty = id(key)?; + ascending(&mut previous, ty)?; + let mut pairs = BTreeSet::new(); + let mut previous_pair = None; + for pair in array(value)? { + let [source, target] = array(pair)? else { + return Err(EvaluationError::InvalidArtifact); + }; + let pair = (id(source)?, id(target)?); + ascending(&mut previous_pair, pair)?; + pairs.insert(pair); + } + edges.insert(ty, pairs); + } + Ok(Self { nodes, edges }) + } +} +fn id(value: &Value) -> Result { + bytes(value)? + .try_into() + .map(TypeId) + .map_err(|_| EvaluationError::InvalidArtifact) +} +fn ascending(previous: &mut Option, value: T) -> Result<(), EvaluationError> { + if previous.is_some_and(|previous| previous >= value) { + return Err(EvaluationError::InvalidArtifact); + } + *previous = Some(value); + Ok(()) +} diff --git a/crates/warp-core/src/edict_effects/typed_reads.rs b/crates/warp-core/src/edict_effects/typed_reads.rs new file mode 100644 index 000000000..91218fe20 --- /dev/null +++ b/crates/warp-core/src/edict_effects/typed_reads.rs @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Typed resolutions and root-first portal authority for an immutable frame. +use super::{EffectRangeReadError as Error, EffectRangeReadSession, OBSERVATION_STORAGE_BYTES}; +use crate::edict_pure::evaluate::Meter; +use crate::{ + AttachmentKey, AttachmentOwner, AttachmentPlane, AttachmentValue, NodeKey, NodeRecord, SlotId, + SlotReading, TypeId, WarpId, +}; +use std::collections::BTreeSet; + +/// Node aperture declaration for a typed recorded-read component. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TypedNodeRead { + /// Exact node key selected by the trusted host. + pub node: NodeKey, + /// Type authorizing absence only; present records use their actual type. + pub absent_node_type: TypeId, +} + +pub(super) enum ReadPermission { + Node, + Atom, + Portal, +} + +impl EffectRangeReadSession<'_> { + pub(super) fn authorize( + &mut self, + slot: SlotId, + ty: TypeId, + permission: ReadPermission, + meter: &mut Meter, + ) -> Result<(), Error> { + let law = self.law.as_ref().ok_or(Error::FootprintLawViolation)?; + let allowed = match permission { + ReadPermission::Node => law.nodes.contains(&ty), + ReadPermission::Atom => law.atoms.contains(&ty), + ReadPermission::Portal => law.portals.contains(&ty), + }; + if !allowed { + return Err(Error::FootprintLawViolation); + } + self.retain_authorization(slot, ty, meter) + } + + pub(super) fn retain_authorization( + &mut self, + slot: SlotId, + ty: TypeId, + meter: &mut Meter, + ) -> Result<(), Error> { + if self.authorized_reads.contains(&(slot, ty)) { + return Ok(()); + } + let law = self.law.as_ref().ok_or(Error::FootprintLawViolation)?; + if self.authorized_reads.len() as u64 >= law.max_reads { + return Err(Error::FootprintLawViolation); + } + if !self.charge(0, 0, 64, Some(meter)) { + return Err(Error::BudgetExceeded); + } + self.authorized_reads.insert((slot, ty)); + Ok(()) + } + + pub(super) fn typed_node( + &mut self, + key: NodeKey, + absent_type: TypeId, + meter: &mut Meter, + ) -> Result, Error> { + self.reachable(key.warp_id, meter)?; + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(Error::BudgetExceeded); + } + let value = self + .frontier + .state() + .store(&key.warp_id) + .and_then(|store| store.node(&key.local_id)) + .cloned(); + let ty = value.as_ref().map_or(absent_type, |node| node.ty); + self.authorize(SlotId::Node(key), ty, ReadPermission::Node, meter)?; + self.readings + .insert(SlotId::Node(key), SlotReading::Node(value.clone())); + self.footprint.n_read.insert(key); + Ok(value) + } + + pub(super) fn reachable(&mut self, target: WarpId, meter: &mut Meter) -> Result<(), Error> { + let root = self.frontier.state().root().warp_id; + if target == root || self.validated_warps.contains(&target) { + return Ok(()); + } + let mut current = target; + let mut seen = BTreeSet::new(); + let mut chain = Vec::new(); + while current != root && !self.validated_warps.contains(¤t) { + // Reverse instance metadata is bounded preparation for root-first + // resolutions. Each hop reserves both vector and cycle-index storage. + if !self.charge(1, 64, 256, Some(&mut *meter)) { + return Err(Error::BudgetExceeded); + } + if !seen.insert(current) { + return Err(Error::UnreachableWarp); + } + let parent = self + .frontier + .state() + .warp_state() + .instance(¤t) + .and_then(|instance| instance.parent) + .ok_or(Error::UnreachableWarp)?; + let AttachmentOwner::Node(owner) = parent.owner else { + return Err(Error::FootprintLawViolation); + }; + if parent.plane != AttachmentPlane::Alpha { + return Err(Error::FootprintLawViolation); + } + chain + .try_reserve(1) + .map_err(|_| Error::HostAllocationFailed)?; + chain.push((current, owner)); + current = owner.warp_id; + } + for (child, owner) in chain.into_iter().rev() { + self.portal(owner, child, meter)?; + self.validated_warps.insert(child); + } + Ok(()) + } + + fn portal(&mut self, owner: NodeKey, child: WarpId, meter: &mut Meter) -> Result<(), Error> { + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(Error::BudgetExceeded); + } + let ty = self + .frontier + .state() + .store(&owner.warp_id) + .and_then(|store| store.node(&owner.local_id)) + .ok_or(Error::UnreachableWarp)? + .ty; + self.authorize(SlotId::Node(owner), ty, ReadPermission::Portal, meter)?; + self.readings.insert( + SlotId::Node(owner), + SlotReading::Node(Some(NodeRecord { ty })), + ); + self.footprint.n_read.insert(owner); + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, Some(&mut *meter)) { + return Err(Error::BudgetExceeded); + } + let key = AttachmentKey::node_alpha(owner); + if self + .frontier + .state() + .store(&owner.warp_id) + .and_then(|store| store.node_attachment(&owner.local_id)) + != Some(&AttachmentValue::Descend(child)) + { + return Err(Error::UnreachableWarp); + } + self.authorize(SlotId::Attachment(key), ty, ReadPermission::Portal, meter)?; + self.readings.insert( + SlotId::Attachment(key), + SlotReading::Attachment(Some(AttachmentValue::Descend(child))), + ); + self.footprint.a_read.insert(key); + Ok(()) + } +} diff --git a/crates/warp-core/src/edict_effects/value_digest.rs b/crates/warp-core/src/edict_effects/value_digest.rs new file mode 100644 index 000000000..5a77cd6a6 --- /dev/null +++ b/crates/warp-core/src/edict_effects/value_digest.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! One bridge atom-value digest recipe for read evidence and mutation comparisons. +pub(super) fn atom(atom: &crate::AtomPayload) -> crate::Hash { + let mut hash = blake3::Hasher::new(); + hash.update(b"echo.bridge.attachment-value.v1\0"); + hash.update(&[0]); + hash.update(&atom.type_id.0); + hash.update(&(atom.bytes.len() as u64).to_le_bytes()); + hash.update(&atom.bytes); + hash.finalize().into() +} diff --git a/crates/warp-core/src/edict_pure.rs b/crates/warp-core/src/edict_pure.rs index dc35cee1f..c5f46428b 100644 --- a/crates/warp-core/src/edict_pure.rs +++ b/crates/warp-core/src/edict_pure.rs @@ -22,6 +22,10 @@ use echo_edict_canonical::decode_canonical_cbor_v1; /// Host ceilings, intersected with the compiler's declared evaluation budget. #[derive(Clone, Copy, Debug)] +#[allow( + clippy::struct_field_names, + reason = "Each field is an explicit resource ceiling, including in the private compiler checker." +)] pub struct EvaluationLimits { /// Maximum encoded package size before canonical decoding. pub max_package_bytes: usize, @@ -67,6 +71,9 @@ pub enum EvaluationError { StepBudgetExceeded, /// The cumulative value storage budget was exhausted. AllocationBudgetExceeded, + /// Reserved shared-frame storage could not be provisioned by the host. + /// This is a host fault and must never become an Action obstruction. + HostAllocationFailed, /// The canonical result exceeds its output budget. OutputBudgetExceeded, } @@ -82,6 +89,16 @@ pub fn evaluate( input_bytes: &[u8], limits: EvaluationLimits, ) -> Result { + let (program, input) = prepare(package_bytes, verified_package_digest, input_bytes, limits)?; + evaluate::run(&program, input) +} + +pub(crate) fn prepare( + package_bytes: &[u8], + verified_package_digest: [u8; 32], + input_bytes: &[u8], + limits: EvaluationLimits, +) -> Result<(model::Program, echo_edict_canonical::CanonicalValueV1), EvaluationError> { if package_bytes.len() > limits.max_package_bytes.min(model::MAX_ARTIFACT_BYTES) { return Err(EvaluationError::PackageTooLarge); } @@ -92,7 +109,7 @@ pub fn evaluate( decode_canonical_cbor_v1(package_bytes).map_err(|_| EvaluationError::InvalidArtifact)?; let program = decode::package(&package, verified_package_digest, limits)?; let input = decode_canonical_cbor_v1(input_bytes).map_err(|_| EvaluationError::InvalidInput)?; - evaluate::run(&program, input) + Ok((program, input)) } #[cfg(test)] diff --git a/crates/warp-core/src/edict_pure/evaluate.rs b/crates/warp-core/src/edict_pure/evaluate.rs index 4f09a6a99..c3ef5ad1c 100644 --- a/crates/warp-core/src/edict_pure/evaluate.rs +++ b/crates/warp-core/src/edict_pure/evaluate.rs @@ -10,10 +10,12 @@ use super::{EvaluationError as Error, EvaluationLimits, EvaluationResult}; // Fixed interpreter storage units; Rust layout and pointer width cannot alter cost. pub(crate) const VALUE_CELL_BYTES: u64 = 64; +#[derive(Debug, Clone, Copy)] pub(crate) struct Meter { limits: EvaluationLimits, steps: u64, allocated: u64, + atomic_charges: bool, } impl Meter { @@ -22,6 +24,14 @@ impl Meter { limits, steps: 0, allocated: 0, + atomic_charges: false, + } + } + + pub(crate) fn new_atomic(limits: EvaluationLimits) -> Self { + Self { + atomic_charges: true, + ..Self::new(limits) } } @@ -29,6 +39,37 @@ impl Meter { (self.steps, self.allocated) } + pub(crate) fn with_component( + &mut self, + limits: EvaluationLimits, + run: impl FnOnce(&mut Self) -> Result, + ) -> Result { + let saved = self.limits; + self.limits.max_steps = saved + .max_steps + .min(self.steps.saturating_add(limits.max_steps)); + self.limits.max_allocated_bytes = saved + .max_allocated_bytes + .min(self.allocated.saturating_add(limits.max_allocated_bytes)); + let result = run(self); + self.limits = saved; + result + } + + // The caller already charged work and storage; only atomic frame paths + // translate allocator refusal into a host fault. Standalone behavior stays. + pub(crate) fn copy_precharged_read_bytes(&self, bytes: &[u8]) -> Result { + if !self.atomic_charges { + return Ok(Value::Bytes(bytes.to_vec())); + } + let mut value = Vec::new(); + value + .try_reserve_exact(bytes.len()) + .map_err(|_| Error::HostAllocationFailed)?; + value.extend_from_slice(bytes); + Ok(Value::Bytes(value)) + } + pub(crate) fn step(&mut self, depth: usize) -> Result<(), Error> { if depth > MAX_DEPTH { return Err(Error::UnsupportedProgram); @@ -37,10 +78,14 @@ impl Meter { } pub(crate) fn charge_steps(&mut self, count: u64) -> Result<(), Error> { - self.steps = self + let next = self .steps .checked_add(count) .ok_or(Error::StepBudgetExceeded)?; + if self.atomic_charges && next > self.limits.max_steps { + return Err(Error::StepBudgetExceeded); + } + self.steps = next; if self.steps > self.limits.max_steps { return Err(Error::StepBudgetExceeded); } @@ -48,10 +93,14 @@ impl Meter { } pub(crate) fn allocate(&mut self, bytes: u64) -> Result<(), Error> { - self.allocated = self + let next = self .allocated .checked_add(bytes) .ok_or(Error::AllocationBudgetExceeded)?; + if self.atomic_charges && next > self.limits.max_allocated_bytes { + return Err(Error::AllocationBudgetExceeded); + } + self.allocated = next; if self.allocated > self.limits.max_allocated_bytes { return Err(Error::AllocationBudgetExceeded); } @@ -84,9 +133,13 @@ impl Meter { .ok_or(Error::AllocationBudgetExceeded)?; self.charge_bytes(length)?; let mut bytes = Vec::new(); - bytes - .try_reserve_exact(length) - .map_err(|_| Error::AllocationBudgetExceeded)?; + bytes.try_reserve_exact(length).map_err(|_| { + if self.atomic_charges { + Error::HostAllocationFailed + } else { + Error::AllocationBudgetExceeded + } + })?; bytes.extend_from_slice(left); bytes.extend_from_slice(right); Ok(Value::Bytes(bytes)) @@ -117,12 +170,19 @@ impl Meter { } pub(super) fn run(program: &Program, input: Value) -> Result { - let mut meter = Meter { - limits: program.limits, - steps: 0, - allocated: 0, - }; - validate(&input, &program.input_type, &mut meter, 0).map_err(|error| { + run_with_meter(program, input, &mut Meter::new(program.limits)) +} + +pub(crate) fn run_with_meter( + program: &Program, + input: Value, + meter: &mut Meter, +) -> Result { + meter.with_component(program.limits, |meter| run_body(program, input, meter)) +} + +fn run_body(program: &Program, input: Value, meter: &mut Meter) -> Result { + validate(&input, &program.input_type, meter, 0).map_err(|error| { if error == Error::InvalidArtifact { Error::InvalidInput } else { @@ -132,17 +192,17 @@ pub(super) fn run(program: &Program, input: Value) -> Result Result<(), Error> { meter.step(depth)?; - match (value, ty) { + match (value, ty.representation()) { (Value::Bool(_), RuntimeType::Bool) => Ok(()), (Value::Integer(value), RuntimeType::Unsigned(max)) if *value >= 0 && *value <= i128::from(*max) => @@ -588,6 +648,7 @@ mod tests { limits, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( meter.copy_bytes(&[0, 128, 255])?, @@ -601,6 +662,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( work.copy_bytes(&[0, 128, 255]), @@ -614,6 +676,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( storage.copy_bytes(&[0, 128, 255]), @@ -634,6 +697,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; let length = Expr::ByteLength { min: 0, @@ -662,6 +726,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; let subtract = Expr::UnsignedSubtract { max: u64::MAX, @@ -690,6 +755,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; let value = Value::Map(vec![( Value::Text("id".into()), diff --git a/crates/warp-core/src/edict_pure/functions.rs b/crates/warp-core/src/edict_pure/functions.rs index b43287093..f4e9300a1 100644 --- a/crates/warp-core/src/edict_pure/functions.rs +++ b/crates/warp-core/src/edict_pure/functions.rs @@ -13,18 +13,19 @@ use super::{ use echo_edict_canonical::CanonicalValueV1 as Value; use std::collections::{BTreeMap, BTreeSet}; -struct Definition<'a> { +pub(crate) struct Definition<'a> { params: &'a [Value], body: &'a Value, result_type: &'a str, source_owned: bool, } -pub(crate) fn decode( - core: &Value, - exports: &Value, - parser: &mut Parser<'_>, -) -> Result, Error> { +/// Collect the one current source/imported helper signature authority. +pub(crate) fn definitions<'a>( + core: &'a Value, + exports: &'a Value, + coordinate: &str, +) -> Result>, Error> { let mut definitions = BTreeMap::new(); for item in array(field(exports, "pureFunctions")?)? { require_text(item, "source", "edict")?; @@ -83,7 +84,7 @@ pub(crate) fn decode( return Err(Error::InvalidArtifact); } exact_fields(value, &["params", "returnType", "body"])?; - let coordinate = format!("{}.{}", parser.coordinate, name); + let coordinate = format!("{coordinate}.{name}"); if coordinate.starts_with("core.") || effect_names.contains(coordinate.as_str()) || definitions @@ -102,6 +103,15 @@ pub(crate) fn decode( } } } + Ok(definitions) +} + +pub(crate) fn decode( + core: &Value, + exports: &Value, + parser: &mut Parser<'_>, +) -> Result, Error> { + let definitions = definitions(core, exports, parser.coordinate)?; let mut helpers = BTreeMap::new(); // Collect all signatures before resolving calls: definition order carries // no execution meaning. Every body is checked, including unused functions. @@ -237,7 +247,8 @@ pub(crate) fn decode( Ok(helpers) } -fn local(value: &Value) -> Result<(), Error> { +/// Checks the one current complete Core local identity. +pub(crate) fn local(value: &Value) -> Result<(), Error> { exact_fields(value, &["id", "type", "alphaName"])?; for name in ["id", "type", "alphaName"] { let value = text_field(value, name)?; @@ -248,7 +259,7 @@ fn local(value: &Value) -> Result<(), Error> { Ok(()) } -fn accepts(expected: &RuntimeType, actual: &RuntimeType) -> bool { +pub(crate) fn accepts(expected: &RuntimeType, actual: &RuntimeType) -> bool { match (expected, actual) { (RuntimeType::Bytes { min, max }, RuntimeType::Bytes { min: a, max: b }) => { min <= a && b <= max @@ -283,15 +294,21 @@ type Scope<'a> = BTreeMap<&'a str, &'a Value>; /// What is proven at one evaluation point: carried binding caps and the /// dominating order facts (earlier conjuncts of enclosing `all`s). #[derive(Clone, Copy)] -struct Proof<'c, 'v> { +pub(crate) struct Proof<'c, 'v> { caps: &'c Caps<'c>, facts: &'c [&'v Value], } +impl<'c, 'v> Proof<'c, 'v> { + /// What a caller has proven: binding caps it owns and dominating facts. + pub(crate) const fn new(caps: &'c Caps<'c>, facts: &'c [&'v Value]) -> Self { + Self { caps, facts } + } +} /// Proven upper bounds of earlier unsigned bindings in the same helper body. /// Local identities are unique and immutable, so a cap names exactly the /// bound value. Bounds never cross helper results or record fields. -type Caps<'a> = BTreeMap<&'a str, u64>; -fn infer<'v>( +pub(crate) type Caps<'a> = BTreeMap<&'a str, u64>; +pub(crate) fn infer<'v>( value: &'v Value, scope: &Scope<'_>, proof: Proof<'_, 'v>, @@ -589,7 +606,7 @@ fn unsigned_literal(value: &Value) -> Result { } number(field(field(value, "value")?, "value")?) } -fn check_predicate<'v>( +pub(crate) fn check_predicate<'v>( value: &'v Value, scope: &Scope<'_>, proof: Proof<'_, 'v>, @@ -659,6 +676,20 @@ fn check_predicate<'v>( parser, depth + 1, )?; + // Compare opaque values only within the exact same nominal contract. The + // scalar representation determines supported operators and runtime cost. + let (left, right) = + if matches!(left, RuntimeType::Nominal(..)) || matches!(right, RuntimeType::Nominal(..)) { + if left != right { + return Err(Error::InvalidArtifact); + } + ( + left.representation().clone(), + right.representation().clone(), + ) + } else { + (left, right) + }; match (text_field(value, "op")?, left, right) { ("==", RuntimeType::Bool, RuntimeType::Bool) | ("==", RuntimeType::Bytes { .. }, RuntimeType::Bytes { .. }) => Ok(()), @@ -830,6 +861,7 @@ impl Heights<'_> { } fn type_height(ty: &RuntimeType) -> usize { match ty { + RuntimeType::Nominal(_, representation) => type_height(representation), RuntimeType::Record(fields) => fields .iter() .map(|(_, ty)| type_height(ty) + 1) diff --git a/crates/warp-core/src/edict_pure/model.rs b/crates/warp-core/src/edict_pure/model.rs index dd02efbd3..6df0fc159 100644 --- a/crates/warp-core/src/edict_pure/model.rs +++ b/crates/warp-core/src/edict_pure/model.rs @@ -84,6 +84,18 @@ pub(crate) enum RuntimeType { Unsigned(u64), Bytes { min: u64, max: u64 }, Record(Vec<(String, RuntimeType)>), + Nominal(String, Box), +} + +impl RuntimeType { + /// Nominal identity is static; the wire value uses its representation. + pub(crate) fn representation(&self) -> &Self { + let mut ty = self; + while let Self::Nominal(_, inner) = ty { + ty = inner; + } + ty + } } pub(crate) struct Binding { diff --git a/crates/warp-core/src/edict_pure/syntax.rs b/crates/warp-core/src/edict_pure/syntax.rs index 1c7fd1420..c8f31c60a 100644 --- a/crates/warp-core/src/edict_pure/syntax.rs +++ b/crates/warp-core/src/edict_pure/syntax.rs @@ -50,8 +50,21 @@ impl Parser<'_> { let definition = field(self.types, name)?; match text_field(definition, "kind")? { "Bool" => Ok(RuntimeType::Bool), - "Int" => self.ty(text_field(definition, "width")?, depth + 1), - "Nominal" => self.ty(text_field(definition, "representation")?, depth + 1), + kind @ ("U8" | "U16" | "U32" | "U64") => { + exact_fields(definition, &["kind"])?; + self.ty(kind, depth + 1) + } + "Nominal" => { + exact_fields(definition, &["kind", "contract", "representation"])?; + let contract = text_field(definition, "contract")?; + if contract != name { + return Err(Error::InvalidArtifact); + } + Ok(RuntimeType::Nominal( + contract.to_owned(), + Box::new(self.ty(text_field(definition, "representation")?, depth + 1)?), + )) + } "Bytes" => { let min = field(definition, "min").map_or(Ok(0), number)?; let max = number(field(definition, "max")?)?; diff --git a/crates/warp-core/src/edict_read.rs b/crates/warp-core/src/edict_read.rs index 35c11246b..7d9e5c58d 100644 --- a/crates/warp-core/src/edict_read.rs +++ b/crates/warp-core/src/edict_read.rs @@ -7,12 +7,14 @@ //! The host owns the package pin and view selection. No callbacks or mutations //! are available to the interpreted program. +mod access; mod decode; mod evaluate; mod instructions; mod model; mod view; +pub(crate) use access::NodeAtomAccess; pub use view::{ReadBasis, ReadView}; use crate::edict_pure::{EvaluationError, EvaluationLimits}; @@ -106,6 +108,16 @@ pub fn evaluate( view: &ReadView<'_>, limits: ReadLimits, ) -> Result { + let (program, input) = prepare(package_bytes, verified_package_digest, input_bytes, limits)?; + evaluate::run(&program, input, view) +} + +fn prepare( + package_bytes: &[u8], + verified_package_digest: [u8; 32], + input_bytes: &[u8], + limits: ReadLimits, +) -> Result<(model::Program, echo_edict_canonical::CanonicalValueV1), ReadError> { let program = decode::package(package_bytes, verified_package_digest, limits)?; if input_bytes.len() > limits @@ -117,5 +129,20 @@ pub fn evaluate( } let input = echo_edict_canonical::decode_canonical_cbor_v1(input_bytes) .map_err(|_| EvaluationError::InvalidInput)?; - evaluate::run(&program, input, view) + Ok((program, input)) +} + +pub(crate) fn evaluate_recorded( + package: &[u8], + pin: [u8; 32], + input: &[u8], + reads: &mut crate::edict_effects::EffectRangeReadSession<'_>, + aperture: crate::edict_effects::NodeAperture<'_>, + meter: &mut crate::edict_pure::evaluate::Meter, + limits: ReadLimits, +) -> Result { + aperture.validate()?; + let (program, input) = prepare(package, pin, input, limits)?; + let mut access = crate::edict_effects::RecordedNodeAccess { reads, aperture }; + evaluate::run_with_meter(&program, input, &mut access, meter) } diff --git a/crates/warp-core/src/edict_read/access.rs b/crates/warp-core/src/edict_read/access.rs new file mode 100644 index 000000000..747bf90a4 --- /dev/null +++ b/crates/warp-core/src/edict_read/access.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! The same bounded-read interpreter can use plain or runtime-recording access. +use super::{ReadBasis, ReadError, ReadObstruction, ReadView}; +use crate::edict_pure::{evaluate::Meter, EvaluationError}; +use crate::{NodeKey, TypeId}; + +pub(crate) trait NodeAtomAccess { + type Error: From + From; + fn basis(&self) -> ReadBasis; + fn contains(&self, node: NodeKey) -> bool; + fn atom( + &mut self, + node: NodeKey, + expected: TypeId, + max_bytes: u64, + meter: &mut Meter, + ) -> Result, Self::Error>; +} + +impl NodeAtomAccess for &ReadView<'_> { + type Error = ReadError; + fn basis(&self) -> ReadBasis { + ReadView::basis(self) + } + fn contains(&self, node: NodeKey) -> bool { + ReadView::contains(self, node) + } + fn atom( + &mut self, + node: NodeKey, + expected: TypeId, + max_bytes: u64, + _meter: &mut Meter, + ) -> Result, Self::Error> { + Ok(ReadView::atom(self, node, expected, max_bytes)) + } +} diff --git a/crates/warp-core/src/edict_read/evaluate.rs b/crates/warp-core/src/edict_read/evaluate.rs index 36a0f309c..987e5175d 100644 --- a/crates/warp-core/src/edict_read/evaluate.rs +++ b/crates/warp-core/src/edict_read/evaluate.rs @@ -2,7 +2,7 @@ // © James Ross Ω FLYING•ROBOTS use super::{ model::{Instruction, Program, ReadInstruction}, - ReadError, ReadObstruction, ReadResult, ReadView, + NodeAtomAccess, ReadError, ReadObstruction, ReadResult, ReadView, }; use crate::edict_pure::{ evaluate::{expression, predicate, validate, Meter, VALUE_CELL_BYTES}, @@ -18,8 +18,33 @@ pub(super) fn run( input: Value, view: &ReadView<'_>, ) -> Result { - let mut meter = Meter::new(program.limits.evaluation); - validate(&input, &program.input_type, &mut meter, 0).map_err(|error| { + let mut access = view; + run_body( + program, + input, + &mut access, + &mut Meter::new(program.limits.evaluation), + ) +} + +pub(super) fn run_with_meter( + program: &Program, + input: Value, + access: &mut A, + meter: &mut Meter, +) -> Result { + meter.with_component(program.limits.evaluation, |meter| { + run_body(program, input, access, meter) + }) +} + +fn run_body( + program: &Program, + input: Value, + view: &mut A, + meter: &mut Meter, +) -> Result { + validate(&input, &program.input_type, meter, 0).map_err(|error| { if error == Error::InvalidArtifact { Error::InvalidInput } else { @@ -29,12 +54,12 @@ pub(super) fn run( meter.charge(&input, 0)?; let mut locals = BTreeMap::from([(program.input_id.as_str(), input)]); let helpers = &program.helpers; - let application_basis = expression(&program.basis, &locals, helpers, &mut meter, 0)?; + let application_basis = expression(&program.basis, &locals, helpers, meter, 0)?; let application_basis = bytes(&application_basis)? .try_into() .map_err(|_| Error::InvalidInput)?; for (coordinate, constraint) in &program.constraints { - if !predicate(constraint, &locals, helpers, &mut meter, 0)? { + if !predicate(constraint, &locals, helpers, meter, 0)? { return Err(Error::InputConstraintFailed(coordinate.clone()).into()); } } @@ -44,24 +69,24 @@ pub(super) fn run( meter.step(0)?; match instruction { Instruction::Read(read) => { - let address = expression(&read.input, &locals, helpers, &mut meter, 0)?; + let address = expression(&read.input, &locals, helpers, meter, 0)?; let (node, expected_type) = address_value(&address)?; if !view.contains(node) { - return Err(ReadError::OutsideAperture(node)); + return Err(ReadError::OutsideAperture(node).into()); } reads = reads.checked_add(1).ok_or(ReadError::ReadCountExceeded)?; if reads > program.limits.max_reads { - return Err(ReadError::ReadCountExceeded); + return Err(ReadError::ReadCountExceeded.into()); } let bytes = view - .atom(node, expected_type, read.max_bytes) + .atom(node, expected_type, read.max_bytes, meter)? .map_err(|cause| obstructed(read, cause))?; let count = bytes.len() as u64; read_bytes = read_bytes .checked_add(count) .ok_or(ReadError::ReadBytesExceeded)?; if read_bytes > program.limits.max_read_bytes { - return Err(ReadError::ReadBytesExceeded); + return Err(ReadError::ReadBytesExceeded.into()); } // Charge deterministic I/O work and storage BEFORE copying atom bytes. meter.charge_steps(count)?; @@ -70,30 +95,31 @@ pub(super) fn run( .checked_add(count) .ok_or(Error::AllocationBudgetExceeded)?, )?; - let value = Value::Bytes(bytes.to_vec()); - validate(&value, &read.ty, &mut meter, 0)?; + let value = meter.copy_precharged_read_bytes(bytes)?; + validate(&value, &read.ty, meter, 0)?; locals.insert(read.binding.as_str(), value); } Instruction::Let { binding, ty, value } => { - let value = expression(value, &locals, helpers, &mut meter, 0)?; - validate(&value, ty, &mut meter, 0)?; + let value = expression(value, &locals, helpers, meter, 0)?; + validate(&value, ty, meter, 0)?; locals.insert(binding.as_str(), value); } Instruction::Require { predicate: condition, obstruction, } => { - if !predicate(condition, &locals, helpers, &mut meter, 0)? { + if !predicate(condition, &locals, helpers, meter, 0)? { return Err(ReadError::Obstructed { coordinate: obstruction.clone(), cause: ReadObstruction::Guard, - }); + } + .into()); } } } } - let value = expression(&program.result, &locals, helpers, &mut meter, 0)?; - validate(&value, &program.output_type, &mut meter, 0)?; + let value = expression(&program.result, &locals, helpers, meter, 0)?; + validate(&value, &program.output_type, meter, 0)?; meter.charge(&value, 0)?; let output = encode(&value).map_err(|_| Error::InvalidArtifact)?; if output.len() as u64 > program.limits.evaluation.max_output_bytes { diff --git a/crates/warp-core/src/edict_read/view.rs b/crates/warp-core/src/edict_read/view.rs index db8f92cca..e2a99bb3f 100644 --- a/crates/warp-core/src/edict_read/view.rs +++ b/crates/warp-core/src/edict_read/view.rs @@ -95,7 +95,7 @@ impl<'a> ReadView<'a> { }) } - fn validate_aperture(aperture: &[NodeKey]) -> Result<(), ReadError> { + pub(crate) fn validate_aperture(aperture: &[NodeKey]) -> Result<(), ReadError> { if aperture.len() > MAX_APERTURE_NODES || aperture.windows(2).any(|pair| pair[0] >= pair[1]) { return Err(ReadError::InvalidAperture); diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 53717d853..b2b133388 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -14,8 +14,8 @@ use crate::contract_registry::{ }; use crate::contract_registry::{InstalledContractPackageId, InstalledContractPackageRecord}; use crate::echo_operation::{ - install_recovered_v1, EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, - InstalledEchoOperationV1, + restore_operation_catalog_entry, EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, + InstalledEchoOperation, }; use crate::execution_evidence::ExecutionFootprintEvidence; use crate::graph::GraphStore; @@ -455,7 +455,7 @@ pub struct Engine { #[cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] installed_contract_packages: BTreeMap, - installed_echo_operation_packages: BTreeMap, + installed_echo_operation_packages: BTreeMap, #[cfg_attr( not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) @@ -1433,10 +1433,10 @@ impl Engine { /// Returns one installed executable operation package by exact package id. #[must_use] - pub fn installed_echo_operation_package_v1( + pub fn installed_echo_operation_package( &self, package_id: EchoOperationPackageIdV1, - ) -> Option<&InstalledEchoOperationV1> { + ) -> Option<&InstalledEchoOperation> { self.installed_echo_operation_packages.get(&package_id) } @@ -1444,14 +1444,14 @@ impl Engine { not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) )] - pub(crate) fn preflight_recovered_echo_operation_packages_v1( + pub(crate) fn preflight_recovered_echo_operation_packages( &self, - recovered: &[InstalledEchoOperationV1], + recovered: &[InstalledEchoOperation], ) -> Result<(), EchoOperationInstallationErrorV1> { let mut packages = self.installed_echo_operation_packages.clone(); let mut operations = self.installed_echo_operations_by_coordinate.clone(); for installed in recovered { - install_recovered_v1(&mut packages, &mut operations, installed.clone())?; + restore_operation_catalog_entry(&mut packages, &mut operations, installed.clone())?; } Ok(()) } @@ -1460,14 +1460,14 @@ impl Engine { not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) )] - pub(crate) fn restore_recovered_echo_operation_packages_v1( + pub(crate) fn restore_recovered_echo_operation_packages( &mut self, - recovered: &[InstalledEchoOperationV1], + recovered: &[InstalledEchoOperation], ) -> Result<(), EchoOperationInstallationErrorV1> { let mut packages = self.installed_echo_operation_packages.clone(); let mut operations = self.installed_echo_operations_by_coordinate.clone(); for installed in recovered { - install_recovered_v1(&mut packages, &mut operations, installed.clone())?; + restore_operation_catalog_entry(&mut packages, &mut operations, installed.clone())?; } self.installed_echo_operation_packages = packages; self.installed_echo_operations_by_coordinate = operations; @@ -1478,9 +1478,9 @@ impl Engine { not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) )] - pub(crate) fn installed_echo_operation_packages_v1( + pub(crate) fn installed_echo_operation_packages( &self, - ) -> impl Iterator { + ) -> impl Iterator { self.installed_echo_operation_packages.values() } diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 23cddb1c9..efcf10569 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -70,9 +70,24 @@ pub mod domain; )] mod echo_operation; #[cfg(feature = "trusted_runtime")] +pub mod edict_effects; +// Compile the same data checker without exposing trusted-host helper APIs. +#[cfg(not(feature = "trusted_runtime"))] +#[allow(dead_code, unused_imports)] +mod edict_effects; +#[cfg(feature = "trusted_runtime")] pub mod edict_pure; +// Compile the same data checker without exposing trusted-host helper APIs. +#[cfg(not(feature = "trusted_runtime"))] +#[allow(dead_code, unused_imports)] +mod edict_pure; #[cfg(feature = "trusted_runtime")] pub mod edict_read; +// Compile the same data checker without exposing trusted-host helper APIs. +#[cfg(not(feature = "trusted_runtime"))] +#[allow(dead_code, unused_imports)] +mod edict_read; +pub use edict_effects::EffectPackageError; mod edict_target_ir; mod engine_impl; pub mod evidence; @@ -257,24 +272,24 @@ pub use echo_operation::{ echo_operation_anchored_node_creation_application_basis_v1, echo_operation_atom_value_digest_v1, echo_operation_create_if_absent_target_profile_identity_v1, echo_operation_package_id_v1, - echo_operation_target_profile_identity_v1, AdmittedEchoOperationInvocationV1, - AdmittedExecutableOperationPackageV1, EchoOperationActionDecisionCoordinateV1, - EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorKindV1, EchoOperationAdmissionErrorV1, - EchoOperationAdmissionPolicyV1, EchoOperationAnchoredNodeOccupancyV1, + echo_operation_target_profile_identity_v1, AdmittedEchoOperationInvocation, + AdmittedExecutableOperationPackage, EchoOperationActionDecisionCoordinateV1, + EchoOperationActionOutcomeV1, EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, + EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, EchoOperationApplicationResultV1, EchoOperationArtifactErrorKindV1, EchoOperationArtifactErrorV1, EchoOperationBudgetV1, EchoOperationCommitErrorV1, EchoOperationEvaluationBasisIdV1, EchoOperationEvaluationBasisV1, EchoOperationExecutionEvidenceV1, EchoOperationFootprintConflictV1, EchoOperationFootprintContractV1, EchoOperationInstallationErrorKindV1, - EchoOperationInstallationErrorV1, EchoOperationInvocationAdmissionErrorKindV1, - EchoOperationInvocationAdmissionErrorV1, EchoOperationInvocationAdmissionIdV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationIdV1, - EchoOperationInvocationV1, EchoOperationObservationV1, EchoOperationObstructionIdV1, + EchoOperationInstallationErrorV1, EchoOperationInvocation, + EchoOperationInvocationAdmissionError, EchoOperationInvocationAdmissionErrorKind, + EchoOperationInvocationAdmissionIdV1, EchoOperationInvocationAdmissionPolicy, + EchoOperationInvocationIdV1, EchoOperationObservationV1, EchoOperationObstructionIdV1, EchoOperationObstructionKindV1, EchoOperationObstructionV1, EchoOperationPackageAdmissionIdV1, EchoOperationPackageIdV1, EchoOperationPreparationV1, EchoOperationPrivateEvaluationIdV1, - EchoOperationProgramIdV1, EchoOperationProgramV1, EchoOperationReceiptV1, - EchoOperationResultIdV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, - ExecutableOperationPackageV1, InstalledEchoOperationIdV1, InstalledEchoOperationV1, + EchoOperationProgram, EchoOperationProgramIdV1, EchoOperationReceiptV1, + EchoOperationResultIdV1, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, + ExecutableOperationPackage, InstalledEchoOperation, InstalledEchoOperationIdV1, PreparedEchoOperationIdV1, PreparedEchoOperationV1, ACTION_BATCH_CANDIDATE_LIMIT_V1, }; pub use edict_target_ir::{ diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index d59ce69a4..ec04ebdfc 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -50,33 +50,35 @@ use crate::{ action_admission_evidence_matches_v1, action_application_basis_matches_state_v1, action_batch_composition_digest_from_receipts_v1, action_batch_patch_from_preparations_v1, action_preparation_identity_matches_v1, admit_action_invocation_v1, admit_invocation_v1, - admit_package_v1, commit_prepared_to_state, commit_scheduler_action_batch_to_state_v1, - decode_invocation_route_v1, echo_operation_action_invocation_bytes_v1, - inspect_action_invocation_v1, install_recovered_v1, installed_from_admitted, - not_committed_basis_changed, not_committed_evaluation_authority_mismatch, - not_committed_installation_unavailable, operation_descent_stack, prepare_operation_v1, - reconstruct_action_evaluation_v1, reconstruct_action_preparation_v1, - recover_action_outcome_v1, recover_committed_execution_receipt_v1, recover_installation_v1, - retain_action_outcome_v1, retain_committed_execution_v1, retain_installation_v1, - validate_receipt_application_result_v1, validate_receipt_installation_v1, - EchoOperationEvaluationAuthorityV1, SchedulerEchoOperationCandidateV1, + admit_operation_package, commit_prepared_to_state, + commit_scheduler_action_batch_to_state_v1, decode_invocation_route_v1, + echo_operation_action_invocation_bytes_v1, inspect_action_invocation_v1, + installed_from_admitted, not_committed_basis_changed, + not_committed_evaluation_authority_mismatch, not_committed_installation_unavailable, + operation_descent_stack, prepare_operation_v1, reconstruct_action_evaluation_v1, + reconstruct_action_preparation_v1, recover_action_outcome_v1, + recover_committed_execution_receipt_v1, recover_operation_installation, + restore_operation_catalog_entry, retain_action_outcome_v1, retain_committed_execution_v1, + retain_operation_installation, validate_receipt_application_result_v1, + validate_receipt_installation_v1, EchoOperationEvaluationAuthorityV1, + SchedulerEchoOperationCandidateV1, }, provider_contract::admit_provider_contract_package_v1, - AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackageV1, + AdmittedEchoOperationInvocation, AdmittedExecutableOperationPackage, AdmittedProviderContractPackageV1, CausalAnchorAdmissionRequest, CausalAnchorClaim, CausalAnchorError, CausalAnchorId, CausalAnchorRootSupportPolicy, CausalAnchorSupportError, CausalFrontierRef, CausalReceiptHistoryObstruction, CausalReceiptHistoryPage, CausalReceiptHistoryRequest, ContractInverseAdmissionRequest, ContractInverseContext, ContractInverseDerivation, ContractInverseHistoryObstruction, ContractInverseObstruction, - ContractOperationKind, EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorV1, - EchoOperationAdmissionPolicyV1, EchoOperationApplicationBasisV1, EchoOperationArtifactErrorV1, + ContractOperationKind, EchoOperationActionOutcomeV1, EchoOperationAdmissionError, + EchoOperationAdmissionPolicy, EchoOperationApplicationBasisV1, EchoOperationArtifactErrorV1, EchoOperationCommitErrorV1, EchoOperationEvaluationBasisV1, EchoOperationExecutionEvidenceV1, - EchoOperationInstallationErrorV1, EchoOperationInvocationAdmissionErrorKindV1, - EchoOperationInvocationAdmissionErrorV1, EchoOperationInvocationAdmissionPolicyV1, + EchoOperationInstallationErrorV1, EchoOperationInvocationAdmissionError, + EchoOperationInvocationAdmissionErrorKind, EchoOperationInvocationAdmissionPolicy, EchoOperationPreparationV1, EchoOperationReceiptV1, EchoOperationTerminalPostureV1, Engine, IngressCausalParent, IngressEnvelope, IngressEnvelopeDecodeError, IngressPayload, IngressSubmissionGeneration, InstalledContractPackage, InstalledContractPackageError, - InstalledContractPackageRecord, InstalledEchoOperationV1, + InstalledContractPackageRecord, InstalledEchoOperation, InstalledProviderContractPackageRecordV1, IntentOutcome, IntentOutcomeDecision, IntentOutcomeObservation, IntentSubmissionHandle, IntentSubmissionRecord, ObservationArtifact, ObservationError, ObservationRequest, ObservationService, OpticAdmissionTicket, @@ -104,6 +106,9 @@ const ECHO_OPERATION_ACTION_ADMISSION_DIGEST_DOMAIN: &[u8] = /// Error returned by the reference trusted host loop. #[derive(Debug, Error)] pub enum TrustedRuntimeHostError { + /// A private effect frame could not bind its parent or resource limits. + #[error("trusted runtime host effect frame setup error: {0}")] + EffectFrameSetup(#[from] crate::edict_effects::EffectFrameSetupError), /// Provenance initialization failed. #[error("trusted runtime host provenance error: {0}")] Provenance(#[from] HistoryError), @@ -153,7 +158,7 @@ pub enum TrustedRuntimeHostError { EchoOperationActionAdmissionPolicyUnavailable, /// Runtime-owned admission refused a durably accepted executable Action. #[error("trusted runtime host executable-operation Action admission error: {0}")] - EchoOperationActionAdmission(#[from] EchoOperationInvocationAdmissionErrorV1), + EchoOperationActionAdmission(#[from] EchoOperationInvocationAdmissionError), /// Executable-operation installation conflicted with installed authority. #[error("trusted runtime host executable-operation installation error: {0}")] EchoOperationInstallation(#[from] EchoOperationInstallationErrorV1), @@ -513,7 +518,7 @@ pub struct TrustedRuntimeWalRecovery { /// Ordered Echo control history for committed causal-anchor admissions. pub causal_anchor_history: Vec, /// Exact executable meaning reconstructed from retained installation records. - pub installed_echo_operations: Vec, + pub installed_echo_operations: Vec, /// Typed receipts reconstructed from executable-operation tick records. pub echo_operation_receipts: Vec, /// Typed executable-operation Action outcomes reconstructed from @@ -861,12 +866,12 @@ pub struct TrustedRuntimeHost { runtime_wal: Option, causal_anchor_support_policy: Option, echo_operation_evaluation_authority: EchoOperationEvaluationAuthorityV1, - echo_operation_action_admission_policy: Option, + echo_operation_action_admission_policy: Option, echo_operation_action_outcomes: BTreeMap, pending_echo_operation_actions: BTreeSet, - admitted_echo_operation_actions: BTreeMap, + admitted_echo_operation_actions: BTreeMap, echo_operation_action_admission_obstructions: - BTreeMap, + BTreeMap, } /// Owned decomposition of a [`TrustedRuntimeHost`]. @@ -882,12 +887,12 @@ pub struct TrustedRuntimeHostParts { runtime_wal: Option, causal_anchor_support_policy: Option, echo_operation_evaluation_authority: EchoOperationEvaluationAuthorityV1, - echo_operation_action_admission_policy: Option, + echo_operation_action_admission_policy: Option, echo_operation_action_outcomes: BTreeMap, pending_echo_operation_actions: BTreeSet, - admitted_echo_operation_actions: BTreeMap, + admitted_echo_operation_actions: BTreeMap, echo_operation_action_admission_obstructions: - BTreeMap, + BTreeMap, } impl TrustedRuntimeHostParts { @@ -1064,7 +1069,7 @@ impl TrustedRuntimeHost { /// executable-operation Actions before scheduler selection. pub fn install_echo_operation_action_admission_policy_v1( &mut self, - policy: EchoOperationInvocationAdmissionPolicyV1, + policy: EchoOperationInvocationAdmissionPolicy, ) { self.echo_operation_action_admission_policy = Some(policy); self.requeue_obstructed_echo_operation_actions_v1(); @@ -1086,7 +1091,7 @@ impl TrustedRuntimeHost { pub fn echo_operation_action_admission_obstruction_v1( &self, submission_id: &Hash, - ) -> Option { + ) -> Option { self.echo_operation_action_admission_obstructions .get(submission_id) .copied() @@ -1097,12 +1102,20 @@ impl TrustedRuntimeHost { /// A successful result is an opaque admission token. It does not install /// the package and the program digest inside the package does not confer an /// operation coordinate, invocability, or authority. - pub fn admit_echo_operation_package_v1( + /// Application handles cannot admit executable packages: + /// + /// ```compile_fail,E0599 + /// use warp_core::{TrustedRuntimeApp, EchoOperationAdmissionPolicy}; + /// fn application_cannot_admit(app: &TrustedRuntimeApp<'_>, policy: &EchoOperationAdmissionPolicy, bytes: Vec) { + /// app.admit_echo_operation_package(policy, bytes); + /// } + /// ``` + pub fn admit_echo_operation_package( &self, - policy: &EchoOperationAdmissionPolicyV1, + policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, - ) -> Result { - admit_package_v1(policy, canonical_package_bytes) + ) -> Result { + admit_operation_package(policy, canonical_package_bytes) } /// Installs exact executable meaning carried by an admitted operation package. @@ -1110,33 +1123,40 @@ impl TrustedRuntimeHost { /// Installation stores no native callback and still does not authorize an /// invocation. Application-facing [`TrustedRuntimeApp`] handles cannot call /// this runtime-owner method. - pub fn install_admitted_echo_operation_package_v1( + /// + /// ```compile_fail,E0599 + /// use warp_core::{TrustedRuntimeApp, AdmittedExecutableOperationPackage}; + /// fn application_cannot_install(app: &mut TrustedRuntimeApp<'_>, admitted: AdmittedExecutableOperationPackage) { + /// app.install_admitted_echo_operation_package(admitted); + /// } + /// ``` + pub fn install_admitted_echo_operation_package( &mut self, - admitted: AdmittedExecutableOperationPackageV1, - ) -> Result { - let installed = installed_from_admitted(admitted)?; + admitted: AdmittedExecutableOperationPackage, + ) -> Result { + let installed = installed_from_admitted(admitted); self.engine - .preflight_recovered_echo_operation_packages_v1(core::slice::from_ref(&installed))?; + .preflight_recovered_echo_operation_packages(core::slice::from_ref(&installed))?; if self .engine - .installed_echo_operation_package_v1(installed.package_id()) + .installed_echo_operation_package(installed.package_id()) == Some(&installed) { return Ok(installed); } if let Some(runtime_wal) = self.runtime_wal.as_mut() { - let retained = retain_installation_v1(&installed)?; + let retained = retain_operation_installation(&installed)?; if let Err(error) = runtime_wal.record_executable_operation_installation(&retained) { - if !runtime_wal.recover_filesystem_executable_operation_installation_after_error( - installed.package_id(), - ) { + if !runtime_wal + .recover_filesystem_executable_operation_installation_after_error(&installed) + { return Err(error.into()); } } } self.engine - .restore_recovered_echo_operation_packages_v1(core::slice::from_ref(&installed))?; + .restore_recovered_echo_operation_packages(core::slice::from_ref(&installed))?; self.requeue_obstructed_echo_operation_actions_v1(); Ok(installed) } @@ -1160,16 +1180,127 @@ impl TrustedRuntimeHost { .map_err(TrustedRuntimeHostError::from) } + /// Starts a private computation/read frame at an exact current parent. + /// + /// The runtime resolves the writer, ticks, root and commit identity itself + /// and borrows its own frontier. It never accepts caller-supplied storage. + /// The immutable host borrow prevents a scheduler tick during this frame. + /// Policy and limits are trusted-host inputs, not application authority. + /// The typed application basis remains a proposition; this constructor does + /// not validate it against application input. This is not package admission, + /// installed dispatch or a mutation path. + /// + /// # Errors + /// + /// Refuses an unknown writer, a stale or substituted full parent, or invalid + /// resource declarations before creating a frame or recording a read. + pub fn effect_evaluation_frame_v1( + &self, + expected: EchoOperationEvaluationBasisV1, + allowed_range_types: &[crate::TypeId], + max_range_edges: u64, + budget: crate::EchoOperationBudgetV1, + limits: crate::edict_pure::EvaluationLimits, + ) -> Result, TrustedRuntimeHostError> { + let current = self.echo_operation_evaluation_basis_v1( + expected.writer_head(), + expected.application_basis(), + )?; + if current != expected { + return Err(crate::edict_effects::EffectFrameSetupError::BasisMismatch.into()); + } + let frontier = self + .runtime + .worldlines() + .get(¤t.writer_head().worldline_id) + .ok_or_else(|| RuntimeError::UnknownWorldline(current.writer_head().worldline_id))?; + let reads = crate::edict_effects::EffectRangeReadSession::new( + frontier, + allowed_range_types, + max_range_edges, + budget, + ); + Ok(crate::edict_effects::EffectEvaluationFrame::at_parent( + reads, + limits, + current, + self.echo_operation_evaluation_authority.clone(), + )?) + } + + /// Privately derives a patch from completed authored work without applying it. + /// # Errors + /// Refuses mismatched authority/origin and invalid mutation proposals. + pub fn derive_echo_operation_effects( + &self, + admitted: &crate::AdmittedEchoOperationInvocation, + completed: crate::edict_effects::CompletedEffectEvaluation, + ) -> Result + { + use crate::edict_effects::{EffectDerivationError as Error, EffectDerivationKind as Kind}; + let fail = |kind| Error::new(kind, crate::EchoOperationBudgetV1::new(0, 0, 0)); + if !admitted.is_owned_by(&self.echo_operation_evaluation_authority) + || !completed.is_owned_by(&self.echo_operation_evaluation_authority) + { + return Err(fail(Kind::AuthorityMismatch)); + } + let installed = self + .engine + .installed_echo_operation_package(admitted.package_id()) + .ok_or_else(|| fail(Kind::OperationUnavailable))?; + if installed.installed_operation_id() != admitted.installed_operation_id() { + return Err(fail(Kind::OperationUnavailable)); + } + let program = installed + .program() + .as_effect_program() + .ok_or_else(|| fail(Kind::OperationUnavailable))?; + if completed.program_identity() != installed.program_identity() + || completed.evaluation_basis() != admitted.evaluation_basis() + { + return Err(fail(Kind::OriginMismatch)); + } + let basis = self + .echo_operation_evaluation_basis_v1( + admitted.evaluation_basis().writer_head(), + admitted.evaluation_basis().application_basis(), + ) + .map_err(|_| fail(Kind::BasisChanged))?; + if basis != admitted.evaluation_basis() { + return Err(fail(Kind::BasisChanged)); + } + let state = self + .runtime + .worldlines() + .get(&basis.writer_head().worldline_id) + .ok_or_else(|| fail(Kind::BasisChanged))? + .state(); + let invocation = admitted + .invocation() + .identity() + .map_err(|_| fail(Kind::OriginMismatch))? + .as_hash(); + crate::edict_effects::derive::derive( + state, + program, + completed, + admitted.invocation().delegated_budget(), + self.engine.echo_operation_policy_id(), + installed.identity(), + invocation, + ) + } + /// Independently admits a canonical installed-operation invocation. /// /// Admission checks exact installed package identity, public operation /// coordinate, authority profile and grant, delegated budget, and current /// parent basis. It does not evaluate or mutate the graph. - pub fn admit_echo_operation_invocation_v1( + pub fn admit_echo_operation_invocation( &self, - policy: &EchoOperationInvocationAdmissionPolicyV1, + policy: &EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], - ) -> Result { + ) -> Result { let (package_id, claimed_basis) = decode_invocation_route_v1(canonical_invocation_bytes)?; let current_basis = self .echo_operation_evaluation_basis_v1( @@ -1188,7 +1319,7 @@ impl TrustedRuntimeHost { ) })?; admit_invocation_v1( - self.engine.installed_echo_operation_package_v1(package_id), + self.engine.installed_echo_operation_package(package_id), *policy, canonical_invocation_bytes, current_basis, @@ -1207,7 +1338,7 @@ impl TrustedRuntimeHost { #[doc(hidden)] pub fn prepare_echo_operation_v1( &self, - admitted: AdmittedEchoOperationInvocationV1, + admitted: AdmittedEchoOperationInvocation, ) -> EchoOperationPreparationV1 { let current_basis = self.echo_operation_evaluation_basis_v1( admitted.evaluation_basis().writer_head(), @@ -1226,7 +1357,7 @@ impl TrustedRuntimeHost { }; prepare_operation_v1( self.engine - .installed_echo_operation_package_v1(admitted.package_id()), + .installed_echo_operation_package(admitted.package_id()), admitted, current_basis, state, @@ -1269,7 +1400,7 @@ impl TrustedRuntimeHost { } if self .engine - .installed_echo_operation_package_v1(prepared.package_id()) + .installed_echo_operation_package(prepared.package_id()) .is_none_or(|installed| { installed.installed_operation_id() != prepared.installed_operation_id() }) @@ -1338,7 +1469,7 @@ impl TrustedRuntimeHost { let installed = self .engine - .installed_echo_operation_package_v1(prepared.package_id()); + .installed_echo_operation_package(prepared.package_id()); if let Some(runtime_wal) = self.runtime_wal.as_mut() { let installed = installed.ok_or(TrustedRuntimeWalError::EchoOperationExecutionMismatch { @@ -1415,7 +1546,7 @@ impl TrustedRuntimeHost { &recovery, )?; self.engine - .preflight_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; + .preflight_recovered_echo_operation_packages(&recovery.installed_echo_operations)?; let mut restored_runtime = forked_runtime; let mut restored_provenance = forked_provenance; @@ -1440,7 +1571,7 @@ impl TrustedRuntimeHost { validate_recovered_tick_decisions(&restored_runtime, &recovery.receipts)?; self.engine - .restore_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; + .restore_recovered_echo_operation_packages(&recovery.installed_echo_operations)?; self.echo_operation_action_outcomes = recovery .echo_operation_action_outcomes .iter() @@ -1942,7 +2073,7 @@ impl TrustedRuntimeHost { fn admit_pending_echo_operation_actions_v1( &mut self, staging: &mut TicketedIngressStagingRollback, - ) -> Result, TrustedRuntimeHostError> { + ) -> Result, TrustedRuntimeHostError> { if self.pending_echo_operation_actions.is_empty() { return Ok(self.admitted_echo_operation_actions.clone()); } @@ -2027,7 +2158,7 @@ impl TrustedRuntimeHost { } }; let admitted = match admit_action_invocation_v1( - self.engine.installed_echo_operation_package_v1(package_id), + self.engine.installed_echo_operation_package(package_id), policy, &invocation_bytes, self.echo_operation_evaluation_authority.clone(), @@ -2322,14 +2453,11 @@ fn ensure_runtime_authority_is_durable( .iter() .map(|installed| (installed.package_id(), installed)) .collect::>(); - if engine - .installed_echo_operation_packages_v1() - .any(|installed| { - recovered_operations - .get(&installed.package_id()) - .is_none_or(|recovered| **recovered != *installed) - }) - { + if engine.installed_echo_operation_packages().any(|installed| { + recovered_operations + .get(&installed.package_id()) + .is_none_or(|recovered| **recovered != *installed) + }) { return Err(TrustedRuntimeWalError::RuntimeAuthorityNotDurable { gap: RuntimeWalActivationGap::ExecutableOperationInstallation, }); @@ -3360,7 +3488,7 @@ impl TrustedRuntimeWal { fn recover_filesystem_executable_operation_installation_after_error( &mut self, - package_id: crate::EchoOperationPackageIdV1, + expected: &InstalledEchoOperation, ) -> bool { if !self.uses_filesystem_store() { return false; @@ -3372,7 +3500,7 @@ impl TrustedRuntimeWal { recovery .installed_echo_operations .iter() - .any(|installed| installed.package_id() == package_id) + .any(|installed| installed == expected) }) } @@ -3596,10 +3724,10 @@ impl TrustedRuntimeWal { &mut self, retained_installation_bytes: &[u8], ) -> Result { - let installed = recover_installation_v1(retained_installation_bytes)?; + let installed = recover_operation_installation(retained_installation_bytes)?; let next_catalog_frontier = executable_operation_catalog_frontier_digest( self.executable_operation_catalog_frontier_digest, - installed.package_id(), + installed.package_identity(), retained_installation_bytes, ); let transaction = build_executable_operation_installation_transaction( @@ -3608,7 +3736,7 @@ impl TrustedRuntimeWal { WalAppendAuthority::RuntimeControl, WalTransactionId::from_hash(executable_operation_installation_transaction_digest( self.executable_operation_catalog_frontier_digest, - installed.package_id(), + installed.package_identity(), retained_installation_bytes, )), ), @@ -3630,7 +3758,7 @@ impl TrustedRuntimeWal { retained_execution_bytes: Vec, state_delta: &WalRuntimeStateDeltaRecord, state_delta_digest: Hash, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, ) -> Result { let retained_receipt = recover_committed_execution_receipt_v1(&retained_execution_bytes)?; if &retained_receipt != receipt || state_delta.digest()? != state_delta_digest { @@ -4256,7 +4384,7 @@ impl TrustedRuntimeWalCursor { let catalog_before = cursor.executable_operation_catalog_frontier_digest; let catalog_after = executable_operation_catalog_frontier_digest( catalog_before, - installed.package_id(), + installed.package_identity(), &retained_bytes, ); validate_echo_operation_frontier_root( @@ -4488,7 +4616,7 @@ fn restore_provenance_entries( } struct RecoveredEchoOperationMaterial { - installations: Vec, + installations: Vec, receipts: Vec, } @@ -4509,7 +4637,7 @@ fn recover_echo_operation_material( match transaction.commit.transaction_kind { WalTransactionKind::ExecutableOperationInstallation => { let (installed, _) = operation_installation_from_transaction(transaction)?; - install_recovered_v1(&mut installations, &mut operations, installed)?; + restore_operation_catalog_entry(&mut installations, &mut operations, installed)?; } WalTransactionKind::ExecutableOperationTick => { let (receipt, state_delta, _) = @@ -4557,10 +4685,11 @@ fn recover_echo_operation_material( /// performed independently by the surrounding provenance checks. fn operation_patch_scope_v1( patch: &crate::WorldlineTickPatchV1, - program: &crate::EchoOperationProgramV1, + program: &crate::EchoOperationProgram, ) -> Option { match program { - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { + crate::EchoOperationProgram::EdictEffect(_) => None, + crate::EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { required_attachment_type, max_replacement_bytes, .. @@ -4586,7 +4715,7 @@ fn operation_patch_scope_v1( } _ => None, }, - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + crate::EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { required_node_type, required_attachment_type, max_replacement_bytes, @@ -4665,7 +4794,7 @@ fn operation_patch_inputs_match_v1( fn operation_patch_scope_in_parent_state_v1( patch: &crate::WorldlineTickPatchV1, - program: &crate::EchoOperationProgramV1, + program: &crate::EchoOperationProgram, parent_state: &crate::WorldlineState, ) -> Option { let node = operation_patch_scope_v1(patch, program)?; @@ -4675,7 +4804,7 @@ fn operation_patch_scope_in_parent_state_v1( let descent_stack = operation_descent_stack(parent_state, node.warp_id)?; if matches!( program, - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } + crate::EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } ) { let store = parent_state.store(&node.warp_id)?; if store.node(&node.local_id).is_some() || store.node_attachment(&node.local_id).is_some() { @@ -4703,7 +4832,7 @@ fn operation_tick_binds_patch_v1( tick_receipt: &crate::TickReceipt, patch: &crate::WorldlineTickPatchV1, expected_rule_id: Hash, - program: &crate::EchoOperationProgramV1, + program: &crate::EchoOperationProgram, ) -> bool { let Some(expected_scope) = operation_patch_scope_v1(patch, program) else { return false; @@ -4724,7 +4853,7 @@ fn operation_tick_scope_in_parent_state_v1( tick_receipt: &crate::TickReceipt, patch: &crate::WorldlineTickPatchV1, expected_rule_id: Hash, - program: &crate::EchoOperationProgramV1, + program: &crate::EchoOperationProgram, parent_state: &crate::WorldlineState, ) -> Option { let expected_scope = operation_patch_scope_in_parent_state_v1(patch, program, parent_state)?; @@ -4742,13 +4871,14 @@ fn operation_tick_scope_in_parent_state_v1( } fn operation_application_basis_matches_scope_v1( - program: &crate::EchoOperationProgramV1, + program: &crate::EchoOperationProgram, node: crate::NodeKey, application_basis: EchoOperationApplicationBasisV1, ) -> bool { match program { - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } => true, - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + crate::EchoOperationProgram::EdictEffect(_) => false, + crate::EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } => true, + crate::EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } => { application_basis == crate::echo_operation_anchored_node_absent_application_basis_v1(node) } @@ -5594,7 +5724,7 @@ fn evaluation_basis_matches_recovered_coordinate( fn validate_operation_receipt_state_delta( receipt: &EchoOperationReceiptV1, state_delta: &WalRuntimeStateDeltaRecord, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, ) -> Result<(), TrustedRuntimeWalError> { let entry = state_delta.provenance_entry(); let basis = receipt.evaluation_basis(); @@ -5920,7 +6050,7 @@ fn validate_recovered_echo_operation_action_outcomes( witnessed_submissions: &WitnessedSubmissionPersistenceSnapshot, receipt_correlations: &[ReceiptCorrelationPersistenceRecord], provenance_entries: &[ProvenanceEntry], - installed_echo_operations: &[InstalledEchoOperationV1], + installed_echo_operations: &[InstalledEchoOperation], outcomes: &[(Hash, Hash, EchoOperationActionOutcomeV1)], decisions: &BTreeMap, installation_order: &RecoveredEchoOperationActionInstallationOrder, @@ -6246,13 +6376,13 @@ fn wal_tick_decision_for_action_outcome(outcome: &EchoOperationActionOutcomeV1) fn operation_installation_from_transaction( transaction: &crate::causal_wal::WalRecoveredTransaction, -) -> Result<(InstalledEchoOperationV1, Vec), TrustedRuntimeWalError> { +) -> Result<(InstalledEchoOperation, Vec), TrustedRuntimeWalError> { let frame = required_unique_transaction_frame( transaction, WalRecordKind::ExecutableOperationPackageInstalled, )?; let retained_bytes = frame.payload.canonical_bytes.clone(); - let installed = recover_installation_v1(&retained_bytes)?; + let installed = recover_operation_installation(&retained_bytes)?; Ok((installed, retained_bytes)) } @@ -7203,7 +7333,7 @@ fn provider_contract_host_admission_digest( fn echo_operation_action_admission_digest( submission: &IntentSubmissionRecord, - admitted: &AdmittedEchoOperationInvocationV1, + admitted: &AdmittedEchoOperationInvocation, ) -> Hash { echo_operation_action_admission_digest_from_parts( submission, @@ -7488,61 +7618,95 @@ mod tests { .expect("the recovery parent-state fixture is lawful") } - #[test] - fn executable_operation_index_preserves_legacy_root_without_action_outcomes() { - let operation_coordinate = "echo.test.LegacyRecoveryIndex.v1"; + fn recovery_index_installation(operation_coordinate: &str) -> InstalledEchoOperation { let authority_profile_identity = [0x17; 32]; let budget = crate::EchoOperationBudgetV1::new(7, 1_024, 1_024); - let package = crate::ExecutableOperationPackageV1::new( + let package = crate::ExecutableOperationPackage::new( operation_coordinate, - "echo.test.LegacyRecoveryIndex.Obstruction.v1", - crate::EchoOperationSemanticClosureV1::new( + "echo.test.RecoveryIndex.Obstruction.v1", + crate::EchoOperationSemanticClosure::new( [0x10; 32], [0x11; 32], [0x12; 32], [0x13; 32], - "echo.test.legacy-index-schema/v1", + "echo.test.recovery-index-schema/v1", [0x14; 32], - "echo.test.legacy-index-lawpack/v1", + "echo.test.recovery-index-lawpack/v1", [0x15; 32], ), crate::echo_operation_target_profile_identity_v1(), authority_profile_identity, budget, - crate::EchoOperationProgramV1::anchored_node_attachment_compare_and_set( - crate::make_type_id("legacy-index-node"), - crate::make_type_id("legacy-index-attachment"), + crate::EchoOperationProgram::anchored_node_attachment_compare_and_set( + crate::make_type_id("recovery-index-node"), + crate::make_type_id("recovery-index-attachment"), 128, ), ); let package_bytes = package .to_canonical_bytes() - .expect("the legacy-index package is canonical"); + .expect("the recovery-index package is canonical"); let package_id = crate::echo_operation_package_id_v1(&package_bytes); - let admitted = admit_package_v1( - &crate::EchoOperationAdmissionPolicyV1::exact( - package_id, + let admitted = admit_operation_package( + &crate::EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile_identity, budget, ), package_bytes, ) - .expect("the legacy-index package is admitted"); - let installed = - installed_from_admitted(admitted).expect("the legacy-index package installs"); + .expect("the recovery-index package is admitted"); + installed_from_admitted(admitted) + } + #[test] + fn operation_index_uses_one_current_recipe_even_without_action_outcomes() { + let base = [0x20; 32]; + let installed = recovery_index_installation("echo.test.RecoveryIndex.a"); + let bytes = retain_operation_installation(&installed).expect("retained installation"); + let mut expected = blake3::Hasher::new(); + expected.update(b"echo:trusted-runtime-wal:executable-operation-index:v3\0"); + expected.update(&base); + expected.update(&1u64.to_le_bytes()); + expected.update(&(bytes.len() as u64).to_le_bytes()); + expected.update(&bytes); + expected.update(&0u64.to_le_bytes()); // No execution receipts. + expected.update(&0u64.to_le_bytes()); // No Action outcomes. assert_eq!( - recovered_echo_operation_index_root([0x20; 32], &[installed], &[], &[]) - .expect("the legacy index root is computable"), - [ - 0xf9, 0xcf, 0x53, 0xf2, 0xad, 0xaf, 0x93, 0x1a, 0xa0, 0xe6, 0x06, 0xc6, 0x5a, 0x91, - 0x6f, 0xfa, 0x0b, 0x4f, 0xf5, 0x50, 0x66, 0x32, 0x8c, 0x1c, 0x48, 0xf7, 0x22, 0xcd, - 0x1e, 0x0d, 0x40, 0x2e, - ] + recovered_echo_operation_index_root(base, &[installed], &[], &[],) + .expect("current index"), + *expected.finalize().as_bytes(), ); } + #[test] + fn operation_index_canonicalizes_installation_order_without_a_second_catalog() { + let a = recovery_index_installation("echo.test.RecoveryIndex.a"); + let b = recovery_index_installation("echo.test.RecoveryIndex.b"); + let forward = + recovered_echo_operation_index_root([0x20; 32], &[a.clone(), b.clone()], &[], &[]) + .expect("forward catalog"); + let reversed = recovered_echo_operation_index_root([0x20; 32], &[b, a], &[], &[]) + .expect("reversed catalog"); + assert_eq!(forward, reversed); + } + + #[test] + fn operation_index_commits_empty_sections_under_the_same_current_domain() { + let base = [0x20; 32]; + let mut expected = blake3::Hasher::new(); + expected.update(b"echo:trusted-runtime-wal:executable-operation-index:v3\0"); + expected.update(&base); + for _ in 0..3 { + expected.update(&0u64.to_le_bytes()); + } + let root = + recovered_echo_operation_index_root(base, &[], &[], &[]).expect("empty current index"); + assert_eq!(root, *expected.finalize().as_bytes()); + assert_ne!(root, base); + } + #[test] fn creation_wal_scope_accepts_descendants_and_rejects_mutated_shapes() { let node = crate::NodeKey { @@ -7550,17 +7714,16 @@ mod tests { local_id: crate::make_node_id("operation-wal-created-node"), }; let installed_program = - crate::EchoOperationProgramV1::anchored_node_attachment_create_if_absent( - crate::make_type_id("operation-wal-created-node"), - crate::make_type_id("operation-wal-created-attachment"), - 7, - ); - let update_program = - crate::EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + crate::EchoOperationProgram::anchored_node_attachment_create_if_absent( crate::make_type_id("operation-wal-created-node"), crate::make_type_id("operation-wal-created-attachment"), 7, ); + let update_program = crate::EchoOperationProgram::anchored_node_attachment_compare_and_set( + crate::make_type_id("operation-wal-created-node"), + crate::make_type_id("operation-wal-created-attachment"), + 7, + ); let patch = creation_scope_patch(node); let parent_state = creation_scope_parent_state(node); assert_eq!( @@ -7853,7 +8016,7 @@ mod tests { let attachment_slot = crate::AttachmentKey::node_alpha(node); let rule_id = [21; 32]; let attachment_type = crate::make_type_id("operation-recovery-attachment"); - let program = crate::EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + let program = crate::EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("operation-recovery-node"), attachment_type, 7, @@ -8770,14 +8933,14 @@ fn wal_tick_decision_code(decision: WalTickDecision) -> u8 { fn executable_operation_catalog_frontier_digest( previous: Hash, - package_id: crate::EchoOperationPackageIdV1, + package_id: Hash, retained_installation_bytes: &[u8], ) -> Hash { let mut hasher = blake3::Hasher::new(); hasher.update(TRUSTED_RUNTIME_WAL_DOMAIN); hasher.update(b"executable-operation-catalog-frontier"); hasher.update(&previous); - hasher.update(&package_id.as_hash()); + hasher.update(&package_id); hasher.update(&(retained_installation_bytes.len() as u64).to_le_bytes()); hasher.update(retained_installation_bytes); hasher.finalize().into() @@ -8794,13 +8957,13 @@ fn executable_operation_receipt_frontier_digest(previous: Hash, receipt_digest: fn executable_operation_installation_transaction_digest( catalog_frontier: Hash, - package_id: crate::EchoOperationPackageIdV1, + package_id: Hash, retained_installation_bytes: &[u8], ) -> Hash { let mut hasher = blake3::Hasher::new(); hasher.update(b"echo:trusted-runtime:executable-operation-installation-transaction:v1\0"); hasher.update(&catalog_frontier); - hasher.update(&package_id.as_hash()); + hasher.update(&package_id); hasher.update(&(retained_installation_bytes.len() as u64).to_le_bytes()); hasher.update(retained_installation_bytes); hasher.finalize().into() @@ -8875,7 +9038,7 @@ struct RecoveredRuntimeWalIndexEvidence<'a> { provenance_entries: &'a [ProvenanceEntry], missing_runtime_state_deltas: &'a [Hash], causal_anchor_history: &'a [WitnessedCausalAnchorAdmission], - installed_echo_operations: &'a [InstalledEchoOperationV1], + installed_echo_operations: &'a [InstalledEchoOperation], echo_operation_receipts: &'a [EchoOperationReceiptV1], echo_operation_action_outcomes: &'a [(Hash, Hash, EchoOperationActionOutcomeV1)], } @@ -8923,41 +9086,26 @@ fn recovered_runtime_wal_indexes_root( fn recovered_echo_operation_index_root( base_root: Hash, - installations: &[InstalledEchoOperationV1], + installations: &[InstalledEchoOperation], receipts: &[EchoOperationReceiptV1], action_outcomes: &[(Hash, Hash, EchoOperationActionOutcomeV1)], ) -> Result { - let legacy_root = - recovered_echo_operation_legacy_index_root(base_root, installations, receipts)?; - if action_outcomes.is_empty() { - return Ok(legacy_root); - } - let mut hasher = blake3::Hasher::new(); - hasher.update(b"echo:trusted-runtime-wal:executable-operation-index:v2\0"); - hasher.update(&legacy_root); - hasher.update(&(action_outcomes.len() as u64).to_le_bytes()); - for (submission_id, ingress_id, outcome) in action_outcomes { - let bytes = retain_action_outcome_v1(*submission_id, *ingress_id, outcome)?; - hasher.update(&(bytes.len() as u64).to_le_bytes()); - hasher.update(&bytes); - } - Ok(hasher.finalize().into()) -} + let mut catalog = installations + .iter() + .map(|installed| { + Ok(( + installed.package_id().as_hash(), + retain_operation_installation(installed)?, + )) + }) + .collect::, TrustedRuntimeWalError>>()?; + catalog.sort_unstable(); -fn recovered_echo_operation_legacy_index_root( - base_root: Hash, - installations: &[InstalledEchoOperationV1], - receipts: &[EchoOperationReceiptV1], -) -> Result { - if installations.is_empty() && receipts.is_empty() { - return Ok(base_root); - } let mut hasher = blake3::Hasher::new(); - hasher.update(b"echo:trusted-runtime-wal:executable-operation-index:v1\0"); + hasher.update(b"echo:trusted-runtime-wal:executable-operation-index:v3\0"); hasher.update(&base_root); - hasher.update(&(installations.len() as u64).to_le_bytes()); - for installed in installations { - let bytes = retain_installation_v1(installed)?; + hasher.update(&(catalog.len() as u64).to_le_bytes()); + for (_, bytes) in catalog { hasher.update(&(bytes.len() as u64).to_le_bytes()); hasher.update(&bytes); } @@ -8967,6 +9115,12 @@ fn recovered_echo_operation_legacy_index_root( hasher.update(&(bytes.len() as u64).to_le_bytes()); hasher.update(&bytes); } + hasher.update(&(action_outcomes.len() as u64).to_le_bytes()); + for (submission_id, ingress_id, outcome) in action_outcomes { + let bytes = retain_action_outcome_v1(*submission_id, *ingress_id, outcome)?; + hasher.update(&(bytes.len() as u64).to_le_bytes()); + hasher.update(&bytes); + } Ok(hasher.finalize().into()) } diff --git a/crates/warp-core/src/trusted_runtime_host/observed_context.rs b/crates/warp-core/src/trusted_runtime_host/observed_context.rs index 88d08ac03..8f8f6f0be 100644 --- a/crates/warp-core/src/trusted_runtime_host/observed_context.rs +++ b/crates/warp-core/src/trusted_runtime_host/observed_context.rs @@ -7,7 +7,7 @@ use super::{ }; use crate::EchoOperationAnchoredNodeOccupancyV1 as Occupancy; use crate::{ - EchoOperationInvocationV1, EchoOperationObservationV1, NodeKey, ProvenanceStore, WriterHeadKey, + EchoOperationInvocation, EchoOperationObservationV1, NodeKey, ProvenanceStore, WriterHeadKey, }; use echo_edict_canonical::{ decode_canonical_cbor_v1, encode_canonical_cbor_v1, CanonicalValueV1 as V, @@ -106,7 +106,7 @@ impl Contexts { let digest: Hash = field_bytes(&fields[4])?.try_into().map_err(error)?; let invocation_bytes = field_bytes(&fields[5])?.to_vec(); let invocation = - EchoOperationInvocationV1::from_canonical_bytes(&invocation_bytes) + EchoOperationInvocation::from_canonical_bytes(&invocation_bytes) .map_err(error)?; let observation = self .observations @@ -344,7 +344,7 @@ impl TrustedRuntimeHost { &mut self, request: &str, attempt: &str, - invocation: EchoOperationInvocationV1, + invocation: EchoOperationInvocation, ) -> Result> { check_id(request)?; let wal = self diff --git a/crates/warp-core/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs new file mode 100644 index 000000000..f190674d4 --- /dev/null +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Canonical publication fixtures for package and runtime-owner admission tests. +#![allow(dead_code)] +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V}; +use warp_core::edict_effects::EffectPackageError; +use warp_core::ExecutableOperationPackage; +pub fn map(fields: impl IntoIterator) -> V { + V::Map( + fields + .into_iter() + .map(|(k, v)| (V::Text(k.into()), v)) + .collect(), + ) +} +pub fn text(s: &str) -> V { + V::Text(s.into()) +} +pub fn id(n: u8) -> V { + V::Bytes(vec![n; 32]) +} +pub fn encoded(v: &V) -> Vec { + encode(v).expect("fixture") +} +pub fn hash(domain: &[u8], data: &[u8]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(domain); + h.update(&(data.len() as u64).to_le_bytes()); + h.update(data); + h.finalize().into() +} +pub fn profile(label: &str) -> V { + V::Bytes(hash(b"echo:operation-profile:v1\0", label.as_bytes()).to_vec()) +} +pub fn graph() -> V { + map([ + ( + "node_types", + V::Map(vec![( + id(1), + map([ + ("addressing", V::Integer(1)), + ("atom_types", V::Array(vec![id(2)])), + ]), + )]), + ), + ("edge_types", V::Map(vec![])), + ]) +} +pub fn law() -> V { + map([ + ("read_node_types", V::Array(vec![id(1)])), + ("write_node_types", V::Array(vec![id(1)])), + ("atom_types", V::Array(vec![id(2)])), + ("range_edge_types", V::Array(vec![])), + ("write_edge_types", V::Array(vec![])), + ("portal_owner_types", V::Array(vec![])), + ("max_reads", V::Integer(1)), + ("max_writes", V::Integer(1)), + ("max_range_edges", V::Integer(0)), + ]) +} +pub fn footprint(v: &V) -> V { + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-footprint-law:v1\0"); + h.update(&encoded(v)); + V::Bytes(h.finalize().as_bytes().to_vec()) +} +pub fn compiled_core() -> V { + map([ + ("apiVersion", text("edict.core/v1")), + ("coordinate", text("app")), + ("imports", V::Array(vec![])), + ("types", map([("Result", map([("kind", text("U32"))]))])), + ( + "intents", + map([( + "update@1", + map([ + ("input", text("U32")), + ("output", text("app.Result")), + ( + "requiredOperationProfile", + text("continuum.profile.total/v1"), + ), + ("inputConstraints", V::Array(vec![])), + ( + "coreEvaluationBudget", + map([ + ("maxSteps", V::Integer(1)), + ("maxAllocatedBytes", V::Integer(16 * 1024 * 1024)), + ("maxOutputBytes", V::Integer(65536)), + ]), + ), + ( + "body", + map([ + ( + "locals", + V::Array(vec![map([ + ("id", text("arg.0")), + ("type", text("U32")), + ("alphaName", text("$arg0")), + ])]), + ), + ("nodes", V::Array(vec![])), + ( + "result", + map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("int")), + ("width", text("U32")), + ("value", V::Integer(42)), + ]), + ), + ]), + ), + ]), + ), + ]), + )]), + ), + ("requiredCoreCapabilities", V::Array(vec![])), + ]) +} +pub fn get<'a>(v: &'a V, key: &str) -> &'a V { + let V::Map(fields) = v else { panic!("map") }; + &fields + .iter() + .find(|(name, _)| name == &text(key)) + .expect("field") + .1 +} +pub fn artifact_identity(domain: &str, v: &V) -> V { + V::Bytes( + echo_edict_canonical::digest_canonical_value_bytes_v1(domain, v) + .expect("artifact") + .to_vec(), + ) +} +pub fn compiled_target(core: &V) -> V { + let coordinate = get(core, "coordinate").clone(); + let intent = get(get(core, "intents"), "update@1"); + map([ + ("domain", text("echo.span-ir/v2")), + ("kind", text("orderedTargetIrArtifact")), + ("sourceCoreCoordinate", coordinate.clone()), + ( + "targetProfile", + map([ + ("id", text("echo.dpo@1")), + ("digest", V::Array(vec![text("sha256"), id(6)])), + ]), + ), + ( + "semanticClosure", + map([ + ( + "sourceCore", + map([ + ("id", coordinate), + ( + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.core.module/v1", core), + ]), + ), + ]), + ), + ("lawpacks", V::Array(vec![])), + ]), + ), + ( + "intents", + map([( + "update@1", + map([ + ("steps", V::Array(vec![])), + ("requirements", V::Array(vec![])), + ("pureBindings", V::Array(vec![])), + ("executionOrder", V::Array(vec![])), + ("result", get(get(intent, "body"), "result").clone()), + ("inputConstraints", get(intent, "inputConstraints").clone()), + ( + "coreEvaluationBudget", + get(intent, "coreEvaluationBudget").clone(), + ), + ( + "operationProfile", + get(intent, "requiredOperationProfile").clone(), + ), + ]), + )]), + ), + ]) +} +pub fn compiled_exports() -> V { + map([ + ("types", V::Array(vec![])), + ("effects", V::Array(vec![])), + ("constants", V::Array(vec![])), + ("obstructions", V::Array(vec![])), + ("pureFunctions", V::Array(vec![])), + ("operationProfiles", map([])), + ]) +} +pub fn compiled_bundle(core: &V, target: &V, exports: &V, intent: &str) -> V { + let has_effects = matches!(get(exports, "effects"), V::Array(effects) if !effects.is_empty()); + let lowering = if has_effects { + repinned_lowering_artifacts() + } else { + V::Null + }; + map([ + ("lowering_artifacts", lowering), + ("schema", text("echo.compiler-produced-effect-program/v1")), + ("intent", text(intent)), + ("core_artifact", V::Bytes(encoded(core))), + ("target_ir_artifact", V::Bytes(encoded(target))), + ("lawpack_exports_artifact", V::Bytes(encoded(exports))), + ]) +} +// This is a synthetic, internally repinned test chain built from the retained +// public capture. It is not evidence of a successful compiler/verifier run. +pub fn repinned_lowering_artifacts() -> V { + lowering_fixture(None) +} +pub fn lowering_fixture(catalog: Option) -> V { + lowering_fixture_inputs(catalog, None, None) +} +pub fn lowering_fixture_inputs( + catalog: Option, + exports_override: Option, + adapter_override: Option, +) -> V { + let captured = |value: &str| { + echo_edict_canonical::decode_canonical_cbor_v1( + &hex::decode(value.trim()).expect("captured bytes"), + ) + .expect("captured value") + }; + let mut lawpack = captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/03-lawpack.hex" + )); + let adapter = adapter_override.unwrap_or_else(|| catalog_named_adapter(&captured_adapter())); + let exports = exports_override.unwrap_or_else(|| { + captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex" + )) + }); + let catalog = catalog.unwrap_or_else(|| { + let V::Array(effects) = get(&exports, "effects") else { + panic!("effects") + }; + let effect = &effects[0]; + let implementation = get( + get(&adapter, "effectImplementations"), + "jedit.text@1.readFact", + ); + let V::Map(mappings) = get(implementation, "failureMappings") else { + panic!("mappings") + }; + let failures = V::Map( + mappings + .iter() + .map(|(source, tag)| { + let V::Text(source) = source else { + panic!("failure") + }; + // The catalog's failure carries the read ABI's input; the + // exported failure keeps the application's own type. + let exported = get(get(effect, "effectFailures"), source); + ( + tag.clone(), + map([ + ("authorityClass", get(exported, "authorityClass").clone()), + ("payloadType", text("echo.dpo@1.node-atom-read.input")), + ]), + ) + }) + .collect(), + ); + map([ + ("apiVersion", text("edict.target-profile.intrinsics/v1")), + ( + "intrinsics", + map([( + "echo.dpo@1.node-atom-read", + map([ + ("intrinsicClass", text("effect")), + ( + "argumentTypes", + V::Array(vec![text("echo.dpo@1.node-atom-read.input")]), + ), + ("returnType", text("echo.dpo@1.node-atom-read.result")), + ("typeParameters", V::Array(vec![])), + ("effectKind", text("read")), + ("writeClass", text("read")), + ("guardSupport", V::Bool(true)), + ("canParticipateInAtomicGuard", V::Bool(true)), + ( + "costTemplate", + get(implementation, "costObligation").clone(), + ), + ( + "footprintTemplate", + get(implementation, "footprintObligation").clone(), + ), + ("effectFailures", failures), + ]), + )]), + ), + ]) + }); + let mut target_profile = captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/profile.hex" + )); + let mut intrinsics_ref = get(&target_profile, "intrinsics").clone(); + set( + &mut intrinsics_ref, + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("echo.dpo.intrinsics/v1", &catalog), + ]), + ); + set(&mut target_profile, "intrinsics", intrinsics_ref); + let pin = |domain, value: &V| V::Array(vec![text("sha256"), artifact_identity(domain, value)]); + let mut reference = get(&lawpack, "exports").clone(); + set( + &mut reference, + "digest", + pin("edict.lawpack-exports/v1", &exports), + ); + set(&mut lawpack, "exports", reference); + let mut adapters = get(&lawpack, "targetAdapters").clone(); + let V::Array(entries) = &mut adapters else { + panic!("adapters") + }; + set( + &mut target_profile, + "targetIr", + get(&entries[0], "acceptedTargetIr").clone(), + ); + let mut target_profile_ref = get(&entries[0], "acceptedTargetProfile").clone(); + set( + &mut target_profile_ref, + "digest", + pin("edict.target-profile/v1", &target_profile), + ); + set(&mut entries[0], "acceptedTargetProfile", target_profile_ref); + let mut reference = get(&entries[0], "adapter").clone(); + set( + &mut reference, + "digest", + pin("edict.lawpack-adapter/v1", &adapter), + ); + set(&mut entries[0], "adapter", reference); + set(&mut lawpack, "targetAdapters", adapters); + let original_source = captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/04-source.hex" + )); + let V::Bytes(source) = original_source else { + panic!("source") + }; + let source = String::from_utf8(source).expect("utf8"); + let digest = echo_edict_canonical::digest_canonical_value_v1("edict.lawpack/v1", &lawpack) + .expect("digest"); + let source = source.replace( + "sha256:2ea164293719d2b03231c4904eb75ddcdf55fd29cc31f4fc5a5319d9ae5e60e7", + &digest, + ); + map([ + ( + "source_artifact", + V::Bytes(encoded(&V::Bytes(source.into_bytes()))), + ), + ("lawpack_artifact", V::Bytes(encoded(&lawpack))), + ("adapter_artifact", V::Bytes(encoded(&adapter))), + ( + "target_profile_artifact", + V::Bytes(encoded(&target_profile)), + ), + ("intrinsics_artifact", V::Bytes(encoded(&catalog))), + ]) +} +pub fn compiler_program() -> V { + let core = compiled_core(); + compiled_bundle( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + ) +} +pub fn compiled_artifact_identity(p: &V, key: &str, domain: &str) -> V { + let V::Bytes(bytes) = get(p, "compiled_program") else { + return id(2); + }; + let Ok(bundle) = echo_edict_canonical::decode_canonical_cbor_v1(bytes) else { + return id(2); + }; + let V::Map(fields) = bundle else { return id(2) }; + let Some((_, V::Bytes(bytes))) = fields.iter().find(|(name, _)| name == &text(key)) else { + return id(2); + }; + echo_edict_canonical::decode_canonical_cbor_v1(bytes) + .map_or_else(|_| id(2), |value| artifact_identity(domain, &value)) +} +pub fn core_identity(p: &V) -> V { + compiled_artifact_identity(p, "core_artifact", "edict.core.module/v1") +} +pub fn target_identity(p: &V) -> V { + compiled_artifact_identity(p, "target_ir_artifact", "edict.target-ir.artifact/v1") +} +pub fn exports_identity(p: &V) -> V { + compiled_artifact_identity(p, "lawpack_exports_artifact", "edict.lawpack-exports/v1") +} +pub fn program() -> V { + map([ + ("schema", text("echo.operation-program/v1")), + ("kind", text("edict-effect-program/v1")), + ( + "interpreter_profile_identity", + profile("echo.operation-interpreter.edict-effect/v1"), + ), + ( + "intrinsic_profile_identity", + profile("echo.operation-intrinsics.edict-effect/v1"), + ), + ("compiled_program", V::Bytes(encoded(&compiler_program()))), + ( + "target_ir_identity", + artifact_identity( + "edict.target-ir.artifact/v1", + &compiled_target(&compiled_core()), + ), + ), + ("identity_law", V::Integer(1)), + ("graph_schema", V::Bytes(encoded(&graph()))), + ("footprint_law", V::Bytes(encoded(&law()))), + ("max_input_bytes", V::Integer(65536)), + ("max_result_bytes", V::Integer(65536)), + ("max_allocated_bytes", V::Integer(16 * 1024 * 1024)), + ("output_type", text("app.Result")), + ]) +} +pub fn package(program: &V) -> V { + map([ + ("schema", text("echo.operation-package/v1")), + ("operation_coordinate", text("app.update@1")), + ("authority_profile_identity", id(8)), + ( + "semantic_closure", + map([ + ("application_schema_coordinate", text("app.Schema")), + ("application_schema_identity", exports_identity(program)), + ("canonical_meaning_identity", id(3)), + ("core_identity", core_identity(program)), + ("edict_source_identity", id(1)), + ("target_ir_identity", target_identity(program)), + ]), + ), + ("program", V::Bytes(encoded(program))), + ( + "budget_ceiling", + map([ + ("steps", V::Integer(1)), + ("read_bytes", V::Integer(0)), + ("write_bytes", V::Integer(0)), + ]), + ), + ( + "target_profile_identity", + profile("echo.operation-target.edict-effect/v1"), + ), + ( + "application_basis_schema_identity", + profile("echo.operation.basis.edict-effect/v1"), + ), + ( + "input_schema_identity", + profile("echo.operation.input.edict-effect/v1"), + ), + ( + "result_schema_identity", + profile("echo.operation.result.edict-effect/v1"), + ), + ( + "obstruction_schema_identity", + profile("echo.operation.obstruction.edict-effect/v1"), + ), + ( + "result_interpretation_identity", + profile("echo.operation.result-interpretation.edict-effect/v1"), + ), + ( + "obstruction_interpretation_identity", + profile("echo.operation.obstruction-interpretation.edict-effect/v1"), + ), + ( + "evaluation_basis_schema_identity", + profile("echo.operation.evaluation-basis/v1"), + ), + ( + "interpreter_profile_identity", + profile("echo.operation-interpreter.edict-effect/v1"), + ), + ( + "intrinsic_profile_identity", + profile("echo.operation-intrinsics.edict-effect/v1"), + ), + ("footprint_contract_identity", footprint(&law())), + ]) +} +pub fn set(v: &mut V, k: &str, x: V) { + let V::Map(m) = v else { panic!("map") }; + m.iter_mut() + .find(|(key, _)| key == &text(k)) + .expect("field") + .1 = x; +} +pub fn add(v: &mut V, k: &str, x: V) { + let V::Map(m) = v else { panic!("map") }; + m.push((text(k), x)); +} +pub fn prepare(v: &V) -> Result { + let b = encoded(v); + let expected = hash(b"echo:operation-package:v1\0", &b); + ExecutableOperationPackage::prepare(&b, expected) +} +pub fn refused(v: &V) { + assert!( + prepare(v).is_err(), + "must refuse malformed or inconsistent package" + ); +} + +/// The read's catalog identifiers and the runtime tags Echo records for them, +/// written out independently of Echo's own table. +pub const READ_FAILURE_NAMES: [(&str, &str); 4] = [ + ("echo.atom-missing/v1", "atomMissing"), + ("echo.atom-required/v1", "atomRequired"), + ("echo.atom-type-mismatch/v1", "atomTypeMismatch"), + ("echo.atom-byte-bound-exceeded/v1", "atomByteBoundExceeded"), +]; +fn catalog_name(tag: &V) -> V { + let V::Text(tag) = tag else { panic!("tag") }; + let (_, name) = READ_FAILURE_NAMES + .iter() + .find(|(runtime, _)| runtime == tag) + .expect("a captured runtime tag"); + text(name) +} +/// The captured bounded-read adapter, whose mappings target runtime tags. +pub fn captured_adapter() -> V { + echo_edict_canonical::decode_canonical_cbor_v1( + &hex::decode( + include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/01-lawpack-adapter.hex" + ) + .trim(), + ) + .expect("captured bytes"), + ) + .expect("captured value") +} +/// The adapter an author writes for the authored route: Jim's own failure +/// names map to the read's catalog identifiers. Edict copies mapping targets +/// verbatim, so this is the only change from the captured adapter. +pub fn catalog_named_adapter(adapter: &V) -> V { + let mut adapter = adapter.clone(); + let implementation = value_field_mut( + value_field_mut(&mut adapter, "effectImplementations"), + "jedit.text@1.readFact", + ); + let V::Map(mappings) = value_field_mut(implementation, "failureMappings") else { + panic!("mappings") + }; + for (_, target) in mappings.iter_mut() { + *target = catalog_name(target); + } + adapter +} +/// The Target IR Edict emits for `catalog_named_adapter`: its read step's arms +/// and declared failures are keyed by the mapping targets. +pub fn catalog_named_target(target: &mut V) { + let V::Map(intents) = value_field_mut(target, "intents") else { + panic!("intents") + }; + for (_, intent) in intents.iter_mut() { + let V::Array(steps) = value_field_mut(intent, "steps") else { + panic!("steps") + }; + for step in steps { + let V::Map(fields) = step else { panic!("step") }; + for (key, value) in fields.iter_mut() { + match (key, value) { + (V::Text(key), V::Map(arms)) if key == "obstructionArms" => { + for (tag, _) in arms.iter_mut() { + *tag = catalog_name(tag); + } + } + (V::Text(key), V::Array(failures)) if key == "obstructionFailures" => { + for tag in failures.iter_mut() { + *tag = catalog_name(tag); + } + } + _ => {} + } + } + } + } +} +pub fn value_field_mut<'a>(value: &'a mut V, key: &str) -> &'a mut V { + let V::Map(fields) = value else { panic!("map") }; + &mut fields + .iter_mut() + .find(|(name, _)| name == &text(key)) + .unwrap_or_else(|| panic!("field {key}")) + .1 +} diff --git a/crates/warp-core/tests/current_operation_publication_tests.rs b/crates/warp-core/tests/current_operation_publication_tests.rs new file mode 100644 index 000000000..5d2d750c0 --- /dev/null +++ b/crates/warp-core/tests/current_operation_publication_tests.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! One current operation package and program codec for every program kind. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used, clippy::panic)] +#[path = "common/edict_effect_publication.rs"] +mod fixture; +use fixture::*; +use warp_core::{ExecutableOperationPackage, TypeId}; +#[test] +fn current_package_codec_prepares_effects_without_a_parallel_package_definition() { + let mut program = program(); + set(&mut program, "schema", text("echo.operation-program/v1")); + let mut publication = package(&program); + set( + &mut publication, + "schema", + text("echo.operation-package/v1"), + ); + let bytes = encoded(&publication); + let prepared = + ExecutableOperationPackage::prepare(&bytes, hash(b"echo:operation-package:v1\0", &bytes)) + .expect("one current decoder accepts the effect program"); + assert_eq!(prepared.to_canonical_bytes().expect("canonical"), bytes); + let effect = prepared + .program() + .as_effect_program() + .expect("effect program kind"); + assert!(effect + .graph_schema() + .allows_atom(TypeId([1; 32]), TypeId([2; 32]))); + assert_eq!(prepared.lawpack_identity(), None); +} + +#[test] +fn current_package_uses_program_kind_to_require_or_forbid_a_real_lawpack() { + use echo_edict_canonical::{decode_canonical_cbor_v1 as decode, CanonicalValueV1 as V}; + use warp_core::{EchoOperationBudgetV1, EchoOperationProgram, EchoOperationSemanticClosure}; + let fixed = ExecutableOperationPackage::new( + "app.fixed@1", + "app.Refusal", + EchoOperationSemanticClosure::new( + [1; 32], + [2; 32], + [3; 32], + [4; 32], + "app.Schema", + [5; 32], + "app.Law", + [6; 32], + ), + warp_core::echo_operation_target_profile_identity_v1(), + [8; 32], + EchoOperationBudgetV1::new(4, 64, 32), + EchoOperationProgram::anchored_node_attachment_compare_and_set( + TypeId([1; 32]), + TypeId([2; 32]), + 16, + ), + ); + let fixed_bytes = fixed.to_canonical_bytes().expect("fixed publication"); + assert!(ExecutableOperationPackage::prepare( + &fixed_bytes, + hash(b"echo:operation-package:v1\0", &fixed_bytes) + ) + .is_ok()); + let mut fixed_value = decode(&fixed_bytes).expect("canonical fixed publication"); + let V::Map(fields) = &mut fixed_value else { + panic!("map") + }; + let (_, V::Map(closure)) = fields + .iter_mut() + .find(|(k, _)| k == &text("semantic_closure")) + .expect("closure") + else { + panic!("closure map") + }; + closure + .retain(|(key, _)| key != &text("lawpack_coordinate") && key != &text("lawpack_identity")); + refused(&fixed_value); + + let mut effect = package(&program()); + let V::Map(fields) = &mut effect else { + panic!("map") + }; + let (_, closure) = fields + .iter_mut() + .find(|(k, _)| k == &text("semantic_closure")) + .expect("closure"); + add(closure, "lawpack_coordinate", text("app.Law")); + add(closure, "lawpack_identity", id(6)); + refused(&effect); +} + +// The single obstruction header belongs to fixed-profile packages, which main +// already publishes; their bytes and identities are unchanged. An authored +// effect program declares every obstruction in its embedded exports instead, +// so its package carries no header. +#[test] +fn current_package_uses_program_kind_to_require_or_forbid_the_obstruction_header() { + use echo_edict_canonical::{decode_canonical_cbor_v1 as decode, CanonicalValueV1 as V}; + use warp_core::{EchoOperationBudgetV1, EchoOperationProgram, EchoOperationSemanticClosure}; + let fixed = ExecutableOperationPackage::new( + "app.fixed@1", + "app.Refusal", + EchoOperationSemanticClosure::new( + [1; 32], + [2; 32], + [3; 32], + [4; 32], + "app.Schema", + [5; 32], + "app.Law", + [6; 32], + ), + warp_core::echo_operation_target_profile_identity_v1(), + [8; 32], + EchoOperationBudgetV1::new(4, 64, 32), + EchoOperationProgram::anchored_node_attachment_compare_and_set( + TypeId([1; 32]), + TypeId([2; 32]), + 16, + ), + ); + assert_eq!(fixed.obstruction_coordinate(), Some("app.Refusal")); + let fixed_bytes = fixed.to_canonical_bytes().expect("fixed publication"); + let mut fixed_value = decode(&fixed_bytes).expect("canonical fixed publication"); + let V::Map(fields) = &mut fixed_value else { + panic!("map") + }; + assert!(fields + .iter() + .any(|(key, _)| key == &text("obstruction_coordinate"))); + fields.retain(|(key, _)| key != &text("obstruction_coordinate")); + refused(&fixed_value); + + let effect = package(&program()); + let prepared = prepare(&effect).expect("an effect package has no header"); + assert_eq!(prepared.obstruction_coordinate(), None); + let mut carrying = effect; + add(&mut carrying, "obstruction_coordinate", text("app.Refusal")); + refused(&carrying); + let mut empty = package(&program()); + add(&mut empty, "obstruction_coordinate", text("")); + refused(&empty); +} + +#[test] +fn historical_package_and_program_versions_are_refused_even_with_matching_pins() { + let mut value = package(&program()); + set(&mut value, "schema", text("echo.operation-package/v2")); + refused(&value); + let mut old_program = program(); + set( + &mut old_program, + "schema", + text("echo.operation-program/v2"), + ); + refused(&package(&old_program)); +} diff --git a/crates/warp-core/tests/edict_effect_admission_tests.rs b/crates/warp-core/tests/edict_effect_admission_tests.rs new file mode 100644 index 000000000..c3c12d3c4 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owner V2 package admission under independent policy. +#![cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] +#![allow(clippy::expect_used, clippy::panic)] +#[path = "common/edict_effect_publication.rs"] +mod fixture; +use fixture::*; +use warp_core::EchoOperationAdmissionErrorKind; +use warp_core::EchoOperationAdmissionPolicy; +use warp_core::{ + make_node_id, make_type_id, EchoOperationBudgetV1, EngineBuilder, GraphStore, NodeRecord, + TrustedRuntimeHost, WorldlineRuntime, +}; +fn host() -> TrustedRuntimeHost { + let mut graph = GraphStore::default(); + let root = make_node_id("effect-admission-root"); + graph.insert_node( + root, + NodeRecord { + ty: make_type_id("effect-admission-node"), + }, + ); + let engine = EngineBuilder::new(graph, root).build(); + TrustedRuntimeHost::new(WorldlineRuntime::new(), engine).expect("runtime owner") +} +fn policy( + bytes: &[u8], + operation: &str, + authority: [u8; 32], + budget: EchoOperationBudgetV1, +) -> EchoOperationAdmissionPolicy { + EchoOperationAdmissionPolicy::exact( + hash(b"echo:operation-package:v1\0", bytes), + operation, + authority, + budget, + ) +} +#[test] +fn runtime_owner_admits_exact_publication_and_retains_separate_policy_evidence() { + let bytes = encoded(&package(&program())); + let policy = policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + let host = host(); + let admitted = host + .admit_echo_operation_package(&policy, bytes.clone()) + .expect("exact policy admits"); + assert_eq!(admitted.canonical_package_bytes(), bytes); + assert_eq!( + admitted.package_identity(), + hash(b"echo:operation-package:v1\0", &bytes) + ); + let mut oracle = blake3::Hasher::new(); + oracle.update(b"echo:operation-admission-policy:v1\0"); + oracle.update(&hash(b"echo:operation-package:v1\0", &bytes)); + oracle.update(&12u64.to_le_bytes()); + oracle.update(b"app.update@1"); + oracle.update(&[8; 32]); + oracle.update(&1u64.to_le_bytes()); + oracle.update(&0u64.to_le_bytes()); + oracle.update(&0u64.to_le_bytes()); + assert_eq!( + admitted.admission_policy_identity(), + *oracle.finalize().as_bytes() + ); + let mut id = blake3::Hasher::new(); + id.update(b"echo:operation-package-admission:v1\0"); + id.update(&hash(b"echo:operation-package:v1\0", &bytes)); + id.update(oracle.finalize().as_bytes()); + assert_eq!(admitted.identity(), *id.finalize().as_bytes()); +} +#[test] +fn runtime_policy_refuses_operation_authority_and_budget_substitution() { + let bytes = encoded(&package(&program())); + let host = host(); + for (operation, authority, budget, want) in [ + ( + "app.other@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + EchoOperationAdmissionErrorKind::OperationCoordinateMismatch, + ), + ( + "app.update@1", + [9; 32], + EchoOperationBudgetV1::new(1, 0, 0), + EchoOperationAdmissionErrorKind::AuthorityProfileMismatch, + ), + ( + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(0, 0, 0), + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy, + ), + ] { + assert_eq!( + host.admit_echo_operation_package( + &policy(&bytes, operation, authority, budget), + bytes.clone() + ) + .expect_err("independent policy refuses") + .kind(), + want + ); + } +} +#[test] +fn pin_identity_does_not_substitute_for_supported_artifact_validation() { + let host = host(); + let mut value = package(&program()); + set(&mut value, "schema", text("echo.operation-package/v2")); + let bytes = encoded(&value); + assert_eq!( + host.admit_echo_operation_package( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0) + ), + bytes + ) + .expect_err("pinned but unsupported") + .kind(), + EchoOperationAdmissionErrorKind::ArtifactInvalid + ); + let bytes = encoded(&package(&program())); + let pin = policy( + b"wrong", + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + assert_eq!( + host.admit_echo_operation_package(&pin, bytes) + .expect_err("wrong pin") + .kind(), + EchoOperationAdmissionErrorKind::PackageIdentityMismatch + ); +} +#[test] +fn each_package_budget_axis_is_subject_to_the_independent_policy() { + let host = host(); + for (steps, read, write) in [(2, 0, 0), (1, 1, 0), (1, 0, 1)] { + let mut value = package(&program()); + set( + &mut value, + "budget_ceiling", + map([ + ( + "steps", + echo_edict_canonical::CanonicalValueV1::Integer(steps), + ), + ( + "read_bytes", + echo_edict_canonical::CanonicalValueV1::Integer(read), + ), + ( + "write_bytes", + echo_edict_canonical::CanonicalValueV1::Integer(write), + ), + ]), + ); + let bytes = encoded(&value); + assert_eq!( + host.admit_echo_operation_package( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0) + ), + bytes + ) + .expect_err("ceiling exceeds policy") + .kind(), + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy + ); + } +} +#[test] +fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() { + let bytes = encoded(&package(&program())); + let host = host(); + let narrow = policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + let wide = policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(10, 20, 30), + ); + let first = host + .admit_echo_operation_package(&narrow, bytes.clone()) + .expect("narrow policy"); + let second = host + .admit_echo_operation_package(&wide, bytes) + .expect("wide policy"); + assert_eq!(first.package(), second.package()); + assert_eq!(first.admission_policy(), &narrow); + assert_eq!(second.admission_policy(), &wide); + assert_ne!( + first.admission_policy_identity(), + second.admission_policy_identity() + ); + assert_ne!(first.identity(), second.identity()); + let mut wrong_domain = blake3::Hasher::new(); + wrong_domain.update(b"echo:operation-package-admission:v2\0"); + wrong_domain.update(&first.package_identity()); + wrong_domain.update(&first.admission_policy_identity()); + assert_ne!(first.identity(), *wrong_domain.finalize().as_bytes()); +} +#[path = "edict_effect_admission_tests/installation.rs"] +mod installation; + +#[test] +fn one_admitted_token_binds_fixed_and_effect_programs_under_the_same_recipe() { + use warp_core::{ + AdmittedExecutableOperationPackage, EchoOperationProgram, EchoOperationSemanticClosure, + ExecutableOperationPackage, TypeId, + }; + let fixed = ExecutableOperationPackage::new( + "app.update@1", + "app.Refusal", + EchoOperationSemanticClosure::new( + [1; 32], + [2; 32], + [3; 32], + [4; 32], + "app.Schema", + [5; 32], + "app.Law", + [6; 32], + ), + warp_core::echo_operation_target_profile_identity_v1(), + [8; 32], + EchoOperationBudgetV1::new(4, 64, 32), + EchoOperationProgram::anchored_node_attachment_compare_and_set( + TypeId([1; 32]), + TypeId([2; 32]), + 16, + ), + ) + .to_canonical_bytes() + .expect("fixed publication"); + let owner = host(); + for (bytes, budget) in [ + (fixed, EchoOperationBudgetV1::new(4, 64, 32)), + ( + encoded(&package(&program())), + EchoOperationBudgetV1::new(1, 0, 0), + ), + ] { + let policy = policy(&bytes, "app.update@1", [8; 32], budget); + let token: AdmittedExecutableOperationPackage = owner + .admit_echo_operation_package(&policy, bytes.clone()) + .expect("one owner crossing admits each program kind"); + let package_id = hash(b"echo:operation-package:v1\0", &bytes); + let mut oracle = blake3::Hasher::new(); + oracle.update(b"echo:operation-package-admission:v1\0"); + oracle.update(&package_id); + oracle.update(&policy.identity()); + assert_eq!(token.identity(), *oracle.finalize().as_bytes()); + assert_eq!(token.package_identity(), package_id); + assert_eq!(token.canonical_package_bytes(), bytes); + assert_eq!(token.admission_policy(), &policy); + assert_eq!(token.admission_policy_identity(), policy.identity()); + assert_eq!( + token.program_identity(), + token + .package() + .program() + .identity() + .expect("program identity") + .as_hash() + ); + } +} diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs new file mode 100644 index 000000000..fe9826147 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -0,0 +1,520 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Owner installation, exact retained evidence and fresh-host WAL recovery. +use super::*; +use echo_edict_canonical::{decode_canonical_cbor_v1 as decode, CanonicalValueV1 as V}; +use warp_core::InstalledEchoOperation; +use warp_core::TrustedRuntimeWalConfig; +struct WalDir(std::path::PathBuf); +impl WalDir { + fn new() -> Self { + let root = std::path::PathBuf::from("target").join("warp-core-test-tmp"); + std::fs::create_dir_all(&root).expect("owned fixture root"); + for ordinal in 0..1024 { + let path = root.join(format!("echo-current-install-{ordinal}")); + match std::fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => panic!("could not create owned WAL fixture: {error}"), + } + } + panic!("exhausted owned WAL fixture names"); + } +} +impl Drop for WalDir { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.0).expect("remove owned WAL fixture"); + } +} +fn admitted(host: &TrustedRuntimeHost) -> warp_core::AdmittedExecutableOperationPackage { + let bytes = encoded(&package(&program())); + host.admit_echo_operation_package( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ), + bytes, + ) + .expect("admit") +} +#[test] +fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact_evidence() { + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("activate empty WAL"); + let token = admitted(&owner); + let installed = owner + .install_admitted_echo_operation_package(token.clone()) + .expect("install"); + let mut identity_oracle = blake3::Hasher::new(); + identity_oracle.update(b"echo:operation-installation:v1\0"); + identity_oracle.update(&token.package_identity()); + identity_oracle.update(&token.identity()); + identity_oracle.update(&12u64.to_le_bytes()); + identity_oracle.update(b"app.update@1"); + identity_oracle.update(&token.program_identity()); + identity_oracle.update(&(token.canonical_package_bytes().len() as u64).to_le_bytes()); + identity_oracle.update(token.canonical_package_bytes()); + assert_eq!(installed.identity(), *identity_oracle.finalize().as_bytes()); + let first_frames = owner.runtime_wal().expect("wal").frames().len(); + assert_eq!( + owner + .engine() + .installed_echo_operation_package((installed.package_identity()).into()), + Some(&installed) + ); + assert_eq!( + owner + .install_admitted_echo_operation_package(token) + .expect("idempotent"), + installed + ); + assert_eq!( + owner.runtime_wal().expect("wal").frames().len(), + first_frames + ); + let bytes = installed.retained_bytes().expect("retained"); + assert_eq!( + InstalledEchoOperation::from_retained_bytes(&bytes).expect("reconstruct"), + installed + ); + let recovered = owner + .runtime_wal() + .expect("wal") + .recover_read_only() + .expect("recover"); + assert_eq!(recovered.installed_echo_operations, vec![installed.clone()]); + drop(owner); + let mut fresh = host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("fresh engine recovery"); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package((installed.package_identity()).into()), + Some(&installed) + ); +} +#[test] +fn retained_installation_refuses_substituted_package_policy_and_evidence_ids() { + let mut owner = host(); + let token = admitted(&owner); + let installed = owner + .install_admitted_echo_operation_package(token) + .expect("install"); + let bytes = installed.retained_bytes().expect("bytes"); + for key in [ + "package_id", + "admission_policy_id", + "package_admission_id", + "installed_operation_id", + ] { + let mut value = decode(&bytes).expect("canonical"); + set(&mut value, key, id(99)); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); + } + for schema in [ + "echo.operation-installation/v2", + "echo.operation-installation/v3", + ] { + let mut value = decode(&bytes).expect("canonical"); + set(&mut value, "schema", text(schema)); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); + } + let mut value = decode(&bytes).expect("canonical"); + set( + &mut value, + "package_bytes", + V::Bytes(encoded(&V::Integer(0))), + ); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); + let mut value = decode(&bytes).expect("canonical"); + add(&mut value, "extra", V::Integer(0)); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); + let mut value = decode(&bytes).expect("canonical"); + let V::Map(fields) = &mut value else { + panic!("map") + }; + let policy = &mut fields + .iter_mut() + .find(|(k, _)| k == &text("admission_policy")) + .expect("policy") + .1; + set(policy, "expected_authority_profile_identity", id(99)); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); +} +#[test] +fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_append_wal() { + let mut owner = host(); + owner.enable_in_memory_runtime_wal().expect("wal"); + let installed = owner + .install_admitted_echo_operation_package(admitted(&owner)) + .expect("install"); + let before = owner.runtime_wal().expect("wal").frames().len(); + let mut p = program(); + set(&mut p, "max_input_bytes", V::Integer(65535)); + let bytes = encoded(&package(&p)); + let token = owner + .admit_echo_operation_package( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ), + bytes, + ) + .expect("new pinned input contract"); + assert!(owner + .install_admitted_echo_operation_package(token) + .is_err()); + let bytes = encoded(&package(&program())); + let token = owner + .admit_echo_operation_package( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(2, 0, 0), + ), + bytes, + ) + .expect("new policy"); + assert!(owner + .install_admitted_echo_operation_package(token) + .is_err()); + assert_eq!( + owner + .engine() + .installed_echo_operation_package((installed.package_identity()).into()), + Some(&installed) + ); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); +} +#[test] +fn activating_empty_wal_cannot_attest_an_earlier_volatile_installation() { + let mut owner = host(); + owner + .install_admitted_echo_operation_package(admitted(&owner)) + .expect("volatile install"); + assert!(owner.enable_in_memory_runtime_wal().is_err()); +} +fn install_fixed_program( + owner: &mut TrustedRuntimeHost, + coordinate: &str, +) -> Result { + use warp_core::{ + EchoOperationAdmissionPolicy, EchoOperationProgram, EchoOperationSemanticClosure, + ExecutableOperationPackage, + }; + let budget = EchoOperationBudgetV1::new(16, 4096, 4096); + let legacy = ExecutableOperationPackage::new( + coordinate, + "app.LegacyRefusal", + EchoOperationSemanticClosure::new( + [1; 32], + [2; 32], + [3; 32], + [4; 32], + "app.Schema", + [5; 32], + "app.Lawpack", + [6; 32], + ), + warp_core::echo_operation_target_profile_identity_v1(), + [8; 32], + budget, + EchoOperationProgram::anchored_node_attachment_compare_and_set( + make_type_id("legacy-node"), + make_type_id("legacy-atom"), + 1024, + ), + ); + let bytes = legacy.to_canonical_bytes().expect("V1 fixture"); + let policy = EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&bytes).as_hash(), + coordinate, + [8; 32], + budget, + ); + let admitted = owner + .admit_echo_operation_package(&policy, bytes) + .expect("V1 policy"); + owner.install_admitted_echo_operation_package(admitted) +} +#[test] +fn program_kinds_share_coordinate_exclusion_in_both_installation_orders() { + for effect_first in [false, true] { + let mut owner = host(); + owner.enable_in_memory_runtime_wal().expect("wal"); + if effect_first { + owner + .install_admitted_echo_operation_package(admitted(&owner)) + .expect("V2 first"); + let before = owner.runtime_wal().expect("wal").frames().len(); + assert!(install_fixed_program(&mut owner, "app.update@1").is_err()); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); + } else { + install_fixed_program(&mut owner, "app.update@1").expect("V1 first"); + let before = owner.runtime_wal().expect("wal").frames().len(); + assert!(owner + .install_admitted_echo_operation_package(admitted(&owner)) + .is_err()); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); + } + } +} +#[test] +fn one_catalog_recovers_program_kinds_and_certificate_binds_installations() { + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("wal"); + let legacy = install_fixed_program(&mut owner, "app.legacy@1").expect("V1"); + let effect = owner + .install_admitted_echo_operation_package(admitted(&owner)) + .expect("V2"); + let mut recovered = owner + .runtime_wal() + .expect("wal") + .recover_read_only() + .expect("mixed recovery"); + assert_eq!(recovered.installed_echo_operations.len(), 2); + assert!(recovered.installed_echo_operations.contains(&legacy)); + assert!(recovered.installed_echo_operations.contains(&effect)); + assert_eq!( + recovered.recomputed_indexes_root().expect("root"), + recovered.certificate.recovered_indexes_root + ); + recovered + .installed_echo_operations + .retain(|installed| installed.package_id() != effect.package_id()); + assert_ne!( + recovered.recomputed_indexes_root().expect("altered root"), + recovered.certificate.recovered_indexes_root + ); + drop(owner); + let mut fresh = host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("mixed fresh recovery"); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package(legacy.package_id()), + Some(&legacy) + ); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package((effect.package_identity()).into()), + Some(&effect) + ); +} +#[cfg(feature = "host_test")] +#[test] +fn failed_wal_append_and_commit_do_not_publish_registry_entries() { + use warp_core::causal_wal::{FilesystemWalFaultPlan, FilesystemWalFaultTarget}; + for target in [ + FilesystemWalFaultTarget::AppendFrame, + FilesystemWalFaultTarget::FlushCommit, + ] { + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("wal"); + let token = admitted(&owner); + let pin = token.package_identity(); + owner + .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next(target)) + .expect("fault"); + assert!(owner + .install_admitted_echo_operation_package(token) + .is_err()); + assert!(owner + .engine() + .installed_echo_operation_package((pin).into()) + .is_none()); + drop(owner); + let mut fresh = host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("recover incomplete transaction"); + assert!(fresh + .engine() + .installed_echo_operation_package((pin).into()) + .is_none()); + } +} +#[test] +fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission() { + let mut owner = host(); + let token = admitted(&owner); + let installed = owner + .install_admitted_echo_operation_package(token.clone()) + .expect("install"); + let mut value = decode(&installed.retained_bytes().expect("bytes")).expect("value"); + let V::Map(fields) = &mut value else { + panic!("map") + }; + set( + &mut fields + .iter_mut() + .find(|(k, _)| k == &text("admission_policy")) + .expect("policy") + .1, + "expected_authority_profile_identity", + id(99), + ); + let pin = token.package_identity(); + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-admission-policy:v1\0"); + h.update(&pin); + h.update(&12u64.to_le_bytes()); + h.update(b"app.update@1"); + h.update(&[99; 32]); + h.update(&1u64.to_le_bytes()); + h.update(&0u64.to_le_bytes()); + h.update(&0u64.to_le_bytes()); + let policy_id = *h.finalize().as_bytes(); + set( + &mut value, + "admission_policy_id", + V::Bytes(policy_id.to_vec()), + ); + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-package-admission:v1\0"); + h.update(&pin); + h.update(&policy_id); + let admission_id = *h.finalize().as_bytes(); + set( + &mut value, + "package_admission_id", + V::Bytes(admission_id.to_vec()), + ); + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-installation:v1\0"); + h.update(&pin); + h.update(&admission_id); + h.update(&12u64.to_le_bytes()); + h.update(b"app.update@1"); + h.update(&token.program_identity()); + h.update(&(token.canonical_package_bytes().len() as u64).to_le_bytes()); + h.update(token.canonical_package_bytes()); + set( + &mut value, + "installed_operation_id", + V::Bytes(h.finalize().as_bytes().to_vec()), + ); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); +} +#[cfg(feature = "host_test")] +#[test] +fn readable_unsynced_installation_is_not_published_until_a_fresh_store_reopens() { + use warp_core::causal_wal::{FilesystemWalFaultPlan, FilesystemWalFaultTarget}; + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("wal"); + let token = admitted(&owner); + let pin = token.package_identity(); + let program = token.package().program().clone(); + owner + .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next( + FilesystemWalFaultTarget::CommitMarkerSync, + )) + .expect("sync fault"); + assert!(owner + .install_admitted_echo_operation_package(token) + .is_err()); + assert!(owner + .engine() + .installed_echo_operation_package((pin).into()) + .is_none()); + assert!( + owner + .runtime_wal() + .expect("wal") + .recover_read_only() + .is_err(), + "readable bytes alone cannot attest durability" + ); + drop(owner); + let mut fresh = host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("fresh store establishes durable recovered history"); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package((pin).into()) + .expect("recovered installation") + .program(), + &program + ); +} + +#[test] +fn one_installation_type_catalog_and_retained_decoder_cover_all_program_kinds() { + use warp_core::InstalledEchoOperation; + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("WAL"); + let fixed = install_fixed_program(&mut owner, "app.fixed@1").expect("fixed install"); + let effect: InstalledEchoOperation = owner + .install_admitted_echo_operation_package(admitted(&owner)) + .expect("effect install"); + assert_eq!(effect.lawpack_identity(), None); + assert_eq!(fixed.lawpack_identity(), Some([6; 32])); + for installed in [&fixed, &effect] { + assert_eq!( + owner + .engine() + .installed_echo_operation_package(installed.package_id()), + Some(installed) + ); + let bytes = installed.retained_bytes().expect("record"); + assert_eq!( + InstalledEchoOperation::from_retained_bytes(&bytes).expect("current record"), + *installed + ); + let mut historical = decode(&bytes).expect("record value"); + set( + &mut historical, + "schema", + text("echo.operation-installation/v2"), + ); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&historical)).is_err()); + } + let recovered = owner + .runtime_wal() + .expect("WAL") + .recover_read_only() + .expect("recovery"); + assert_eq!(recovered.installed_echo_operations.len(), 2); + assert!(recovered.installed_echo_operations.contains(&fixed)); + assert!(recovered.installed_echo_operations.contains(&effect)); + drop(owner); + let mut fresh = host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("fresh recovery"); + for installed in [&fixed, &effect] { + assert_eq!( + fresh + .engine() + .installed_echo_operation_package(installed.package_id()), + Some(installed) + ); + } +} diff --git a/crates/warp-core/tests/edict_effect_frame_tests.rs b/crates/warp-core/tests/edict_effect_frame_tests.rs new file mode 100644 index 000000000..bf55a34b2 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_frame_tests.rs @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! One resource envelope across real compiled expressions and graph reads. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used)] +// Coherent budget mutants test runtime defense, not verifier approval. +#[allow(dead_code, clippy::unwrap_used, clippy::panic)] +#[path = "support/edict_byte_concat.rs"] +mod concat; + +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as Value}; +use warp_core::edict_effects::{ + EffectEvaluationFrame, EffectFrameError, EffectRangeReadError, EffectRangeReadSession, +}; +use warp_core::edict_pure::{evaluate, EvaluationError, EvaluationLimits}; +use warp_core::{ + make_node_id, make_type_id, EchoOperationBudgetV1, GraphStore, NodeKey, NodeRecord, + OutgoingEdgeRange, WorldlineFrontier, WorldlineId, WorldlineState, +}; + +fn package() -> Vec { + hex::decode( + include_str!("fixtures/edict-byte-concat/executable-operation-package.cbor.hex").trim(), + ) + .expect("retained package") +} +fn pin() -> [u8; 32] { + hex::decode("ed480a332a9d5f84f7425e46ab0b08cca886c8a040e048145b88fba1e6e0415f") + .expect("pin") + .try_into() + .expect("digest") +} +fn input() -> Vec { + encode(&Value::Map(vec![ + ( + Value::Text("firstFragment".into()), + Value::Bytes(vec![b'a']), + ), + ( + Value::Text("secondFragment".into()), + Value::Bytes(vec![b'b']), + ), + ])) + .expect("input") +} +fn expected() -> Vec { + encode(&Value::Map(vec![( + Value::Text("bytes".into()), + Value::Bytes(b"ab".to_vec()), + )])) + .expect("literal result") +} +fn limits() -> EvaluationLimits { + EvaluationLimits { + max_package_bytes: 32768, + max_input_bytes: 1024, + max_steps: 10000, + max_allocated_bytes: 16777216, + max_output_bytes: 1024, + } +} +fn fixture() -> (WorldlineFrontier, OutgoingEdgeRange) { + let mut store = GraphStore::default(); + let root = make_node_id("frame/root"); + store.insert_node( + root, + NodeRecord { + ty: make_type_id("node"), + }, + ); + let key = OutgoingEdgeRange { + source: NodeKey { + warp_id: store.warp_id(), + local_id: root, + }, + edge_type: make_type_id("edge"), + }; + let state = WorldlineState::from_root_store(store, root).expect("state"); + ( + WorldlineFrontier::new(WorldlineId::from_bytes([7; 32]), state), + key, + ) +} +fn session(frontier: &WorldlineFrontier, key: OutgoingEdgeRange) -> EffectRangeReadSession<'_> { + EffectRangeReadSession::new( + frontier, + &[key.edge_type], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + ) +} + +#[test] +fn read_then_real_compiler_package_spend_one_allocation_limit() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("standalone control"); + assert_eq!(control.output, expected()); + let (frontier, key) = fixture(); + for (extra, succeeds) in [(255, false), (256, true)] { + let mut frame = EffectEvaluationFrame::new( + session(&frontier, key), + EvaluationLimits { + max_allocated_bytes: control.allocated_bytes + extra, + ..limits() + }, + ) + .expect("frame"); + assert_eq!(frame.read_range(key).expect("read"), Some(Vec::new())); + match frame.evaluate_pure(&package(), pin(), &input()) { + Ok(output) => { + assert!(succeeds); + assert_eq!(output, expected()); + assert_eq!(frame.allocated_bytes(), control.allocated_bytes + 256); + } + Err(error) => { + assert!(!succeeds); + assert_eq!( + error, + EffectFrameError::Evaluation(EvaluationError::AllocationBudgetExceeded) + ); + } + } + assert!(frame.allocated_bytes() <= control.allocated_bytes + extra); + } +} + +#[test] +fn real_compiler_package_then_read_uses_the_same_remaining_storage() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("control"); + let (frontier, key) = fixture(); + let mut frame = EffectEvaluationFrame::new( + session(&frontier, key), + EvaluationLimits { + max_allocated_bytes: control.allocated_bytes + 255, + ..limits() + }, + ) + .expect("frame"); + assert_eq!( + frame + .evaluate_pure(&package(), pin(), &input()) + .expect("pure"), + expected() + ); + assert_eq!( + frame.read_range(key), + Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)) + ); + assert_eq!(frame.allocated_bytes(), control.allocated_bytes + 128); + assert_eq!(frame.readings().len(), 1); + assert_eq!(frame.read_range(key), Err(EffectFrameError::Terminated)); +} + +#[test] +fn expressions_and_reads_share_step_capacity_too() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("control"); + let (frontier, key) = fixture(); + let mut frame = EffectEvaluationFrame::new( + session(&frontier, key), + EvaluationLimits { + max_steps: control.steps, + ..limits() + }, + ) + .expect("frame"); + frame.read_range(key).expect("read"); + assert_eq!( + frame.evaluate_pure(&package(), pin(), &input()), + Err(EffectFrameError::Evaluation( + EvaluationError::StepBudgetExceeded + )) + ); + assert!(frame.consumed().steps() <= control.steps); + assert_eq!(frame.consumed().read_bytes(), 64); + assert_eq!( + frame.evaluate_pure(&package(), pin(), &input()), + Err(EffectFrameError::Terminated) + ); +} + +#[test] +fn another_package_evaluation_cannot_reset_the_frame_budget() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("control"); + let (frontier, key) = fixture(); + let mut frame = EffectEvaluationFrame::new( + session(&frontier, key), + EvaluationLimits { + max_allocated_bytes: 2 * control.allocated_bytes - 1, + ..limits() + }, + ) + .expect("frame"); + assert_eq!( + frame + .evaluate_pure(&package(), pin(), &input()) + .expect("first"), + expected() + ); + assert_eq!( + frame.evaluate_pure(&package(), pin(), &input()), + Err(EffectFrameError::Evaluation( + EvaluationError::AllocationBudgetExceeded + )) + ); + assert!(frame.allocated_bytes() < 2 * control.allocated_bytes); +} + +#[test] +fn a_failed_package_identity_ends_the_frame_before_any_graph_read() { + let (frontier, key) = fixture(); + let mut frame = EffectEvaluationFrame::new(session(&frontier, key), limits()).expect("frame"); + assert_eq!( + frame.evaluate_pure(&package(), [0; 32], &input()), + Err(EffectFrameError::Evaluation( + EvaluationError::PackageIdentityMismatch + )) + ); + assert_eq!(frame.read_range(key), Err(EffectFrameError::Terminated)); + assert_eq!(frame.allocated_bytes(), 0); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(frame.readings().is_empty()); +} + +#[test] +fn preexisting_reads_cannot_be_reintroduced_as_an_unspent_frame() { + let (frontier, key) = fixture(); + let mut reads = session(&frontier, key); + reads.read(key).expect("preexisting read"); + assert!(matches!( + EffectEvaluationFrame::new(reads, limits()), + Err(warp_core::edict_effects::EffectFrameSetupError::ReadSessionAlreadyUsed) + )); +} + +#[test] +fn source_session_ceilings_bind_computation_as_well_as_reads() { + let (frontier, key) = fixture(); + let step_limited = EffectRangeReadSession::new( + &frontier, + &[key.edge_type], + 0, + EchoOperationBudgetV1::new(1, 1024, 0), + ); + let mut frame = EffectEvaluationFrame::new(step_limited, limits()).expect("frame"); + assert_eq!( + frame.evaluate_pure(&package(), pin(), &input()), + Err(EffectFrameError::Evaluation( + EvaluationError::StepBudgetExceeded + )) + ); + assert!(frame.consumed().steps() <= 1); + let storage_limited = EffectRangeReadSession::with_allocation_limit( + &frontier, + &[key.edge_type], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + 127, + ) + .expect("read view"); + let mut frame = EffectEvaluationFrame::new(storage_limited, limits()).expect("frame"); + assert_eq!( + frame.evaluate_pure(&package(), pin(), &input()), + Err(EffectFrameError::Evaluation( + EvaluationError::AllocationBudgetExceeded + )) + ); + assert!(frame.allocated_bytes() <= 127); +} + +#[test] +fn the_fixed_effect_ceilings_are_checked_before_a_frame_starts() { + let (frontier, key) = fixture(); + for budget in [ + EchoOperationBudgetV1::new(1_048_577, 1024, 0), + EchoOperationBudgetV1::new(10000, 67_108_865, 0), + EchoOperationBudgetV1::new(10000, 1024, 16_777_217), + ] { + let reads = EffectRangeReadSession::new(&frontier, &[key.edge_type], 0, budget); + assert!(matches!( + EffectEvaluationFrame::new(reads, limits()), + Err(warp_core::edict_effects::EffectFrameSetupError::ResourceCeilingExceeded) + )); + } + assert!(matches!( + EffectEvaluationFrame::new( + session(&frontier, key), + EvaluationLimits { + max_allocated_bytes: 16_777_217, + ..limits() + } + ), + Err(warp_core::edict_effects::EffectFrameSetupError::ResourceCeilingExceeded) + )); + let exact = EffectRangeReadSession::new( + &frontier, + &[key.edge_type], + 0, + EchoOperationBudgetV1::new(1_048_576, 67_108_864, 16_777_216), + ); + let mut frame = EffectEvaluationFrame::new(exact, limits()).expect("exact ceilings"); + assert_eq!( + frame + .evaluate_pure(&package(), pin(), &input()) + .expect("computation"), + expected() + ); +} + +#[test] +fn each_component_keeps_its_declared_cap_inside_a_larger_frame() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("control"); + let (frontier, key) = fixture(); + for (package_key, core_key, amount, expected_error) in [ + ( + "max_steps", + "maxSteps", + control.steps - 1, + EvaluationError::StepBudgetExceeded, + ), + ( + "max_allocated_bytes", + "maxAllocatedBytes", + control.allocated_bytes - 1, + EvaluationError::AllocationBudgetExceeded, + ), + ] { + let (limited, test_pin) = concat::budget(package_key, core_key, amount); + let mut frame = + EffectEvaluationFrame::new(session(&frontier, key), limits()).expect("frame"); + assert_eq!( + frame.evaluate_pure(&limited, test_pin, &input()), + Err(EffectFrameError::Evaluation(expected_error)) + ); + } +} + +#[test] +fn component_caps_are_relative_and_the_global_cap_is_restored_between_calls() { + let control = evaluate(&package(), pin(), &input(), limits()).expect("control"); + let (exact, test_pin) = concat::budget( + "max_allocated_bytes", + "maxAllocatedBytes", + control.allocated_bytes, + ); + let (frontier, key) = fixture(); + let mut frame = EffectEvaluationFrame::new(session(&frontier, key), limits()).expect("frame"); + for count in 1..=2 { + assert_eq!( + frame + .evaluate_pure(&exact, test_pin, &input()) + .expect("component"), + expected() + ); + assert_eq!(frame.allocated_bytes(), count * control.allocated_bytes); + } +} + +#[test] +fn a_low_level_frame_does_not_claim_a_runtime_parent() { + let (frontier, key) = fixture(); + let frame = + EffectEvaluationFrame::new(session(&frontier, key), limits()).expect("private frame"); + assert_eq!(frame.evaluation_basis(), None); +} + +#[test] +fn recorded_reads_cannot_use_an_unbound_caller_frontier() { + let (frontier, key) = fixture(); + let mut frame = + EffectEvaluationFrame::new(session(&frontier, key), limits()).expect("private frame"); + let package = hex::decode( + include_str!("fixtures/edict-node-read/single-executable-operation-package.hex").trim(), + ) + .expect("compiler read package"); + let pin = hex::decode("a2ae35d39ae74a26da916544464529bc6c7af32a76b6e6f354ef79c5063ab109") + .expect("pin") + .try_into() + .expect("digest"); + let address = Value::Map(vec![ + ( + Value::Text("warpId".into()), + Value::Bytes(key.source.warp_id.0.to_vec()), + ), + ( + Value::Text("nodeId".into()), + Value::Bytes(key.source.local_id.0.to_vec()), + ), + ( + Value::Text("typeId".into()), + Value::Bytes(make_type_id("node").0.to_vec()), + ), + ]); + let input = encode(&Value::Map(vec![ + (Value::Text("address".into()), address), + (Value::Text("expected".into()), Value::Bytes(vec![])), + ])) + .expect("input"); + let read_limits = warp_core::edict_read::ReadLimits { + evaluation: limits(), + max_reads: 1, + max_read_bytes: 1024, + }; + assert_eq!( + frame.evaluate_read(&package, pin, &input, &[key.source], read_limits), + Err(EffectFrameError::UnboundParent) + ); + assert!(frame.readings().is_empty()); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert_eq!(frame.read_range(key), Err(EffectFrameError::Terminated)); +} diff --git a/crates/warp-core/tests/edict_effect_graph_schema_tests.rs b/crates/warp-core/tests/edict_effect_graph_schema_tests.rs new file mode 100644 index 000000000..6efe827fb --- /dev/null +++ b/crates/warp-core/tests/edict_effect_graph_schema_tests.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Executable graph-schema relations for the authored-effect program boundary. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used, clippy::panic)] +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as Value}; +use warp_core::edict_effects::{EffectGraphSchema, EffectGraphSchemaError, EffectNodeAddressing}; +use warp_core::TypeId; +fn map(fields: impl IntoIterator) -> Value { + Value::Map( + fields + .into_iter() + .map(|(key, value)| (Value::Text(key.into()), value)) + .collect(), + ) +} +fn id(byte: u8) -> Value { + Value::Bytes(vec![byte; 32]) +} +fn schema() -> Value { + map([ + ( + "node_types", + Value::Map(vec![ + ( + id(1), + map([ + ("addressing", Value::Integer(0)), + ("atom_types", Value::Array(vec![id(10), id(11)])), + ]), + ), + ( + id(2), + map([ + ("addressing", Value::Integer(1)), + ("atom_types", Value::Array(vec![id(12)])), + ]), + ), + ]), + ), + ( + "edge_types", + Value::Map(vec![( + id(3), + Value::Array(vec![ + Value::Array(vec![id(1), id(2)]), + Value::Array(vec![id(2), id(1)]), + ]), + )]), + ), + ]) +} +fn field_mut<'a>(value: &'a mut Value, name: &str) -> &'a mut Value { + let Value::Map(fields) = value else { + panic!("map") + }; + &mut fields + .iter_mut() + .find(|(key, _)| key == &Value::Text(name.into())) + .expect("field") + .1 +} +fn node_mut(value: &mut Value) -> &mut Value { + let Value::Map(nodes) = field_mut(value, "node_types") else { + panic!("nodes") + }; + &mut nodes[0].1 +} +fn edges_mut(value: &mut Value) -> &mut Vec { + let Value::Map(edges) = field_mut(value, "edge_types") else { + panic!("edges") + }; + let Value::Array(pairs) = &mut edges[0].1 else { + panic!("pairs") + }; + pairs +} +fn refused(value: Value) { + assert_eq!( + EffectGraphSchema::from_canonical_bytes(&encode(&value).expect("canonical fixture")), + Err(EffectGraphSchemaError::InvalidSchema) + ); +} +#[test] +fn canonical_graph_schema_executes_distinct_node_atom_and_edge_relations() { + let schema = EffectGraphSchema::from_canonical_bytes(&encode(&schema()).expect("schema bytes")) + .expect("valid graph schema"); + assert_eq!( + schema.node_addressing(TypeId([1; 32])), + Some(EffectNodeAddressing::Content) + ); + assert_eq!( + schema.node_addressing(TypeId([2; 32])), + Some(EffectNodeAddressing::Mutable) + ); + assert_eq!(schema.node_addressing(TypeId([9; 32])), None); + assert!(schema.allows_atom(TypeId([1; 32]), TypeId([10; 32]))); + assert!(!schema.allows_atom(TypeId([1; 32]), TypeId([12; 32]))); + assert!(!schema.allows_atom(TypeId([10; 32]), TypeId([1; 32]))); + assert!(schema.allows_edge(TypeId([3; 32]), TypeId([1; 32]), TypeId([2; 32]))); + assert!(!schema.allows_edge(TypeId([3; 32]), TypeId([1; 32]), TypeId([1; 32]))); + assert!(!schema.allows_edge(TypeId([4; 32]), TypeId([1; 32]), TypeId([2; 32]))); +} +#[test] +fn graph_schema_refuses_wrong_fields_addressing_identifiers_and_pair_shapes() { + for mode in 0..7 { + let mut value = schema(); + match mode { + 0 => { + let Value::Map(fields) = &mut value else { + panic!("root") + }; + fields.push((Value::Text("extra".into()), Value::Integer(0))); + } + 1 => { + let Value::Map(fields) = &mut value else { + panic!("root") + }; + fields.pop(); + } + 2 => *field_mut(node_mut(&mut value), "addressing") = Value::Integer(2), + 3 => { + let Value::Map(fields) = node_mut(&mut value) else { + panic!("node") + }; + fields.push((Value::Text("extra".into()), Value::Integer(0))); + } + 4 => { + *field_mut(node_mut(&mut value), "atom_types") = + Value::Array(vec![Value::Bytes(vec![10; 31])]); + } + 5 => { + let Value::Map(nodes) = field_mut(&mut value, "node_types") else { + panic!("nodes") + }; + nodes[0].0 = Value::Text("not-an-id".into()); + } + _ => edges_mut(&mut value)[0] = Value::Array(vec![id(1)]), + } + refused(value); + } +} +#[test] +fn graph_schema_set_order_and_duplicates_are_refused_without_normalizing() { + for mode in 0..4 { + let mut value = schema(); + match mode { + 0 => { + *field_mut(node_mut(&mut value), "atom_types") = Value::Array(vec![id(11), id(10)]); + } + 1 => { + *field_mut(node_mut(&mut value), "atom_types") = Value::Array(vec![id(10), id(10)]); + } + 2 => edges_mut(&mut value).reverse(), + _ => { + let first = edges_mut(&mut value)[0].clone(); + edges_mut(&mut value).push(first); + } + } + refused(value); + } +} + +#[test] +fn empty_schema_relations_deny_and_endpoint_pairs_order_by_both_types() { + let empty = map([ + ("node_types", Value::Map(vec![])), + ("edge_types", Value::Map(vec![])), + ]); + let parsed = EffectGraphSchema::from_canonical_bytes(&encode(&empty).expect("empty bytes")) + .expect("empty sets are valid"); + assert_eq!(parsed.node_addressing(TypeId([1; 32])), None); + assert!(!parsed.allows_atom(TypeId([1; 32]), TypeId([10; 32]))); + assert!(!parsed.allows_edge(TypeId([3; 32]), TypeId([1; 32]), TypeId([2; 32]))); + let mut value = schema(); + *edges_mut(&mut value) = vec![ + Value::Array(vec![id(1), id(1)]), + Value::Array(vec![id(1), id(2)]), + Value::Array(vec![id(2), id(1)]), + ]; + let parsed = EffectGraphSchema::from_canonical_bytes(&encode(&value).expect("ordered pairs")) + .expect("same-source pairs sort by target"); + assert!(parsed.allows_edge(TypeId([3; 32]), TypeId([1; 32]), TypeId([1; 32]))); + edges_mut(&mut value).swap(0, 1); + refused(value); +} + +#[test] +fn graph_schema_refuses_noncanonical_root_order_and_trailing_bytes() { + let mut value = schema(); + let mut reversed = vec![0xa2]; // Two-entry CBOR map, node_types before edge_types. + for name in ["node_types", "edge_types"] { + reversed.extend(encode(&Value::Text(name.into())).expect("key")); + reversed.extend(encode(field_mut(&mut value, name)).expect("field")); + } + assert_eq!( + EffectGraphSchema::from_canonical_bytes(&reversed), + Err(EffectGraphSchemaError::InvalidSchema) + ); + let mut trailing = encode(&schema()).expect("valid schema"); + trailing.push(0); + assert_eq!( + EffectGraphSchema::from_canonical_bytes(&trailing), + Err(EffectGraphSchemaError::InvalidSchema) + ); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs new file mode 100644 index 000000000..a72429797 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -0,0 +1,1710 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Current compiler-artifact binding and executable-law preparation. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used, clippy::panic)] +use echo_edict_canonical::CanonicalValueV1 as V; +use warp_core::ExecutableOperationPackage; +use warp_core::TypeId; +#[path = "common/edict_effect_publication.rs"] +mod fixture; +use fixture::*; +#[test] +fn current_package_binds_program_and_executes_its_schema_relations() { + let p = program(); + let v = package(&p); + let b = encoded(&v); + let package = prepare(&v).expect("valid effect package"); + assert_eq!(package.to_canonical_bytes().expect("canonical package"), b); + assert_eq!( + package + .program() + .identity() + .expect("program identity") + .as_hash(), + hash(b"echo:operation-program:v1\0", &encoded(&p)) + ); + assert!(package + .program() + .as_effect_program() + .expect("effect program") + .graph_schema() + .allows_atom(TypeId([1; 32]), TypeId([2; 32]))); + assert!(!package + .program() + .as_effect_program() + .expect("effect program") + .graph_schema() + .allows_atom(TypeId([2; 32]), TypeId([1; 32]))); + assert_eq!( + package + .program() + .as_effect_program() + .expect("effect program") + .compiled_program(), + encoded(&compiler_program()) + ); +} +#[test] +fn substituted_program_schema_law_and_closure_cannot_keep_the_original_pin() { + let original = encoded(&package(&program())); + let pin = hash(b"echo:operation-package:v1\0", &original); + for mode in 0..4 { + let mut p = program(); + let mut v = package(&p); + match mode { + 0 => { + set(&mut p, "compiled_program", V::Bytes(vec![0xa1])); + set(&mut v, "program", V::Bytes(encoded(&p))); + } + 1 => { + let mut g = graph(); + set( + &mut g, + "edge_types", + V::Map(vec![(id(6), V::Array(vec![]))]), + ); + set(&mut p, "graph_schema", V::Bytes(encoded(&g))); + set(&mut v, "program", V::Bytes(encoded(&p))); + } + 2 => { + let mut l = law(); + set(&mut l, "max_reads", V::Integer(2)); + set(&mut p, "footprint_law", V::Bytes(encoded(&l))); + set(&mut v, "program", V::Bytes(encoded(&p))); + set(&mut v, "footprint_contract_identity", footprint(&l)); + } + _ => set(&mut v, "operation_coordinate", text("app.other@1")), + } + assert!(ExecutableOperationPackage::prepare(&encoded(&v), pin).is_err()); + } + assert!(ExecutableOperationPackage::prepare( + &original, + hash(b"echo:operation-package:v2\0", &original) + ) + .is_err()); +} +#[test] +fn exact_package_and_closure_shapes_refuse_effect_lawpacks_projection_and_extra_fields() { + for (k, x) in [ + ("schema", text("echo.operation-package/v2")), + ("program", program()), + ] { + let mut v = package(&program()); + set(&mut v, k, x); + refused(&v); + } + for key in ["application_result_projection", "extra"] { + let mut v = package(&program()); + add(&mut v, key, V::Integer(0)); + refused(&v); + } + for key in ["lawpack_coordinate", "lawpack_identity"] { + let mut v = package(&program()); + let V::Map(m) = &mut v else { panic!("map") }; + let c = &mut m + .iter_mut() + .find(|(k, _)| k == &text("semantic_closure")) + .expect("closure") + .1; + add(c, key, id(1)); + refused(&v); + } + let mut v = package(&program()); + let V::Map(m) = &mut v else { panic!("map") }; + m.retain(|(k, _)| k != &text("operation_coordinate")); + refused(&v); +} +#[test] +fn all_package_profile_equalities_and_target_ir_binding_are_enforced() { + for key in [ + "target_profile_identity", + "application_basis_schema_identity", + "input_schema_identity", + "result_schema_identity", + "obstruction_schema_identity", + "result_interpretation_identity", + "obstruction_interpretation_identity", + "evaluation_basis_schema_identity", + "interpreter_profile_identity", + "intrinsic_profile_identity", + "footprint_contract_identity", + ] { + let mut v = package(&program()); + set(&mut v, key, id(55)); + refused(&v); + } + let mut p = program(); + set(&mut p, "target_ir_identity", id(5)); + refused(&package(&p)); + let mut p = program(); + set(&mut p, "target_ir_identity", V::Bytes(vec![4; 31])); + refused(&package(&p)); +} +#[test] +fn program_refuses_unknown_profiles_laws_shapes_and_unbounded_declarations() { + for (k, x) in [ + ("schema", text("echo.operation-program/v2")), + ("kind", text("other")), + ("identity_law", V::Integer(0)), + ("identity_law", V::Integer(2)), + ("max_input_bytes", V::Integer(65537)), + ("max_result_bytes", V::Integer(65537)), + ("max_allocated_bytes", V::Integer(16 * 1024 * 1024 + 1)), + ("max_input_bytes", V::Integer(-1)), + ("output_type", text("")), + ("output_type", text(&"x".repeat(1025))), + ("graph_schema", V::Bytes(vec![0xa0])), + ("footprint_law", V::Bytes(vec![0xa0])), + ("interpreter_profile_identity", id(44)), + ("intrinsic_profile_identity", id(44)), + ] { + let mut p = program(); + set(&mut p, k, x); + refused(&package(&p)); + } + let mut p = program(); + add(&mut p, "extra", V::Integer(0)); + refused(&package(&p)); + let mut v = package(&program()); + set( + &mut v, + "budget_ceiling", + map([ + ("steps", V::Integer(0)), + ("read_bytes", V::Integer(0)), + ("write_bytes", V::Integer(0)), + ]), + ); + refused(&v); +} +#[test] +fn nested_canonical_bytes_and_exact_footprint_law_digest_are_required() { + let mut p = program(); + let mut g = encoded(&graph()); + g.push(0); + set(&mut p, "graph_schema", V::Bytes(g)); + refused(&package(&p)); + let mut p = program(); + let mut l = law(); + set(&mut l, "max_reads", V::Integer(2)); + set(&mut p, "footprint_law", V::Bytes(encoded(&l))); + refused(&package(&p)); + let mut v = package(&program()); + set( + &mut v, + "footprint_contract_identity", + V::Bytes(hash(b"echo:operation-footprint-law:v1\0", &encoded(&law())).to_vec()), + ); + refused(&v); + let mut v = package(&program()); + let mut b = encoded(&program()); + b.push(0); + set(&mut v, "program", V::Bytes(b)); + refused(&v); + let mut b = encoded(&package(&program())); + b.push(0); + assert!( + ExecutableOperationPackage::prepare(&b, hash(b"echo:operation-package:v1\0", &b)).is_err() + ); +} +#[test] +fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_current_coordinate_rules() { + let mut p = program(); + for key in ["max_input_bytes", "max_result_bytes", "max_allocated_bytes"] { + set(&mut p, key, V::Integer(0)); + } + set(&mut p, "output_type", text(&"é".repeat(512))); + let mut core = compiled_core(); + let V::Map(ref mut core_fields) = core else { + panic!("core") + }; + let V::Map(types) = &mut core_fields + .iter_mut() + .find(|(key, _)| key == &text("types")) + .expect("types") + .1 + else { + panic!("types") + }; + types.push((text(&"é".repeat(512)), map([("kind", text("U32"))]))); + set(&mut core, "coordinate", text(&"x".repeat(1016))); + let V::Map(ref mut fields) = core else { + panic!("core map") + }; + let intents = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("intents")) + .expect("intents") + .1; + let V::Map(intents) = intents else { + panic!("intents map") + }; + set(&mut intents[0].1, "output", text(&"é".repeat(512))); + set( + &mut p, + "compiled_program", + V::Bytes(encoded(&compiled_bundle( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + ))), + ); + set( + &mut p, + "target_ir_identity", + artifact_identity("edict.target-ir.artifact/v1", &compiled_target(&core)), + ); + let mut v = package(&p); + set( + &mut v, + "operation_coordinate", + text(&format!("{}.update@1", "x".repeat(1016))), + ); + let prepared = prepare(&v).expect("zero byte limits and current package coordinate shape"); + assert_eq!( + prepared + .program() + .as_effect_program() + .expect("effect program") + .max_input_bytes(), + 0 + ); + assert_eq!( + prepared + .program() + .as_effect_program() + .expect("effect program") + .max_result_bytes(), + 0 + ); + assert_eq!( + prepared + .program() + .as_effect_program() + .expect("effect program") + .max_allocated_bytes(), + 0 + ); + assert_eq!( + prepared + .program() + .as_effect_program() + .expect("effect program") + .output_type() + .len(), + 1024 + ); + assert_eq!(prepared.budget_ceiling().steps(), 1); + assert_eq!(prepared.budget_ceiling().read_bytes(), 0); + assert_eq!(prepared.budget_ceiling().write_bytes(), 0); + let mut p = program(); + set(&mut p, "output_type", text(&"é".repeat(513))); + refused(&package(&p)); +} +#[test] +fn a_new_pin_can_bind_changed_laws_but_cannot_reuse_the_old_contract_identity() { + let mut p = program(); + let mut changed_law = law(); + set(&mut changed_law, "max_reads", V::Integer(2)); + set(&mut p, "footprint_law", V::Bytes(encoded(&changed_law))); + let mut v = package(&p); + refused(&v); + set( + &mut v, + "footprint_contract_identity", + footprint(&changed_law), + ); + let prepared = prepare(&v).expect("new pin binds the new exact law"); + assert_ne!( + prepared + .program() + .identity() + .expect("program identity") + .as_hash(), + prepare(&package(&program())) + .expect("original") + .program() + .identity() + .expect("program identity") + .as_hash() + ); +} +#[path = "edict_effect_package_tests/invocation.rs"] +mod invocation; + +#[test] +fn repinning_cannot_turn_opaque_or_noncanonical_compiler_bytes_into_an_effect_program() { + // Removing compiled-Core validation admits these malformed programs even + // after the surrounding publication has a newly matching package pin. + for compiled in [ + vec![], + vec![0xa0], + vec![0xa1], + vec![0xf6], + vec![0x18, 0], + vec![0, 0], + ] { + let mut p = program(); + set(&mut p, "compiled_program", V::Bytes(compiled)); + assert!( + prepare(&package(&p)).is_err(), + "opaque compiler bytes must refuse even with a new publication pin" + ); + } +} + +#[test] +fn compiler_core_identity_operation_and_output_cannot_be_rebound_by_a_new_package_pin() { + let p = program(); + for (key, value) in [ + ("operation_coordinate", text("app.other@1")), + ("operation_coordinate", text("application.update@1")), + ] { + let mut value_package = package(&p); + set(&mut value_package, key, value); + refused(&value_package); + } + let mut p = program(); + set(&mut p, "output_type", text("app.Other")); + refused(&package(&p)); + let mut v = package(&program()); + let V::Map(ref mut fields) = v else { + panic!("package") + }; + let closure = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("semantic_closure")) + .expect("closure") + .1; + set(closure, "core_identity", id(33)); + refused(&v); +} + +#[test] +fn retained_public_capture_refuses_its_inconsistent_lawpack_artifact_pins() { + // These are unchanged artifacts from a public Edict JSONL build attempt. + // Their lawpack pins do not match the retained exports/adapter bytes. + // They must not become accepted evidence through fresh package pins. + let bytes = hex::decode( + include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/core.hex") + .trim(), + ) + .expect("retained compiler artifact"); + let core = echo_edict_canonical::decode_canonical_cbor_v1(&bytes).expect("Core"); + let V::Map(fields) = &core else { + panic!("Core map") + }; + let get = |key| { + &fields + .iter() + .find(|(name, _)| name == &text(key)) + .expect("Core field") + .1 + }; + let V::Text(coordinate) = get("coordinate") else { + panic!("coordinate") + }; + let V::Map(intents) = get("intents") else { + panic!("intents") + }; + let [(V::Text(name), V::Map(intent))] = intents.as_slice() else { + panic!("one intent") + }; + let output = intent + .iter() + .find(|(key, _)| key == &text("output")) + .expect("output") + .1 + .clone(); + let mut p = program(); + let target = echo_edict_canonical::decode_canonical_cbor_v1( + &hex::decode( + include_str!( + "../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/06-target-ir.hex" + ) + .trim(), + ) + .expect("target bytes"), + ) + .expect("Target IR"); + let exports=echo_edict_canonical::decode_canonical_cbor_v1(&hex::decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex").trim()).expect("exports bytes")).expect("exports"); + let mut bundle = compiled_bundle(&core, &target, &exports, name); + let captured = |value: &str| V::Bytes(hex::decode(value.trim()).expect("capture")); + set(&mut bundle, "lowering_artifacts", map([ + ("source_artifact", captured(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/04-source.hex"))), + ("lawpack_artifact", captured(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/03-lawpack.hex"))), + ("adapter_artifact", captured(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/01-lawpack-adapter.hex"))), + ("target_profile_artifact", captured(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/profile.hex"))), + ("intrinsics_artifact", V::Bytes(include_bytes!("../../echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor").to_vec())), + ])); + let bundle_bytes = encoded(&bundle); + set(&mut p, "compiled_program", V::Bytes(bundle_bytes)); + set( + &mut p, + "target_ir_identity", + artifact_identity("edict.target-ir.artifact/v1", &target), + ); + set(&mut p, "output_type", output); + let mut v = package(&p); + set( + &mut v, + "operation_coordinate", + text(&format!("{coordinate}.{name}")), + ); + refused(&v); +} + +fn bound_compiler_package(core: &V, target: &V, exports: &V, intent: &str) -> V { + let mut p = program(); + set( + &mut p, + "compiled_program", + V::Bytes(encoded(&compiled_bundle(core, target, exports, intent))), + ); + set( + &mut p, + "target_ir_identity", + artifact_identity("edict.target-ir.artifact/v1", target), + ); + let V::Map(intents) = get(core, "intents") else { + panic!("Core intents") + }; + if let Some((_, selected)) = intents.iter().find(|(key, _)| key == &text(intent)) { + set(&mut p, "output_type", get(selected, "output").clone()); + } + let mut publication = package(&p); + let V::Text(coordinate) = get(core, "coordinate") else { + panic!("Core coordinate") + }; + set( + &mut publication, + "operation_coordinate", + text(&format!("{coordinate}.{intent}")), + ); + publication +} +fn selected_target_mut(target: &mut V) -> &mut V { + let V::Map(fields) = target else { + panic!("target") + }; + let V::Map(intents) = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("intents")) + .expect("intents") + .1 + else { + panic!("intent map") + }; + &mut intents[0].1 +} +#[test] +fn repinned_target_still_requires_exact_core_result_budget_profile_constraints_and_order() { + let core = compiled_core(); + let exports = compiled_exports(); + for (key, value) in [ + ( + "result", + map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("int")), + ("width", text("U32")), + ("value", V::Integer(43)), + ]), + ), + ]), + ), + ( + "coreEvaluationBudget", + map([ + ("maxSteps", V::Integer(2)), + ("maxAllocatedBytes", V::Integer(16 * 1024 * 1024)), + ("maxOutputBytes", V::Integer(65536)), + ]), + ), + ("operationProfile", text("different.profile/v1")), + ("inputConstraints", V::Array(vec![map([])])), + ("executionOrder", V::Array(vec![text("unknown")])), + ] { + let mut target = compiled_target(&core); + set(selected_target_mut(&mut target), key, value); + refused(&bound_compiler_package( + &core, &target, &exports, "update@1", + )); + } + for (steps, order) in [ + (vec![map([("id", text("s"))])], vec![]), + (vec![map([("id", text("s"))])], vec![text("s"), text("s")]), + ( + vec![map([("id", text("s"))]), map([("id", text("s"))])], + vec![text("s")], + ), + (vec![map([("id", text(""))])], vec![text("")]), + ] { + let mut target = compiled_target(&core); + let selected = selected_target_mut(&mut target); + set(selected, "steps", V::Array(steps)); + set(selected, "executionOrder", V::Array(order)); + refused(&bound_compiler_package( + &core, &target, &exports, "update@1", + )); + } + let mut target = compiled_target(&core); + set(&mut target, "sourceCoreCoordinate", text("other")); + refused(&bound_compiler_package( + &core, &target, &exports, "update@1", + )); + let mut target = compiled_target(&core); + set( + &mut target, + "semanticClosure", + map([ + ( + "sourceCore", + map([ + ("id", text("app")), + ("digest", V::Array(vec![text("sha256"), id(7)])), + ]), + ), + ("lawpacks", V::Array(vec![])), + ]), + ); + refused(&bound_compiler_package( + &core, &target, &exports, "update@1", + )); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &exports, + "other", + )); +} + +#[test] +fn compiled_artifact_claims_and_nested_canonical_bytes_cannot_be_substituted() { + for key in [ + "core_identity", + "target_ir_identity", + "application_schema_identity", + ] { + let mut v = package(&program()); + let V::Map(fields) = &mut v else { + panic!("package") + }; + let closure = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("semantic_closure")) + .expect("closure") + .1; + set(closure, key, id(7)); + refused(&v); + } + for key in [ + "core_artifact", + "target_ir_artifact", + "lawpack_exports_artifact", + ] { + let mut bundle = compiler_program(); + let V::Bytes(mut bytes) = get(&bundle, key).clone() else { + panic!("artifact") + }; + bytes.push(0); + set(&mut bundle, key, V::Bytes(bytes)); + let mut p = program(); + set(&mut p, "compiled_program", V::Bytes(encoded(&bundle))); + refused(&package(&p)); + } + let mut bundle = compiler_program(); + add(&mut bundle, "extra", V::Null); + let mut p = program(); + set(&mut p, "compiled_program", V::Bytes(encoded(&bundle))); + refused(&package(&p)); +} + +fn repinned_compiler_artifacts() -> (V, V, V) { + compiler_fixture(&repinned_lowering_artifacts()) +} +fn compiler_fixture(artifacts: &V) -> (V, V, V) { + let decode = |hex: &str| { + echo_edict_canonical::decode_canonical_cbor_v1( + &hex::decode(hex.trim()).expect("artifact bytes"), + ) + .expect("canonical compiler artifact") + }; + let mut core = decode(include_str!( + "../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/core.hex" + )); + let mut target = decode(include_str!( + "../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/06-target-ir.hex" + )); + catalog_named_target(&mut target); + let exports = decode(include_str!( + "../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex" + )); + let V::Bytes(bytes) = get(artifacts, "lawpack_artifact") else { + panic!("lawpack") + }; + let lawpack = echo_edict_canonical::decode_canonical_cbor_v1(bytes).expect("lawpack"); + let V::Array(adapters) = get(&lawpack, "targetAdapters") else { + panic!("adapters") + }; + set( + &mut target, + "targetProfile", + get(&adapters[0], "acceptedTargetProfile").clone(), + ); + let reference = map([ + ("id", text("jedit.text@1")), + ( + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.lawpack/v1", &lawpack), + ]), + ), + ]); + let V::Array(imports) = value_field_mut(&mut core, "imports") else { + panic!("imports") + }; + set(&mut imports[0], "ref", reference.clone()); + let closure = value_field_mut(&mut target, "semanticClosure"); + set(closure, "lawpacks", V::Array(vec![reference])); + set( + value_field_mut(closure, "sourceCore"), + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.core.module/v1", &core), + ]), + ); + (core, target, exports) +} +fn first_step_mut(target: &mut V) -> &mut V { + let selected = selected_target_mut(target); + let V::Map(fields) = selected else { + panic!("intent") + }; + let V::Array(steps) = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("steps")) + .expect("steps") + .1 + else { + panic!("steps") + }; + &mut steps[0] +} +#[test] +fn repinning_cannot_change_a_compiled_instruction_while_retaining_its_core_body() { + // Disabling the ordered Core/Target relation admits each forged lowering, + // despite every artifact identity and the new package pin being correct. + let (core, target, exports) = repinned_compiler_artifacts(); + assert!(prepare(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange" + )) + .is_ok()); + for mode in 0..4 { + let mut target = target.clone(); + match mode { + 0 => set( + first_step_mut(&mut target), + "input", + map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("bool")), ("value", V::Bool(false))]), + ), + ]), + ), + 1 => set( + first_step_mut(&mut target), + "effect", + text("text.otherRead"), + ), + 2 => { + let mut order = get(selected_target_mut(&mut target), "executionOrder").clone(); + let V::Array(ref mut order_values) = order else { + panic!("order") + }; + order_values.reverse(); + set(selected_target_mut(&mut target), "executionOrder", order); + } + _ => { + let V::Map(fields) = selected_target_mut(&mut target) else { + panic!("intent") + }; + let V::Array(requirements) = &mut fields + .iter_mut() + .find(|(key, _)| key == &text("requirements")) + .expect("requirements") + .1 + else { + panic!("requirements") + }; + set( + &mut requirements[0], + "onFailure", + map([ + ("kind", text("terminal")), + ( + "reason", + map([("reasonKind", text("text.Unrelated")), ("payload", map([]))]), + ), + ]), + ); + } + } + assert!( + prepare(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange" + )) + .is_err(), + "forged lowering {mode} survived a fresh pin" + ); + } +} + +fn value_field_mut<'a>(value: &'a mut V, name: &str) -> &'a mut V { + let V::Map(fields) = value else { panic!("map") }; + &mut fields + .iter_mut() + .find(|(key, _)| key == &text(name)) + .expect("field") + .1 +} +fn core_body_mut(core: &mut V) -> &mut V { + let V::Map(intents) = value_field_mut(core, "intents") else { + panic!("intents") + }; + value_field_mut(&mut intents[0].1, "body") +} +#[test] +fn matching_core_and_target_still_cannot_forge_local_identity_or_scope() { + let (original_core, original_target, exports) = repinned_compiler_artifacts(); + for mode in 0..3 { + let mut core = original_core.clone(); + let mut target = original_target.clone(); + match mode { + 0 => { + let V::Array(nodes) = value_field_mut(core_body_mut(&mut core), "nodes") else { + panic!("nodes") + }; + let mut input = get(&nodes[0], "input").clone(); + set( + value_field_mut(&mut input, "base"), + "ref", + map([ + ("id", text("arg.0")), + ("type", text("jedit.text.replace_range@1.Input")), + ("alphaName", text("forged")), + ]), + ); + set(&mut nodes[0], "input", input.clone()); + set(first_step_mut(&mut target), "input", input); + } + 1 => { + let V::Array(nodes) = value_field_mut(core_body_mut(&mut core), "nodes") else { + panic!("nodes") + }; + nodes.reverse(); + let V::Array(order) = + value_field_mut(selected_target_mut(&mut target), "executionOrder") + else { + panic!("order") + }; + order.reverse(); + } + _ => { + let V::Array(locals) = value_field_mut(core_body_mut(&mut core), "locals") else { + panic!("locals") + }; + locals.push(map([ + ("id", text("orphan")), + ("type", text("U32")), + ("alphaName", text("$orphan")), + ])); + } + } + let source = value_field_mut( + value_field_mut(&mut target, "semanticClosure"), + "sourceCore", + ); + set( + source, + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.core.module/v1", &core), + ]), + ); + assert!( + prepare(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange" + )) + .is_err(), + "forged scope {mode} survived matching artifact pins" + ); + } +} + +#[test] +fn pure_bindings_preserve_values_and_become_available_only_after_their_definition() { + let mut core = compiled_core(); + let binding = map([ + ("id", text("local.0")), + ("type", text("U32")), + ("alphaName", text("$value")), + ]); + let value = map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("int")), + ("width", text("U32")), + ("value", V::Integer(7)), + ]), + ), + ]); + let local = map([("kind", text("local")), ("ref", binding.clone())]); + let body = core_body_mut(&mut core); + let V::Array(locals) = value_field_mut(body, "locals") else { + panic!("locals") + }; + locals.push(binding.clone()); + set( + body, + "nodes", + V::Array(vec![map([ + ("kind", text("let")), + ("binding", binding.clone()), + ("value", value.clone()), + ])]), + ); + set(body, "result", local); + let mut target = compiled_target(&core); + let selected = selected_target_mut(&mut target); + set( + selected, + "pureBindings", + V::Array(vec![map([ + ("id", text("binding.0")), + ("binding", binding), + ("value", value), + ])]), + ); + set( + selected, + "executionOrder", + V::Array(vec![text("binding.0")]), + ); + assert!(prepare(&bound_compiler_package( + &core, + &target, + &compiled_exports(), + "update@1" + )) + .is_ok()); + let V::Array(bindings) = value_field_mut(selected_target_mut(&mut target), "pureBindings") + else { + panic!("bindings") + }; + set( + &mut bindings[0], + "value", + map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("bool")), ("value", V::Bool(false))]), + ), + ]), + ); + refused(&bound_compiler_package( + &core, + &target, + &compiled_exports(), + "update@1", + )); +} + +#[test] +fn lowered_failure_arms_preserve_the_source_binders_and_values_without_duplicates() { + let (core, target, exports) = repinned_compiler_artifacts(); + for mode in 0..3 { + let mut target = target.clone(); + let step = first_step_mut(&mut target); + match mode { + 0 => { + let V::Map(arms) = value_field_mut(step, "obstructionArms") else { + panic!("arms") + }; + set( + &mut arms[0].1, + "value", + map([ + ("kind", text("call")), + ("callee", text("text.OtherFailure")), + ("args", V::Array(vec![])), + ("typeArgs", V::Array(vec![])), + ]), + ); + } + 1 => { + let V::Array(failures) = value_field_mut(step, "obstructionFailures") else { + panic!("failures") + }; + failures.push(failures[0].clone()); + } + _ => { + let V::Map(arms) = value_field_mut(step, "obstructionArms") else { + panic!("arms") + }; + arms[1].1 = arms[0].1.clone(); + } + } + refused(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange", + )); + } +} + +#[test] +fn pinned_adapter_rejects_swapped_failure_tags_and_substituted_intrinsics() { + let (core, original, exports) = repinned_compiler_artifacts(); + assert!(prepare(&bound_compiler_package( + &core, + &original, + &exports, + "replaceRange" + )) + .is_ok()); + for mode in 0..2 { + let mut target = original.clone(); + let step = first_step_mut(&mut target); + if mode == 0 { + let V::Map(arms) = value_field_mut(step, "obstructionArms") else { + panic!("arms") + }; + let first = arms[0].1.clone(); + arms[0].1 = arms[1].1.clone(); + arms[1].1 = first; + } else { + set(step, "targetIntrinsic", text("echo.dpo@1.node-delete")); + } + assert!( + prepare(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange" + )) + .is_err(), + "adapter substitution {mode} survived fresh pins" + ); + } +} + +#[test] +fn fresh_bundle_pins_cannot_replace_or_omit_the_pinned_lowering_artifacts() { + let (core, target, exports) = repinned_compiler_artifacts(); + for mode in 0..5 { + let mut bundle = compiled_bundle(&core, &target, &exports, "replaceRange"); + let lowering = value_field_mut(&mut bundle, "lowering_artifacts"); + if mode == 0 { + *lowering = V::Null; + } else { + let key = [ + "", + "source_artifact", + "lawpack_artifact", + "adapter_artifact", + "adapter_artifact", + ][mode]; + let V::Bytes(raw) = get(lowering, key) else { + panic!("artifact") + }; + let mut value = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("artifact"); + match mode { + 1 => { + let V::Bytes(source) = &mut value else { + panic!("source") + }; + *source = String::from_utf8(source.clone()) + .expect("utf8") + .replace(" as text;", " as other;") + .into_bytes(); + } + 2 => set(&mut value, "version", text("2")), + 3 => { + let implementations = value_field_mut(&mut value, "effectImplementations"); + let implementation = value_field_mut(implementations, "jedit.text@1.readFact"); + set( + implementation, + "targetIntrinsic", + text("echo.dpo@1.node-delete"), + ); + } + _ => { + add(&mut value, "extra", V::Null); + } + } + set(lowering, key, V::Bytes(encoded(&value))); + } + let mut package = bound_compiler_package(&core, &target, &exports, "replaceRange"); + let V::Bytes(raw) = get(&package, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + set(&mut program, "compiled_program", V::Bytes(encoded(&bundle))); + set(&mut package, "program", V::Bytes(encoded(&program))); + assert!( + prepare(&package).is_err(), + "lowering substitution {mode} survived fresh pins" + ); + } +} + +fn repin_core(core: &V, target: &mut V) { + set( + value_field_mut(value_field_mut(target, "semanticClosure"), "sourceCore"), + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.core.module/v1", core), + ]), + ); +} +#[test] +fn typed_contract_refuses_an_effect_input_with_the_wrong_representation() { + let (mut core, mut target, exports) = repinned_compiler_artifacts(); + let V::Array(nodes) = value_field_mut(core_body_mut(&mut core), "nodes") else { + panic!("nodes") + }; + let mut input = get(&nodes[0], "input").clone(); + set(&mut input, "field", text("expected")); + set(&mut nodes[0], "input", input.clone()); + set(first_step_mut(&mut target), "input", input); + repin_core(&core, &mut target); + refused(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange", + )); +} +#[test] +fn typed_contract_refuses_wrong_result_types() { + let mut core = compiled_core(); + set( + core_body_mut(&mut core), + "result", + map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("bool")), ("value", V::Bool(false))]), + ), + ]), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} +#[test] +fn typed_contract_refuses_unauthorized_helper_calls() { + let mut core = compiled_core(); + set( + core_body_mut(&mut core), + "result", + map([ + ("kind", text("call")), + ("callee", text("app.unpublished")), + ("args", V::Array(vec![])), + ("typeArgs", V::Array(vec![])), + ]), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} +#[test] +fn typed_contract_refuses_a_matching_core_target_let_with_the_wrong_type() { + let mut core = compiled_core(); + let binding = map([ + ("id", text("local.0")), + ("type", text("U32")), + ("alphaName", text("$local0")), + ]); + let value = map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("bool")), ("value", V::Bool(false))]), + ), + ]); + let body = core_body_mut(&mut core); + let V::Array(locals) = value_field_mut(body, "locals") else { + panic!("locals") + }; + locals.push(binding.clone()); + set( + body, + "nodes", + V::Array(vec![map([ + ("kind", text("let")), + ("binding", binding.clone()), + ("value", value.clone()), + ])]), + ); + let mut target = compiled_target(&core); + let selected = selected_target_mut(&mut target); + set( + selected, + "pureBindings", + V::Array(vec![map([ + ("id", text("binding.0")), + ("binding", binding), + ("value", value), + ])]), + ); + set( + selected, + "executionOrder", + V::Array(vec![text("binding.0")]), + ); + refused(&bound_compiler_package( + &core, + &target, + &compiled_exports(), + "update@1", + )); +} +#[test] +fn typed_contract_refuses_mapping_an_integrity_failure_to_a_resource_obstruction() { + let (mut core, mut target, exports) = repinned_compiler_artifacts(); + let V::Array(nodes) = value_field_mut(core_body_mut(&mut core), "nodes") else { + panic!("nodes") + }; + let arms = value_field_mut(&mut nodes[0], "obstructionMap"); + let arm = value_field_mut(arms, "notAtom"); + set( + value_field_mut(arm, "value"), + "callee", + text("text.FactMissing"), + ); + let arm = arm.clone(); + set( + value_field_mut(first_step_mut(&mut target), "obstructionArms"), + "atomRequired", + arm, + ); + repin_core(&core, &mut target); + refused(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange", + )); +} + +#[test] +fn typed_contract_accepts_checked_source_helpers_and_refuses_wrong_arguments() { + let mut core = compiled_core(); + let parameter = map([ + ("id", text("param.0")), + ("type", text("U32")), + ("alphaName", text("$param0")), + ]); + let reference = map([("kind", text("local")), ("ref", parameter.clone())]); + add( + &mut core, + "functions", + map([( + "identity", + map([ + ("params", V::Array(vec![parameter])), + ("returnType", text("U32")), + ( + "body", + map([ + ("locals", V::Array(vec![])), + ("bindings", V::Array(vec![])), + ("result", reference), + ]), + ), + ]), + )]), + ); + let V::Array(locals) = get(core_body_mut(&mut core), "locals") else { + panic!("locals") + }; + let argument = map([("kind", text("local")), ("ref", locals[0].clone())]); + let call = |arg| { + map([ + ("kind", text("call")), + ("callee", text("app.identity")), + ("args", V::Array(vec![arg])), + ("typeArgs", V::Array(vec![])), + ]) + }; + set(core_body_mut(&mut core), "result", call(argument)); + assert!(prepare(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1" + )) + .is_ok()); + set( + core_body_mut(&mut core), + "result", + call(map([ + ("kind", text("const")), + ( + "value", + map([("kind", text("bool")), ("value", V::Bool(false))]), + ), + ])), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} + +#[test] +fn pinned_exports_refuse_core_redefinitions_of_imported_type_contracts() { + let (original, target, exports) = repinned_compiler_artifacts(); + for mode in 0..4 { + let mut core = original.clone(); + let types = value_field_mut(&mut core, "types"); + match mode { + 0 => set( + value_field_mut(value_field_mut(types, "jedit.text@1.FactAddress"), "fields"), + "nodeId", + text("Bytes"), + ), + 1 => set( + value_field_mut(types, "jedit.text@1.FactBytes"), + "max", + V::Integer(2_097_152), + ), + 2 => add( + types, + "jedit.text@1.Unpublished", + map([("kind", text("Bool"))]), + ), + _ => add( + value_field_mut(types, "jedit.text@1.FactBytes"), + "extra", + V::Null, + ), + } + let mut target = target.clone(); + repin_core(&core, &mut target); + assert!( + prepare(&bound_compiler_package( + &core, + &target, + &exports, + "replaceRange" + )) + .is_err(), + "Core type substitution {mode} survived the pinned exports" + ); + } +} + +fn nominal_identity_core() -> V { + let mut core = compiled_core(); + for (name, contract) in [ + ("nominal@1.A", "nominal@1.A"), + ("nominal@1.B", "nominal@1.B"), + ] { + add( + value_field_mut(&mut core, "types"), + name, + map([ + ("kind", text("Nominal")), + ("contract", text(contract)), + ("representation", text("U32")), + ]), + ); + } + let intent = value_field_mut(value_field_mut(&mut core, "intents"), "update@1"); + set(intent, "input", text("nominal@1.A")); + set(intent, "output", text("nominal@1.A")); + let body = value_field_mut(intent, "body"); + let V::Array(locals) = value_field_mut(body, "locals") else { + panic!("locals") + }; + set(&mut locals[0], "type", text("nominal@1.A")); + let result = map([("kind", text("local")), ("ref", locals[0].clone())]); + set(body, "result", result); + core +} +#[test] +fn nominal_contracts_cannot_be_interchanged_through_matching_representations() { + let mut core = nominal_identity_core(); + assert!(prepare(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1" + )) + .is_ok()); + set( + value_field_mut(value_field_mut(&mut core, "intents"), "update@1"), + "output", + text("nominal@1.B"), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} +#[test] +fn nominal_contracts_cannot_implicitly_unwrap_into_their_representation() { + let mut core = nominal_identity_core(); + set( + value_field_mut(value_field_mut(&mut core, "intents"), "update@1"), + "output", + text("U32"), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} +#[test] +fn nominal_contracts_cannot_be_constructed_by_an_untyped_literal() { + let mut core = nominal_identity_core(); + set( + core_body_mut(&mut core), + "result", + map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("int")), + ("width", text("U32")), + ("value", V::Integer(7)), + ]), + ), + ]), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} +#[test] +fn nominal_declarations_must_bind_their_own_core_table_identity() { + let mut core = nominal_identity_core(); + set( + value_field_mut(value_field_mut(&mut core, "types"), "nominal@1.A"), + "contract", + text("nominal@1.B"), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} + +#[test] +fn checked_helpers_preserve_nominal_arguments_and_return_identity() { + let mut core = nominal_identity_core(); + let helper = |name| { + let parameter = map([ + ("id", text("param.0")), + ("type", text(name)), + ("alphaName", text("$param0")), + ]); + map([ + ("params", V::Array(vec![parameter.clone()])), + ("returnType", text(name)), + ( + "body", + map([ + ("locals", V::Array(vec![])), + ("bindings", V::Array(vec![])), + ("result", map([("kind", text("local")), ("ref", parameter)])), + ]), + ), + ]) + }; + add( + &mut core, + "functions", + map([("identity", helper("nominal@1.A"))]), + ); + let result = get(core_body_mut(&mut core), "result").clone(); + set( + core_body_mut(&mut core), + "result", + map([ + ("kind", text("call")), + ("callee", text("app.identity")), + ("args", V::Array(vec![result])), + ("typeArgs", V::Array(vec![])), + ]), + ); + assert!(prepare(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1" + )) + .is_ok()); + set( + value_field_mut(&mut core, "functions"), + "identity", + helper("nominal@1.B"), + ); + set( + value_field_mut(value_field_mut(&mut core, "intents"), "update@1"), + "output", + text("nominal@1.B"), + ); + refused(&bound_compiler_package( + &core, + &compiled_target(&core), + &compiled_exports(), + "update@1", + )); +} + +#[test] +fn effect_preparation_requires_the_pinned_target_profile_and_intrinsic_catalog() { + let (core, target, exports) = repinned_compiler_artifacts(); + let mut publication = bound_compiler_package(&core, &target, &exports, "replaceRange"); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let V::Bytes(raw) = get(&program, "compiled_program") else { + panic!("bundle") + }; + let mut bundle = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("bundle"); + let V::Map(fields) = value_field_mut(&mut bundle, "lowering_artifacts") else { + panic!("lowering") + }; + fields.retain(|(key, _)| { + key != &text("target_profile_artifact") && key != &text("intrinsics_artifact") + }); + set(&mut program, "compiled_program", V::Bytes(encoded(&bundle))); + set(&mut publication, "program", V::Bytes(encoded(&program))); + refused(&publication); +} + +fn catalog_fixture() -> V { + let lowering = repinned_lowering_artifacts(); + let V::Bytes(raw) = get(&lowering, "intrinsics_artifact") else { + panic!("catalog") + }; + echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("catalog") +} +fn publication_for_lowering(lowering: &V) -> V { + let (core, target, exports) = compiler_fixture(lowering); + publication_from_parts(&core, &target, &exports, lowering) +} +fn publication_from_parts(core: &V, target: &V, exports: &V, lowering: &V) -> V { + let mut publication = bound_compiler_package(core, target, exports, "replaceRange"); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let mut bundle = compiled_bundle(core, target, exports, "replaceRange"); + set(&mut bundle, "lowering_artifacts", lowering.clone()); + set(&mut program, "compiled_program", V::Bytes(encoded(&bundle))); + set(&mut publication, "program", V::Bytes(encoded(&program))); + publication +} +#[test] +fn intrinsic_catalog_signatures_must_agree_with_the_exported_effect() { + assert!(prepare(&publication_for_lowering(&repinned_lowering_artifacts())).is_ok()); + for mode in 0..3 { + let mut catalog = catalog_fixture(); + let intrinsic = value_field_mut( + value_field_mut(&mut catalog, "intrinsics"), + "echo.dpo@1.node-atom-read", + ); + match mode { + 0 => set(intrinsic, "argumentTypes", V::Array(vec![text("U32")])), + 1 => set(intrinsic, "returnType", text("U32")), + _ => set(intrinsic, "typeParameters", V::Array(vec![text("T")])), + } + let lowering = lowering_fixture(Some(catalog)); + assert!( + prepare(&publication_for_lowering(&lowering)).is_err(), + "catalog signature substitution {mode} survived a fully repinned artifact chain" + ); + } +} +#[test] +fn intrinsic_catalog_fault_contracts_must_agree_with_exported_failures() { + for mode in 0..2 { + let mut catalog = catalog_fixture(); + let intrinsic = value_field_mut( + value_field_mut(&mut catalog, "intrinsics"), + "echo.dpo@1.node-atom-read", + ); + let failure = value_field_mut(value_field_mut(intrinsic, "effectFailures"), "atomRequired"); + if mode == 0 { + set(failure, "authorityClass", text("resourceFault")); + } else { + set(failure, "payloadType", text("U32")); + } + let lowering = lowering_fixture(Some(catalog)); + assert!( + prepare(&publication_for_lowering(&lowering)).is_err(), + "catalog fault substitution {mode} survived a fully repinned artifact chain" + ); + } +} +#[test] +fn a_profile_cannot_authorize_an_intrinsic_missing_from_its_catalog() { + let mut catalog = catalog_fixture(); + set(&mut catalog, "intrinsics", map([])); + refused(&publication_for_lowering(&lowering_fixture(Some(catalog)))); +} + +#[test] +fn runtime_read_abi_rejects_a_self_consistent_sixteen_byte_node_address() { + let (_, _, mut exports) = repinned_compiler_artifacts(); + let V::Array(types) = value_field_mut(&mut exports, "types") else { + panic!("types") + }; + let address = types + .iter_mut() + .find(|ty| get(ty, "coordinate") == &text("jedit.text@1.FactAddress")) + .expect("address"); + set( + address, + "definition", + text("Record,typeId:Bytes,warpId:Bytes>"), + ); + let lowering = lowering_fixture_inputs(None, Some(exports.clone()), None); + let (mut core, mut target, _) = compiler_fixture(&lowering); + set( + value_field_mut( + value_field_mut( + value_field_mut(&mut core, "types"), + "jedit.text@1.FactAddress", + ), + "fields", + ), + "nodeId", + text("Bytes"), + ); + repin_core(&core, &mut target); + refused(&publication_from_parts(&core, &target, &exports, &lowering)); +} +#[test] +fn runtime_read_abi_rejects_catalog_and_lawpack_agreement_on_wrong_fault_authority() { + let (_, _, mut exports) = repinned_compiler_artifacts(); + let V::Array(effects) = value_field_mut(&mut exports, "effects") else { + panic!("effects") + }; + for failure in ["notAtom", "tooLarge", "typeMismatch"] { + set( + value_field_mut(value_field_mut(&mut effects[0], "effectFailures"), failure), + "authorityClass", + text("resourceFault"), + ); + } + let V::Array(obstructions) = value_field_mut(&mut exports, "obstructions") else { + panic!("obstructions") + }; + let malformed = obstructions + .iter_mut() + .find(|item| get(item, "coordinate") == &text("jedit.text@1.FactMalformed")) + .expect("obstruction"); + set(malformed, "authorityClass", text("resourceFault")); + let lowering = lowering_fixture_inputs(None, Some(exports.clone()), None); + let (core, target, _) = compiler_fixture(&lowering); + refused(&publication_from_parts(&core, &target, &exports, &lowering)); +} + +#[test] +fn outer_package_pins_cannot_replace_profile_or_catalog_bytes() { + for key in ["target_profile_artifact", "intrinsics_artifact"] { + let mut lowering = repinned_lowering_artifacts(); + let V::Bytes(raw) = get(&lowering, key) else { + panic!("artifact") + }; + let mut artifact = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("artifact"); + if key == "target_profile_artifact" { + set( + value_field_mut(&mut artifact, "diagnosticAbi"), + "digest", + V::Array(vec![text("sha256"), id(55)]), + ); + } else { + let declaration = + get(get(&artifact, "intrinsics"), "echo.dpo@1.node-atom-read").clone(); + add( + value_field_mut(&mut artifact, "intrinsics"), + "echo.dpo@1.unused", + declaration, + ); + } + set(&mut lowering, key, V::Bytes(encoded(&artifact))); + refused(&publication_for_lowering(&lowering)); + } +} +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/execution.rs"] +mod execution; + +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/mutations.rs"] +mod mutations; + +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/abi_conformance.rs"] +mod abi_conformance; + +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/completion.rs"] +mod completion; + +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/derivation.rs"] +mod derivation; diff --git a/crates/warp-core/tests/edict_effect_package_tests/abi_conformance.rs b/crates/warp-core/tests/edict_effect_package_tests/abi_conformance.rs new file mode 100644 index 000000000..4a39f75f1 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/abi_conformance.rs @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-ABI conformance for the six shared `echo.dpo@1` intrinsics. +//! +//! The provider's pinned catalog names Echo-owned ABI coordinates. An +//! application keeps its own nominal types; its declarations must conform to +//! the ABI representation of the intrinsic its adapter explicitly selects. +//! Expected coordinates are written out here rather than read from the +//! implementation, so the two can disagree. +use super::mutations::{fixture_with, Shape}; +use super::*; + +const PROFILE: &str = "continuum.profile.graph-effects/v1"; +const MUTATIONS: [&str; 5] = [ + "ensure-node", + "create-node", + "replace-atom", + "ensure-edge", + "replace-edge", +]; +const NODE_PAYLOAD_MUTATIONS: [&str; 3] = ["ensure-node", "create-node", "replace-atom"]; + +fn accepts(name: &str, shape: &Shape) { + let (publication, _) = fixture_with(name, PROFILE, shape); + prepare(&publication).unwrap_or_else(|error| panic!("{name}: {error:?}")); +} + +fn refuses(name: &str, shape: &Shape, why: &str) { + let (publication, _) = fixture_with(name, PROFILE, shape); + assert!(prepare(&publication).is_err(), "{name}: {why} was accepted"); +} + +fn with_field(name: &str, field: &'static str, ty: &'static str) -> Shape { + let mut shape = Shape::conforming(name); + for entry in &mut shape.fields { + if entry.0 == field { + entry.1 = ty; + } + } + shape +} + +#[test] +fn differently_named_application_types_conform_to_the_same_abi() { + for name in MUTATIONS { + accepts(name, &Shape::conforming(name)); + accepts( + name, + &Shape { + input_name: "JimRopeEdit", + output_name: "JimRopeEditDone", + ..Shape::conforming(name) + }, + ); + } +} + +#[test] +fn the_catalog_must_name_the_selected_intrinsics_abi_not_an_application_type() { + for name in MUTATIONS { + let mut shape = Shape::conforming(name); + shape.catalog_argument = shape.input_name.to_owned(); + shape.catalog_return = shape.output_name.to_owned(); + refuses(name, &shape, "a catalog entry naming application types"); + } + let mut crossed = Shape::conforming("replace-atom"); + crossed.catalog_argument = "echo.dpo@1.ensure-node.input".to_owned(); + refuses("replace-atom", &crossed, "another intrinsic's ABI input"); + let mut crossed = Shape::conforming("ensure-edge"); + crossed.catalog_return = "echo.dpo@1.node-atom-read.result".to_owned(); + refuses("ensure-edge", &crossed, "another intrinsic's ABI result"); +} + +#[test] +fn fixed_identity_fields_require_exactly_thirty_two_bytes() { + for name in MUTATIONS { + refuses( + name, + &with_field(name, "warpId", "Bytes"), + "a maximum-bounded identity", + ); + refuses( + name, + &with_field(name, "warpId", "Bytes"), + "a 31-byte identity", + ); + } +} + +#[test] +fn field_membership_is_exact() { + for name in MUTATIONS { + let mut missing = Shape::conforming(name); + missing.fields.pop(); + refuses(name, &missing, "a missing field"); + + let mut renamed = Shape::conforming(name); + renamed.fields[0].0 = "warp"; + refuses(name, &renamed, "a renamed field"); + + let mut extra = Shape::conforming(name); + extra.fields.push(("note", "Bytes")); + refuses(name, &extra, "an extra field"); + } +} + +#[test] +fn payload_maxima_conform_within_the_runtime_ceiling() { + for name in NODE_PAYLOAD_MUTATIONS { + accepts(name, &with_field(name, "atomBytes", "Bytes")); + accepts(name, &with_field(name, "atomBytes", "Bytes")); + refuses( + name, + &with_field(name, "atomBytes", "Bytes"), + "a payload above the 16 MiB ceiling", + ); + refuses( + name, + &with_field(name, "atomBytes", "Bytes"), + "a payload with a nonzero minimum", + ); + } +} + +#[test] +fn the_successful_result_must_be_the_empty_record() { + for name in MUTATIONS { + let mut shape = Shape::conforming(name); + shape.output = map([ + ("kind", text("Record")), + ("fields", map([("done", text("Bytes"))])), + ]); + refuses(name, &shape, "a non-empty successful result"); + } +} + +fn read_catalog_with(edit: impl Fn(&mut V)) -> V { + let mut catalog = catalog_fixture(); + edit(value_field_mut( + value_field_mut(&mut catalog, "intrinsics"), + "echo.dpo@1.node-atom-read", + )); + catalog +} + +#[test] +fn the_read_catalog_names_its_abi_and_its_failures_carry_the_abi_input() { + let intrinsic = get( + get(&catalog_fixture(), "intrinsics"), + "echo.dpo@1.node-atom-read", + ) + .clone(); + assert_eq!( + get(&intrinsic, "argumentTypes"), + &V::Array(vec![text("echo.dpo@1.node-atom-read.input")]) + ); + assert_eq!( + get(&intrinsic, "returnType"), + &text("echo.dpo@1.node-atom-read.result") + ); + let V::Map(failures) = get(&intrinsic, "effectFailures") else { + panic!("failures") + }; + assert_eq!(failures.len(), 4); + for (_, failure) in failures { + assert_eq!( + get(failure, "payloadType"), + &text("echo.dpo@1.node-atom-read.input") + ); + } + assert!(prepare(&publication_for_lowering(&repinned_lowering_artifacts())).is_ok()); +} + +#[test] +fn a_read_catalog_naming_the_application_type_is_refused() { + let catalog = read_catalog_with(|intrinsic| { + set( + intrinsic, + "argumentTypes", + V::Array(vec![text("jedit.text@1.FactAddress")]), + ); + }); + refused(&publication_for_lowering(&lowering_fixture(Some(catalog)))); + let catalog = read_catalog_with(|intrinsic| { + let failure = value_field_mut(value_field_mut(intrinsic, "effectFailures"), "atomMissing"); + set(failure, "payloadType", text("jedit.text@1.FactAddress")); + }); + refused(&publication_for_lowering(&lowering_fixture(Some(catalog)))); +} + +#[test] +fn a_read_failure_must_carry_the_effects_own_input_type() { + let (_, _, mut exports) = repinned_compiler_artifacts(); + let V::Array(effects) = value_field_mut(&mut exports, "effects") else { + panic!("effects") + }; + let V::Map(failures) = value_field_mut(&mut effects[0], "effectFailures") else { + panic!("failures") + }; + set(&mut failures[0].1, "payloadType", text("U32")); + let lowering = lowering_fixture_inputs(None, Some(exports), None); + refused(&publication_for_lowering(&lowering)); +} + +#[test] +fn each_mutation_declares_its_own_effect_kind() { + for name in MUTATIONS { + accepts(name, &Shape::conforming(name)); + for wrong in ["write", "read", "delete"] { + let mut shape = Shape::conforming(name); + shape.effect_kind = wrong.to_owned(); + refuses(name, &shape, "a mutation declaring another effect kind"); + } + } + let mut crossed = Shape::conforming("create-node"); + crossed.effect_kind = "ensure".to_owned(); + refuses("create-node", &crossed, "create-node declaring ensure"); +} + +#[test] +fn each_mutation_write_class_is_its_effect_kind() { + for name in MUTATIONS { + for wrong in ["write", "read", "delete"] { + let mut shape = Shape::conforming(name); + shape.write_class = wrong.to_owned(); + refuses( + name, + &shape, + "a write class outside Edict's ABI or another kind", + ); + } + } +} + +const READ: &str = "echo.dpo@1.node-atom-read"; + +fn rename_read_failure(catalog: &mut V, from: &str, to: &str) { + let V::Map(failures) = value_field_mut( + value_field_mut(value_field_mut(catalog, "intrinsics"), READ), + "effectFailures", + ) else { + panic!("failures") + }; + let entry = failures + .iter_mut() + .find(|(name, _)| name == &text(from)) + .expect("failure"); + entry.0 = text(to); +} + +#[test] +fn the_read_catalog_names_its_failures_by_identifier() { + let catalog = catalog_fixture(); + let V::Map(failures) = get(get(get(&catalog, "intrinsics"), READ), "effectFailures") else { + panic!("failures") + }; + let mut names = failures + .iter() + .map(|(name, _)| match name { + V::Text(name) => name.clone(), + _ => panic!("name"), + }) + .collect::>(); + names.sort(); + assert_eq!( + names, + [ + "atomByteBoundExceeded", + "atomMissing", + "atomRequired", + "atomTypeMismatch" + ] + ); + assert!(prepare(&publication_for_lowering(&repinned_lowering_artifacts())).is_ok()); +} + +#[test] +fn a_read_spelled_with_runtime_tags_is_refused_on_the_authored_route() { + // The bounded-read route's adapter maps to runtime tags. The authored + // route's catalog names failures by identifier, so no tag passes through. + let mut catalog = catalog_fixture(); + for (tag, name) in READ_FAILURE_NAMES { + rename_read_failure(&mut catalog, name, tag); + } + let lowering = lowering_fixture_inputs(Some(catalog), None, Some(captured_adapter())); + refused(&publication_for_lowering(&lowering)); +} + +#[test] +fn unknown_missing_and_crossed_read_failure_names_are_refused() { + // Unknown: a catalog identifier the read does not define, consistently + // used by catalog and adapter. + let mut catalog = catalog_fixture(); + rename_read_failure(&mut catalog, "atomMissing", "atomAbsent"); + let mut adapter = catalog_named_adapter(&captured_adapter()); + let V::Map(mappings) = value_field_mut( + value_field_mut( + value_field_mut(&mut adapter, "effectImplementations"), + "jedit.text@1.readFact", + ), + "failureMappings", + ) else { + panic!("mappings") + }; + for (_, target) in mappings.iter_mut() { + if target == &text("atomMissing") { + *target = text("atomAbsent"); + } + } + refused(&publication_for_lowering(&lowering_fixture_inputs( + Some(catalog), + None, + Some(adapter), + ))); + // Missing: a catalog without one of the read's four failures. + let catalog = read_catalog_with(|intrinsic| { + let V::Map(failures) = value_field_mut(intrinsic, "effectFailures") else { + panic!("failures") + }; + failures.retain(|(name, _)| name != &text("atomRequired")); + }); + refused(&publication_for_lowering(&lowering_fixture(Some(catalog)))); + // Crossed: two identifiers swapping their authority classes. + let catalog = read_catalog_with(|intrinsic| { + let failures = value_field_mut(intrinsic, "effectFailures"); + set( + value_field_mut(failures, "atomMissing"), + "authorityClass", + text("integrityFault"), + ); + set( + value_field_mut(failures, "atomRequired"), + "authorityClass", + text("resourceFault"), + ); + }); + refused(&publication_for_lowering(&lowering_fixture(Some(catalog)))); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/completion.rs b/crates/warp-core/tests/edict_effect_package_tests/completion.rs new file mode 100644 index 000000000..f579cb760 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/completion.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Whole authored-operation origin, distinct from private component composition. +use super::execution::{host, limits}; +use super::mutations::fixture; +use super::*; +use warp_core::{EchoOperationApplicationBasisV1, EchoOperationBudgetV1}; +fn basis(input: &[u8]) -> EchoOperationApplicationBasisV1 { + EchoOperationApplicationBasisV1::new( + hash( + b"echo:operation-profile:v1\0", + b"echo.operation.basis.edict-effect/v1", + ), + hash(b"echo:operation-edict-effect-basis:v1\0", input), + ) +} +#[test] +fn authored_operation_cannot_adopt_work_from_another_private_component() { + let (publication, input) = fixture("create-node", "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let parent = host + .echo_operation_evaluation_basis_v1(head, basis(&input)) + .expect("parent"); + let mut frame = host + .effect_evaluation_frame_v1( + parent, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .read_typed_node(node, TypeId([1; 32])) + .expect("prior component read"); + let usage = frame.consumed(); + assert_eq!( + frame.evaluate_effect(program, &input, &[]), + Err(warp_core::edict_effects::EffectFrameError::UsedFrame) + ); + assert_eq!(frame.consumed(), usage); + assert!(frame.buffered_mutations().is_empty()); +} +#[test] +fn completed_authored_evidence_binds_the_exact_program_input_parent_and_output() { + use warp_core::edict_effects::EffectFrameError; + for name in [ + "ensure-node", + "create-node", + "replace-atom", + "ensure-edge", + "replace-edge", + ] { + let (publication, input) = fixture(name, "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let V::Bytes(raw_program) = get(&publication, "program") else { + panic!("program") + }; + let expected_id = hash(b"echo:operation-program:v1\0", raw_program); + assert_eq!( + package.program().identity().expect("identity").as_hash(), + expected_id + ); + let (host, head, _) = host(); + let parent = host + .echo_operation_evaluation_basis_v1(head, basis(&input)) + .expect("parent"); + let mut frame = host + .effect_evaluation_frame_v1( + parent, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame + .evaluate_effect(program, &input, &[]) + .expect("execute"), + encoded(&map([])) + ); + let usage = frame.consumed(); + let allocated = frame.allocated_bytes(); + assert_eq!( + frame.evaluate_effect(program, &input, &[]), + Err(EffectFrameError::Terminated) + ); + assert_eq!(frame.consumed(), usage); + let completed = frame + .into_effect_evaluation(1024) + .expect("completed evidence"); + assert_eq!(completed.program_identity(), expected_id); + assert_eq!(completed.input_basis(), basis(&input)); + assert_eq!(completed.evaluation_basis(), parent); + assert_eq!(completed.output(), encoded(&map([]))); + assert_eq!(completed.mutations().len(), 1); + assert_eq!(completed.read_set_image().as_bytes(), &[0; 16]); + assert_eq!(completed.read_set_image().consumed(), usage); + assert_eq!(completed.read_set_image().allocated_bytes(), allocated); + } +} +#[test] +fn input_substitution_cannot_be_sealed_under_the_original_application_basis() { + use warp_core::edict_effects::EffectCompletionError; + let (publication, input) = fixture("create-node", "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let mut changed = echo_edict_canonical::decode_canonical_cbor_v1(&input).expect("input"); + set(&mut changed, "nodeId", id(99)); + let changed = encoded(&changed); + let (host, head, _) = host(); + let parent = host + .echo_operation_evaluation_basis_v1(head, basis(&input)) + .expect("original basis"); + let mut frame = host + .effect_evaluation_frame_v1( + parent, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .evaluate_effect(program, &changed, &[]) + .expect("valid private computation"); + assert!(matches!( + frame.into_effect_evaluation(1024), + Err(EffectCompletionError::InputBasisMismatch) + )); +} +#[test] +fn unused_failed_and_export_limited_frames_cannot_deliver_completed_evidence() { + use warp_core::edict_effects::{EffectCompletionError, EffectReadImageError}; + let (publication, input) = fixture("create-node", "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, _) = host(); + let parent = host + .echo_operation_evaluation_basis_v1(head, basis(&input)) + .expect("basis"); + for mode in 0..3 { + let mut frame = host + .effect_evaluation_frame_v1( + parent, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + let expected = match mode { + 0 => EffectCompletionError::NoCompletedOperation, + 1 => { + assert!(frame.evaluate_effect(program, &[0xff], &[]).is_err()); + EffectCompletionError::NoCompletedOperation + } + _ => { + frame + .evaluate_effect(program, &input, &[]) + .expect("completed"); + EffectCompletionError::ReadImage(EffectReadImageError::ExportLimitExceeded) + } + }; + assert!(matches!(frame.into_effect_evaluation(0),Err(error) if error==expected)); + } +} +#[test] +fn completed_authored_read_retains_the_actual_read_image_and_result() { + use warp_core::edict_effects::{EffectReadSetView, TypedNodeRead}; + let package = prepare(&super::execution::publication()).expect("read program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let input = super::execution::input(node, b"before"); + let parent = host + .echo_operation_evaluation_basis_v1(head, basis(&input)) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + parent, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .evaluate_effect( + program, + &input, + &[TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]), + }], + ) + .expect("read"); + let completed = frame.into_effect_evaluation(4096).expect("completed read"); + assert_eq!(completed.output(), encoded(&V::Bytes(b"before".to_vec()))); + assert_eq!(completed.evaluation_basis(), parent); + assert!(completed.mutations().is_empty()); + let reads = + EffectReadSetView::parse(completed.read_set_image().as_bytes()).expect("read image"); + assert_eq!(reads.rows().len(), 2); + assert_eq!(reads.rows()[0].slot.class, 0); + assert_eq!(reads.rows()[0].slot.warp_id, node.warp_id.0); + assert_eq!(reads.rows()[0].slot.local_id, node.local_id.0); + assert_eq!(reads.rows()[0].read_type, [1; 32]); + assert_eq!(reads.rows()[1].slot.class, 1); + assert_eq!(reads.rows()[1].read_type, [2; 32]); + assert_eq!(completed.read_set_image().consumed().read_bytes(), 134); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/derivation.rs b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs new file mode 100644 index 000000000..64f6414bc --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs @@ -0,0 +1,997 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owner patch derivation from actual completed compiled work. +use super::execution::{host, limits}; +use super::mutations::fixture; +use super::*; +use warp_core::{ + AttachmentKey, EchoOperationAdmissionPolicy, EchoOperationApplicationBasisV1, + EchoOperationBudgetV1, EchoOperationInvocationAdmissionPolicy, NodeId, NodeKey, SlotId, WarpOp, +}; +fn publication() -> V { + publication_for("create-node") +} +fn publication_for(name: &str) -> V { + let (mut publication, _) = fixture(name, "continuum.profile.graph-effects/v1"); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let graph = map([ + ( + "node_types", + V::Map(vec![ + ( + id(1), + map([ + ("addressing", V::Integer(1)), + ("atom_types", V::Array(vec![id(2)])), + ]), + ), + ( + id(7), + map([ + ("addressing", V::Integer(0)), + ("atom_types", V::Array(vec![id(2)])), + ]), + ), + ]), + ), + ( + "edge_types", + V::Map(vec![( + id(6), + V::Array(vec![ + V::Array(vec![id(1), id(1)]), + V::Array(vec![id(1), id(7)]), + V::Array(vec![id(7), id(1)]), + V::Array(vec![id(7), id(7)]), + ]), + )]), + ), + ]); + set(&mut program, "graph_schema", V::Bytes(encoded(&graph))); + let mut permissions = law(); + set( + &mut permissions, + "read_node_types", + V::Array(vec![id(1), id(7)]), + ); + set( + &mut permissions, + "write_node_types", + V::Array(vec![id(1), id(7)]), + ); + set(&mut permissions, "write_edge_types", V::Array(vec![id(6)])); + set(&mut permissions, "max_writes", V::Integer(8)); + set( + &mut program, + "footprint_law", + V::Bytes(encoded(&permissions)), + ); + set(&mut publication, "program", V::Bytes(encoded(&program))); + set( + &mut publication, + "footprint_contract_identity", + footprint(&permissions), + ); + set( + &mut publication, + "budget_ceiling", + map([ + ("steps", V::Integer(10000)), + ("read_bytes", V::Integer(4096)), + ("write_bytes", V::Integer(4096)), + ]), + ); + publication +} +fn budget() -> EchoOperationBudgetV1 { + EchoOperationBudgetV1::new(10000, 4096, 4096) +} +fn evaluate( + host: &mut warp_core::TrustedRuntimeHost, + head: warp_core::WriterHeadKey, + publication: &V, + input: &[u8], +) -> ( + warp_core::InstalledEchoOperation, + warp_core::AdmittedEchoOperationInvocation, + warp_core::edict_effects::CompletedEffectEvaluation, +) { + evaluate_with_aperture(host, head, publication, input, &[]) +} +fn evaluate_with_aperture( + host: &mut warp_core::TrustedRuntimeHost, + head: warp_core::WriterHeadKey, + publication: &V, + input: &[u8], + aperture: &[warp_core::edict_effects::TypedNodeRead], +) -> ( + warp_core::InstalledEchoOperation, + warp_core::AdmittedEchoOperationInvocation, + warp_core::edict_effects::CompletedEffectEvaluation, +) { + let raw = encoded(publication); + let package = prepare(publication).expect("publication"); + let policy = EchoOperationAdmissionPolicy::exact( + hash(b"echo:operation-package:v1\0", &raw), + package.operation_coordinate(), + [8; 32], + budget(), + ); + let admitted_package = host + .admit_echo_operation_package(&policy, raw) + .expect("package policy"); + let installed = host + .install_admitted_echo_operation_package(admitted_package) + .expect("install"); + let app = EchoOperationApplicationBasisV1::new( + hash( + b"echo:operation-profile:v1\0", + b"echo.operation.basis.edict-effect/v1", + ), + hash(b"echo:operation-edict-effect-basis:v1\0", input), + ); + let basis = host + .echo_operation_evaluation_basis_v1(head, app) + .expect("parent"); + let invocation = map([ + ("schema", text("echo.operation-invocation/v1")), + ("kind", text("edict-effect-program/v1")), + ( + "package_id", + V::Bytes(installed.package_identity().to_vec()), + ), + ( + "operation_coordinate", + text(installed.operation_coordinate()), + ), + ("authority_grant_identity", id(9)), + ( + "delegated_budget", + get(publication, "budget_ceiling").clone(), + ), + ("application_input_bytes", V::Bytes(input.to_vec())), + ( + "evaluation_basis", + map([ + ( + "worldline_id", + V::Bytes(head.worldline_id.as_bytes().to_vec()), + ), + ("head_id", V::Bytes(head.head_id.as_bytes().to_vec())), + ( + "worldline_tick", + V::Integer(i128::from(basis.worldline_tick().as_u64())), + ), + ("commit_global_tick", V::Null), + ("state_root", V::Bytes(basis.state_root().to_vec())), + ("commit_id", V::Bytes(basis.commit_id().to_vec())), + ( + "application_basis_schema_identity", + V::Bytes(app.schema_identity().to_vec()), + ), + ( + "application_basis_value_identity", + V::Bytes(app.value_identity().to_vec()), + ), + ]), + ), + ]); + let admitted = host + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new([8; 32], [9; 32], budget()), + &encoded(&invocation), + ) + .expect("invocation"); + let program = installed.program().as_effect_program().expect("effect"); + let mut frame = host + .effect_evaluation_frame_v1(basis, &[], 0, budget(), limits()) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .evaluate_effect(program, input, aperture) + .expect("compiled mutation"); + let completed = frame.into_effect_evaluation(4096).expect("origin"); + (installed, admitted, completed) +} +#[test] +fn runtime_owner_derives_create_node_patch_without_applying_it() { + let (mut host, head, root) = host(); + let publication = publication(); + let input = encoded(&map([ + ("warpId", V::Bytes(root.warp_id.0.to_vec())), + ("nodeId", id(17)), + ("nodeType", id(1)), + ("atomType", id(2)), + ("atomBytes", V::Bytes(b"new".to_vec())), + ])); + let (installed, admitted, completed) = evaluate(&mut host, head, &publication, &input); + let basis = admitted.invocation().evaluation_basis(); + let app = basis.application_basis(); + let derived = host + .derive_echo_operation_effects(&admitted, completed) + .expect("derived patch"); + let node = NodeKey { + warp_id: root.warp_id, + local_id: NodeId([17; 32]), + }; + assert_eq!(derived.patch().ops().len(), 2); + assert!(derived.patch().ops().iter().any(|op|matches!(op,WarpOp::UpsertNode {node:target,record} if *target==node && record.ty==TypeId([1;32])))); + assert!(derived.patch().ops().iter().any( + |op| matches!(op,WarpOp::SetAttachment {key,..} if *key==AttachmentKey::node_alpha(node)) + )); + assert_eq!(derived.patch().rule_pack_id(), installed.identity()); + assert!(derived.patch().out_slots().contains(&SlotId::Node(node))); + assert!(derived + .patch() + .out_slots() + .contains(&SlotId::Attachment(AttachmentKey::node_alpha(node)))); + assert_eq!(derived.consumed().write_bytes(), 67); + assert_eq!(derived.consumed().read_bytes(), 192); + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, app) + .expect("unchanged"), + basis + ); +} +fn atom_hash(ty: u8, bytes: &[u8]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(b"echo.bridge.attachment-value.v1\0"); + h.update(&[0]); + h.update(&[ty; 32]); + h.update(&(bytes.len() as u64).to_le_bytes()); + h.update(bytes); + h.finalize().into() +} +fn leaf_id() -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(b"echo.bridge.node-id.v1\0"); + h.update(&[3; 32]); + h.update(&[7; 32]); + h.update(&atom_hash(2, b"new")); + h.update(&0u64.to_le_bytes()); + h.finalize().into() +} +fn edge_id(from: [u8; 32], to: Option<[u8; 32]>) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(if to.is_some() { + b"echo.bridge.edge-id.v1\0" + } else { + b"echo.bridge.slot-edge-id.v1\0" + }); + h.update(&[3; 32]); + h.update(&from); + h.update(&[6; 32]); + if let Some(to) = to { + h.update(&to); + } + h.finalize().into() +} +fn make_host( + edge: bool, + leaf: bool, + alias: bool, +) -> ( + warp_core::TrustedRuntimeHost, + warp_core::WriterHeadKey, + NodeKey, +) { + use warp_core::{ + AtomPayload, AttachmentValue, EdgeId, EdgeRecord, GraphStore, NodeRecord, WarpId, + }; + let root = NodeKey { + warp_id: WarpId([3; 32]), + local_id: NodeId([4; 32]), + }; + let mut store = GraphStore::new(root.warp_id); + for n in [4, 5] { + store.insert_node( + NodeId([n; 32]), + NodeRecord { + ty: TypeId([1; 32]), + }, + ); + } + store.set_node_attachment( + root.local_id, + Some(AttachmentValue::Atom(AtomPayload { + type_id: TypeId([2; 32]), + bytes: b"before".to_vec().into(), + })), + ); + if leaf { + store.insert_node( + NodeId(leaf_id()), + NodeRecord { + ty: TypeId([7; 32]), + }, + ); + store.set_node_attachment( + NodeId(leaf_id()), + Some(AttachmentValue::Atom(AtomPayload { + type_id: TypeId([2; 32]), + bytes: b"new".to_vec().into(), + })), + ); + } + if edge { + store.insert_edge( + root.local_id, + EdgeRecord { + id: EdgeId(edge_id([4; 32], None)), + from: root.local_id, + to: root.local_id, + ty: TypeId([6; 32]), + }, + ); + } + if alias { + store.insert_edge( + root.local_id, + EdgeRecord { + id: EdgeId([99; 32]), + from: root.local_id, + to: NodeId([5; 32]), + ty: TypeId([6; 32]), + }, + ); + } + super::execution::host_with_store(store, root) +} +fn input(name: &str) -> V { + let mut fields = vec![("warpId", id(3))]; + match name { + "ensure-node" => fields.extend([ + ("nodeId", V::Bytes(leaf_id().to_vec())), + ("nodeType", id(7)), + ("atomType", id(2)), + ("atomBytes", V::Bytes(b"new".to_vec())), + ]), + "create-node" => fields.extend([ + ("nodeId", id(17)), + ("nodeType", id(1)), + ("atomType", id(2)), + ("atomBytes", V::Bytes(b"new".to_vec())), + ]), + "replace-atom" => fields.extend([ + ("nodeId", id(4)), + ( + "expectedAtomDigest", + V::Bytes(atom_hash(2, b"before").to_vec()), + ), + ("atomType", id(2)), + ("atomBytes", V::Bytes(b"new".to_vec())), + ]), + "ensure-edge" => fields.extend([ + ("edgeId", V::Bytes(edge_id([4; 32], None).to_vec())), + ("fromId", id(4)), + ("toId", id(5)), + ("edgeType", id(6)), + ]), + "replace-edge" => fields.extend([ + ("edgeId", V::Bytes(edge_id([4; 32], None).to_vec())), + ("expectedToId", id(4)), + ("toId", id(5)), + ]), + _ => panic!("kind"), + } + map(fields) +} +fn derive_case( + name: &str, + body: V, + publication: V, + edge: bool, + leaf: bool, + alias: bool, +) -> Result< + warp_core::edict_effects::DerivedEffectPatch, + warp_core::edict_effects::EffectDerivationError, +> { + let (mut host, head, _) = make_host(edge, leaf, alias); + let (_, admitted, completed) = evaluate(&mut host, head, &publication, &encoded(&body)); + let before = admitted.invocation().evaluation_basis(); + let result = host.derive_echo_operation_effects(&admitted, completed); + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, before.application_basis()) + .expect(name), + before + ); + result +} +#[test] +fn all_five_mutations_derive_their_exact_operation_shapes() { + for name in [ + "ensure-node", + "create-node", + "replace-atom", + "ensure-edge", + "replace-edge", + ] { + let derived = derive_case( + name, + input(name), + publication_for(name), + name == "replace-edge", + false, + false, + ) + .expect(name); + let ops = derived.patch().ops(); + assert_eq!(ops.len(), if name.ends_with("node") { 2 } else { 1 }); + assert_eq!( + derived.consumed().write_bytes(), + if name.ends_with("edge") { 128 } else { 67 } + ); + match name { + "ensure-node"=>assert!(ops.iter().any(|op|matches!(op,WarpOp::UpsertNode {node,record} if node.local_id.0==leaf_id() && record.ty==TypeId([7;32])))), + "create-node"=>assert!(ops.iter().any(|op|matches!(op,WarpOp::UpsertNode {node,record} if node.local_id.0==[17;32] && record.ty==TypeId([1;32])))), + "replace-atom"=>assert!(matches!(&ops[0],WarpOp::SetAttachment {key,value:Some(warp_core::AttachmentValue::Atom(atom))} if key==&AttachmentKey::node_alpha(NodeKey {warp_id:warp_core::WarpId([3;32]),local_id:NodeId([4;32])}) && atom.type_id==TypeId([2;32]) && atom.bytes.as_ref()==b"new")), + "ensure-edge"|"replace-edge"=>assert!(matches!(&ops[0],WarpOp::UpsertEdge {warp_id,record} if warp_id.0==[3;32] && record.id.0==edge_id([4;32],None) && record.from.0==[4;32] && record.to.0==[5;32] && record.ty.0==[6;32])), + _=>panic!("kind"), + } + derived + .patch() + .validate_digest() + .expect("canonical patch digest"); + } +} +#[test] +fn identical_ensures_derive_no_writes_but_keep_emission_charges() { + for name in ["ensure-node", "ensure-edge"] { + let mut body = input(name); + if name == "ensure-edge" { + set(&mut body, "toId", id(4)); + } + let derived = derive_case( + name, + body, + publication_for(name), + name == "ensure-edge", + name == "ensure-node", + false, + ) + .expect("identical Ensure"); + assert!(derived.patch().ops().is_empty()); + assert!(derived.patch().out_slots().is_empty()); + assert_eq!( + derived.consumed().write_bytes(), + if name == "ensure-node" { 67 } else { 128 } + ); + } +} +#[test] +fn mutation_comparisons_missing_endpoints_and_aliases_refuse_before_patch_derivation() { + use warp_core::edict_effects::EffectDerivationKind as K; + for (name, field, value, edge, want) in [ + ( + "replace-atom", + "expectedAtomDigest", + id(99), + false, + K::PreconditionMismatch, + ), + ( + "replace-edge", + "expectedToId", + id(99), + true, + K::PreconditionMismatch, + ), + ("ensure-edge", "toId", id(99), false, K::DanglingReference), + ( + "create-node", + "nodeId", + id(4), + false, + K::PreconditionMismatch, + ), + ( + "ensure-node", + "nodeId", + id(17), + false, + K::IdentityLawViolation, + ), + ] { + let mut body = input(name); + set(&mut body, field, value); + let error = derive_case(name, body, publication_for(name), edge, false, false) + .expect_err("must refuse"); + assert_eq!(error.kind(), want, "{name}"); + } + let error = derive_case( + "ensure-edge", + input("ensure-edge"), + publication_for("ensure-edge"), + false, + false, + true, + ) + .expect_err("alias"); + assert_eq!(error.kind(), K::IdentityLawViolation); +} + +// Removing the owner-token check would admit a foreign runtime's authority. +#[test] +fn derivation_rejects_another_runtime_owners_invocation() { + let (mut producing, head, _) = make_host(false, false, false); + let (_, admitted, completed) = evaluate( + &mut producing, + head, + &publication(), + &encoded(&input("create-node")), + ); + let (other, _, _) = make_host(false, false, false); + let error = other + .derive_echo_operation_effects(&admitted, completed) + .expect_err("foreign admission authority"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::AuthorityMismatch + ); +} + +// Resetting the validation meter would lose the successful resolution prefix. +#[test] +fn validation_budget_refusal_retains_evaluation_and_resolution_costs() { + let mut publication = publication(); + set( + &mut publication, + "budget_ceiling", + map([ + ("steps", V::Integer(10000)), + ("read_bytes", V::Integer(191)), + ("write_bytes", V::Integer(4096)), + ]), + ); + let error = derive_case( + "create-node", + input("create-node"), + publication, + false, + false, + false, + ) + .expect_err("three presence reads require 192 bytes"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::BudgetExceeded + ); + assert_eq!(error.consumed().read_bytes(), 128); + assert_eq!(error.consumed().write_bytes(), 67); +} + +fn with_law_field(mut publication: V, name: &str, value: V) -> V { + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let V::Bytes(raw) = get(&program, "footprint_law") else { + panic!("law") + }; + let mut law = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("law"); + set(&mut law, name, value); + set(&mut program, "footprint_law", V::Bytes(encoded(&law))); + set(&mut publication, "program", V::Bytes(encoded(&program))); + set( + &mut publication, + "footprint_contract_identity", + footprint(&law), + ); + publication +} + +// Ignoring range writes would incorrectly fit an edge write into a one-slot law. +#[test] +fn resolved_write_limit_counts_attachment_and_range_dependencies() { + for name in ["create-node", "ensure-edge", "replace-edge"] { + let publication = with_law_field(publication_for(name), "max_writes", V::Integer(1)); + let error = derive_case( + name, + input(name), + publication, + name == "replace-edge", + false, + false, + ) + .expect_err("each mutation resolves two write slots"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::FootprintLawViolation, + "{name}" + ); + } +} + +// Returning early for an idempotent Ensure must not skip typed permissions. +#[test] +fn identical_ensure_still_requires_write_permission() { + let publication = with_law_field( + publication_for("ensure-node"), + "write_node_types", + V::Array(vec![]), + ); + let error = derive_case( + "ensure-node", + input("ensure-node"), + publication, + false, + true, + false, + ) + .expect_err("no-op Ensure lacks node write permission"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::FootprintLawViolation + ); +} + +fn portal_host( + depth: u8, + beta: bool, +) -> ( + warp_core::TrustedRuntimeHost, + warp_core::WriterHeadKey, + Vec, + NodeKey, +) { + use warp_core::{ + EdgeId, EdgeKey, EdgeRecord, PortalInit, TickCommitStatus, WarpId, WarpTickPatchV1, + }; + let (host, head, root) = make_host(false, false, false); + let mut state = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("worldline") + .state() + .warp_state() + .clone(); + let mut owner = root; + let mut owners = Vec::new(); + for index in 0..depth { + owners.push(owner); + let child = NodeKey { + warp_id: WarpId([8 + index; 32]), + local_id: NodeId([4; 32]), + }; + let key = if beta { + let edge = EdgeKey { + warp_id: owner.warp_id, + local_id: EdgeId([6; 32]), + }; + state + .store_mut(&owner.warp_id) + .expect("parent store") + .insert_edge( + owner.local_id, + EdgeRecord { + id: edge.local_id, + from: owner.local_id, + to: owner.local_id, + ty: TypeId([6; 32]), + }, + ); + AttachmentKey::edge_beta(edge) + } else { + AttachmentKey::node_alpha(owner) + }; + WarpTickPatchV1::new( + 0, + [0; 32], + TickCommitStatus::Committed, + vec![], + vec![], + vec![WarpOp::OpenPortal { + key, + child_warp: child.warp_id, + child_root: child.local_id, + init: PortalInit::Empty { + root_record: warp_core::NodeRecord { + ty: TypeId([1; 32]), + }, + }, + }], + ) + .apply_to_state(&mut state) + .expect("portal"); + owner = child; + } + let (host, head, _) = super::execution::host_with_state( + warp_core::WorldlineState::new(state, root).expect("portal state"), + ); + (host, head, owners, owner) +} + +// Omitting portal resolutions would lose ancestors from the derived footprint. +#[test] +fn descendant_mutation_records_every_portal_dependency_and_charge() { + for depth in [1, 2] { + let (mut host, head, owners, target) = portal_host(depth, false); + let mut body = input("create-node"); + set(&mut body, "warpId", V::Bytes(target.warp_id.0.to_vec())); + let publication = + with_law_field(publication(), "portal_owner_types", V::Array(vec![id(1)])); + let (_, admitted, completed) = evaluate(&mut host, head, &publication, &encoded(&body)); + let basis = admitted.invocation().evaluation_basis(); + let derived = host + .derive_echo_operation_effects(&admitted, completed) + .expect("descendant mutation"); + assert_eq!( + derived.consumed().read_bytes(), + 192 + u64::from(depth) * 128 + ); + assert_eq!(derived.consumed().write_bytes(), 67); + for owner in owners { + assert!(derived.patch().in_slots().contains(&SlotId::Node(owner))); + assert!(derived + .patch() + .in_slots() + .contains(&SlotId::Attachment(AttachmentKey::node_alpha(owner)))); + assert!(derived.footprint().n_read.iter().any(|key| *key == owner)); + assert!(derived + .footprint() + .a_read + .iter() + .any(|key| *key == AttachmentKey::node_alpha(owner))); + } + assert!(derived.patch().ops().iter().any(|op|matches!(op,WarpOp::UpsertNode {node,..} if node.warp_id==target.warp_id && node.local_id==NodeId([17;32])))); + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, basis.application_basis()) + .expect("unchanged"), + basis + ); + } +} + +// Authorizing ordinary node writes must not implicitly authorize portal traversal. +#[test] +fn descendant_mutation_refuses_unpermitted_portal_owners_and_beta_portals() { + use warp_core::edict_effects::EffectDerivationKind as K; + for beta in [false, true] { + let (mut host, head, _, target) = portal_host(1, beta); + let mut body = input("create-node"); + set(&mut body, "warpId", V::Bytes(target.warp_id.0.to_vec())); + let (_, admitted, completed) = evaluate(&mut host, head, &publication(), &encoded(&body)); + let error = host + .derive_echo_operation_effects(&admitted, completed) + .expect_err("portal not authorized"); + assert_eq!(error.kind(), K::FootprintLawViolation); + assert_eq!(error.consumed().write_bytes(), 67); + assert_eq!(error.consumed().read_bytes(), if beta { 0 } else { 64 }); + } +} + +// A reachable-state root does not bind detached records at an uncommitted parent. +// Removing the completed-origin owner check would import a different store's reads. +#[test] +fn completed_origin_from_another_owner_refuses_matching_parent_coordinates() { + let (mut host, head, _) = make_host(false, false, false); + let input = encoded(&input("create-node")); + let (_, admitted, _) = evaluate(&mut host, head, &publication(), &input); + let (mut other, other_head, _) = make_host(false, true, false); + let (_, _, completed) = evaluate(&mut other, other_head, &publication(), &input); + assert_eq!( + completed.evaluation_basis(), + admitted.invocation().evaluation_basis() + ); + let result = host.derive_echo_operation_effects(&admitted, completed); + assert!( + matches!(result,Err(ref error) if error.kind()==warp_core::edict_effects::EffectDerivationKind::AuthorityMismatch), + "foreign completed work crossed the runtime owner" + ); +} + +// Program identity is independent of identical inputs and identical runtime parents. +#[test] +fn completed_work_cannot_be_substituted_from_another_program() { + let (mut host, head, _) = make_host(false, false, false); + let input = encoded(&input("create-node")); + let (installed, admitted, _) = evaluate(&mut host, head, &publication(), &input); + let alternate = prepare(&publication_for("ensure-node")).expect("alternate program"); + let program = alternate.program().as_effect_program().expect("effect"); + let basis = admitted.invocation().evaluation_basis(); + let mut frame = host + .effect_evaluation_frame_v1(basis, &[], 0, budget(), limits()) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .evaluate_effect(program, &input, &[]) + .expect("alternate private execution"); + let completed = frame.into_effect_evaluation(4096).expect("completion"); + assert_ne!(completed.program_identity(), installed.program_identity()); + let error = host + .derive_echo_operation_effects(&admitted, completed) + .expect_err("wrong program"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::OriginMismatch + ); +} + +// Derivation must preserve the actual read footprint even without mutations. +#[test] +fn read_only_completion_derives_empty_patch_without_recharging_witnessed_reads() { + let (mut host, head, node) = host(); + let mut publication = super::execution::publication(); + set( + &mut publication, + "budget_ceiling", + get(&self::publication(), "budget_ceiling").clone(), + ); + let input = super::execution::input(node, b"before"); + let (_, admitted, completed) = evaluate_with_aperture( + &mut host, + head, + &publication, + &input, + &[warp_core::edict_effects::TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]), + }], + ); + let consumed = completed.read_set_image().consumed(); + assert_eq!(consumed.read_bytes(), 134); + let derived = host + .derive_echo_operation_effects(&admitted, completed) + .expect("read-only patch"); + assert_eq!(derived.consumed(), consumed); + assert_eq!( + derived.completed().output(), + encoded(&V::Bytes(b"before".to_vec())) + ); + assert!(derived.patch().ops().is_empty()); + assert!(derived.patch().out_slots().is_empty()); + assert_eq!(derived.patch().in_slots().len(), 2); + assert!(derived.patch().in_slots().contains(&SlotId::Node(node))); + assert!(derived + .patch() + .in_slots() + .contains(&SlotId::Attachment(AttachmentKey::node_alpha(node)))); +} + +// Matching program identity alone cannot authorize another input's bound parent. +#[test] +fn completed_work_cannot_be_substituted_from_another_application_basis() { + let (mut host, head, _) = host(); + let input = encoded(&input("create-node")); + let (installed, admitted, _) = evaluate(&mut host, head, &publication(), &input); + let other = encoded(&input_for_other_basis()); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new( + hash( + b"echo:operation-profile:v1\0", + b"echo.operation.basis.edict-effect/v1", + ), + hash(b"echo:operation-edict-effect-basis:v1\0", &other), + ), + ) + .expect("other basis"); + let program = installed.program().as_effect_program().expect("effect"); + let mut frame = host + .effect_evaluation_frame_v1(basis, &[], 0, budget(), limits()) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + frame + .evaluate_effect(program, &other, &[]) + .expect("other input"); + let completed = frame.into_effect_evaluation(4096).expect("completion"); + assert_eq!(completed.program_identity(), installed.program_identity()); + assert_ne!( + completed.evaluation_basis(), + admitted.invocation().evaluation_basis() + ); + let error = host + .derive_echo_operation_effects(&admitted, completed) + .expect_err("wrong application basis"); + assert_eq!( + error.kind(), + warp_core::edict_effects::EffectDerivationKind::OriginMismatch + ); +} +fn input_for_other_basis() -> V { + let mut body = input("create-node"); + set(&mut body, "nodeId", id(18)); + body +} + +// Reconstruction through owned parts must retain both admission and origin owners. +#[test] +fn host_reconfiguration_preserves_its_completed_evaluation_authority() { + let (mut host, head, _) = host(); + let (_, admitted, completed) = evaluate( + &mut host, + head, + &publication(), + &encoded(&input("create-node")), + ); + let host = warp_core::TrustedRuntimeHost::from_parts(host.into_parts()); + let derived = host + .derive_echo_operation_effects(&admitted, completed) + .expect("same runtime owner after parts round trip"); + assert_eq!(derived.patch().ops().len(), 2); +} + +// Each case violates two phases; returning whichever check happens to run first +// would make the public refusal taxonomy unstable after refactoring. +#[test] +fn competing_mutation_failures_follow_the_validation_phase_order() { + use warp_core::edict_effects::EffectDerivationKind as K; + for (name, input_field, input_value, law_field, want, edge) in [ + ( + "ensure-node", + "nodeType", + id(99), + None, + K::SchemaViolation, + false, + ), + ( + "ensure-node", + "nodeId", + id(17), + Some("write_node_types"), + K::IdentityLawViolation, + false, + ), + ( + "replace-atom", + "nodeId", + id(99), + None, + K::NodeMissing, + false, + ), + ( + "create-node", + "nodeId", + id(4), + Some("write_node_types"), + K::FootprintLawViolation, + false, + ), + ( + "ensure-edge", + "edgeId", + id(88), + None, + K::IdentityLawViolation, + false, + ), + ( + "replace-edge", + "expectedToId", + id(99), + Some("write_edge_types"), + K::FootprintLawViolation, + true, + ), + ] { + let mut body = input(name); + if name == "ensure-node" { + set(&mut body, "nodeId", id(17)); + } + if name == "replace-atom" { + set(&mut body, "atomType", id(99)); + } + if name == "ensure-edge" { + set(&mut body, "toId", id(99)); + } + set(&mut body, input_field, input_value); + let mut publication = publication_for(name); + if let Some(field) = law_field { + publication = with_law_field(publication, field, V::Array(vec![])); + } + let error = derive_case(name, body, publication, edge, false, false) + .expect_err("multiple invalid claims"); + assert_eq!(error.kind(), want, "{name}: {input_field}"); + } +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/execution.rs b/crates/warp-core/tests/edict_effect_package_tests/execution.rs new file mode 100644 index 000000000..18703585e --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Private authored instruction execution, without scheduler mutation authority. +use super::*; +use warp_core::edict_effects::{EffectFrameError, TypedNodeRead}; +use warp_core::edict_pure::EvaluationLimits; +use warp_core::{ + make_head_id, AtomPayload, AttachmentValue, EchoOperationBudgetV1, EngineBuilder, GraphStore, + InboxPolicy, NodeId, NodeKey, NodeRecord, PlaybackMode, TrustedRuntimeHost, WarpId, + WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, +}; +pub(super) fn host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { + let key = NodeKey { + warp_id: WarpId([3; 32]), + local_id: NodeId([4; 32]), + }; + let mut store = GraphStore::new(key.warp_id); + store.insert_node( + key.local_id, + NodeRecord { + ty: TypeId([1; 32]), + }, + ); + store.set_node_attachment( + key.local_id, + Some(AttachmentValue::Atom(AtomPayload { + type_id: TypeId([2; 32]), + bytes: b"before".to_vec().into(), + })), + ); + host_with_store(store, key) +} +pub(super) fn host_with_store( + store: GraphStore, + key: NodeKey, +) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { + let state = WorldlineState::from_root_store(store, key.local_id).expect("state"); + host_with_state(state) +} +pub(super) fn host_with_state( + state: WorldlineState, +) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { + let key = *state.root(); + let store = state.store(&key.warp_id).expect("root store").clone(); + let head = WriterHeadKey { + worldline_id: WorldlineId::from_bytes([5; 32]), + head_id: make_head_id("authored-execution"), + }; + let mut runtime = WorldlineRuntime::new(); + runtime + .register_worldline(head.worldline_id, state) + .expect("worldline"); + runtime + .register_writer_head(WriterHead::with_routing( + head, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )) + .expect("head"); + let engine = EngineBuilder::new(store, key.local_id).build(); + ( + TrustedRuntimeHost::new(runtime, engine).expect("host"), + head, + key, + ) +} +pub(super) fn input(node: NodeKey, expected: &[u8]) -> Vec { + encoded(&map([ + ( + "address", + map([ + ("nodeId", V::Bytes(node.local_id.0.to_vec())), + ("warpId", V::Bytes(node.warp_id.0.to_vec())), + ("typeId", id(2)), + ]), + ), + ("expected", V::Bytes(expected.to_vec())), + ])) +} +pub(super) fn publication() -> V { + let mut publication = publication_for_lowering(&repinned_lowering_artifacts()); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let mut permissions = law(); + // A real typed atom read resolves both a node slot and its attachment slot. + set(&mut permissions, "max_reads", V::Integer(2)); + set( + &mut program, + "footprint_law", + V::Bytes(encoded(&permissions)), + ); + set(&mut publication, "program", V::Bytes(encoded(&program))); + set( + &mut publication, + "footprint_contract_identity", + footprint(&permissions), + ); + publication +} +pub(super) fn limits() -> EvaluationLimits { + EvaluationLimits { + max_package_bytes: 1_048_576, + max_input_bytes: 65536, + max_steps: 10000, + max_allocated_bytes: 1_048_576, + max_output_bytes: 65536, + } +} +#[test] +fn authored_instruction_execution_reads_the_real_atom_and_records_dependencies() { + // Returning an input field or skipping the read/require must fail this witness. + let publication = publication(); + let package = prepare(&publication).expect("synthetic bound program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + let result = frame + .evaluate_effect( + program, + &input(node, b"before"), + &[TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]), + }], + ) + .expect("authored evaluation"); + assert_eq!(result, encoded(&V::Bytes(b"before".to_vec()))); + assert_eq!(frame.readings().len(), 2); + assert!(frame.footprint().n_read.iter().any(|key| *key == node)); + assert!(frame.consumed().steps() > 2); + assert!(frame.consumed().read_bytes() >= 134); +} +#[test] +fn authored_require_uses_the_real_read_and_ends_the_frame_on_failure() { + let package = prepare(&publication()).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + let aperture = [TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]), + }]; + let error = frame + .evaluate_effect(program, &input(node, b"different"), &aperture) + .expect_err("require must run"); + assert!(matches!( + error, + EffectFrameError::AuthoredObstruction { .. } + )); + assert_eq!(frame.readings().len(), 2); + let consumed = frame.consumed(); + assert_eq!( + frame.evaluate_effect(program, &input(node, b"before"), &aperture), + Err(EffectFrameError::Terminated) + ); + assert_eq!(frame.consumed(), consumed); +} +#[test] +fn authored_execution_refuses_unbound_frames_and_mismatched_laws_before_reads() { + use warp_core::edict_effects::{ + EffectEvaluationFrame, EffectFootprintLaw, EffectRangeReadError, EffectRangeReadSession, + }; + let package = prepare(&publication()).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let frontier = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("frontier"); + let mut unbound = EffectEvaluationFrame::new( + EffectRangeReadSession::new(frontier, &[], 0, EchoOperationBudgetV1::new(10000, 4096, 0)), + limits(), + ) + .expect("unbound"); + assert_eq!( + unbound.evaluate_effect(program, &input(node, b"before"), &[]), + Err(EffectFrameError::UnboundParent) + ); + assert!(unbound.readings().is_empty()); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law( + EffectFootprintLaw::from_canonical_bytes(&encoded(&law())).expect("different law"), + ) + .expect("bind"); + assert_eq!( + frame.evaluate_effect(program, &input(node, b"before"), &[]), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(frame.readings().is_empty()); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); +} +#[test] +fn authored_execution_enforces_aperture_and_shared_resource_limits() { + use warp_core::edict_pure::EvaluationError; + use warp_core::edict_read::ReadError; + let package = prepare(&publication()).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + for (aperture, cap, want) in [ + ( + vec![], + 10000, + EffectFrameError::BoundedRead(ReadError::OutsideAperture(node)), + ), + ( + vec![TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]), + }], + 1, + EffectFrameError::BoundedRead(ReadError::Evaluation( + EvaluationError::StepBudgetExceeded, + )), + ), + ] { + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(cap, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect(program, &input(node, b"before"), &aperture), + Err(want) + ); + assert!(frame.readings().is_empty()); + assert!(frame.consumed().steps() <= cap); + assert_eq!( + frame.evaluate_effect(program, &input(node, b"before"), &aperture), + Err(EffectFrameError::Terminated) + ); + } +} +#[test] +fn authored_absence_maps_the_declared_failure_without_returning_a_value() { + let package = prepare(&publication()).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, node) = host(); + let missing = NodeKey { + local_id: NodeId([9; 32]), + ..node + }; + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect( + program, + &input(missing, b"before"), + &[TypedNodeRead { + node: missing, + absent_node_type: TypeId([1; 32]) + }] + ), + Err(EffectFrameError::AuthoredObstruction { + reason_kind: "text.FactMissing".into(), + payload: encoded(&map([])) + }) + ); + assert_eq!(frame.readings().len(), 1); + assert!(frame.footprint().n_read.iter().any(|key| *key == missing)); +} +#[test] +fn authored_execution_runs_ordered_pure_bindings_under_the_same_program_model() { + let mut core = compiled_core(); + set( + value_field_mut( + value_field_mut(value_field_mut(&mut core, "intents"), "update@1"), + "coreEvaluationBudget", + ), + "maxSteps", + V::Integer(1000), + ); + let binding = map([ + ("id", text("local.0")), + ("type", text("U32")), + ("alphaName", text("$value")), + ]); + let value = map([ + ("kind", text("const")), + ( + "value", + map([ + ("kind", text("int")), + ("width", text("U32")), + ("value", V::Integer(7)), + ]), + ), + ]); + let body = core_body_mut(&mut core); + let V::Array(locals) = value_field_mut(body, "locals") else { + panic!("locals") + }; + locals.push(binding.clone()); + set( + body, + "nodes", + V::Array(vec![map([ + ("kind", text("let")), + ("binding", binding.clone()), + ("value", value.clone()), + ])]), + ); + set( + body, + "result", + map([("kind", text("local")), ("ref", binding.clone())]), + ); + let mut target = compiled_target(&core); + let selected = selected_target_mut(&mut target); + set( + selected, + "pureBindings", + V::Array(vec![map([ + ("id", text("binding.0")), + ("binding", binding), + ("value", value), + ])]), + ); + set( + selected, + "executionOrder", + V::Array(vec![text("binding.0")]), + ); + let package = prepare(&bound_compiler_package( + &core, + &target, + &compiled_exports(), + "update@1", + )) + .expect("pure bindings"); + let program = package.program().as_effect_program().expect("program"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame + .evaluate_effect(program, &encoded(&V::Integer(123)), &[]) + .expect("let/result"), + encoded(&V::Integer(7)) + ); + assert!(frame.readings().is_empty()); +} +#[test] +fn authored_reads_enforce_the_prepared_graph_schema() { + let mut publication = publication(); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut program = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + let schema = map([ + ( + "node_types", + V::Map(vec![( + id(1), + map([ + ("addressing", V::Integer(1)), + ("atom_types", V::Array(vec![])), + ]), + )]), + ), + ("edge_types", V::Map(vec![])), + ]); + set(&mut program, "graph_schema", V::Bytes(encoded(&schema))); + set(&mut publication, "program", V::Bytes(encoded(&program))); + let package = prepare(&publication).expect("schema"); + let program = package.program().as_effect_program().expect("program"); + let (host, head, node) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + warp_core::EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect( + program, + &input(node, b"before"), + &[TypedNodeRead { + node, + absent_node_type: TypeId([1; 32]) + }] + ), + Err(EffectFrameError::GraphSchemaViolation) + ); + assert_eq!(frame.readings().len(), 2); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/invocation.rs b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs new file mode 100644 index 000000000..b4f3d13d7 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Invocation byte/basis binding without runtime authority admission. +use super::*; +use warp_core::EchoOperationInvocation; +fn invocation(input: Vec) -> V { + let package_bytes = encoded(&package(&program())); + map([ + ("schema", text("echo.operation-invocation/v1")), + ("kind", text("edict-effect-program/v1")), + ("authority_grant_identity", id(9)), + ( + "delegated_budget", + map([ + ("steps", V::Integer(1)), + ("read_bytes", V::Integer(0)), + ("write_bytes", V::Integer(0)), + ]), + ), + ("operation_coordinate", text("app.update@1")), + ( + "package_id", + V::Bytes(hash(b"echo:operation-package:v1\0", &package_bytes).to_vec()), + ), + ("application_input_bytes", V::Bytes(input.clone())), + ( + "evaluation_basis", + map([ + ( + "application_basis_schema_identity", + profile("echo.operation.basis.edict-effect/v1"), + ), + ( + "application_basis_value_identity", + V::Bytes(hash(b"echo:operation-edict-effect-basis:v1\0", &input).to_vec()), + ), + ("commit_global_tick", V::Null), + ("commit_id", id(5)), + ("head_id", id(6)), + ("state_root", id(7)), + ("worldline_id", id(8)), + ("worldline_tick", V::Integer(0)), + ]), + ), + ]) +} +fn check(v: &V) -> Result { + EchoOperationInvocation::prepare( + &encoded(v), + &prepare(&package(&program())).expect("package"), + ) +} +fn basis_set(v: &mut V, k: &str, x: V) { + let V::Map(m) = v else { panic!("map") }; + set( + &mut m + .iter_mut() + .find(|(key, _)| key == &text("evaluation_basis")) + .expect("basis") + .1, + k, + x, + ); +} +#[test] +fn invocation_binds_canonical_input_and_retains_the_exact_v1_parent_basis() { + let value = invocation(encoded(&V::Integer(42))); + let bytes = encoded(&value); + let prepared = check(&value).expect("valid current invocation"); + assert_eq!(prepared.to_canonical_bytes().expect("canonical"), bytes); + assert_eq!( + prepared.identity().expect("identity").as_hash(), + hash(b"echo:operation-invocation:v1\0", &bytes) + ); + assert_eq!( + prepared.application_input_bytes(), + Some(encoded(&V::Integer(42)).as_slice()) + ); + assert_eq!(prepared.evaluation_basis().state_root(), [7; 32]); + assert_eq!(prepared.evaluation_basis().commit_id(), [5; 32]); + assert_eq!(prepared.authority_grant_identity(), [9; 32]); +} +#[test] +fn altered_input_cannot_keep_its_predecessors_basis_digest() { + let mut value = invocation(encoded(&V::Integer(42))); + set( + &mut value, + "application_input_bytes", + V::Bytes(encoded(&V::Integer(43))), + ); + assert!(check(&value).is_err()); + basis_set( + &mut value, + "application_basis_value_identity", + V::Bytes( + hash( + b"echo:operation-edict-effect-basis:v1\0", + &encoded(&V::Integer(43)), + ) + .to_vec(), + ), + ); + assert!(check(&value).is_ok()); + basis_set(&mut value, "application_basis_schema_identity", id(88)); + assert!(check(&value).is_err()); +} +#[test] +fn invocation_refuses_cross_version_cross_package_and_cross_operation_substitution() { + for (k, v) in [ + ("schema", text("echo.operation-invocation/v2")), + ("package_id", id(5)), + ("operation_coordinate", text("app.other@1")), + ("authority_grant_identity", V::Bytes(vec![9; 31])), + ] { + let mut value = invocation(encoded(&V::Integer(42))); + set(&mut value, k, v); + assert!(check(&value).is_err()); + } + for k in ["node", "node_id", "warp_id", "replacement_bytes", "extra"] { + let mut value = invocation(encoded(&V::Integer(42))); + add(&mut value, k, id(1)); + assert!(check(&value).is_err()); + } +} +#[test] +fn invocation_refuses_noncanonical_input_or_input_exceeding_its_declared_ceiling() { + for input in [ + vec![], + vec![0x18, 0], + vec![0, 0], + encoded(&V::Bytes(vec![0; 65536])), + ] { + assert!(check(&invocation(input)).is_err()); + } + // A 65,536-byte canonical bstr fits; its CBOR header counts toward the limit. + let input = encoded(&V::Bytes(vec![0; 65533])); + assert_eq!(input.len(), 65536); + assert!(check(&invocation(input)).is_ok()); +} +#[test] +fn delegated_budget_cannot_be_zero_or_exceed_any_package_axis() { + for (steps, read, write) in [(0, 0, 0), (2, 0, 0), (1, 1, 0), (1, 0, 1)] { + let mut value = invocation(encoded(&V::Integer(42))); + set( + &mut value, + "delegated_budget", + map([ + ("steps", V::Integer(steps)), + ("read_bytes", V::Integer(read)), + ("write_bytes", V::Integer(write)), + ]), + ); + assert!(check(&value).is_err()); + } +} +#[test] +fn invocation_parent_uses_the_exact_v1_map_without_normalizing_unknown_fields() { + let mut value = invocation(encoded(&V::Integer(42))); + basis_set(&mut value, "worldline_tick", V::Integer(-1)); + assert!(check(&value).is_err()); + let mut value = invocation(encoded(&V::Integer(42))); + basis_set(&mut value, "commit_id", V::Bytes(vec![0; 33])); + assert!(check(&value).is_err()); + let mut value = invocation(encoded(&V::Integer(42))); + let V::Map(m) = &mut value else { panic!("map") }; + add( + &mut m + .iter_mut() + .find(|(k, _)| k == &text("evaluation_basis")) + .expect("basis") + .1, + "extra", + id(1), + ); + assert!(check(&value).is_err()); +} +#[test] +fn input_uses_the_selected_packages_smaller_ceiling_and_exact_outer_bytes() { + let mut p = program(); + set(&mut p, "max_input_bytes", V::Integer(1)); + let value = package(&p); + let selected = prepare(&value).expect("smaller ceiling package"); + for (input, accepts) in [ + (encoded(&V::Integer(0)), true), + (encoded(&V::Integer(42)), false), + ] { + let mut value = invocation(input); + set( + &mut value, + "package_id", + V::Bytes( + hash( + b"echo:operation-package:v1\0", + &selected.to_canonical_bytes().expect("selected publication"), + ) + .to_vec(), + ), + ); + assert_eq!( + EchoOperationInvocation::prepare(&encoded(&value), &selected).is_ok(), + accepts + ); + } + let mut bytes = encoded(&invocation(encoded(&V::Integer(42)))); + bytes.push(0); + assert!(EchoOperationInvocation::prepare( + &bytes, + &prepare(&package(&program())).expect("original") + ) + .is_err()); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/mutations.rs b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs new file mode 100644 index 000000000..dcd122000 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Synthetic compiled mutation emitters; not public compiler/verifier evidence. +use super::execution::{host, limits}; +use super::*; +use warp_core::edict_effects::EffectMutation; +use warp_core::{ + AtomPayload, EchoOperationApplicationBasisV1, EchoOperationBudgetV1, EdgeId, EdgeKey, NodeId, + NodeKey, WarpId, +}; +const NAMES: [&str; 5] = [ + "ensure-node", + "create-node", + "replace-atom", + "ensure-edge", + "replace-edge", +]; +/// The provider effect kind each mutation intrinsic declares; Edict's +/// intrinsic ABI also makes it the write class. +pub(super) fn effect_kind(name: &str) -> &'static str { + match name { + "create-node" => "create", + "ensure-node" | "ensure-edge" => "ensure", + "replace-atom" | "replace-edge" => "replace", + _ => panic!("fixture kind"), + } +} +fn layout(name: &str) -> Vec<(&'static str, &'static str)> { + match name { + "ensure-node" | "create-node" => vec![ + ("warpId", "Bytes"), + ("nodeId", "Bytes"), + ("nodeType", "Bytes"), + ("atomType", "Bytes"), + ("atomBytes", "Bytes"), + ], + "replace-atom" => vec![ + ("warpId", "Bytes"), + ("nodeId", "Bytes"), + ("expectedAtomDigest", "Bytes"), + ("atomType", "Bytes"), + ("atomBytes", "Bytes"), + ], + "ensure-edge" => vec![ + ("warpId", "Bytes"), + ("edgeId", "Bytes"), + ("fromId", "Bytes"), + ("toId", "Bytes"), + ("edgeType", "Bytes"), + ], + "replace-edge" => vec![ + ("warpId", "Bytes"), + ("edgeId", "Bytes"), + ("expectedToId", "Bytes"), + ("toId", "Bytes"), + ], + _ => panic!("fixture kind"), + } +} +/// One application's view of a runtime-ABI call: its own type names, the +/// catalog coordinates the provider pins, and the declared representations. +pub(super) struct Shape { + pub(super) input_name: &'static str, + pub(super) output_name: &'static str, + pub(super) catalog_argument: String, + pub(super) catalog_return: String, + pub(super) fields: Vec<(&'static str, &'static str)>, + pub(super) output: V, + pub(super) effect_kind: String, + pub(super) write_class: String, +} +impl Shape { + /// The conforming shape: the catalog names Echo's ABI coordinates and the + /// application declares its own type names over the ABI representation. + pub(super) fn conforming(name: &str) -> Self { + Self { + input_name: "MutationInput", + output_name: "MutationUnit", + catalog_argument: format!("echo.dpo@1.{name}.input"), + catalog_return: format!("echo.dpo@1.{name}.result"), + fields: layout(name), + output: map([("kind", text("Record")), ("fields", map([]))]), + effect_kind: effect_kind(name).to_owned(), + write_class: effect_kind(name).to_owned(), + } + } +} +pub(super) fn fixture(name: &str, profile: &str) -> (V, Vec) { + fixture_with(name, profile, &Shape::conforming(name)) +} +pub(super) fn fixture_with(name: &str, profile: &str, shape: &Shape) -> (V, Vec) { + let artifacts = repinned_lowering_artifacts(); + let decode = |key| { + let V::Bytes(raw) = get(&artifacts, key) else { + panic!("artifact") + }; + echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("artifact") + }; + let (_, _, mut exports) = compiler_fixture(&artifacts); + let V::Array(effects) = value_field_mut(&mut exports, "effects") else { + panic!("effects") + }; + let effect = &mut effects[0]; + set(effect, "inputType", text(shape.input_name)); + set(effect, "outputType", text(shape.output_name)); + set(effect, "effectFailures", map([])); + let mut adapter = decode("adapter_artifact"); + let implementation = value_field_mut( + value_field_mut(&mut adapter, "effectImplementations"), + "jedit.text@1.readFact", + ); + set( + implementation, + "targetIntrinsic", + text(&format!("echo.dpo@1.{name}")), + ); + set(implementation, "writeClass", text(&shape.write_class)); + set(implementation, "failureMappings", map([])); + let V::Map(profiles) = value_field_mut(&mut adapter, "operationProfiles") else { + panic!("profiles") + }; + set(&mut profiles[0].1, "core", text(profile)); + let mut catalog = catalog_fixture(); + let intrinsic = get(get(&catalog, "intrinsics"), "echo.dpo@1.node-atom-read").clone(); + let mut intrinsic = intrinsic; + set( + &mut intrinsic, + "argumentTypes", + V::Array(vec![text(&shape.catalog_argument)]), + ); + set(&mut intrinsic, "returnType", text(&shape.catalog_return)); + set(&mut intrinsic, "effectKind", text(&shape.effect_kind)); + set(&mut intrinsic, "writeClass", text(&shape.write_class)); + set(&mut intrinsic, "effectFailures", map([])); + set( + &mut catalog, + "intrinsics", + V::Map(vec![(text(&format!("echo.dpo@1.{name}")), intrinsic)]), + ); + let lowering = lowering_fixture_inputs(Some(catalog), Some(exports.clone()), Some(adapter)); + let (mut core, mut target, _) = compiler_fixture(&lowering); + let fields = shape.fields.clone(); + let input_type = map([ + ("kind", text("Record")), + ( + "fields", + V::Map(fields.iter().map(|(k, t)| (text(k), text(t))).collect()), + ), + ]); + add( + value_field_mut(&mut core, "types"), + shape.input_name, + input_type, + ); + add( + value_field_mut(&mut core, "types"), + shape.output_name, + shape.output.clone(), + ); + let arg = map([ + ("id", text("arg.0")), + ("type", text(shape.input_name)), + ("alphaName", text("$arg0")), + ]); + let local = map([ + ("id", text("local.0")), + ("type", text(shape.output_name)), + ("alphaName", text("$local0")), + ]); + let input = map([("kind", text("local")), ("ref", arg.clone())]); + let result = map([("kind", text("local")), ("ref", local.clone())]); + let node = map([ + ("kind", text("effect")), + ("binding", local.clone()), + ("effect", text("text.readFact")), + ("input", input.clone()), + ("obstructionMap", map([])), + ]); + let intent = value_field_mut(value_field_mut(&mut core, "intents"), "replaceRange"); + set(intent, "input", text(shape.input_name)); + set(intent, "output", text(shape.output_name)); + set(intent, "requiredOperationProfile", text(profile)); + if let V::Map(fields) = intent { + fields.retain(|(k, _)| k != &text("basis")); + } + set( + intent, + "body", + map([ + ("locals", V::Array(vec![arg, local.clone()])), + ("nodes", V::Array(vec![node])), + ("result", result.clone()), + ]), + ); + let selected = selected_target_mut(&mut target); + set(selected, "operationProfile", text(profile)); + if let V::Map(fields) = selected { + fields.retain(|(k, _)| k != &text("basis")); + } + set( + selected, + "steps", + V::Array(vec![map([ + ("id", text("effect.0")), + ("binding", local), + ("effect", text("text.readFact")), + ("input", input), + ("targetIntrinsic", text(&format!("echo.dpo@1.{name}"))), + ("obstructionArms", map([])), + ("obstructionFailures", V::Array(vec![])), + ])]), + ); + set(selected, "requirements", V::Array(vec![])); + set(selected, "result", result); + set(selected, "executionOrder", V::Array(vec![text("effect.0")])); + set( + value_field_mut( + value_field_mut(&mut target, "semanticClosure"), + "sourceCore", + ), + "digest", + V::Array(vec![ + text("sha256"), + artifact_identity("edict.core.module/v1", &core), + ]), + ); + let publication = publication_from_parts(&core, &target, &exports, &lowering); + let input = V::Map( + fields + .iter() + .enumerate() + .map(|(i, (name, _))| { + ( + text(name), + if *name == "atomBytes" { + V::Bytes(b"payload".to_vec()) + } else { + id(u8::try_from(i).expect("field index") + 10) + }, + ) + }) + .collect(), + ); + (publication, encoded(&input)) +} +#[test] +fn all_five_compiled_mutation_emitters_prepare_and_run_without_writing_the_frontier() { + for name in NAMES { + let (publication, input) = fixture(name, "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).unwrap_or_else(|e| panic!("{name}: {e:?}")); + let program = package.program().as_effect_program().expect("program"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect(program, &input, &[]).expect(name), + encoded(&map([])) + ); + let node = NodeKey { + warp_id: WarpId([10; 32]), + local_id: NodeId([11; 32]), + }; + let edge = EdgeKey { + warp_id: WarpId([10; 32]), + local_id: EdgeId([11; 32]), + }; + let atom = AtomPayload { + type_id: TypeId([13; 32]), + bytes: b"payload".to_vec().into(), + }; + let expected = match name { + "ensure-node" => EffectMutation::EnsureNode { + node, + ty: TypeId([12; 32]), + atom, + }, + "create-node" => EffectMutation::CreateNode { + node, + ty: TypeId([12; 32]), + atom, + }, + "replace-atom" => EffectMutation::ReplaceAtom { + node, + expected: [12; 32], + atom, + }, + "ensure-edge" => EffectMutation::EnsureEdge { + edge, + from: NodeId([12; 32]), + to: NodeId([13; 32]), + ty: TypeId([14; 32]), + }, + "replace-edge" => EffectMutation::ReplaceEdge { + edge, + expected_to: NodeId([12; 32]), + to: NodeId([13; 32]), + }, + _ => panic!("kind"), + }; + assert_eq!(frame.buffered_mutations(), &[expected]); + assert!(frame.readings().is_empty()); + assert_eq!( + frame.consumed().write_bytes(), + if name.ends_with("edge") { 128 } else { 71 } + ); + assert_eq!(frame.evaluation_basis(), Some(basis)); + } +} +fn parts(publication: &V) -> (V, V, V, V) { + let embedded = |value: &V| { + let V::Bytes(raw) = value else { + panic!("bytes") + }; + echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("canonical") + }; + let program = embedded(get(publication, "program")); + let bundle = embedded(get(&program, "compiled_program")); + ( + embedded(get(&bundle, "core_artifact")), + embedded(get(&bundle, "target_ir_artifact")), + embedded(get(&bundle, "lawpack_exports_artifact")), + get(&bundle, "lowering_artifacts").clone(), + ) +} +#[test] +fn mutation_emitters_cannot_be_authorized_by_a_read_only_profile() { + for name in NAMES { + assert!( + prepare(&fixture(name, "continuum.profile.read-only/v1").0).is_err(), + "{name}" + ); + } +} +#[test] +fn mutation_emitter_abi_rejects_redefined_address_widths_under_fresh_pins() { + for name in NAMES { + let (publication, _) = fixture(name, "continuum.profile.graph-effects/v1"); + let (mut core, mut target, exports, lowering) = parts(&publication); + let fields = value_field_mut( + value_field_mut(value_field_mut(&mut core, "types"), "MutationInput"), + "fields", + ); + set(fields, "warpId", text("Bytes")); + repin_core(&core, &mut target); + assert!( + prepare(&publication_from_parts(&core, &target, &exports, &lowering)).is_err(), + "{name}" + ); + } +} +#[test] +fn failure_after_emission_does_not_publish_a_partial_intent_buffer() { + use warp_core::edict_effects::EffectFrameError; + use warp_core::edict_pure::EvaluationError; + use warp_core::edict_read::ReadError; + for name in NAMES { + let (mut publication, input) = fixture(name, "continuum.profile.graph-effects/v1"); + let V::Bytes(raw) = get(&publication, "program") else { + panic!("program") + }; + let mut value = echo_edict_canonical::decode_canonical_cbor_v1(raw).expect("program"); + set(&mut value, "max_result_bytes", V::Integer(0)); + set(&mut publication, "program", V::Bytes(encoded(&value))); + let package = prepare(&publication).expect("zero output ceiling"); + let program = package.program().as_effect_program().expect("program"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect(program, &input, &[]), + Err(EffectFrameError::BoundedRead(ReadError::Evaluation( + EvaluationError::OutputBudgetExceeded + ))) + ); + assert!(frame.buffered_mutations().is_empty()); + assert_eq!( + frame.consumed().write_bytes(), + if name.ends_with("edge") { 128 } else { 71 }, + "a later failure must not refund an admitted emission" + ); + assert_eq!( + frame.evaluate_effect(program, &input, &[]), + Err(EffectFrameError::Terminated) + ); + } +} +#[test] +fn mutation_emission_refuses_a_write_budget_one_byte_short_before_buffering() { + for name in NAMES { + let (publication, input) = fixture(name, "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let write = if name.ends_with("edge") { 127 } else { 70 }; + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, write), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert!( + frame.evaluate_effect(program, &input, &[]).is_err(), + "{name} ignored its emission write budget" + ); + assert!(frame.buffered_mutations().is_empty()); + assert_eq!( + frame.consumed().write_bytes(), + 0, + "failed write charge must remain atomic" + ); + } +} +fn repeat_emission(publication: &V, count: usize) -> V { + let (mut core, mut target, exports, lowering) = parts(publication); + let body = core_body_mut(&mut core); + let V::Array(original_nodes) = get(body, "nodes") else { + panic!("nodes") + }; + let node_template = original_nodes[0].clone(); + let V::Array(original_locals) = get(body, "locals") else { + panic!("locals") + }; + let mut locals = vec![original_locals[0].clone()]; + let V::Array(original_steps) = get(selected_target_mut(&mut target), "steps") else { + panic!("steps") + }; + let step_template = original_steps[0].clone(); + let mut nodes = Vec::new(); + let mut steps = Vec::new(); + let mut order = Vec::new(); + for index in 0..count { + let local = map([ + ("id", text(&format!("local.{index}"))), + ("type", text("MutationUnit")), + ("alphaName", text(&format!("$local{index}"))), + ]); + let mut node = node_template.clone(); + set(&mut node, "binding", local.clone()); + nodes.push(node); + let id = text(&format!("effect.{index}")); + let mut step = step_template.clone(); + set(&mut step, "id", id.clone()); + set(&mut step, "binding", local.clone()); + steps.push(step); + order.push(id); + locals.push(local); + } + let result = map([ + ("kind", text("local")), + ("ref", locals.last().expect("local").clone()), + ]); + set(body, "locals", V::Array(locals)); + set(body, "nodes", V::Array(nodes)); + set(body, "result", result.clone()); + let selected = selected_target_mut(&mut target); + set(selected, "steps", V::Array(steps)); + set(selected, "executionOrder", V::Array(order)); + set(selected, "result", result); + repin_core(&core, &mut target); + publication_from_parts(&core, &target, &exports, &lowering) +} +#[test] +fn emitted_effect_count_is_bounded_even_when_all_intents_share_one_target() { + for count in [1024, 1025] { + let (publication, input) = fixture("ensure-edge", "continuum.profile.graph-effects/v1"); + let package = + prepare(&repeat_emission(&publication, count)).expect("bounded repeated source"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut allocation = limits(); + allocation.max_package_bytes = 4 * 1024 * 1024; + allocation.max_steps = 1_048_576; + allocation.max_allocated_bytes = 16 * 1024 * 1024; + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(1_048_576, 4096, 16 * 1024 * 1024), + allocation, + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + let result = frame.evaluate_effect(program, &input, &[]); + if count == 1024 { + assert!(result.is_ok()); + assert_eq!(frame.buffered_mutations().len(), 1024); + } else { + assert!( + result.is_err(), + "the 1025th effect bypassed the fixed emission ceiling" + ); + assert!(frame.buffered_mutations().is_empty()); + } + assert_eq!( + frame.consumed().write_bytes(), + 131_072, + "the successful prefix must remain charged, including on refusal" + ); + } +} +#[test] +fn row_reservation_failure_does_not_commit_its_emission_write_charge() { + use warp_core::edict_effects::EffectFrameError; + use warp_core::edict_pure::EvaluationError; + use warp_core::edict_read::ReadError; + for name in NAMES { + let (publication, input) = fixture(name, "continuum.profile.graph-effects/v1"); + let package = prepare(&publication).expect("program"); + let program = package.program().as_effect_program().expect("effect"); + let (host, head, _) = host(); + let basis = host + .echo_operation_evaluation_basis_v1( + head, + EchoOperationApplicationBasisV1::new([6; 32], [7; 32]), + ) + .expect("basis"); + let mut control = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + limits(), + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + control + .evaluate_effect(program, &input, &[]) + .expect("control"); + // After the row, this empty-record program reserves three 64-byte + // value cells: the unit binding, local result copy and encoding scratch. + // Remove that tail and one extra byte to miss the row by exactly one. + let mut ceiling = limits(); + ceiling.max_allocated_bytes = control + .allocated_bytes() + .checked_sub(193) + .expect("row and output storage"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 4096), + ceiling, + ) + .expect("frame") + .with_footprint_law(program.footprint_law().clone()) + .expect("law"); + assert_eq!( + frame.evaluate_effect(program, &input, &[]), + Err(EffectFrameError::BoundedRead(ReadError::Evaluation( + EvaluationError::AllocationBudgetExceeded + ))) + ); + assert_eq!( + frame.consumed().write_bytes(), + 0, + "{name} committed its failed row's write reservation" + ); + assert!(frame.buffered_mutations().is_empty()); + } +} diff --git a/crates/warp-core/tests/edict_effect_range_reads.rs b/crates/warp-core/tests/edict_effect_range_reads.rs new file mode 100644 index 000000000..6b5c42ff8 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_range_reads.rs @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Recording reads for the future authored-effect interpreter, not admission. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used)] +use warp_core::edict_effects::{EffectRangeReadError, EffectRangeReadSession}; +use warp_core::{ + make_edge_id, make_node_id, make_type_id, EchoOperationBudgetV1, EdgeRecord, GraphStore, + NodeKey, NodeRecord, OutgoingEdgeRange, SlotId, WorldlineFrontier, WorldlineId, WorldlineState, +}; + +fn fixture(edges: bool) -> (WorldlineFrontier, OutgoingEdgeRange) { + let mut store = GraphStore::default(); + let source = make_node_id("source"); + let target = make_node_id("target"); + for id in [source, target] { + store.insert_node( + id, + NodeRecord { + ty: make_type_id("node"), + }, + ); + } + let key = OutgoingEdgeRange { + source: NodeKey { + warp_id: store.warp_id(), + local_id: source, + }, + edge_type: make_type_id("selected"), + }; + if edges { + for (name, ty) in [ + ("selected", key.edge_type), + ("other", make_type_id("other")), + ] { + store.insert_edge( + source, + EdgeRecord { + id: make_edge_id(name), + from: source, + to: target, + ty, + }, + ); + } + } + let state = WorldlineState::from_root_store(store, source).expect("fixture state"); + ( + WorldlineFrontier::new(WorldlineId::from_bytes([7; 32]), state), + key, + ) +} + +#[test] +fn forbidden_type_is_refused_before_read_or_charge() { + let (frontier, key) = fixture(true); + let mut session = + EffectRangeReadSession::new(&frontier, &[], 8, EchoOperationBudgetV1::new(10, 1024, 0)); + assert_eq!( + session.read(key), + Err(EffectRangeReadError::FootprintLawViolation) + ); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(session.readings().is_empty()); + assert_eq!(session.allocated_bytes(), 0); +} + +#[test] +fn nonmatching_edges_count_toward_the_preinspection_budget() { + let (frontier, key) = fixture(true); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 8, + EchoOperationBudgetV1::new(2, 128, 0), + ); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + // Source presence was resolved, but neither edge was inspected or recorded. + assert_eq!(session.allocated_bytes(), 128); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + assert_eq!(session.readings().len(), 1); + assert!(session.readings().contains_key(&SlotId::Node(key.source))); + assert!(!session.readings().contains_key(&SlotId::OutgoingEdges(key))); +} + +#[test] +fn empty_range_records_the_node_and_range_dependencies() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + let mut session = + EffectRangeReadSession::new(&frontier, &allowed, 8, EchoOperationBudgetV1::new(1, 64, 0)); + assert_eq!(session.read(key).expect("empty range"), Some(Vec::new())); + assert_eq!(session.readings().len(), 2); + assert!(session + .footprint() + .n_read + .iter() + .any(|node| *node == key.source)); + assert_eq!(session.footprint().r_read.len(), 1); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); +} + +#[test] +fn a_successful_scan_filters_values_but_charges_every_edge() { + let (frontier, key) = fixture(true); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 2, + EchoOperationBudgetV1::new(3, 192, 0), + ); + let expected = EdgeRecord { + id: make_edge_id("selected"), + from: key.source.local_id, + to: make_node_id("target"), + ty: key.edge_type, + }; + let mut delivered = session + .read(key) + .expect("bounded scan") + .expect("source present"); + assert_eq!(delivered, vec![expected.clone()]); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(3, 192, 0)); + // A host result copy cannot change the runtime-owned observation. + delivered.clear(); + assert_eq!( + session.readings().get(&SlotId::OutgoingEdges(key)), + Some(&warp_core::SlotReading::OutgoingEdges(Some(vec![expected]))) + ); +} + +#[test] +fn absent_source_and_absent_warp_are_recorded_as_absence() { + let (frontier, mut key) = fixture(false); + let allowed = [key.edge_type]; + key.source.local_id = make_node_id("missing"); + for range in [ + key, + OutgoingEdgeRange { + source: NodeKey { + warp_id: warp_core::WarpId([99; 32]), + local_id: key.source.local_id, + }, + edge_type: key.edge_type, + }, + ] { + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(1, 64, 0), + ); + assert_eq!(session.read(range).expect("absent source"), None); + assert_eq!( + session.readings().get(&SlotId::Node(range.source)), + Some(&warp_core::SlotReading::Node(None)) + ); + assert_eq!( + session.readings().get(&SlotId::OutgoingEdges(range)), + Some(&warp_core::SlotReading::OutgoingEdges(None)) + ); + assert_eq!(session.readings().len(), 2); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + } +} + +#[test] +fn the_bucket_limit_includes_nonmatching_edges_even_with_spare_budget() { + let (frontier, key) = fixture(true); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 1, + EchoOperationBudgetV1::new(10, 1024, 0), + ); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + assert_eq!(session.readings().len(), 1); + assert!(session.footprint().r_read.is_empty()); +} + +#[test] +fn exhausted_node_budget_records_nothing_and_failed_charges_are_atomic() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + for budget in [ + EchoOperationBudgetV1::new(0, 64, 0), + EchoOperationBudgetV1::new(1, 63, 0), + ] { + let mut session = EffectRangeReadSession::new(&frontier, &allowed, 0, budget); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(session.readings().is_empty()); + assert!(session.footprint().n_read.is_empty()); + assert_eq!(session.allocated_bytes(), 0); + } +} + +#[test] +fn matching_rows_are_canonical_despite_bucket_insertion_order() { + let mut store = GraphStore::default(); + let source = make_node_id("source"); + let target = make_node_id("target"); + let ty = make_type_id("selected"); + for id in [source, target] { + store.insert_node( + id, + NodeRecord { + ty: make_type_id("node"), + }, + ); + } + for (id, edge_type) in [(2, ty), (3, make_type_id("other")), (1, ty)] { + store.insert_edge( + source, + EdgeRecord { + id: warp_core::EdgeId([id; 32]), + from: source, + to: target, + ty: edge_type, + }, + ); + } + let key = OutgoingEdgeRange { + source: NodeKey { + warp_id: store.warp_id(), + local_id: source, + }, + edge_type: ty, + }; + let state = WorldlineState::from_root_store(store, source).expect("state"); + let frontier = WorldlineFrontier::new(WorldlineId::from_bytes([7; 32]), state); + let allowed = [ty]; + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 3, + EchoOperationBudgetV1::new(4, 256, 0), + ); + let rows = session.read(key).expect("read").expect("source"); + assert_eq!( + rows.iter().map(|row| row.id).collect::>(), + vec![warp_core::EdgeId([1; 32]), warp_core::EdgeId([2; 32])] + ); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(4, 256, 0)); +} + +#[test] +fn repeated_reads_charge_each_attempt_without_duplicating_observations() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::new( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(2, 128, 0), + ); + for _ in 0..2 { + assert_eq!(session.read(key).expect("empty range"), Some(Vec::new())); + assert_eq!(session.readings().len(), 2); + } + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(2, 128, 0)); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(2, 128, 0)); + assert_eq!(session.readings().len(), 2); +} + +#[test] +fn allocation_refuses_before_node_resolution_and_failed_reservations_are_atomic() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(10, 1024, 0), + 127, + ) + .expect("valid declared limit"); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.allocated_bytes(), 0); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(session.readings().is_empty()); +} + +#[test] +fn empty_range_storage_has_an_exact_success_boundary() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + for (limit, succeeds, allocated, observations) in [(255, false, 128, 1), (256, true, 256, 2)] { + let mut session = EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(1, 64, 0), + limit, + ) + .expect("valid limit"); + assert_eq!(session.read(key).is_ok(), succeeds); + assert_eq!(session.allocated_bytes(), allocated); + assert_eq!(session.readings().len(), observations); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + } +} + +#[test] +fn scan_reserves_both_row_vectors_for_the_whole_bucket_before_filtering() { + let (frontier, key) = fixture(true); + let allowed = [key.edge_type]; + // 128 node + 128 range + 2 copies * 2 bucket edges * 128 row bytes. + for (limit, succeeds, allocated, steps, bytes) in + [(767, false, 128, 1, 64), (768, true, 768, 3, 192)] + { + let mut session = EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 8, + EchoOperationBudgetV1::new(10, 1024, 0), + limit, + ) + .expect("valid limit"); + assert_eq!(session.read(key).is_ok(), succeeds); + assert_eq!(session.allocated_bytes(), allocated); + assert_eq!( + session.consumed(), + EchoOperationBudgetV1::new(steps, bytes, 0) + ); + } +} + +#[test] +fn allocation_declarations_above_the_fixed_ceiling_are_refused() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + assert!(matches!( + EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(1, 64, 0), + 16_777_217, + ), + Err(warp_core::edict_effects::EffectReadSetupError::AllocationLimitTooLarge) + )); + let mut zero = EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(1, 64, 0), + 0, + ) + .expect("zero is a valid declaration"); + assert_eq!(zero.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(zero.allocated_bytes(), 0); + assert!(zero.readings().is_empty()); +} + +#[test] +fn repeated_reads_do_not_refund_cumulative_storage() { + let (frontier, key) = fixture(false); + let allowed = [key.edge_type]; + let mut session = EffectRangeReadSession::with_allocation_limit( + &frontier, + &allowed, + 0, + EchoOperationBudgetV1::new(10, 1024, 0), + 511, + ) + .expect("valid declaration"); + assert_eq!( + session.read(key).expect("first empty range"), + Some(Vec::new()) + ); + assert_eq!(session.allocated_bytes(), 256); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.allocated_bytes(), 384); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(2, 128, 0)); + assert_eq!(session.readings().len(), 2); + assert_eq!(session.read(key), Err(EffectRangeReadError::BudgetExceeded)); + assert_eq!(session.allocated_bytes(), 384); + assert_eq!(session.consumed(), EchoOperationBudgetV1::new(2, 128, 0)); +} diff --git a/crates/warp-core/tests/edict_effect_read_image_tests.rs b/crates/warp-core/tests/edict_effect_read_image_tests.rs new file mode 100644 index 000000000..9c1e40627 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_read_image_tests.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Independent wire fixtures for the private bridge read-set image. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used)] +use warp_core::edict_effects::{EffectReadImageError, EffectReadSetView}; + +fn row(class: u8, outcome: u8, ty: u8) -> [u8; 192] { + let mut row = [0; 192]; + row[0] = class; + row[104] = outcome; + row[8..40].fill(0x11); + row[40..72].fill(0x22); + row[112..144].fill(ty); + if outcome == 0 || outcome == 3 { + row[144..176].fill(0x55); + } + if class == 3 { + row[72..104].fill(0x33); + } + row +} +fn image(rows: &[[u8; 192]], pairs: &[[u8; 64]]) -> Vec { + let mut bytes = (rows.len() as u64).to_le_bytes().to_vec(); + for row in rows { + bytes.extend_from_slice(row); + } + bytes.extend_from_slice(&(pairs.len() as u64).to_le_bytes()); + for pair in pairs { + bytes.extend_from_slice(pair); + } + bytes + .chunks_exact(8) + .map(|part| u64::from_ne_bytes(part.try_into().expect("eight bytes"))) + .collect() +} +fn parse(words: &[u64]) -> Result, EffectReadImageError> { + EffectReadSetView::parse(bytemuck::cast_slice(words)) +} +fn invalid(rows: &[[u8; 192]], pairs: &[[u8; 64]]) { + let words = image(rows, pairs); + assert!(matches!( + parse(&words), + Err(EffectReadImageError::InvalidImage) + )); +} +#[test] +fn fixed_wire_order_differs_from_native_slot_order_and_rows_borrow_the_input() { + let mut range = row(3, 2, 0); + range[184..192].copy_from_slice(&2_u64.to_le_bytes()); + let mut first = [0; 64]; + first[..32].fill(1); + first[32..].fill(0x41); + let mut second = [0; 64]; + second[..32].fill(2); + second[32..].fill(0x42); + let rows = [row(0, 0, 1), row(1, 0, 1), row(2, 0, 1), range]; + let words = image(&rows, &[first, second]); + let view = parse(&words).expect("canonical wire order"); + assert_eq!( + view.rows() + .iter() + .map(|row| row.slot.class) + .collect::>(), + vec![0, 1, 2, 3] + ); + assert_eq!(view.pairs()[0].edge_local_id, [1; 32]); + assert_eq!(view.pairs()[1].target_local_id, [0x42; 32]); + assert_eq!( + view.rows().as_ptr().cast::(), + bytemuck::cast_slice::(&words)[8..].as_ptr() + ); + invalid(&[rows[0], rows[2], rows[1], rows[3]], &[first, second]); +} +#[test] +fn only_distinct_absence_types_may_repeat_a_slot() { + let a = row(0, 1, 1); + let b = row(0, 1, 2); + let words = image(&[a, b], &[]); + assert_eq!(parse(&words).expect("two absent types").rows().len(), 2); + invalid(&[a, a], &[]); + invalid(&[b, a], &[]); + invalid(&[row(0, 0, 1), row(0, 0, 2)], &[]); + invalid(&[row(0, 0, 1), b], &[]); +} +#[test] +fn tags_padding_and_unused_fields_have_one_encoding() { + let base = row(0, 1, 1); + for index in [1, 2, 3, 7, 72, 105, 111, 144, 176, 184] { + let mut bad = base; + bad[index] = 1; + invalid(&[bad], &[]); + } + let mut bad = base; + bad[0] = 4; + invalid(&[bad], &[]); + let mut bad = base; + bad[104] = 4; + invalid(&[bad], &[]); + invalid(&[row(0, 3, 1)], &[]); + let mut bad = row(1, 3, 1); + bad[1] = 1; + bad[2] = 1; + invalid(&[bad], &[]); + let mut bad = row(3, 2, 0); + bad[112] = 1; + invalid(&[bad], &[]); + let mut bad = row(3, 2, 0); + bad[144] = 1; + invalid(&[bad], &[]); +} +#[test] +fn edge_pairs_are_sorted_unique_and_packed_in_range_row_order() { + let mut a = row(3, 2, 0); + a[184..192].copy_from_slice(&1_u64.to_le_bytes()); + let mut b = a; + b[72..104].fill(0x44); + b[176..184].copy_from_slice(&1_u64.to_le_bytes()); + let mut pair = [0; 64]; + pair[..32].fill(1); + let words = image(&[a, b], &[pair, pair]); + assert_eq!( + parse(&words) + .expect("same edge ID in separate ranges") + .pairs() + .len(), + 2 + ); + b[176..184].fill(0); + let mut distinct = pair; + distinct[..32].fill(2); + invalid(&[a, b], &[pair, distinct]); + let mut two = a; + two[184..192].copy_from_slice(&2_u64.to_le_bytes()); + invalid(&[two], &[pair, pair]); + let mut higher = pair; + higher[..32].fill(2); + invalid(&[two], &[higher, pair]); + let mut bad = a; + bad[176..184].copy_from_slice(&u64::MAX.to_le_bytes()); + invalid(&[bad], &[pair]); + let mut bad = a; + bad[184..192].copy_from_slice(&u64::MAX.to_le_bytes()); + invalid(&[bad], &[pair]); + invalid(&[row(3, 2, 0)], &[pair]); +} +#[test] +fn counts_alignment_and_tail_are_validated_before_any_row_view() { + let empty = image(&[], &[]); + assert!(parse(&empty).expect("empty set").rows().is_empty()); + let mut tail = empty.clone(); + tail.push(0); + assert!(matches!( + parse(&tail), + Err(EffectReadImageError::InvalidImage) + )); + for count in [4097, u64::MAX] { + let mut bad = empty.clone(); + bad[0] = count.to_le(); + assert!(matches!( + parse(&bad), + Err(EffectReadImageError::InvalidImage) + )); + } + for len in 0..16 { + assert!(matches!( + EffectReadSetView::parse(&bytemuck::cast_slice::(&empty)[..len]), + Err(EffectReadImageError::InvalidImage) + )); + } + let padded = [0_u64; 3]; + assert!(matches!( + EffectReadSetView::parse(&bytemuck::cast_slice::(&padded)[1..17]), + Err(EffectReadImageError::UnalignedImage) + )); +} + +#[test] +fn row_limit_applies_to_complete_otherwise_canonical_images() { + let rows = (0_u32..4097) + .map(|id| { + let mut r = row(0, 1, 0); + r[140..144].copy_from_slice(&id.to_be_bytes()); + r + }) + .collect::>(); + let allowed = image(&rows[..4096], &[]); + assert_eq!( + parse(&allowed) + .expect("full allowed row count") + .rows() + .len(), + 4096 + ); + invalid(&rows, &[]); +} +#[test] +fn pair_limit_is_per_range_and_does_not_shrink_the_total_pair_section() { + let pairs = (0_u32..65537) + .map(|id| { + let mut pair = [0; 64]; + pair[28..32].copy_from_slice(&id.to_be_bytes()); + pair + }) + .collect::>(); + let mut one = row(3, 2, 0); + one[184..192].copy_from_slice(&65536_u64.to_le_bytes()); + let allowed = image(&[one], &pairs[..65536]); + assert_eq!(parse(&allowed).expect("maximum range").pairs().len(), 65536); + let mut too_many = one; + too_many[184..192].copy_from_slice(&65537_u64.to_le_bytes()); + invalid(&[too_many], &pairs); + let mut next = row(3, 2, 0); + next[72..104].fill(0x44); + next[176..184].copy_from_slice(&65536_u64.to_le_bytes()); + next[184..192].copy_from_slice(&1_u64.to_le_bytes()); + let total = image(&[one, next], &pairs); + assert_eq!( + parse(&total) + .expect("two individually bounded ranges") + .pairs() + .len(), + 65537 + ); +} diff --git a/crates/warp-core/tests/edict_projected_cas_tests.rs b/crates/warp-core/tests/edict_projected_cas_tests.rs index d70d8c233..ec41de1f2 100644 --- a/crates/warp-core/tests/edict_projected_cas_tests.rs +++ b/crates/warp-core/tests/edict_projected_cas_tests.rs @@ -19,12 +19,12 @@ use warp_core::{ echo_operation_action_envelope_v1, echo_operation_anchored_node_application_basis_v1, echo_operation_atom_value_digest_v1, echo_operation_package_id_v1, make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, EchoOperationActionOutcomeV1, - EchoOperationAdmissionPolicyV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, - EngineBuilder, GraphStore, InboxPolicy, IngressTarget, NodeId, NodeKey, NodeRecord, - PlaybackMode, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeWalConfig, TypeId, WorldlineId, - WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, + EchoOperationAdmissionPolicy, EchoOperationBudgetV1, EchoOperationInvocation, + EchoOperationInvocationAdmissionErrorKind, EchoOperationInvocationAdmissionPolicy, + EchoOperationObstructionKindV1, EchoOperationPreparationV1, EngineBuilder, GraphStore, + InboxPolicy, IngressTarget, NodeId, NodeKey, NodeRecord, PlaybackMode, SchedulerKind, + TrustedRuntimeHost, TrustedRuntimeWalConfig, TypeId, WorldlineId, WorldlineRuntime, + WorldlineState, WriterHead, WriterHeadKey, }; const OPERATION: &str = "examples.cas_echo@1.updateCell"; @@ -78,8 +78,8 @@ fn attachment_type() -> TypeId { fn budget() -> EchoOperationBudgetV1 { EchoOperationBudgetV1::new(16, 1024, 320) } -fn policy() -> EchoOperationInvocationAdmissionPolicyV1 { - EchoOperationInvocationAdmissionPolicyV1::new( +fn policy() -> EchoOperationInvocationAdmissionPolicy { + EchoOperationInvocationAdmissionPolicy::new( profile("cas.echo.authority.local-demo/v1"), GRANT, budget(), @@ -162,9 +162,9 @@ impl Fixture { let bytes = package(); let admitted = self .host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&bytes), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), @@ -173,7 +173,7 @@ impl Fixture { ) .expect("actual compiler package admitted"); self.host - .install_admitted_echo_operation_package_v1(admitted) + .install_admitted_echo_operation_package(admitted) .expect("install"); } fn enable_wal(&mut self, dir: &WalDir) { @@ -227,7 +227,7 @@ impl Fixture { .expect("input"); // The package itself is the exact compiler output, with no test-side // rewriting; the invocation uses the public projected constructor. - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set_with_application_input( + EchoOperationInvocation::anchored_node_attachment_compare_and_set_with_application_input( echo_operation_package_id_v1(&package()), OPERATION, basis, @@ -542,7 +542,7 @@ fn compiler_cas_recovers_pending_action_committed_result_and_stale_obstruction() assert!(pending .host .engine() - .installed_echo_operation_package_v1(echo_operation_package_id_v1(&package())) + .installed_echo_operation_package(echo_operation_package_id_v1(&package())) .is_some()); assert!(pending .host @@ -620,7 +620,7 @@ fn compiler_cas_recovers_pending_action_committed_result_and_stale_obstruction() ); recovered .host - .admit_echo_operation_invocation_v1(&policy(), &stale) + .admit_echo_operation_invocation(&policy(), &stale) .expect("stale precondition passes static admission"); recovered .host @@ -676,10 +676,10 @@ fn compiler_cas_refuses_substituted_digest_and_missing_node_without_mutation() { assert_eq!( fixture .host - .admit_echo_operation_invocation_v1(&policy(), &substituted) + .admit_echo_operation_invocation(&policy(), &substituted) .expect_err("substituted digest refused") .kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); assert_eq!(fixture.value(), INITIAL); let wrong_basis = @@ -687,13 +687,13 @@ fn compiler_cas_refuses_substituted_digest_and_missing_node_without_mutation() { assert_eq!( fixture .host - .admit_echo_operation_invocation_v1(&policy(), &wrong_basis) + .admit_echo_operation_invocation(&policy(), &wrong_basis) .expect_err("uncorroborated application basis refused") .kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); let valid = fixture.invocation(INITIAL, initial_digest(), initial_digest(), "updated"); - let other_grant = EchoOperationInvocationAdmissionPolicyV1::new( + let other_grant = EchoOperationInvocationAdmissionPolicy::new( profile("cas.echo.authority.local-demo/v1"), [74; 32], budget(), @@ -701,10 +701,10 @@ fn compiler_cas_refuses_substituted_digest_and_missing_node_without_mutation() { assert_eq!( fixture .host - .admit_echo_operation_invocation_v1(&other_grant, &valid) + .admit_echo_operation_invocation(&other_grant, &valid) .expect_err("ungranted authority refused") .kind(), - EchoOperationInvocationAdmissionErrorKindV1::AuthorityGrantMismatch + EchoOperationInvocationAdmissionErrorKind::AuthorityGrantMismatch ); assert_eq!(fixture.value(), INITIAL); let mut missing = Fixture::new(false); @@ -713,10 +713,10 @@ fn compiler_cas_refuses_substituted_digest_and_missing_node_without_mutation() { assert_eq!( missing .host - .admit_echo_operation_invocation_v1(&policy(), &invocation) + .admit_echo_operation_invocation(&policy(), &invocation) .expect_err("missing node refused") .kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); assert!(missing .host @@ -826,9 +826,9 @@ fn compiler_cas_refuses_aliased_projection_before_installation() { // merely a mismatch against the original package's hash. let error = fixture .host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + id.as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), @@ -838,7 +838,7 @@ fn compiler_cas_refuses_aliased_projection_before_installation() { .expect_err("aliased projection refused"); assert_eq!( error.kind(), - warp_core::EchoOperationAdmissionErrorKindV1::ArtifactInvalid + warp_core::EchoOperationAdmissionErrorKind::ArtifactInvalid ); assert_eq!( error.artifact().expect("artifact error").kind(), @@ -847,7 +847,7 @@ fn compiler_cas_refuses_aliased_projection_before_installation() { assert!(fixture .host .engine() - .installed_echo_operation_package_v1(id) + .installed_echo_operation_package(id) .is_none()); assert_eq!(fixture.value(), INITIAL); } @@ -863,7 +863,7 @@ fn a_prepared_action_shares_its_installed_package_instead_of_copying_it() { let invocation = fixture.invocation(INITIAL, initial_digest(), initial_digest(), "updated"); let admitted = fixture .host - .admit_echo_operation_invocation_v1(&policy(), &invocation) + .admit_echo_operation_invocation(&policy(), &invocation) .expect("invocation admitted"); let EchoOperationPreparationV1::Prepared(prepared) = fixture.host.prepare_echo_operation_v1(admitted) @@ -873,7 +873,7 @@ fn a_prepared_action_shares_its_installed_package_instead_of_copying_it() { let registered = fixture .host .engine() - .installed_echo_operation_package_v1(echo_operation_package_id_v1(&package())) + .installed_echo_operation_package(echo_operation_package_id_v1(&package())) .expect("installed"); assert!( std::ptr::eq( diff --git a/crates/warp-core/tests/edict_source_functions_tests.rs b/crates/warp-core/tests/edict_source_functions_tests.rs index d88c24d04..9703a42a6 100644 --- a/crates/warp-core/tests/edict_source_functions_tests.rs +++ b/crates/warp-core/tests/edict_source_functions_tests.rs @@ -272,7 +272,7 @@ fn source_functions_named_int_alias_executes_in_a_helper_signature() { insert( field_mut(core, "types"), "Count", - record([("kind", text("Int")), ("width", text("U64"))]), + record([("kind", text("U64"))]), ); let parameter = local("arg.0", "Count"); insert( @@ -319,7 +319,7 @@ fn source_functions_named_int_alias_executes_in_a_helper_signature() { &concat::input(b"ab", b"cd"), concat::limits(), ) - .expect("named Int parameter/result must decode and execute"); + .expect("current canonical unsigned alias must decode and execute"); assert_eq!( decode(&result.output).unwrap(), record([("bytes", Value::Bytes(b"abcd".to_vec()))]) diff --git a/crates/warp-core/tests/effect_publication_validation.rs b/crates/warp-core/tests/effect_publication_validation.rs new file mode 100644 index 000000000..d33c89b02 --- /dev/null +++ b/crates/warp-core/tests/effect_publication_validation.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! The compiler-facing checker needs no trusted-host feature or authority. +#![allow(clippy::expect_used, clippy::panic)] +use echo_edict_canonical::{ + decode_canonical_cbor_v1 as decode, encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V, +}; +use warp_core::{echo_operation_package_id_v1, EffectPackageError, ExecutableOperationPackage}; + +fn fixture() -> Vec { + hex::decode(include_str!("fixtures/current-effect-publication/package.hex").trim()) + .expect("synthetic control") +} +fn replace(value: &mut V, key: &str, replacement: V) { + let V::Map(entries) = value else { + panic!("map") + }; + entries + .iter_mut() + .find(|(k, _)| k == &V::Text(key.into())) + .expect("field") + .1 = replacement; +} +fn check(bytes: &[u8]) -> bool { + ExecutableOperationPackage::prepare(bytes, echo_operation_package_id_v1(bytes).as_hash()) + .is_ok() +} + +#[test] +fn current_publication_is_checked_without_host_authority_features() { + let bytes = fixture(); + let prepared = + ExecutableOperationPackage::prepare(&bytes, echo_operation_package_id_v1(&bytes).as_hash()) + .expect("current effect publication is data"); + assert_eq!( + prepared.to_canonical_bytes().expect("canonical round trip"), + bytes + ); + assert_eq!( + ExecutableOperationPackage::prepare(&bytes, [0; 32]), + Err(EffectPackageError::IdentityMismatch) + ); +} + +#[test] +fn fresh_outer_pin_does_not_make_invalid_compiler_bytes_valid() { + let mut package = decode(&fixture()).expect("package"); + let V::Map(entries) = &package else { + panic!("map") + }; + let V::Bytes(raw) = &entries + .iter() + .find(|(k, _)| k == &V::Text("program".into())) + .expect("program") + .1 + else { + panic!("bytes") + }; + let mut program = decode(raw).expect("program"); + replace(&mut program, "compiled_program", V::Bytes(vec![0xa0])); + replace( + &mut package, + "program", + V::Bytes(encode(&program).expect("encode program")), + ); + assert!(!check(&encode(&package).expect("encode package"))); +} + +#[test] +fn refreshed_pin_cannot_change_the_selected_core_binding() { + let mut package = decode(&fixture()).expect("package"); + let V::Map(entries) = &package else { + panic!("map") + }; + let mut closure = entries + .iter() + .find(|(k, _)| k == &V::Text("semantic_closure".into())) + .expect("closure") + .1 + .clone(); + replace(&mut closure, "core_identity", V::Bytes(vec![99; 32])); + replace(&mut package, "semantic_closure", closure); + assert!(!check( + &encode(&package).expect("freshly pinned changed closure") + )); +} diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index a061f77f5..93d350d15 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -22,14 +22,14 @@ use warp_core::causal_wal::{FilesystemWalFaultPlan, FilesystemWalFaultTarget, Wa use warp_core::EchoOperationCommitErrorV1; use warp_core::{ make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, - EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorKindV1, - EchoOperationAdmissionPolicyV1, EchoOperationAnchoredNodeOccupancyV1, - EchoOperationApplicationBasisV1, EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, - EchoOperationProgramV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, - EngineBuilder, ExecutableOperationPackageV1, GraphStore, HeadEligibility, InboxPolicy, - IngressTarget, InstalledEchoOperationV1, NodeKey, NodeRecord, PlaybackMode, RuntimeError, + EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, + EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, + EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, EchoOperationInvocation, + EchoOperationInvocationAdmissionErrorKind, EchoOperationInvocationAdmissionPolicy, + EchoOperationObstructionKindV1, EchoOperationPreparationV1, EchoOperationProgram, + EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, EngineBuilder, + ExecutableOperationPackage, GraphStore, HeadEligibility, InboxPolicy, IngressTarget, + InstalledEchoOperation, NodeKey, NodeRecord, PlaybackMode, RuntimeError, RuntimeWalActivationGap, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeHostError, TrustedRuntimeWalConfig, TrustedRuntimeWalError, WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, ACTION_BATCH_CANDIDATE_LIMIT_V1, @@ -124,12 +124,33 @@ fn fixture_host_with_independent_nodes( fn fixture_host_with_independent_nodes_and_state( node_count: usize, configure: impl FnOnce(WorldlineState) -> WorldlineState, +) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { + fixture_host_with_attachment_and_state( + node_count, + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from_static(b"before"), + ))), + configure, + ) +} + +fn fixture_host_with_attachment( + node_count: usize, + attachment: Option, +) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { + fixture_host_with_attachment_and_state(node_count, attachment, |state| state) +} + +fn fixture_host_with_attachment_and_state( + node_count: usize, + attachment: Option, + configure: impl FnOnce(WorldlineState) -> WorldlineState, ) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { assert!(node_count > 0, "the fixture must retain one root node"); let warp_id = make_warp_id("operation-fixture"); let root_id = make_node_id("operation-fixture-root"); let node_type = make_type_id("operation-fixture-node"); - let attachment_type = make_type_id("operation-fixture-atom"); let nodes = (0..node_count) .map(|index| NodeKey { warp_id, @@ -143,18 +164,17 @@ fn fixture_host_with_independent_nodes_and_state( let mut store = GraphStore::new(warp_id); for node in &nodes { store.insert_node(node.local_id, NodeRecord { ty: node_type }); - store.set_node_attachment( - node.local_id, - Some(AttachmentValue::Atom(AtomPayload::new( - attachment_type, - Bytes::from_static(b"before"), - ))), - ); + store.set_node_attachment(node.local_id, attachment.clone()); } let state = configure( WorldlineState::from_root_store(store, root_id) .expect("the fixture state has one lawful root"), ); + let (host, head_key) = fixture_runtime_host(state); + (host, head_key, nodes) +} + +fn fixture_runtime_host(state: WorldlineState) -> (TrustedRuntimeHost, WriterHeadKey) { let worldline_id = WorldlineId::from_bytes(digest("operation-fixture-worldline")); let head_key = WriterHeadKey { worldline_id, @@ -188,7 +208,7 @@ fn fixture_host_with_independent_nodes_and_state( .build(); let host = TrustedRuntimeHost::new(runtime, engine) .expect("the trusted Echo runtime host initializes"); - (host, head_key, nodes) + (host, head_key) } fn fixture_host_with_two_worldlines() -> (TrustedRuntimeHost, WriterHeadKey, WriterHeadKey, NodeKey) @@ -433,8 +453,8 @@ fn fixture_host_with_orphan_attachment( (host, head_key, node, orphan_node) } -fn semantic_closure() -> EchoOperationSemanticClosureV1 { - EchoOperationSemanticClosureV1::new( +fn semantic_closure() -> EchoOperationSemanticClosure { + EchoOperationSemanticClosure::new( digest("fixture-edict-source"), digest("edict-semantic-identity"), digest("fixture-edict-core"), @@ -449,7 +469,7 @@ fn semantic_closure() -> EchoOperationSemanticClosureV1 { fn operation_package( node_type: warp_core::TypeId, attachment_type: warp_core::TypeId, -) -> ExecutableOperationPackageV1 { +) -> ExecutableOperationPackage { operation_package_at(OPERATION_COORDINATE, node_type, attachment_type) } @@ -457,15 +477,15 @@ fn operation_package_at( operation_coordinate: &str, node_type: warp_core::TypeId, attachment_type: warp_core::TypeId, -) -> ExecutableOperationPackageV1 { - ExecutableOperationPackageV1::new( +) -> ExecutableOperationPackage { + ExecutableOperationPackage::new( operation_coordinate, "echo.fixture.OperationObstruction/v1", semantic_closure(), warp_core::echo_operation_target_profile_identity_v1(), digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( node_type, attachment_type, 1_024, @@ -476,15 +496,15 @@ fn operation_package_at( fn creation_operation_package( node_type: warp_core::TypeId, attachment_type: warp_core::TypeId, -) -> ExecutableOperationPackageV1 { - ExecutableOperationPackageV1::new( +) -> ExecutableOperationPackage { + ExecutableOperationPackage::new( CREATE_OPERATION_COORDINATE, "echo.fixture.AlreadyExists/v1", semantic_closure(), warp_core::echo_operation_create_if_absent_target_profile_identity_v1(), digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), - EchoOperationProgramV1::anchored_node_attachment_create_if_absent( + EchoOperationProgram::anchored_node_attachment_create_if_absent( node_type, attachment_type, 1_024, @@ -492,14 +512,14 @@ fn creation_operation_package( ) } -fn install_fixture_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperationV1 { +fn install_fixture_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperation { install_fixture_operation_at(host, OPERATION_COORDINATE) } fn install_fixture_operation_at( host: &mut TrustedRuntimeHost, operation_coordinate: &str, -) -> InstalledEchoOperationV1 { +) -> InstalledEchoOperation { let package_bytes = operation_package_at( operation_coordinate, make_type_id("operation-fixture-node"), @@ -509,9 +529,9 @@ fn install_fixture_operation_at( .expect("fixture package encodes"); let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let admitted = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -519,11 +539,11 @@ fn install_fixture_operation_at( package_bytes, ) .expect("the exact fixture package is admitted"); - host.install_admitted_echo_operation_package_v1(admitted) + host.install_admitted_echo_operation_package(admitted) .expect("the admitted fixture package installs") } -fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperationV1 { +fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperation { let package_bytes = creation_operation_package( make_type_id("operation-fixture-node"), make_type_id("operation-fixture-atom"), @@ -532,9 +552,9 @@ fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> Installe .expect("fixture creation package encodes"); let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let admitted = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), CREATE_OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -542,7 +562,7 @@ fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> Installe package_bytes, ) .expect("the exact fixture creation package is admitted"); - host.install_admitted_echo_operation_package_v1(admitted) + host.install_admitted_echo_operation_package(admitted) .expect("the admitted fixture creation package installs") } @@ -557,8 +577,8 @@ fn application_basis() -> EchoOperationApplicationBasisV1 { ) } -fn invocation_policy() -> EchoOperationInvocationAdmissionPolicyV1 { - EchoOperationInvocationAdmissionPolicyV1::new( +fn invocation_policy() -> EchoOperationInvocationAdmissionPolicy { + EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -566,14 +586,14 @@ fn invocation_policy() -> EchoOperationInvocationAdmissionPolicyV1 { } fn canonical_invocation( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, basis: warp_core::EchoOperationEvaluationBasisV1, node: NodeKey, expected_value_digest: [u8; 32], replacement: Vec, budget: EchoOperationBudgetV1, ) -> Vec { - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), basis, @@ -589,7 +609,7 @@ fn canonical_invocation( fn action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, current: &[u8], @@ -659,24 +679,24 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { .to_canonical_bytes() .expect("package has one canonical Edict encoding"); let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); - let package_policy = EchoOperationAdmissionPolicyV1::exact( - package_id, + let package_policy = EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_package = host - .admit_echo_operation_package_v1(&package_policy, package_bytes) + .admit_echo_operation_package(&package_policy, package_bytes) .expect("the independently pinned package is admitted"); let installed = host - .install_admitted_echo_operation_package_v1(admitted_package) + .install_admitted_echo_operation_package(admitted_package) .expect("admitted executable meaning installs without callbacks"); let application_basis = application_basis(); let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -692,13 +712,13 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the generated-style invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo independently admits the installed operation invocation"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Prepared(prepared) = preparation else { @@ -843,9 +863,9 @@ fn a_prepared_operation_cannot_commit_after_its_parent_basis_changes() { .expect("package encodes"); let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let admitted_package = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -854,7 +874,7 @@ fn a_prepared_operation_cannot_commit_after_its_parent_basis_changes() { ) .expect("the exact package is admitted"); let installed = host - .install_admitted_echo_operation_package_v1(admitted_package) + .install_admitted_echo_operation_package(admitted_package) .expect("the exact package installs"); let application_basis = application_basis(); @@ -862,7 +882,7 @@ fn a_prepared_operation_cannot_commit_after_its_parent_basis_changes() { let basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the current basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), basis, @@ -874,8 +894,8 @@ fn a_prepared_operation_cannot_commit_after_its_parent_basis_changes() { ); let bytes = invocation.to_canonical_bytes().expect("invocation encodes"); let admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -943,7 +963,7 @@ fn a_prepared_program_cannot_substitute_for_installation_authority() { EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted = evaluating_host - .admit_echo_operation_invocation_v1(&invocation_policy(), &bytes) + .admit_echo_operation_invocation(&invocation_policy(), &bytes) .expect("the installed operation invocation is admitted"); let EchoOperationPreparationV1::Prepared(prepared) = evaluating_host.prepare_echo_operation_v1(admitted) @@ -1014,7 +1034,7 @@ fn identical_program_bytes_do_not_merge_admitted_operation_identities() { ); let execute = - |host: &mut TrustedRuntimeHost, installed: &InstalledEchoOperationV1, head, node| { + |host: &mut TrustedRuntimeHost, installed: &InstalledEchoOperation, head, node| { let basis = host .echo_operation_evaluation_basis_v1(head, application_basis()) .expect("Echo resolves the exact package-specific basis"); @@ -1030,7 +1050,7 @@ fn identical_program_bytes_do_not_merge_admitted_operation_identities() { EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &invocation) + .admit_echo_operation_invocation(&invocation_policy(), &invocation) .expect("the exact package-specific invocation is admitted"); let EchoOperationPreparationV1::Prepared(prepared) = host.prepare_echo_operation_v1(admitted) @@ -1083,9 +1103,9 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let digest_mismatch = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(b"different package bytes"), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(b"different package bytes").as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1095,14 +1115,14 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .expect_err("a package digest cannot confer a match on different bytes"); assert_eq!( digest_mismatch.kind(), - EchoOperationAdmissionErrorKindV1::PackageIdentityMismatch + EchoOperationAdmissionErrorKind::PackageIdentityMismatch ); let malformed_bytes = vec![0xff]; let malformed = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&malformed_bytes), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&malformed_bytes).as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1112,17 +1132,17 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .expect_err("malformed canonical bytes fail before installation"); assert_eq!( malformed.kind(), - EchoOperationAdmissionErrorKindV1::ArtifactInvalid + EchoOperationAdmissionErrorKind::ArtifactInvalid ); - let unsupported_target_bytes = ExecutableOperationPackageV1::new( + let unsupported_target_bytes = ExecutableOperationPackage::new( OPERATION_COORDINATE, "echo.fixture.OperationObstruction/v1", semantic_closure(), digest("unsupported-target-profile"), digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( make_type_id("operation-fixture-node"), make_type_id("operation-fixture-atom"), 1_024, @@ -1131,9 +1151,9 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .to_canonical_bytes() .expect("unsupported target package still has canonical source bytes"); let unsupported_target = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&unsupported_target_bytes), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&unsupported_target_bytes).as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1161,9 +1181,9 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by CanonicalValueV1::Bytes(unsupported_program_bytes), ); let unsupported_program = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&unsupported_program_package), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&unsupported_program_package).as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1185,9 +1205,9 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by CanonicalValueV1::Bytes(digest("unsupported-input-schema").to_vec()), ); let unsupported_schema = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&unsupported_schema_bytes), + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&unsupported_schema_bytes).as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1204,9 +1224,9 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by ); let authority_mismatch = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("different-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1216,13 +1236,13 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .expect_err("package identity cannot substitute for authority policy"); assert_eq!( authority_mismatch.kind(), - EchoOperationAdmissionErrorKindV1::AuthorityProfileMismatch + EchoOperationAdmissionErrorKind::AuthorityProfileMismatch ); let over_budget = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(15, 4_096, 4_096), @@ -1232,17 +1252,17 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .expect_err("a package cannot widen the runtime-owned budget ceiling"); assert_eq!( over_budget.kind(), - EchoOperationAdmissionErrorKindV1::BudgetExceedsPolicy + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy ); - let impossible_package = ExecutableOperationPackageV1::new( + let impossible_package = ExecutableOperationPackage::new( OPERATION_COORDINATE, "echo.fixture.OperationObstruction/v1", semantic_closure(), warp_core::echo_operation_target_profile_identity_v1(), digest("fixture-authority-profile"), EchoOperationBudgetV1::new(1, 1, 1), - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( make_type_id("operation-fixture-node"), make_type_id("operation-fixture-atom"), 1_024, @@ -1276,16 +1296,16 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { ); let malformed = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &[0xff]) + .admit_echo_operation_invocation(&invocation_policy(), &[0xff]) .expect_err("malformed invocation bytes fail before routing"); assert_eq!( malformed.kind(), - EchoOperationInvocationAdmissionErrorKindV1::MalformedInvocation + EchoOperationInvocationAdmissionErrorKind::MalformedInvocation ); let wrong_profile = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("wrong-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1295,12 +1315,12 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { .expect_err("runtime authority policy remains independent of package identity"); assert_eq!( wrong_profile.kind(), - EchoOperationInvocationAdmissionErrorKindV1::AuthorityProfileMismatch + EchoOperationInvocationAdmissionErrorKind::AuthorityProfileMismatch ); let wrong_grant = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("wrong-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1310,7 +1330,7 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { .expect_err("the operation package does not confer an invocation grant"); assert_eq!( wrong_grant.kind(), - EchoOperationInvocationAdmissionErrorKindV1::AuthorityGrantMismatch + EchoOperationInvocationAdmissionErrorKind::AuthorityGrantMismatch ); let over_budget_bytes = canonical_invocation( @@ -1325,11 +1345,11 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { EchoOperationBudgetV1::new(17, 4_096, 4_096), ); let over_budget = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &over_budget_bytes) + .admit_echo_operation_invocation(&invocation_policy(), &over_budget_bytes) .expect_err("delegation cannot exceed package or runtime policy"); assert_eq!( over_budget.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded ); let below_minimum_bytes = canonical_invocation( @@ -1344,11 +1364,11 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { EchoOperationBudgetV1::new(1, 1, 1), ); let below_minimum = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &below_minimum_bytes) + .admit_echo_operation_invocation(&invocation_policy(), &below_minimum_bytes) .expect_err("delegation below the program minimum fails during admission"); assert_eq!( below_minimum.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded ); let unbounded_basis_read_bytes = canonical_invocation( @@ -1363,14 +1383,14 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { EchoOperationBudgetV1::new(4, 64, 32), ); let unbounded_basis_read = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &unbounded_basis_read_bytes) + .admit_echo_operation_invocation(&invocation_policy(), &unbounded_basis_read_bytes) .expect_err("basis corroboration must fit inside the delegated read budget"); assert_eq!( unbounded_basis_read.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BudgetExceeded + EchoOperationInvocationAdmissionErrorKind::BudgetExceeded ); - let wrong_operation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let wrong_operation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), "echo.fixture.DifferentOperation.v1", basis, @@ -1386,11 +1406,11 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { .to_canonical_bytes() .expect("the structurally valid wrong operation invocation encodes"); let wrong_operation = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &wrong_operation) + .admit_echo_operation_invocation(&invocation_policy(), &wrong_operation) .expect_err("a package digest does not confer another operation coordinate"); assert_eq!( wrong_operation.kind(), - EchoOperationInvocationAdmissionErrorKindV1::OperationCoordinateMismatch + EchoOperationInvocationAdmissionErrorKind::OperationCoordinateMismatch ); let uncorroborated_basis = host @@ -1414,11 +1434,11 @@ fn invocation_admission_keeps_contract_authority_budget_and_basis_separate() { EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let uncorroborated_basis = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &uncorroborated_basis_bytes) + .admit_echo_operation_invocation(&invocation_policy(), &uncorroborated_basis_bytes) .expect_err("Echo must corroborate the application basis from current graph state"); assert_eq!( uncorroborated_basis.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); } @@ -1476,7 +1496,7 @@ fn private_evaluation_returns_typed_obstructions_without_parent_mutation() { for (invocation_bytes, expected_kind) in cases { let admitted = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy(), &invocation_bytes) .expect("structurally lawful input reaches private evaluation"); let EchoOperationPreparationV1::Obstructed(obstruction) = host.prepare_echo_operation_v1(admitted) @@ -1498,11 +1518,11 @@ fn private_evaluation_returns_typed_obstructions_without_parent_mutation() { EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let error = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &unresolved_basis) + .admit_echo_operation_invocation(&invocation_policy(), &unresolved_basis) .expect_err("Echo must independently resolve the claimed application basis"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); let state = host @@ -1539,21 +1559,21 @@ fn obstruction_identity_binds_the_exact_invocation_admission() { b"after".to_vec(), EchoOperationBudgetV1::new(8, 512, 512), ); - let first_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let first_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); - let second_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let second_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(32, 8_192, 8_192), ); let obstruct = |host: &mut TrustedRuntimeHost, - policy: &EchoOperationInvocationAdmissionPolicyV1| { + policy: &EchoOperationInvocationAdmissionPolicy| { let admitted = host - .admit_echo_operation_invocation_v1(policy, &invocation_bytes) + .admit_echo_operation_invocation(policy, &invocation_bytes) .expect("both runtime policies admit the same invocation bytes"); let EchoOperationPreparationV1::Obstructed(obstruction) = host.prepare_echo_operation_v1(admitted) @@ -1604,7 +1624,7 @@ fn identical_admitted_basis_and_inputs_produce_identical_consequences() { EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted = host - .admit_echo_operation_invocation_v1(&invocation_policy(), &bytes) + .admit_echo_operation_invocation(&invocation_policy(), &bytes) .expect("the exact invocation is admitted"); let EchoOperationPreparationV1::Prepared(prepared) = host.prepare_echo_operation_v1(admitted) @@ -1811,9 +1831,9 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) .expect("the fresh filesystem WAL opens"); let admitted_package = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1822,12 +1842,12 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { ) .expect("the package is admitted"); let installed = host - .install_admitted_echo_operation_package_v1(admitted_package) + .install_admitted_echo_operation_package(admitted_package) .expect("installation enters the WAL before the live catalog"); let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -1839,8 +1859,8 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { ); let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); let admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1866,7 +1886,7 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { let second_evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, second_application_basis) .expect("Echo resolves the retained non-genesis parent basis"); - let second_invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let second_invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), second_evaluation_basis, @@ -1880,8 +1900,8 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { .to_canonical_bytes() .expect("the second invocation encodes"); let second_admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1935,7 +1955,7 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { .expect("a fresh host recovers without executing application callbacks"); assert!(recovered .engine() - .installed_echo_operation_package_v1(package_id) + .installed_echo_operation_package(package_id) .is_some()); let state = recovered .runtime() @@ -1988,7 +2008,7 @@ fn create_from_absence_commits_one_new_node_and_attachment_patch() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2000,13 +2020,13 @@ fn create_from_absence_commits_one_new_node_and_attachment_patch() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo independently admits the create-from-absence invocation"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Prepared(prepared) = preparation else { @@ -2069,7 +2089,7 @@ fn create_from_absence_refuses_when_the_node_already_exists() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2081,13 +2101,13 @@ fn create_from_absence_refuses_when_the_node_already_exists() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo independently admits the invocation"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { @@ -2137,7 +2157,7 @@ fn update_precondition_still_refuses_when_the_node_is_absent() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2150,17 +2170,17 @@ fn update_precondition_still_refuses_when_the_node_is_absent() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the update invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let error = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect_err("the update profile must reject a missing node at admission"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); } @@ -2189,7 +2209,7 @@ fn create_from_absence_refuses_when_the_node_exists_with_the_wrong_type() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2201,13 +2221,13 @@ fn create_from_absence_refuses_when_the_node_exists_with_the_wrong_type() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo admits the invocation -- node type is not an admission-time check"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { @@ -2239,7 +2259,7 @@ fn create_from_absence_refuses_when_the_node_exists_without_its_attachment() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2251,13 +2271,13 @@ fn create_from_absence_refuses_when_the_node_exists_without_its_attachment() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo admits the invocation -- a bare node still corroborates as absent"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { @@ -2289,7 +2309,7 @@ fn create_from_absence_cannot_commit_after_its_parent_basis_changes() { let basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the current basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), basis, @@ -2300,8 +2320,8 @@ fn create_from_absence_cannot_commit_after_its_parent_basis_changes() { ); let bytes = invocation.to_canonical_bytes().expect("invocation encodes"); let admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -2377,7 +2397,7 @@ fn filesystem_wal_recovers_a_create_from_absence_commit() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2388,8 +2408,8 @@ fn filesystem_wal_recovers_a_create_from_absence_commit() { ); let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); let admitted = host - .admit_echo_operation_invocation_v1( - &EchoOperationInvocationAdmissionPolicyV1::new( + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -2446,7 +2466,7 @@ fn filesystem_wal_recovers_a_create_from_absence_commit() { recovery .installed_echo_operations .iter() - .map(InstalledEchoOperationV1::package_id) + .map(InstalledEchoOperation::package_id) .collect::>(), vec![installed_package_id], "the exact creation package installation must recover" @@ -2483,7 +2503,7 @@ fn create_from_absence_refuses_when_the_attachment_exists_without_its_node() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2495,13 +2515,13 @@ fn create_from_absence_refuses_when_the_attachment_exists_without_its_node() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect( "Echo admits the invocation -- the orphaned attachment honestly \ corroborates as present, matching this invocation's claimed basis", @@ -2551,7 +2571,7 @@ fn create_from_absence_invocation_refuses_at_admission_against_an_orphaned_attac let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, claimed_absent_basis) .expect("Echo resolves a basis for the claimed proposition"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2563,17 +2583,17 @@ fn create_from_absence_invocation_refuses_at_admission_against_an_orphaned_attac let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let error = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect_err("a dishonest absence claim against a real orphaned attachment must refuse"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + EchoOperationInvocationAdmissionErrorKind::BasisMismatch ); } @@ -2602,7 +2622,7 @@ fn create_from_absence_charges_the_node_record_against_the_write_budget() { // 64 (node type + attachment type) + replacement.len() = 71: the correct // requirement. This budget sits strictly between the two. let insufficient_for_node_record = EchoOperationBudgetV1::new(3, 64, 70); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2614,13 +2634,13 @@ fn create_from_absence_charges_the_node_record_against_the_write_budget() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the create-from-absence invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("admission's coarser check does not itself meter the write charge"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { @@ -2659,7 +2679,7 @@ fn update_precondition_refuses_when_the_node_has_the_wrong_type() { let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis) .expect("Echo resolves the exact current parent basis"); - let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -2672,13 +2692,13 @@ fn update_precondition_refuses_when_the_node_has_the_wrong_type() { let invocation_bytes = invocation .to_canonical_bytes() .expect("the update invocation is canonical"); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( digest("fixture-authority-profile"), digest("fixture-authority-grant"), EchoOperationBudgetV1::new(16, 4_096, 4_096), ); let admitted_invocation = host - .admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes) + .admit_echo_operation_invocation(&invocation_policy, &invocation_bytes) .expect("Echo admits the invocation -- node type is not an admission-time check"); let preparation = host.prepare_echo_operation_v1(admitted_invocation); let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { @@ -3092,7 +3112,7 @@ fn scheduler_commits_two_independent_executable_actions_in_one_durable_tick() { .expect("a fresh host recovers the composite Action Tick"); assert!(recovered .engine() - .installed_echo_operation_package_v1(package_id) + .installed_echo_operation_package(package_id) .is_some()); assert_eq!(recovered.runtime().pending_witnessed_submission_count(), 0); for submission_id in [first_submission_id, second_submission_id] { @@ -3264,7 +3284,7 @@ fn accepted_executable_action_recovers_pending_before_scheduler_evaluation() { .expect("the accepted pre-Tick Action recovers"); assert!(recovered .engine() - .installed_echo_operation_package_v1(package_id) + .installed_echo_operation_package(package_id) .is_some()); assert!(matches!( recovered.runtime().observe_intent_outcome(&submission_id), @@ -3368,7 +3388,7 @@ fn action_recovery_bounds_replay_work_and_retained_data_with_stale_bases() { ) .expect("the exact absent-node basis resolves"); let invocation = - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), basis, @@ -3409,7 +3429,7 @@ fn action_recovery_bounds_replay_work_and_retained_data_with_stale_bases() { ) .expect("the delayed basis resolves"); stale_invocation = Some( - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), basis, @@ -3747,7 +3767,7 @@ fn unavailable_action_package_does_not_poison_unrelated_scheduler_work() { .echo_operation_evaluation_basis_v1(head_key, application_basis()) .expect("Echo resolves the current exact parent basis"); let unavailable_invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + EchoOperationInvocation::anchored_node_attachment_compare_and_set( unavailable_package_id, "echo.fixture.Unavailable.v1", evaluation_basis, @@ -3798,7 +3818,7 @@ fn unavailable_action_package_does_not_poison_unrelated_scheduler_work() { )); assert_eq!( host.echo_operation_action_admission_obstruction_v1(&unavailable_submission_id), - Some(EchoOperationInvocationAdmissionErrorKindV1::OperationUnavailable) + Some(EchoOperationInvocationAdmissionErrorKind::OperationUnavailable) ); assert!(host .echo_operation_action_outcome_v1(&unavailable_submission_id) @@ -3825,7 +3845,7 @@ fn unavailable_action_package_does_not_poison_unrelated_scheduler_work() { assert!(steps.is_empty()); assert_eq!( recovered.echo_operation_action_admission_obstruction_v1(&unavailable_submission_id), - Some(EchoOperationInvocationAdmissionErrorKindV1::OperationUnavailable) + Some(EchoOperationInvocationAdmissionErrorKind::OperationUnavailable) ); assert!(recovered .tick_once() @@ -3868,7 +3888,7 @@ fn run_until_idle_continues_after_admission_only_progress() { let mut unavailable_envelopes = Vec::new(); for ordinal in 0..4096_u32 { let unavailable_invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + EchoOperationInvocation::anchored_node_attachment_compare_and_set( unavailable_package_id, "echo.fixture.BoundedUnavailable.v1", evaluation_basis, @@ -3943,22 +3963,21 @@ fn action_outcome_attribution_ignores_application_controlled_scope_collisions() let evaluation_basis = host .echo_operation_evaluation_basis_v1(head_key, application_basis()) .expect("Echo resolves the current exact parent basis"); - let obstructed_invocation = - EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( - installed.package_id(), - installed.operation_coordinate(), - evaluation_basis, - digest("fixture-authority-grant"), - EchoOperationBudgetV1::new(16, 4_096, 4_096), - colliding_scope, - warp_core::echo_operation_atom_value_digest_v1( - make_type_id("operation-fixture-atom"), - b"before", - ), - b"must-not-appear".to_vec(), - ) - .to_canonical_bytes() - .expect("the scope-collision invocation is canonical"); + let obstructed_invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + colliding_scope, + warp_core::echo_operation_atom_value_digest_v1( + make_type_id("operation-fixture-atom"), + b"before", + ), + b"must-not-appear".to_vec(), + ) + .to_canonical_bytes() + .expect("the scope-collision invocation is canonical"); let obstructed_envelope = warp_core::echo_operation_action_envelope_v1( IngressTarget::ExactHead { key: head_key }, obstructed_invocation, @@ -4580,7 +4599,7 @@ fn submit_action( fn creation_action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, payload: &[u8], @@ -4591,7 +4610,7 @@ fn creation_action_invocation( warp_core::echo_operation_anchored_node_absent_application_basis_v1(node), ) .expect("Echo resolves the creation basis"); - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + EchoOperationInvocation::anchored_node_attachment_create_if_absent( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -4944,175 +4963,2029 @@ fn an_action_with_a_forged_origin_keeps_the_whole_frontier_rule() { ); } -/// M2: on an application-profile worldline, Actions commit the tagged -/// application root, the stale-basis exception still corroborates origins -/// (including the genesis boundary), and recovery agrees. -#[cfg(feature = "application_root_profile")] -#[test] -fn application_profile_actions_commit_and_recover() { - use warp_core::application_root::{profile_tagged_state_root, CommitmentProfile}; - - const ACTIONS: usize = 70; - let profiled = |nodes: usize| { - fixture_host_with_independent_nodes_and_state(nodes, |state| { - state.with_commitment_profile(CommitmentProfile::ApplicationRadixV0) - }) - }; - let wal_dir = TempWalDir::new(); - let submissions; - let committed_root; - { - let (mut host, head_key, nodes) = profiled(ACTIONS); - host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) - .expect("the WAL opens"); - let installed = install_fixture_operation(&mut host); - host.install_echo_operation_action_admission_policy_v1(invocation_policy()); - // Every Action is built at the genesis boundary; those decided after - // the first tick need the scoped exception to commit. - let invocations = nodes - .iter() - .map(|node| action_invocation(&host, &installed, head_key, *node, b"before", b"after")) - .collect::>(); - submissions = invocations - .into_iter() - .map(|invocation| submit_action(&mut host, head_key, invocation)) - .collect::>(); - for _ in 0..ACTIONS.div_ceil(64) { - host.tick_once().expect("the scheduler decides a batch"); - } - assert!(submissions.iter().all(|id| outcome_is_committed(&host, id))); - let state = host - .runtime() - .worldlines() - .get(&head_key.worldline_id) - .expect("the fixture worldline exists") - .state(); - committed_root = state.state_root(); - assert_eq!( - committed_root, - profile_tagged_state_root(&state.application_root_rebuilt()) - ); +// These exercise a runtime-selected frame, not an admitted effect profile. +fn effect_frame_limits() -> warp_core::edict_pure::EvaluationLimits { + warp_core::edict_pure::EvaluationLimits { + max_package_bytes: 32768, + max_input_bytes: 1024, + max_output_bytes: 1024, + max_steps: 10000, + max_allocated_bytes: 16777216, } - let (mut recovered, head_key, _) = profiled(ACTIONS); - recovered - .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) - .expect("recovery recomputes validity under the profile and agrees"); - assert!(submissions - .iter() - .all(|id| outcome_is_committed(&recovered, id))); +} + +#[test] +fn effect_frame_binds_the_runtime_parent_before_reading() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("actual basis"); + let edge_type = make_type_id("effect-frame-range"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[edge_type], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits(), + ) + .expect("exact runtime parent"); + assert_eq!(frame.evaluation_basis(), Some(basis)); assert_eq!( - recovered - .runtime() - .worldlines() - .get(&head_key.worldline_id) - .expect("the fixture worldline exists") - .state() - .state_root(), - committed_root + frame + .read_range(warp_core::OutgoingEdgeRange { + source: node, + edge_type + }) + .expect("actual root range"), + Some(vec![]) + ); + assert!(matches!( + frame.readings().get(&warp_core::SlotId::Node(node)), + Some(warp_core::SlotReading::Node(Some(_))) + )); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + let package = hex::decode( + include_str!("fixtures/edict-byte-concat/executable-operation-package.cbor.hex").trim(), + ) + .expect("retained compiler package"); + let pin = hex::decode("ed480a332a9d5f84f7425e46ab0b08cca886c8a040e048145b88fba1e6e0415f") + .expect("pin") + .try_into() + .expect("digest"); + let input = encode_canonical_cbor_v1(&CanonicalValueV1::Map(vec![ + ( + CanonicalValueV1::Text("firstFragment".into()), + CanonicalValueV1::Bytes(b"a".to_vec()), + ), + ( + CanonicalValueV1::Text("secondFragment".into()), + CanonicalValueV1::Bytes(b"b".to_vec()), + ), + ])) + .expect("input"); + let expected = encode_canonical_cbor_v1(&CanonicalValueV1::Map(vec![( + CanonicalValueV1::Text("bytes".into()), + CanonicalValueV1::Bytes(b"ab".to_vec()), + )])) + .expect("independent output"); + assert_eq!( + frame + .evaluate_pure(&package, pin, &input) + .expect("real compiler computation"), + expected + ); + assert_eq!(frame.evaluation_basis(), Some(basis)); + assert!( + frame.consumed().steps() > 1, + "read and computation share the meter" + ); + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("unchanged basis"), + basis ); } -// --------------------------------------------------------------------------- -// Comparison-contract regressions (M0) -// --------------------------------------------------------------------------- +fn effect_frame_host_after_round_trip(intermediate: &[u8]) -> (TrustedRuntimeHost, WriterHeadKey) { + let (mut host, head, node) = fixture_host(); + host.enable_runtime_wal(TrustedRuntimeWalConfig::in_memory()) + .expect("WAL"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + for (before, after) in [ + (b"before".as_slice(), intermediate), + (intermediate, b"before".as_slice()), + ] { + let invocation = action_invocation(&host, &installed, head, node, before, after); + let id = submit_action(&mut host, head, invocation); + host.tick_once().expect("decided runtime tick"); + assert!(outcome_is_committed(&host, &id)); + } + (host, head) +} -/// Every application record on the fixture worldline's root warp, in id order. -/// -/// This is a test-only reference tuple for the application-reading comparison. -/// It is not a production digest and is deliberately independent of the v1 -/// state root. -#[derive(Debug, PartialEq, Eq)] -struct ApplicationRecords { - nodes: Vec<(warp_core::NodeId, NodeRecord)>, - edges: Vec<(warp_core::NodeId, Vec)>, - node_attachments: Vec<(warp_core::NodeId, AttachmentValue)>, - edge_attachments: Vec<(warp_core::EdgeId, AttachmentValue)>, +#[test] +fn effect_frame_refuses_equal_reading_coordinates_from_another_commit_history() { + let (first, head) = effect_frame_host_after_round_trip(b"first-intermediate"); + let (second, second_head) = effect_frame_host_after_round_trip(b"second-intermediate"); + assert_eq!(head, second_head); + let expected = first + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("first basis"); + let actual = second + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("second basis"); + assert_eq!(expected.state_root(), actual.state_root()); + assert_eq!(expected.worldline_tick(), actual.worldline_tick()); + assert_eq!(expected.commit_global_tick(), actual.commit_global_tick()); + assert_ne!(expected.commit_id(), actual.commit_id()); + assert!(matches!( + second.effect_evaluation_frame_v1( + expected, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits() + ), + Err(TrustedRuntimeHostError::EffectFrameSetup( + warp_core::edict_effects::EffectFrameSetupError::BasisMismatch + )) + )); + let frame = second + .effect_evaluation_frame_v1( + actual, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits(), + ) + .expect("current exact history succeeds"); + assert_eq!(frame.evaluation_basis(), Some(actual)); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(frame.readings().is_empty()); } -fn application_records(host: &TrustedRuntimeHost, head_key: WriterHeadKey) -> ApplicationRecords { - let state = host - .runtime() - .worldlines() - .get(&head_key.worldline_id) - .expect("the fixture worldline exists") - .state(); - let store = state - .store(&state.root().warp_id) - .expect("the fixture root warp has a store"); - let mut records = ApplicationRecords { - nodes: store - .iter_nodes() - .map(|(id, record)| (*id, record.clone())) - .collect(), - edges: store - .iter_edges() - .map(|(id, edges)| (*id, edges.clone())) - .collect(), - node_attachments: store - .iter_node_attachments() - .map(|(id, value)| (*id, value.clone())) - .collect(), - edge_attachments: store - .iter_edge_attachments() - .map(|(id, value)| (*id, value.clone())) - .collect(), - }; - records.nodes.sort_by_key(|(id, _)| *id); - records.edges.sort_by_key(|(id, _)| *id); - records.node_attachments.sort_by_key(|(id, _)| *id); - records.edge_attachments.sort_by_key(|(id, _)| *id); - records +#[test] +fn effect_frame_rejects_a_parent_for_an_unregistered_writer() { + let (foreign, first, _, _) = fixture_host_with_two_worldlines(); + let basis = foreign + .echo_operation_evaluation_basis_v1(first, application_basis()) + .expect("foreign registered writer"); + let (host, _, _) = fixture_host(); + assert!(matches!(host.effect_evaluation_frame_v1( + basis, &[], 0, EchoOperationBudgetV1::new(10000, 1024, 0), effect_frame_limits() + ), Err(TrustedRuntimeHostError::Runtime(error)) if matches!(*error, RuntimeError::UnknownHead(key) if key == first))); } -fn state_root_and_tick( - host: &TrustedRuntimeHost, - head_key: WriterHeadKey, -) -> (warp_core::Hash, u64) { - let frontier = host - .runtime() - .worldlines() - .get(&head_key.worldline_id) - .expect("the fixture worldline exists"); - ( - frontier.state().state_root(), - frontier.frontier_tick().as_u64(), - ) +#[test] +fn effect_frame_refuses_a_stale_parent_after_a_decided_tick() { + let (mut host, head, node) = fixture_host(); + host.enable_runtime_wal(TrustedRuntimeWalConfig::in_memory()) + .expect("WAL"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + let stale = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("original parent"); + let invocation = action_invocation(&host, &installed, head, node, b"before", b"after"); + let id = submit_action(&mut host, head, invocation); + host.tick_once().expect("decided tick"); + assert!(outcome_is_committed(&host, &id)); + assert!(matches!( + host.effect_evaluation_frame_v1( + stale, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits() + ), + Err(TrustedRuntimeHostError::EffectFrameSetup( + warp_core::edict_effects::EffectFrameSetupError::BasisMismatch + )) + )); } -/// A host with two independent nodes and a second, non-default writer head on -/// the same worldline. The second head holds an accepted Action, dormant, while -/// the first head commits other work. -fn fixture_host_with_holding_head() -> ( - TrustedRuntimeHost, - WriterHeadKey, - WriterHeadKey, - Vec, -) { - let (host, writer, nodes) = fixture_host_with_independent_nodes(2); - let holder = WriterHeadKey { - worldline_id: writer.worldline_id, - head_id: make_head_id("operation-fixture-holding-writer"), - }; - let mut parts = host.into_parts(); - parts - .runtime_mut() - .register_writer_head(WriterHead::with_routing( - holder, - PlaybackMode::Play, - InboxPolicy::AcceptAll, - None, - false, +#[test] +fn effect_frame_bound_parent_retains_the_resource_ceiling() { + let (host, head, _) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("actual parent"); + assert!(matches!( + host.effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(1048577, 1024, 0), + effect_frame_limits() + ), + Err(TrustedRuntimeHostError::EffectFrameSetup( + warp_core::edict_effects::EffectFrameSetupError::ResourceCeilingExceeded )) - .expect("the holding head registers"); - (TrustedRuntimeHost::from_parts(parts), writer, holder, nodes) + )); + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("unchanged parent"), + basis + ); } -fn with_head_eligibility( - host: TrustedRuntimeHost, - head_key: WriterHeadKey, - eligibility: HeadEligibility, -) -> TrustedRuntimeHost { - let mut parts = host.into_parts(); +fn recorded_read_package() -> (Vec, [u8; 32]) { + let raw = hex::decode( + include_str!("fixtures/edict-node-read/single-executable-operation-package.hex").trim(), + ) + .expect("retained compiler package"); + let pin = hex::decode("a2ae35d39ae74a26da916544464529bc6c7af32a76b6e6f354ef79c5063ab109") + .expect("pin") + .try_into() + .expect("digest"); + (raw, pin) +} +fn recorded_read_input(node: NodeKey, expected: &[u8]) -> Vec { + encode_canonical_cbor_v1(&CanonicalValueV1::Map(vec![ + ( + CanonicalValueV1::Text("address".into()), + CanonicalValueV1::Map(vec![ + ( + CanonicalValueV1::Text("warpId".into()), + CanonicalValueV1::Bytes(node.warp_id.0.to_vec()), + ), + ( + CanonicalValueV1::Text("nodeId".into()), + CanonicalValueV1::Bytes(node.local_id.0.to_vec()), + ), + ( + CanonicalValueV1::Text("typeId".into()), + CanonicalValueV1::Bytes(make_type_id("operation-fixture-atom").0.to_vec()), + ), + ]), + ), + ( + CanonicalValueV1::Text("expected".into()), + CanonicalValueV1::Bytes(expected.to_vec()), + ), + ])) + .expect("canonical input") +} +fn recorded_read_limits() -> warp_core::edict_read::ReadLimits { + warp_core::edict_read::ReadLimits { + evaluation: effect_frame_limits(), + max_reads: 64, + max_read_bytes: 1048576, + } +} + +#[test] +fn recorded_effect_frame_interprets_actual_atom_reads_and_retains_dependencies() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let frontier = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("frontier"); + let aperture = [node]; + let plain = warp_core::edict_read::ReadView::at(frontier, &aperture).expect("plain control"); + let (package, pin) = recorded_read_package(); + let input = recorded_read_input(node, b"before"); + let control = + warp_core::edict_read::evaluate(&package, pin, &input, &plain, recorded_read_limits()) + .expect("standalone control"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + let output = frame + .evaluate_read(&package, pin, &input, &aperture, recorded_read_limits()) + .expect("interpreted read"); + assert_eq!( + output, + encode_canonical_cbor_v1(&CanonicalValueV1::Bytes(b"before".to_vec())) + .expect("literal output") + ); + assert_eq!(output, control.output); + assert_eq!(frame.readings().len(), 2); + assert!(matches!( + frame.readings().get(&warp_core::SlotId::Node(node)), + Some(warp_core::SlotReading::Node(Some(_))) + )); + let attachment = warp_core::AttachmentKey::node_alpha(node); + assert_eq!( + frame + .readings() + .get(&warp_core::SlotId::Attachment(attachment)), + Some(&warp_core::SlotReading::Attachment(Some( + AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from_static(b"before") + )) + ))) + ); + assert!(frame.footprint().n_read.iter().any(|key| *key == node)); + assert!(frame + .footprint() + .a_read + .iter() + .any(|key| *key == attachment)); + assert_eq!(frame.consumed().read_bytes(), 134); + assert_eq!(frame.consumed().steps(), control.steps + 2); + assert_eq!(frame.allocated_bytes(), control.allocated_bytes + 262); + assert_eq!(frame.evaluation_basis(), Some(basis)); +} + +#[test] +fn recorded_effect_frame_retains_absence_and_stops_after_an_authored_obstruction() { + let (host, head, root) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let missing = NodeKey { + local_id: make_node_id("recorded-absent"), + ..root + }; + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert!(matches!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(missing, b""), + &[missing], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::Obstructed { + cause: warp_core::edict_read::ReadObstruction::Missing, + .. + } + )) + )); + assert_eq!( + frame.readings().get(&warp_core::SlotId::Node(missing)), + Some(&warp_core::SlotReading::Node(None)) + ); + assert_eq!(frame.readings().len(), 1); + assert!(frame.footprint().n_read.iter().any(|key| *key == missing)); + let consumed = frame.consumed(); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(root, b"before"), + &[root], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::Terminated) + ); + assert_eq!(frame.consumed(), consumed); +} + +#[test] +fn recorded_effect_frame_refuses_outside_aperture_without_graph_observations() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::OutsideAperture(node) + )) + ); + assert!(frame.readings().is_empty()); + assert_eq!(frame.consumed().read_bytes(), 0); +} + +#[test] +fn recorded_effect_frame_keeps_the_resolved_prefix_on_read_budget_failure() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 64, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[node], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::Read( + warp_core::edict_effects::EffectRangeReadError::BudgetExceeded + )) + ); + assert_eq!(frame.consumed().read_bytes(), 64); + assert_eq!(frame.readings().len(), 1); + assert!(matches!( + frame.readings().get(&warp_core::SlotId::Node(node)), + Some(warp_core::SlotReading::Node(Some(_))) + )); +} + +#[test] +fn recorded_effect_frame_cannot_reset_work_between_compiler_packages() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let input = recorded_read_input(node, b"before"); + let mut control = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("control"); + control + .evaluate_read(&package, pin, &input, &[node], recorded_read_limits()) + .expect("control succeeds"); + let steps = control.consumed().steps(); + let limits = warp_core::edict_pure::EvaluationLimits { + max_steps: 2 * steps - 1, + ..effect_frame_limits() + }; + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(limits.max_steps, 4096, 0), + limits, + ) + .expect("bounded frame"); + frame + .evaluate_read(&package, pin, &input, &[node], recorded_read_limits()) + .expect("first package fits"); + assert!(matches!( + frame.evaluate_read(&package, pin, &input, &[node], recorded_read_limits()), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::Evaluation( + warp_core::edict_pure::EvaluationError::StepBudgetExceeded + ) + )) + )); +} + +#[test] +fn recorded_effect_frame_keeps_atom_failures_and_their_resolved_slots() { + use warp_core::edict_read::{ReadError, ReadObstruction}; + for (attachment, cause) in [ + (None, ReadObstruction::Missing), + ( + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("wrong-atom-type"), + Bytes::from_static(b"before"), + ))), + ReadObstruction::TypeMismatch, + ), + ( + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from(vec![0; 1048577]), + ))), + ReadObstruction::AtomTooLarge, + ), + ] { + let (host, head, nodes) = fixture_host_with_attachment(1, attachment.clone()); + let node = nodes[0]; + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 2097152, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b""), + &[node], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + ReadError::Obstructed { + coordinate: if cause == ReadObstruction::Missing { + "text.FactMissing" + } else { + "text.FactMalformed" + } + .into(), + cause, + } + )) + ); + assert_eq!(frame.readings().len(), 2); + assert_eq!( + frame.readings().get(&warp_core::SlotId::Attachment( + warp_core::AttachmentKey::node_alpha(node) + )), + Some(&warp_core::SlotReading::Attachment(attachment)) + ); + } +} + +#[test] +fn recorded_effect_frame_keeps_actual_values_when_the_authored_guard_refuses() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b"different"), + &[node], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::Obstructed { + coordinate: "text.BasisNotCanonical".into(), + cause: warp_core::edict_read::ReadObstruction::Guard, + } + )) + ); + assert_eq!(frame.readings().len(), 2); + assert_eq!( + frame.readings().get(&warp_core::SlotId::Attachment( + warp_core::AttachmentKey::node_alpha(node) + )), + Some(&warp_core::SlotReading::Attachment(Some( + AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from_static(b"before") + )) + ))) + ); +} + +#[test] +fn recorded_effect_frame_cannot_widen_frame_decode_and_output_limits() { + use warp_core::edict_pure::EvaluationError; + for (limits, error, observations) in [ + ( + warp_core::edict_pure::EvaluationLimits { + max_package_bytes: 1, + ..effect_frame_limits() + }, + EvaluationError::PackageTooLarge, + 0, + ), + ( + warp_core::edict_pure::EvaluationLimits { + max_input_bytes: 1, + ..effect_frame_limits() + }, + EvaluationError::InputTooLarge, + 0, + ), + ( + warp_core::edict_pure::EvaluationLimits { + max_output_bytes: 1, + ..effect_frame_limits() + }, + EvaluationError::OutputBudgetExceeded, + 2, + ), + ] { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + limits, + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[node], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::Evaluation(error) + )) + ); + assert_eq!(frame.readings().len(), observations); + } +} + +#[test] +fn recorded_effect_frame_refuses_invalid_apertures_before_reading() { + let (host, head, node) = fixture_host(); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let (package, pin) = recorded_read_package(); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 4096, 0), + effect_frame_limits(), + ) + .expect("bound frame"); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[node, node], + recorded_read_limits() + ), + Err(warp_core::edict_effects::EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::InvalidAperture + )) + ); + assert!(frame.readings().is_empty()); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); +} + +// Typed-law witnesses use real runtime-owned frontiers and the retained compiler package. +fn effect_law_value( + nodes: &[warp_core::TypeId], + atoms: &[warp_core::TypeId], + portals: &[warp_core::TypeId], + max_reads: u64, +) -> CanonicalValueV1 { + use CanonicalValueV1 as V; + let mut fields = Vec::new(); + for (name, ids) in [ + ("read_node_types", nodes), + ("write_node_types", &[][..]), + ("atom_types", atoms), + ("range_edge_types", &[][..]), + ("write_edge_types", &[][..]), + ("portal_owner_types", portals), + ] { + let mut ids = ids.to_vec(); + ids.sort_unstable(); + fields.push(( + V::Text(name.into()), + V::Array(ids.iter().map(|id| V::Bytes(id.0.to_vec())).collect()), + )); + } + for (name, count) in [ + ("max_reads", max_reads), + ("max_writes", 0), + ("max_range_edges", 0), + ] { + fields.push((V::Text(name.into()), V::Integer(i128::from(count)))); + } + V::Map(fields) +} + +fn typed_effect_frame( + host: &TrustedRuntimeHost, + head: WriterHeadKey, + law: CanonicalValueV1, +) -> warp_core::edict_effects::EffectEvaluationFrame<'_> { + let law = warp_core::edict_effects::EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&law).expect("law bytes"), + ) + .expect("valid law"); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + host.effect_evaluation_frame_v1( + basis, + &[], + 65536, + EchoOperationBudgetV1::new(100000, 16777216, 0), + effect_frame_limits(), + ) + .expect("bound frame") + .with_footprint_law(law) + .expect("unused law-bound frame") +} + +#[test] +fn typed_effect_node_reads_authorize_actual_types_and_keep_named_absence_distinct() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError}; + let (host, head, root) = fixture_host(); + let actual = make_type_id("operation-fixture-node"); + let a = make_type_id("absent-a"); + let b = make_type_id("absent-b"); + let c = make_type_id("absent-c"); + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[actual], &[], &[], 1)); + assert_eq!( + frame.read_typed_node(root, a).expect("actual type wins"), + Some(NodeRecord { ty: actual }) + ); + assert!(frame + .authorized_reads() + .contains(&(warp_core::SlotId::Node(root), actual))); + let mut denied = typed_effect_frame(&host, head, effect_law_value(&[a], &[], &[], 1)); + assert_eq!( + denied.read_typed_node(root, a), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(denied.readings().is_empty()); + assert_eq!( + denied.read_typed_node(root, actual), + Err(EffectFrameError::Terminated) + ); + + let mut zero = typed_effect_frame(&host, head, effect_law_value(&[actual], &[], &[], 0)); + assert_eq!( + zero.read_typed_node(root, actual), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(zero.readings().is_empty()); + + let absent = NodeKey { + local_id: make_node_id("typed-missing"), + ..root + }; + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[a, b, c], &[], &[], 2)); + for ty in [a, a, b] { + assert_eq!(frame.read_typed_node(absent, ty), Ok(None)); + } + assert_eq!(frame.authorized_reads().len(), 2); + assert!(frame + .authorized_reads() + .contains(&(warp_core::SlotId::Node(absent), a))); + assert!(frame + .authorized_reads() + .contains(&(warp_core::SlotId::Node(absent), b))); + assert_eq!( + frame.read_typed_node(absent, c), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert_eq!(frame.authorized_reads().len(), 2); +} + +#[test] +fn typed_effect_compiled_reads_check_actual_atom_type_and_reject_untyped_bypass() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError, TypedNodeRead}; + let (host, head, root) = fixture_host(); + let node_type = make_type_id("operation-fixture-node"); + let atom_type = make_type_id("operation-fixture-atom"); + let aperture = [TypedNodeRead { + node: root, + absent_node_type: make_type_id("irrelevant-when-present"), + }]; + let (package, pin) = recorded_read_package(); + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[node_type], &[atom_type], &[], 2), + ); + assert_eq!( + frame + .evaluate_typed_read( + &package, + pin, + &recorded_read_input(root, b"before"), + &aperture, + recorded_read_limits() + ) + .expect("compiled read"), + encode_canonical_cbor_v1(&CanonicalValueV1::Bytes(b"before".to_vec())) + .expect("typed-law fixture and independently encoded witness") + ); + assert_eq!(frame.authorized_reads().len(), 2); + let mut denied = typed_effect_frame(&host, head, effect_law_value(&[node_type], &[], &[], 2)); + assert_eq!( + denied.evaluate_typed_read( + &package, + pin, + &recorded_read_input(root, b"before"), + &aperture, + recorded_read_limits() + ), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert_eq!( + denied.readings().len(), + 1, + "the unauthorized atom is never retained" + ); + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[node_type], &[atom_type], &[], 2), + ); + assert_eq!( + frame.evaluate_read( + &package, + pin, + &recorded_read_input(root, b"before"), + &[root], + recorded_read_limits() + ), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(frame.readings().is_empty()); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); +} + +#[test] +fn typed_effect_unreachable_warp_is_refused_before_target_resolution() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError}; + let (host, head, root) = fixture_host(); + let node_type = make_type_id("operation-fixture-node"); + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[node_type], &[], &[], 4)); + assert_eq!( + frame.read_typed_node( + NodeKey { + warp_id: make_warp_id("unregistered"), + ..root + }, + node_type + ), + Err(EffectFrameError::Read( + EffectRangeReadError::UnreachableWarp + )) + ); + assert!(frame.readings().is_empty()); +} + +#[test] +fn typed_effect_law_refuses_noncanonical_sets_shapes_and_oversized_counts() { + use warp_core::edict_effects::{EffectFootprintLaw, EffectLawError}; + use CanonicalValueV1 as V; + let base = effect_law_value(&[], &[], &[], 0); + let V::Map(fields) = base else { unreachable!() }; + for (name, invalid) in [ + ("max_reads", V::Integer(4097)), + ("max_writes", V::Integer(257)), + ("max_range_edges", V::Integer(65537)), + ("max_reads", V::Integer(-1)), + ( + "read_node_types", + V::Array(vec![V::Bytes(vec![2; 32]), V::Bytes(vec![1; 32])]), + ), + ( + "atom_types", + V::Array(vec![V::Bytes(vec![1; 32]), V::Bytes(vec![1; 32])]), + ), + ("portal_owner_types", V::Array(vec![V::Bytes(vec![1; 31])])), + ] { + let mut bad = fields.clone(); + bad.iter_mut() + .find(|(key, _)| *key == V::Text(name.into())) + .expect("typed-law fixture and independently encoded witness") + .1 = invalid; + assert_eq!( + EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&V::Map(bad)) + .expect("typed-law fixture and independently encoded witness") + ), + Err(EffectLawError::InvalidLaw), + "{name}" + ); + } + let mut missing = fields.clone(); + missing.pop(); + assert_eq!( + EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&V::Map(missing)) + .expect("typed-law fixture and independently encoded witness") + ), + Err(EffectLawError::InvalidLaw) + ); + let mut extra = fields; + extra.push((V::Text("extra".into()), V::Integer(0))); + assert_eq!( + EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&V::Map(extra)) + .expect("typed-law fixture and independently encoded witness") + ), + Err(EffectLawError::InvalidLaw) + ); + assert_eq!( + EffectFootprintLaw::from_canonical_bytes(&[0xbf, 0xff]), + Err(EffectLawError::InvalidLaw) + ); +} + +fn typed_portal_fixture( + beta: bool, + depth: usize, +) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey, Vec) { + use warp_core::{ + AttachmentKey, EdgeRecord, PortalInit, TickCommitStatus, WarpOp, WarpTickPatchV1, + }; + let (host, head, root) = fixture_host(); + let mut state = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("typed-law fixture and independently encoded witness") + .state() + .warp_state() + .clone(); + let mut owner = root; + let mut owners = Vec::new(); + for index in 0..depth { + owners.push(owner); + let child = NodeKey { + warp_id: make_warp_id(&format!("typed-child-{index}")), + local_id: make_node_id("child-root"), + }; + let mut ops = Vec::new(); + let portal = if beta { + let edge = warp_core::EdgeKey { + warp_id: owner.warp_id, + local_id: warp_core::make_edge_id("beta-portal-edge"), + }; + state + .store_mut(&owner.warp_id) + .expect("typed-law fixture and independently encoded witness") + .insert_edge( + owner.local_id, + EdgeRecord { + id: edge.local_id, + from: owner.local_id, + to: owner.local_id, + ty: make_type_id("beta-edge"), + }, + ); + AttachmentKey::edge_beta(edge) + } else { + AttachmentKey::node_alpha(owner) + }; + ops.push(WarpOp::OpenPortal { + key: portal, + child_warp: child.warp_id, + child_root: child.local_id, + init: PortalInit::Empty { + root_record: NodeRecord { + ty: make_type_id("operation-child-node"), + }, + }, + }); + WarpTickPatchV1::new(0, [0; 32], TickCommitStatus::Committed, vec![], vec![], ops) + .apply_to_state(&mut state) + .expect("lawful portal"); + state + .store_mut(&child.warp_id) + .expect("typed-law fixture and independently encoded witness") + .set_node_attachment( + child.local_id, + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from_static(b"before"), + ))), + ); + owner = child; + } + let (host, head) = + fixture_runtime_host(WorldlineState::new(state, root).expect("portal state")); + (host, head, owner, owners) +} + +#[test] +fn typed_effect_portal_chain_is_witnessed_root_first_and_reused_at_the_same_parent() { + use warp_core::edict_effects::TypedNodeRead; + use warp_core::{AttachmentKey, SlotId}; + let (host, head, target, owners) = typed_portal_fixture(false, 2); + let child_type = make_type_id("operation-child-node"); + let law = effect_law_value( + &[child_type], + &[make_type_id("operation-fixture-atom")], + &[make_type_id("operation-fixture-node"), child_type], + 6, + ); + let mut frame = typed_effect_frame(&host, head, law); + let (package, pin) = recorded_read_package(); + assert_eq!( + frame + .evaluate_typed_read( + &package, + pin, + &recorded_read_input(target, b"before"), + &[TypedNodeRead { + node: target, + absent_node_type: child_type + }], + recorded_read_limits() + ) + .expect("descended compiled read"), + encode_canonical_cbor_v1(&CanonicalValueV1::Bytes(b"before".to_vec())) + .expect("typed-law fixture and independently encoded witness") + ); + for owner in owners { + assert!(frame.readings().contains_key(&SlotId::Node(owner))); + assert_eq!( + frame + .readings() + .get(&SlotId::Attachment(AttachmentKey::node_alpha(owner))), + Some(&warp_core::SlotReading::Attachment(Some( + AttachmentValue::Descend(if owner.warp_id == make_warp_id("operation-fixture") { + make_warp_id("typed-child-0") + } else { + target.warp_id + }) + ))) + ); + } + assert_eq!(frame.authorized_reads().len(), 6); + let before = frame.consumed(); + assert_eq!( + frame.read_typed_node(target, child_type), + Ok(Some(NodeRecord { ty: child_type })) + ); + assert_eq!( + frame.consumed().steps() - before.steps(), + 1, + "validated immutable portals are reused" + ); + assert_eq!(frame.consumed().read_bytes() - before.read_bytes(), 64); +} + +#[test] +fn typed_effect_portal_permissions_precede_descended_data_and_beta_owner_reads() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError}; + let child_type = make_type_id("operation-child-node"); + for beta in [false, true] { + let (host, head, target, _) = typed_portal_fixture(beta, 1); + let portal_types = if beta { + vec![make_type_id("operation-fixture-node")] + } else { + vec![] + }; + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[child_type], &[], &portal_types, 4), + ); + assert_eq!( + frame.read_typed_node(target, child_type), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!( + frame.readings().is_empty(), + "neither the unauthorized owner nor the target is retained" + ); + } +} + +fn typed_range_frame( + host: &TrustedRuntimeHost, + head: WriterHeadKey, + node_type: warp_core::TypeId, + edge_type: warp_core::TypeId, + max_edges: u64, +) -> warp_core::edict_effects::EffectEvaluationFrame<'_> { + use CanonicalValueV1 as V; + let V::Map(mut fields) = effect_law_value(&[node_type], &[], &[], 2) else { + unreachable!() + }; + for (name, value) in [ + ( + "range_edge_types", + V::Array(vec![V::Bytes(edge_type.0.to_vec())]), + ), + ("max_range_edges", V::Integer(i128::from(max_edges))), + ] { + fields + .iter_mut() + .find(|(key, _)| *key == V::Text(name.into())) + .expect("typed-law fixture and independently encoded witness") + .1 = value; + } + let law = warp_core::edict_effects::EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&V::Map(fields)) + .expect("typed-law fixture and independently encoded witness"), + ) + .expect("typed-law fixture and independently encoded witness"); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("typed-law fixture and independently encoded witness"); + host.effect_evaluation_frame_v1( + basis, + &[edge_type], + 65536, + EchoOperationBudgetV1::new(100000, 16777216, 0), + effect_frame_limits(), + ) + .expect("typed-law fixture and independently encoded witness") + .with_footprint_law(law) + .expect("typed-law fixture and independently encoded witness") +} + +#[test] +fn typed_effect_ranges_enforce_source_types_whole_bucket_limits_and_no_untyped_bypass() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError}; + use warp_core::{OutgoingEdgeRange, SlotId, TypeId}; + let (host, head, root) = fixture_host(); + let node_type = make_type_id("operation-fixture-node"); + let edge_type = make_type_id("typed-range"); + let range = OutgoingEdgeRange { + source: root, + edge_type, + }; + let mut frame = typed_range_frame(&host, head, node_type, edge_type, 3); + assert_eq!( + frame.read_typed_range(range, make_type_id("unused-if-present")), + Ok(Some(vec![])) + ); + assert!(frame + .authorized_reads() + .contains(&(SlotId::OutgoingEdges(range), TypeId([0; 32])))); + let mut denied = typed_range_frame(&host, head, make_type_id("wrong-owner-type"), edge_type, 3); + assert_eq!( + denied.read_typed_range(range, node_type), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(denied.readings().is_empty()); + let mut denied = typed_range_frame(&host, head, node_type, edge_type, 3); + assert_eq!( + denied.read_range(range), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert_eq!(denied.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + + let mut state = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("typed-law fixture and independently encoded witness") + .state() + .warp_state() + .clone(); + for (index, ty) in [ + edge_type, + make_type_id("distractor"), + make_type_id("distractor"), + ] + .into_iter() + .enumerate() + { + state + .store_mut(&root.warp_id) + .expect("typed-law fixture and independently encoded witness") + .insert_edge( + root.local_id, + warp_core::EdgeRecord { + id: warp_core::make_edge_id(&format!("typed-range-{index}")), + from: root.local_id, + to: root.local_id, + ty, + }, + ); + } + let (host, head) = fixture_runtime_host( + WorldlineState::new(state, root) + .expect("typed-law fixture and independently encoded witness"), + ); + let mut frame = typed_range_frame(&host, head, node_type, edge_type, 1); + assert_eq!( + frame.read_typed_range(range, node_type), + Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)) + ); + assert_eq!( + frame.readings().len(), + 1, + "the whole bucket refuses before any edge observation" + ); + let mut frame = typed_range_frame(&host, head, node_type, edge_type, 3); + assert_eq!( + frame + .read_typed_range(range, node_type) + .expect("typed-law fixture and independently encoded witness") + .expect("typed-law fixture and independently encoded witness") + .len(), + 1 + ); + assert_eq!( + frame.consumed(), + EchoOperationBudgetV1::new(4, 256, 0), + "nonmatching edges count" + ); +} + +#[test] +fn typed_effect_atom_absence_uses_named_type_and_present_mismatch_uses_actual_type() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError, TypedNodeRead}; + use warp_core::edict_read::{ReadError, ReadObstruction}; + use warp_core::{AttachmentKey, SlotId}; + let node_type = make_type_id("operation-fixture-node"); + let atom_type = make_type_id("operation-fixture-atom"); + let (package, pin) = recorded_read_package(); + for allow in [true, false] { + let (host, head, nodes) = fixture_host_with_attachment(1, None); + let node = nodes[0]; + let atom_types = if allow { vec![atom_type] } else { vec![] }; + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[node_type], &atom_types, &[], 2), + ); + let error = frame.evaluate_typed_read( + &package, + pin, + &recorded_read_input(node, b""), + &[TypedNodeRead { + node, + absent_node_type: node_type, + }], + recorded_read_limits(), + ); + if allow { + assert!(matches!( + error, + Err(EffectFrameError::BoundedRead(ReadError::Obstructed { + cause: ReadObstruction::Missing, + .. + })) + )); + assert!(frame.authorized_reads().contains(&( + SlotId::Attachment(AttachmentKey::node_alpha(node)), + atom_type + ))); + assert_eq!( + frame + .readings() + .get(&SlotId::Attachment(AttachmentKey::node_alpha(node))), + Some(&warp_core::SlotReading::Attachment(None)) + ); + } else { + assert_eq!( + error, + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert_eq!(frame.readings().len(), 1); + } + } + let (host, head, node) = fixture_host(); + let mut input = decode_canonical_cbor_v1(&recorded_read_input(node, b"before")) + .expect("typed-law fixture and independently encoded witness"); + let CanonicalValueV1::Map(fields) = &mut input else { + unreachable!() + }; + let (_, CanonicalValueV1::Map(address)) = fields + .iter_mut() + .find(|(key, _)| *key == CanonicalValueV1::Text("address".into())) + .expect("typed-law fixture and independently encoded witness") + else { + unreachable!() + }; + address + .iter_mut() + .find(|(key, _)| *key == CanonicalValueV1::Text("typeId".into())) + .expect("typed-law fixture and independently encoded witness") + .1 = CanonicalValueV1::Bytes(make_type_id("unadmitted-requested-type").0.to_vec()); + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[node_type], &[atom_type], &[], 2), + ); + assert!(matches!( + frame.evaluate_typed_read( + &package, + pin, + &encode_canonical_cbor_v1(&input) + .expect("typed-law fixture and independently encoded witness"), + &[TypedNodeRead { + node, + absent_node_type: node_type + }], + recorded_read_limits() + ), + Err(EffectFrameError::BoundedRead(ReadError::Obstructed { + cause: ReadObstruction::TypeMismatch, + .. + })) + )); + assert!(frame.authorized_reads().contains(&( + SlotId::Attachment(AttachmentKey::node_alpha(node)), + atom_type + ))); +} + +#[test] +fn typed_effect_portal_authorization_does_not_grant_ordinary_node_read_permission() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError}; + let (host, head, target, owners) = typed_portal_fixture(false, 1); + let parent_type = make_type_id("operation-fixture-node"); + let child_type = make_type_id("operation-child-node"); + let mut frame = typed_effect_frame( + &host, + head, + effect_law_value(&[child_type], &[], &[parent_type], 3), + ); + assert_eq!( + frame.read_typed_node(target, child_type), + Ok(Some(NodeRecord { ty: child_type })) + ); + assert!(frame + .authorized_reads() + .contains(&(warp_core::SlotId::Node(owners[0]), parent_type))); + assert_eq!( + frame.read_typed_node(owners[0], parent_type), + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )), + "deduplication must not bypass the permission role" + ); +} + +#[test] +fn typed_effect_portal_resolutions_keep_atomic_budget_and_storage_prefixes() { + use warp_core::edict_effects::{EffectFootprintLaw, EffectFrameError, EffectRangeReadError}; + let (host, head, target, owners) = typed_portal_fixture(false, 1); + let parent_type = make_type_id("operation-fixture-node"); + let child_type = make_type_id("operation-child-node"); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("typed-law fixture and independently encoded witness"); + for (steps, bytes, storage, want_steps, want_bytes, want_storage) in [ + (1, 1024, 16777216, 1, 64, 256), + (10, 128, 16777216, 2, 128, 448), + (10, 1024, 512, 2, 128, 448), + ] { + let law = EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&effect_law_value(&[child_type], &[], &[parent_type], 4)) + .expect("typed-law fixture and independently encoded witness"), + ) + .expect("typed-law fixture and independently encoded witness"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(steps, bytes, 0), + warp_core::edict_pure::EvaluationLimits { + max_allocated_bytes: storage, + ..effect_frame_limits() + }, + ) + .expect("typed-law fixture and independently encoded witness") + .with_footprint_law(law) + .expect("typed-law fixture and independently encoded witness"); + assert_eq!( + frame.read_typed_node(target, child_type), + Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)) + ); + assert_eq!( + frame.consumed(), + EchoOperationBudgetV1::new(want_steps, want_bytes, 0) + ); + assert_eq!(frame.allocated_bytes(), want_storage); + assert!(!frame + .readings() + .contains_key(&warp_core::SlotId::Node(target))); + if want_steps == 2 { + assert!(frame + .readings() + .contains_key(&warp_core::SlotId::Node(owners[0]))); + } + assert_eq!( + frame.read_typed_node(target, child_type), + Err(EffectFrameError::Terminated) + ); + } +} + +#[test] +fn typed_effect_law_cannot_be_installed_after_work_or_on_an_unbound_frame() { + use warp_core::edict_effects::{EffectFootprintLaw, EffectFrameSetupError}; + let (host, head, root) = fixture_host(); + let node_type = make_type_id("operation-fixture-node"); + let bytes = encode_canonical_cbor_v1(&effect_law_value(&[node_type], &[], &[], 1)) + .expect("typed-law fixture and independently encoded witness"); + let law = || { + EffectFootprintLaw::from_canonical_bytes(&bytes) + .expect("typed-law fixture and independently encoded witness") + }; + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[node_type], &[], &[], 1)); + frame + .read_typed_node(root, node_type) + .expect("typed-law fixture and independently encoded witness"); + assert!(matches!( + frame.with_footprint_law(law()), + Err(EffectFrameSetupError::InvalidLawBinding) + )); + let frontier = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("typed-law fixture and independently encoded witness"); + let reads = warp_core::edict_effects::EffectRangeReadSession::new( + frontier, + &[], + 0, + EchoOperationBudgetV1::new(100, 1000, 0), + ); + let frame = warp_core::edict_effects::EffectEvaluationFrame::new(reads, effect_frame_limits()) + .expect("typed-law fixture and independently encoded witness"); + assert!(matches!( + frame.with_footprint_law(law()), + Err(EffectFrameSetupError::InvalidLawBinding) + )); +} + +#[test] +fn typed_effect_direct_portal_atom_probe_requires_portal_owner_permission() { + use warp_core::edict_effects::{EffectFrameError, EffectRangeReadError, TypedNodeRead}; + use warp_core::edict_read::{ReadError, ReadObstruction}; + let (host, head, _, owners) = typed_portal_fixture(false, 1); + let owner = owners[0]; + let ty = make_type_id("operation-fixture-node"); + let (package, pin) = recorded_read_package(); + for allow in [true, false] { + let (nodes, portals) = if allow { + (vec![], vec![ty]) + } else { + (vec![ty], vec![]) + }; + let mut frame = typed_effect_frame(&host, head, effect_law_value(&nodes, &[], &portals, 2)); + let result = frame.evaluate_typed_read( + &package, + pin, + &recorded_read_input(owner, b""), + &[TypedNodeRead { + node: owner, + absent_node_type: ty, + }], + recorded_read_limits(), + ); + if allow { + assert!(matches!( + result, + Err(EffectFrameError::BoundedRead(ReadError::Obstructed { + cause: ReadObstruction::AtomRequired, + .. + })) + )); + assert_eq!(frame.authorized_reads().len(), 2); + assert!(frame.authorized_reads().contains(&( + warp_core::SlotId::Attachment(warp_core::AttachmentKey::node_alpha(owner)), + ty + ))); + } else { + assert_eq!( + result, + Err(EffectFrameError::Read( + EffectRangeReadError::FootprintLawViolation + )) + ); + assert!(frame.readings().is_empty()); + } + } +} + +#[test] +fn typed_effect_compiled_apertures_refuse_duplicate_and_unordered_declarations_before_work() { + use warp_core::edict_effects::{EffectFrameError, TypedNodeRead}; + let (host, head, root) = fixture_host(); + let ty = make_type_id("operation-fixture-node"); + let node = TypedNodeRead { + node: root, + absent_node_type: ty, + }; + let other = TypedNodeRead { + node: NodeKey { + local_id: make_node_id("another-declaration"), + ..root + }, + absent_node_type: ty, + }; + let mut descending = vec![node, other]; + descending.sort_by_key(|declaration| std::cmp::Reverse(declaration.node)); + let (package, pin) = recorded_read_package(); + for aperture in [vec![node, node], descending, vec![node; 65537]] { + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[ty], &[], &[], 4)); + assert_eq!( + frame.evaluate_typed_read( + &package, + pin, + &recorded_read_input(root, b"before"), + &aperture, + recorded_read_limits() + ), + Err(EffectFrameError::BoundedRead( + warp_core::edict_read::ReadError::InvalidAperture + )) + ); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(frame.readings().is_empty()); + } +} + +#[test] +fn typed_read_image_keeps_exact_node_bytes_digest_and_runtime_parent() { + use warp_core::edict_effects::EffectReadSetView; + let (host, head, node) = fixture_host(); + let ty = make_type_id("operation-fixture-node"); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("parent"); + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[ty], &[], &[], 1)); + frame.read_typed_node(node, ty).expect("node resolution"); + let image = frame.into_read_set_image(4096).expect("bounded image"); + assert_eq!(image.evaluation_basis(), basis); + assert_eq!(image.consumed(), EchoOperationBudgetV1::new(1, 64, 0)); + assert_eq!( + image.allocated_bytes(), + 192, + "export preserves evaluation accounting without refunding it" + ); + let mut want = vec![0_u8; 208]; + want[0..8].copy_from_slice(&1_u64.to_le_bytes()); + want[16..48].copy_from_slice(&node.warp_id.0); + want[48..80].copy_from_slice(&node.local_id.0); + want[120..152].copy_from_slice(&ty.0); + let digest = blake3::hash(&[b"echo.bridge.node-value.v1\0".as_slice(), &ty.0].concat()); + want[152..184].copy_from_slice(digest.as_bytes()); + assert_eq!(image.as_bytes(), want); + let view = EffectReadSetView::parse(image.as_bytes()).expect("canonical borrowed view"); + assert_eq!(view.rows().len(), 1); + assert!(view.pairs().is_empty()); + assert_eq!( + view.rows().as_ptr().cast::(), + image.as_bytes()[8..].as_ptr(), + "rows borrow the image" + ); + let want_digest = + blake3::hash(&[b"echo.bridge.read-set.v1\0".as_slice(), want.as_slice()].concat()); + assert_eq!(image.digest(), *want_digest.as_bytes()); + assert_eq!(view.digest(), image.digest()); +} + +#[test] +fn typed_read_image_retains_distinct_named_absence_and_refuses_legacy_export() { + use warp_core::edict_effects::{EffectReadImageError, EffectReadSetView}; + let (host, head, root) = fixture_host(); + let a = make_type_id("image-absent-a"); + let b = make_type_id("image-absent-b"); + let node = NodeKey { + local_id: make_node_id("image-absent"), + ..root + }; + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[a, b], &[], &[], 2)); + for ty in [b, a, a] { + assert_eq!(frame.read_typed_node(node, ty), Ok(None)); + } + let image = frame.into_read_set_image(4096).expect("absence image"); + let view = EffectReadSetView::parse(image.as_bytes()).expect("absence view"); + let mut types = [a.0, b.0]; + types.sort_unstable(); + assert_eq!(view.rows().len(), 2); + for (row, ty) in view.rows().iter().zip(types) { + assert_eq!(row.outcome, 1); + assert_eq!(row.read_type, ty); + assert_eq!(row.subject, [0; 32]); + } + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("basis"); + let frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(100, 1000, 0), + effect_frame_limits(), + ) + .expect("untyped frame"); + assert!(matches!( + frame.into_read_set_image(4096), + Err(EffectReadImageError::MissingTypedParent) + )); + let frame = typed_effect_frame(&host, head, effect_law_value(&[], &[], &[], 0)); + assert!(matches!( + frame.into_read_set_image(15), + Err(EffectReadImageError::ExportLimitExceeded) + )); +} + +#[test] +fn typed_read_image_does_not_turn_a_reserved_unresolved_atom_into_an_observation() { + use warp_core::edict_effects::{ + EffectFootprintLaw, EffectFrameError, EffectRangeReadError, EffectReadSetView, + TypedNodeRead, + }; + let (host, head, node) = fixture_host(); + let ty = make_type_id("operation-fixture-node"); + let atom = make_type_id("operation-fixture-atom"); + let basis = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("basis"); + let law = EffectFootprintLaw::from_canonical_bytes( + &encode_canonical_cbor_v1(&effect_law_value(&[ty], &[atom], &[], 2)).expect("law bytes"), + ) + .expect("law"); + let mut frame = host + .effect_evaluation_frame_v1( + basis, + &[], + 0, + EchoOperationBudgetV1::new(10000, 128, 0), + effect_frame_limits(), + ) + .expect("frame") + .with_footprint_law(law) + .expect("typed frame"); + let (package, pin) = recorded_read_package(); + assert_eq!( + frame.evaluate_typed_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[TypedNodeRead { + node, + absent_node_type: ty + }], + recorded_read_limits() + ), + Err(EffectFrameError::Read(EffectRangeReadError::BudgetExceeded)) + ); + assert_eq!( + frame.authorized_reads().len(), + 2, + "authorization can precede the failed payload charge" + ); + let image = frame + .into_read_set_image(4096) + .expect("failed frame evidence"); + let view = EffectReadSetView::parse(image.as_bytes()).expect("completed prefix"); + assert_eq!(view.rows().len(), 1); + assert_eq!(view.rows()[0].slot.class, 0); +} + +#[test] +fn typed_read_image_witnesses_atom_and_portal_subjects() { + use warp_core::edict_effects::{EffectReadSetView, TypedNodeRead}; + let (host, head, node) = fixture_host(); + let ty = make_type_id("operation-fixture-node"); + let atom = make_type_id("operation-fixture-atom"); + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[ty], &[atom], &[], 2)); + let (package, pin) = recorded_read_package(); + frame + .evaluate_typed_read( + &package, + pin, + &recorded_read_input(node, b"before"), + &[TypedNodeRead { + node, + absent_node_type: ty, + }], + recorded_read_limits(), + ) + .expect("compiled value"); + let image = frame.into_read_set_image(4096).expect("atom image"); + let view = EffectReadSetView::parse(image.as_bytes()).expect("atom view"); + let row = &view.rows()[1]; + assert_eq!(row.slot.class, 1); + assert_eq!(row.outcome, 0); + assert_eq!(row.read_type, atom.0); + let expected = blake3::hash( + &[ + b"echo.bridge.attachment-value.v1\0".as_slice(), + &[0], + &atom.0, + &6_u64.to_le_bytes(), + b"before", + ] + .concat(), + ); + assert_eq!(row.subject, *expected.as_bytes()); + let (host, head, target, owners) = typed_portal_fixture(false, 1); + let child_ty = make_type_id("operation-child-node"); + let mut frame = typed_effect_frame(&host, head, effect_law_value(&[child_ty], &[], &[ty], 3)); + frame + .read_typed_node(target, child_ty) + .expect("descended node"); + let image = frame.into_read_set_image(4096).expect("portal image"); + let view = EffectReadSetView::parse(image.as_bytes()).expect("portal view"); + assert_eq!(view.rows().len(), 3); + let row = &view.rows()[2]; + assert_eq!(row.slot.class, 1); + assert_eq!(row.slot.local_id, owners[0].local_id.0); + assert_eq!(row.outcome, 3); + assert_eq!(row.read_type, ty.0); + let expected = blake3::hash( + &[ + b"echo.bridge.attachment-value.v1\0".as_slice(), + &[1], + &target.warp_id.0, + ] + .concat(), + ); + assert_eq!(row.subject, *expected.as_bytes()); +} + +#[test] +fn typed_read_image_packs_actual_matching_edges_and_distinguishes_absent_sources() { + use warp_core::edict_effects::EffectReadSetView; + let (host, head, root) = fixture_host(); + let ty = make_type_id("operation-fixture-node"); + let edge_ty = make_type_id("image-edge"); + let mut state = host + .runtime() + .worldlines() + .get(&head.worldline_id) + .expect("worldline") + .state() + .warp_state() + .clone(); + for (id, edge_type) in [ + (2, edge_ty), + (1, edge_ty), + (3, make_type_id("nonmatching-image-edge")), + ] { + state + .store_mut(&root.warp_id) + .expect("source store") + .insert_edge( + root.local_id, + warp_core::EdgeRecord { + id: warp_core::EdgeId([id; 32]), + from: root.local_id, + to: root.local_id, + ty: edge_type, + }, + ); + } + let (host, head) = fixture_runtime_host(WorldlineState::new(state, root).expect("range state")); + let range = warp_core::OutgoingEdgeRange { + source: root, + edge_type: edge_ty, + }; + let mut frame = typed_range_frame(&host, head, ty, edge_ty, 3); + frame.read_typed_range(range, ty).expect("real range"); + let image = frame.into_read_set_image(4096).expect("real range image"); + let view = EffectReadSetView::parse(image.as_bytes()).expect("range view"); + assert_eq!(view.rows().len(), 2); + assert_eq!(view.rows()[1].outcome, 2); + assert_eq!(view.rows()[1].read_type, [0; 32]); + assert_eq!(view.rows()[1].pairs.start(), 0); + assert_eq!(view.rows()[1].pairs.len(), 2); + assert_eq!( + view.pairs() + .iter() + .map(|pair| (pair.edge_local_id, pair.target_local_id)) + .collect::>(), + vec![([1; 32], root.local_id.0), ([2; 32], root.local_id.0)] + ); + assert_eq!(image.as_bytes().len(), 528); + assert_eq!(image.consumed(), EchoOperationBudgetV1::new(4, 256, 0)); + assert_eq!(image.allocated_bytes(), 1152); + let absent = NodeKey { + local_id: make_node_id("absent-image-source"), + ..root + }; + let mut frame = typed_range_frame(&host, head, ty, edge_ty, 3); + assert_eq!( + frame.read_typed_range( + warp_core::OutgoingEdgeRange { + source: absent, + edge_type: edge_ty + }, + ty + ), + Ok(None) + ); + let absent_image = frame.into_read_set_image(4096).expect("absent range image"); + let view = EffectReadSetView::parse(absent_image.as_bytes()).expect("absent range view"); + assert_eq!(view.rows()[0].outcome, 1); + assert_eq!(view.rows()[1].outcome, 2); + assert!(view.pairs().is_empty()); + assert_ne!(absent_image.digest(), image.digest()); +} + +/// M2: on an application-profile worldline, Actions commit the tagged +/// application root, the stale-basis exception still corroborates origins +/// (including the genesis boundary), and recovery agrees. +#[cfg(feature = "application_root_profile")] +#[test] +fn application_profile_actions_commit_and_recover() { + use warp_core::application_root::{profile_tagged_state_root, CommitmentProfile}; + + const ACTIONS: usize = 70; + let profiled = |nodes: usize| { + fixture_host_with_independent_nodes_and_state(nodes, |state| { + state.with_commitment_profile(CommitmentProfile::ApplicationRadixV0) + }) + }; + let wal_dir = TempWalDir::new(); + let submissions; + let committed_root; + { + let (mut host, head_key, nodes) = profiled(ACTIONS); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("the WAL opens"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + // Every Action is built at the genesis boundary; those decided after + // the first tick need the scoped exception to commit. + let invocations = nodes + .iter() + .map(|node| action_invocation(&host, &installed, head_key, *node, b"before", b"after")) + .collect::>(); + submissions = invocations + .into_iter() + .map(|invocation| submit_action(&mut host, head_key, invocation)) + .collect::>(); + for _ in 0..ACTIONS.div_ceil(64) { + host.tick_once().expect("the scheduler decides a batch"); + } + assert!(submissions.iter().all(|id| outcome_is_committed(&host, id))); + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists") + .state(); + committed_root = state.state_root(); + assert_eq!( + committed_root, + profile_tagged_state_root(&state.application_root_rebuilt()) + ); + } + let (mut recovered, head_key, _) = profiled(ACTIONS); + recovered + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("recovery recomputes validity under the profile and agrees"); + assert!(submissions + .iter() + .all(|id| outcome_is_committed(&recovered, id))); + assert_eq!( + recovered + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists") + .state() + .state_root(), + committed_root + ); +} + +// --------------------------------------------------------------------------- +// Comparison-contract regressions (M0) +// --------------------------------------------------------------------------- + +/// Every application record on the fixture worldline's root warp, in id order. +/// +/// This is a test-only reference tuple for the application-reading comparison. +/// It is not a production digest and is deliberately independent of the v1 +/// state root. +#[derive(Debug, PartialEq, Eq)] +struct ApplicationRecords { + nodes: Vec<(warp_core::NodeId, NodeRecord)>, + edges: Vec<(warp_core::NodeId, Vec)>, + node_attachments: Vec<(warp_core::NodeId, AttachmentValue)>, + edge_attachments: Vec<(warp_core::EdgeId, AttachmentValue)>, +} + +fn application_records(host: &TrustedRuntimeHost, head_key: WriterHeadKey) -> ApplicationRecords { + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists") + .state(); + let store = state + .store(&state.root().warp_id) + .expect("the fixture root warp has a store"); + let mut records = ApplicationRecords { + nodes: store + .iter_nodes() + .map(|(id, record)| (*id, record.clone())) + .collect(), + edges: store + .iter_edges() + .map(|(id, edges)| (*id, edges.clone())) + .collect(), + node_attachments: store + .iter_node_attachments() + .map(|(id, value)| (*id, value.clone())) + .collect(), + edge_attachments: store + .iter_edge_attachments() + .map(|(id, value)| (*id, value.clone())) + .collect(), + }; + records.nodes.sort_by_key(|(id, _)| *id); + records.edges.sort_by_key(|(id, _)| *id); + records.node_attachments.sort_by_key(|(id, _)| *id); + records.edge_attachments.sort_by_key(|(id, _)| *id); + records +} + +fn state_root_and_tick( + host: &TrustedRuntimeHost, + head_key: WriterHeadKey, +) -> (warp_core::Hash, u64) { + let frontier = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the fixture worldline exists"); + ( + frontier.state().state_root(), + frontier.frontier_tick().as_u64(), + ) +} + +/// A host with two independent nodes and a second, non-default writer head on +/// the same worldline. The second head holds an accepted Action, dormant, while +/// the first head commits other work. +fn fixture_host_with_holding_head() -> ( + TrustedRuntimeHost, + WriterHeadKey, + WriterHeadKey, + Vec, +) { + let (host, writer, nodes) = fixture_host_with_independent_nodes(2); + let holder = WriterHeadKey { + worldline_id: writer.worldline_id, + head_id: make_head_id("operation-fixture-holding-writer"), + }; + let mut parts = host.into_parts(); + parts + .runtime_mut() + .register_writer_head(WriterHead::with_routing( + holder, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + false, + )) + .expect("the holding head registers"); + (TrustedRuntimeHost::from_parts(parts), writer, holder, nodes) +} + +fn with_head_eligibility( + host: TrustedRuntimeHost, + head_key: WriterHeadKey, + eligibility: HeadEligibility, +) -> TrustedRuntimeHost { + let mut parts = host.into_parts(); parts .runtime_mut() .set_head_eligibility(head_key, eligibility) @@ -5237,3 +7110,373 @@ fn equal_application_records_do_not_decide_a_held_actions_eligibility() { "an obstruction writes nothing" ); } + +#[test] +fn recovery_index_binds_retained_sections_and_action_attribution_under_one_recipe() { + let wal_dir = TempWalDir::new(); + let (mut host, head_key, nodes) = fixture_host_with_independent_nodes(2); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("the WAL opens"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + for node in nodes { + let invocation = action_invocation(&host, &installed, head_key, node, b"before", b"after"); + submit_action(&mut host, head_key, invocation); + } + host.tick_once().expect("both Actions commit"); + let recovery = host + .runtime_wal() + .expect("WAL") + .recover_read_only() + .expect("recovery"); + let root = recovery.certificate.recovered_indexes_root; + assert_eq!(recovery.recomputed_indexes_root().expect("index"), root); + assert_eq!(recovery.installed_echo_operations.len(), 1); + assert_eq!(recovery.echo_operation_action_outcomes.len(), 2); + + let mut missing_installation = recovery.clone(); + missing_installation.installed_echo_operations.clear(); + assert_ne!( + missing_installation + .recomputed_indexes_root() + .expect("index"), + root + ); + let mut missing_outcomes = recovery.clone(); + missing_outcomes.echo_operation_action_outcomes.clear(); + assert_ne!( + missing_outcomes.recomputed_indexes_root().expect("index"), + root + ); + let mut changed_submission = recovery.clone(); + changed_submission.echo_operation_action_outcomes[0].0[0] ^= 1; + assert_ne!( + changed_submission.recomputed_indexes_root().expect("index"), + root + ); + let mut changed_ingress = recovery.clone(); + changed_ingress.echo_operation_action_outcomes[0].1[0] ^= 1; + assert_ne!( + changed_ingress.recomputed_indexes_root().expect("index"), + root + ); + let mut changed_order = recovery.clone(); + changed_order.echo_operation_action_outcomes.swap(0, 1); + assert_ne!( + changed_order.recomputed_indexes_root().expect("index"), + root + ); + let EchoOperationActionOutcomeV1::Committed(receipt) = + &recovery.echo_operation_action_outcomes[0].2 + else { + panic!("the first Action committed"); + }; + let mut changed_receipts = recovery.clone(); + changed_receipts + .echo_operation_receipts + .push((**receipt).clone()); + assert_ne!( + changed_receipts.recomputed_indexes_root().expect("index"), + root + ); + + drop(host); + let (mut fresh, _, _) = fixture_host_with_independent_nodes(2); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("fresh host restores the original decisions"); + let reopened = fresh + .runtime_wal() + .expect("reopened WAL") + .recover_read_only() + .expect("recovery"); + assert_eq!(reopened.certificate.recovered_indexes_root, root); + assert_eq!( + reopened.echo_operation_action_outcomes, + recovery.echo_operation_action_outcomes + ); +} + +#[path = "common/edict_effect_publication.rs"] +mod current_effect_publication_fixture; + +fn install_effect_catalog_fixture(host: &mut TrustedRuntimeHost) -> InstalledEchoOperation { + use current_effect_publication_fixture as publication; + let bytes = publication::encoded(&publication::package(&publication::program())); + let policy = EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&bytes).as_hash(), + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + let token = host + .admit_echo_operation_package(&policy, bytes) + .expect("effect policy"); + host.install_admitted_echo_operation_package(token) + .expect("effect installation") +} + +#[test] +fn shared_catalog_preserves_action_execution_and_recovery_in_both_program_installation_orders() { + for effect_first in [false, true] { + let wal_dir = TempWalDir::new(); + let (mut host, head_key, node) = fixture_host(); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("WAL"); + let (fixed, effect) = if effect_first { + let effect = install_effect_catalog_fixture(&mut host); + (install_fixture_operation(&mut host), effect) + } else { + let fixed = install_fixture_operation(&mut host); + (fixed, install_effect_catalog_fixture(&mut host)) + }; + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + let invocation = action_invocation(&host, &fixed, head_key, node, b"before", b"after"); + let submission = submit_action(&mut host, head_key, invocation); + host.tick_once() + .expect("fixed Action commits with both program kinds installed"); + assert!(outcome_is_committed(&host, &submission)); + let outcome = host + .echo_operation_action_outcome_v1(&submission) + .expect("outcome") + .clone(); + let recovery = host + .runtime_wal() + .expect("WAL") + .recover_read_only() + .expect("recovery"); + assert_eq!(recovery.installed_echo_operations.len(), 2); + assert!(recovery.installed_echo_operations.contains(&fixed)); + assert!(recovery.installed_echo_operations.contains(&effect)); + let root = recovery.certificate.recovered_indexes_root; + drop(host); + let (mut fresh, _, _) = fixture_host(); + fresh + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("fresh recovery"); + assert_eq!( + fresh.echo_operation_action_outcome_v1(&submission), + Some(&outcome) + ); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package(effect.package_id()), + Some(&effect) + ); + let recovered = fresh + .runtime_wal() + .expect("WAL") + .recover_read_only() + .expect("recovery"); + assert_eq!(recovered.certificate.recovered_indexes_root, root); + } +} + +#[test] +fn current_invocation_admits_effect_grants_without_an_invented_anchor() { + use current_effect_publication_fixture as publication; + use warp_core::EchoOperationInvocationAdmissionErrorKind as Refusal; + use warp_core::{EchoOperationInvocation, EchoOperationInvocationAdmissionPolicy}; + let (mut host, head, _) = fixture_host(); + let installed = install_effect_catalog_fixture(&mut host); + let input = publication::encoded(&CanonicalValueV1::Integer(42)); + let app_basis = EchoOperationApplicationBasisV1::new( + publication::hash( + b"echo:operation-profile:v1\0", + b"echo.operation.basis.edict-effect/v1", + ), + publication::hash(b"echo:operation-edict-effect-basis:v1\0", &input), + ); + let basis = host + .echo_operation_evaluation_basis_v1(head, app_basis) + .expect("actual parent"); + let basis_value = publication::map([ + ( + "worldline_id", + CanonicalValueV1::Bytes(head.worldline_id.as_bytes().to_vec()), + ), + ( + "head_id", + CanonicalValueV1::Bytes(head.head_id.as_bytes().to_vec()), + ), + ( + "worldline_tick", + CanonicalValueV1::Integer(i128::from(basis.worldline_tick().as_u64())), + ), + ("commit_global_tick", CanonicalValueV1::Null), + ( + "state_root", + CanonicalValueV1::Bytes(basis.state_root().to_vec()), + ), + ( + "commit_id", + CanonicalValueV1::Bytes(basis.commit_id().to_vec()), + ), + ( + "application_basis_schema_identity", + CanonicalValueV1::Bytes(app_basis.schema_identity().to_vec()), + ), + ( + "application_basis_value_identity", + CanonicalValueV1::Bytes(app_basis.value_identity().to_vec()), + ), + ]); + let bytes = publication::encoded(&publication::map([ + ("schema", publication::text("echo.operation-invocation/v1")), + ("kind", publication::text("edict-effect-program/v1")), + ( + "package_id", + CanonicalValueV1::Bytes(installed.package_identity().to_vec()), + ), + ( + "operation_coordinate", + publication::text(installed.operation_coordinate()), + ), + ("authority_grant_identity", publication::id(9)), + ( + "delegated_budget", + publication::map([ + ("steps", CanonicalValueV1::Integer(1)), + ("read_bytes", CanonicalValueV1::Integer(0)), + ("write_bytes", CanonicalValueV1::Integer(0)), + ]), + ), + ("evaluation_basis", basis_value), + ("application_input_bytes", CanonicalValueV1::Bytes(input)), + ])); + let policy = EchoOperationInvocationAdmissionPolicy::new( + [8; 32], + [9; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + let admitted = host + .admit_echo_operation_invocation(&policy, &bytes) + .expect("independent grant and exact parent"); + assert_eq!(admitted.invocation().anchor_node(), None); + let decoded = EchoOperationInvocation::from_canonical_bytes(&bytes).expect("current codec"); + assert_eq!(decoded.to_canonical_bytes().expect("canonical"), bytes); + let refused = host + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new( + [8; 32], + [10; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ), + &bytes, + ) + .expect_err("a claimed grant is not an owner admission"); + assert_eq!( + refused.kind(), + warp_core::EchoOperationInvocationAdmissionErrorKind::AuthorityGrantMismatch + ); + for (profile, grant, budget, expected) in [ + ( + [7; 32], + [9; 32], + EchoOperationBudgetV1::new(1, 0, 0), + Refusal::AuthorityProfileMismatch, + ), + ( + [8; 32], + [9; 32], + EchoOperationBudgetV1::new(0, 0, 0), + Refusal::BudgetExceeded, + ), + ] { + let rejected = host + .admit_echo_operation_invocation( + &EchoOperationInvocationAdmissionPolicy::new(profile, grant, budget), + &bytes, + ) + .expect_err("independent owner restriction"); + assert_eq!(rejected.kind(), expected); + } + for (key, value, expected) in [ + ( + "package_id", + publication::id(77), + Refusal::OperationUnavailable, + ), + ( + "operation_coordinate", + publication::text("app.other@1"), + Refusal::OperationCoordinateMismatch, + ), + ( + "application_input_bytes", + CanonicalValueV1::Bytes(publication::encoded(&CanonicalValueV1::Integer(43))), + Refusal::ApplicationInputMismatch, + ), + ( + "delegated_budget", + publication::map([ + ("steps", CanonicalValueV1::Integer(2)), + ("read_bytes", CanonicalValueV1::Integer(0)), + ("write_bytes", CanonicalValueV1::Integer(0)), + ]), + Refusal::BudgetExceeded, + ), + ( + "delegated_budget", + publication::map([ + ("steps", CanonicalValueV1::Integer(1)), + ("read_bytes", CanonicalValueV1::Integer(1)), + ("write_bytes", CanonicalValueV1::Integer(0)), + ]), + Refusal::BudgetExceeded, + ), + ( + "delegated_budget", + publication::map([ + ("steps", CanonicalValueV1::Integer(1)), + ("read_bytes", CanonicalValueV1::Integer(0)), + ("write_bytes", CanonicalValueV1::Integer(1)), + ]), + Refusal::BudgetExceeded, + ), + ] { + let mut altered = decode_canonical_cbor_v1(&bytes).expect("fixture map"); + publication::set(&mut altered, key, value); + let rejected = host + .admit_echo_operation_invocation(&policy, &publication::encoded(&altered)) + .expect_err("substitution refused"); + assert_eq!(rejected.kind(), expected, "{key}"); + } + for key in [ + "state_root", + "commit_id", + "head_id", + "application_basis_schema_identity", + ] { + let mut altered = decode_canonical_cbor_v1(&bytes).expect("fixture map"); + let CanonicalValueV1::Map(ref mut entries) = altered else { + panic!("map") + }; + let parent = &mut entries + .iter_mut() + .find(|(key, _)| key == &publication::text("evaluation_basis")) + .expect("parent") + .1; + publication::set(parent, key, publication::id(99)); + assert!( + host.admit_echo_operation_invocation(&policy, &publication::encoded(&altered)) + .is_err(), + "forged {key}" + ); + } + match host.prepare_echo_operation_v1(admitted) { + EchoOperationPreparationV1::Obstructed(obstruction) => assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::OperationUnavailable + ), + EchoOperationPreparationV1::Prepared(_) => { + panic!("grant admission must not manufacture an effect execution") + } + } + assert_eq!( + host.echo_operation_evaluation_basis_v1(head, app_basis) + .expect("unchanged runtime parent"), + basis + ); +} diff --git a/crates/warp-core/tests/fixtures/current-effect-publication/README.md b/crates/warp-core/tests/fixtures/current-effect-publication/README.md new file mode 100644 index 000000000..c2951afab --- /dev/null +++ b/crates/warp-core/tests/fixtures/current-effect-publication/README.md @@ -0,0 +1,6 @@ + + + +# Current effect publication control + +This is the synthetic zero-effect serialization control: the exact output of the lowerer's `encode_effect_publication` for the `current-effect-wire` material in `echo-edict-provider-lowerer/tests/fixtures`. A lowerer test requires both retained copies to equal the current emission, so a change to the package shape cannot leave a stale control. It exercises the current runtime package checker without trusted host features. It is not public compiler output, execution evidence, or authority. The package embeds its complete compiler bundle; tests independently corrupt those bytes and refresh the outer pin to exercise semantic rejection rather than only identity rejection. diff --git a/crates/warp-core/tests/fixtures/current-effect-publication/package.hex b/crates/warp-core/tests/fixtures/current-effect-publication/package.hex new file mode 100644 index 000000000..07298284e --- /dev/null +++ b/crates/warp-core/tests/fixtures/current-effect-publication/package.hex @@ -0,0 +1 @@ +b166736368656d6178196563686f2e6f7065726174696f6e2d7061636b6167652f76316770726f6772616d5907c4ad646b696e647765646963742d6566666563742d70726f6772616d2f763166736368656d6178196563686f2e6f7065726174696f6e2d70726f6772616d2f76316b6f75747075745f747970656a6170702e526573756c746c67726170685f736368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026c6964656e746974795f6c6177016d666f6f747072696e745f6c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806f6d61785f696e7075745f62797465731a0001000070636f6d70696c65645f70726f6772616d5904c9a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0706d61785f726573756c745f62797465731a00010000727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db0736d61785f616c6c6f63617465645f62797465731a01000000781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c6e6275646765745f6365696c696e67a36573746570730a6a726561645f62797465731910006b77726974655f62797465731910007073656d616e7469635f636c6f73757265a66d636f72655f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db07565646963745f736f757263655f6964656e746974795820fdf14fbb05ba25e820cd4a23b594e6648429940f7aed5218077eef841288114c781a63616e6f6e6963616c5f6d65616e696e675f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d781b6170706c69636174696f6e5f736368656d615f6964656e74697479582017ff6f37fcc8b1f81e58cc3ef5c0d94a4c3f96fb955bfb9cae7ff0bac29cb249781d6170706c69636174696f6e5f736368656d615f636f6f7264696e6174656b6170702e4578706f727473746f7065726174696f6e5f636f6f7264696e6174656c6170702e757064617465403175696e7075745f736368656d615f6964656e746974795820d277705b5ab03fcb58590bad62fb8058e075c3b4c65efc21282bb531aa22b3ad76726573756c745f736368656d615f6964656e746974795820de5c0c4f0c640354c40bb4f9d4886cd820353a548caea4953269eba15fe902bc777461726765745f70726f66696c655f6964656e746974795820eaca21c05664241efd1e1c5b5636c7132fe8854f6bcaddb2fd4f014e1bd4a479781a617574686f726974795f70726f66696c655f6964656e7469747958208eca01fcc45de0eb065bfd04fb21d2c7881c346ca07dfc1536df5ca2fe0812dc781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781b666f6f747072696e745f636f6e74726163745f6964656e746974795820362b20fb18488ce180f6c1096b5189994354604fd1b1b126a3ea4ef839e5232b781b6f62737472756374696f6e5f736368656d615f6964656e746974795820b1f96fdf3ec65e84d500702951393a931b4fdd843ad02723ea3725f90a671418781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c781e726573756c745f696e746572707265746174696f6e5f6964656e74697479582042734d5e73435acc0fdd5a66b97e5bfd94278e64fce2b4890ec609f054dc969e78206576616c756174696f6e5f62617369735f736368656d615f6964656e74697479582020b6239db569aa86ae1c27596b374df90caa5ea41e8e005fd4fe00bfe5c3b38178216170706c69636174696f6e5f62617369735f736368656d615f6964656e7469747958208b294c69120a2e749c56266691b0974e7e0323c198fc42a467fd3790790f791078236f62737472756374696f6e5f696e746572707265746174696f6e5f6964656e746974795820924462071f3992b34c629f4788cc666bd14a87a0b9d0c398feba929ff4dd25f9 diff --git a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs index b128f8032..5962eed22 100644 --- a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs +++ b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs @@ -1374,7 +1374,7 @@ fn observed_operation_contexts_recover_without_replacing_original_inputs() { "the canonical root exists without an alpha attachment" ); let invocation = |value: &[u8]| { - warp_core::EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + warp_core::EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( warp_core::echo_operation_package_id_v1(b"context-only-test"), "test.context@1.create", observation.basis(), [1;32], warp_core::EchoOperationBudgetV1::new(16, 1024, 320), node, value.to_vec(), vec![0xa0], @@ -1485,7 +1485,7 @@ fn bind_link_request( let observation = host .retain_echo_operation_observation_v1(request, head, &[node]) .expect("capture"); - let invocation = warp_core::EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + let invocation = warp_core::EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( warp_core::echo_operation_package_id_v1(b"context-only-test"), "test.context@1.create", observation.basis(), [1;32], warp_core::EchoOperationBudgetV1::new(16, 1024, 320), node, b"value".to_vec(), vec![0xa0]); host.bind_echo_operation_request_v1(request, request, invocation) @@ -1645,7 +1645,7 @@ fn healthy_contexts_advance_without_reconstructing_the_wal() { .expect("reading"), observation ); - let invocation = warp_core::EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + let invocation = warp_core::EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( warp_core::echo_operation_package_id_v1(b"context-only-test"), "test.context@1.create", observation.basis(), [1;32], warp_core::EchoOperationBudgetV1::new(16, 1024, 320), node, b"value".to_vec(), vec![0xa0]); let original = host @@ -1744,7 +1744,7 @@ fn durable_request_error_resolves_original_bytes_before_retrying() { .retain_echo_operation_observation_v1("attempt", head, &[node]) .expect("observation"); let invocation = |value: &[u8]| { - warp_core::EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + warp_core::EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( warp_core::echo_operation_package_id_v1(b"context-only-test"), "test.context@1.create", observation.basis(), [1;32], warp_core::EchoOperationBudgetV1::new(16, 1024, 320), node, value.to_vec(), vec![0xa0]) }; diff --git a/docs/architecture/authored-operation-effects.md b/docs/architecture/authored-operation-effects.md index 1d838ac86..a79c07e0f 100644 --- a/docs/architecture/authored-operation-effects.md +++ b/docs/architecture/authored-operation-effects.md @@ -1090,38 +1090,38 @@ Consequences: `scope_hash(installed_operation_id, scope)`, as for fixed profiles. It names no graph node, is never read, and recovery recomputes it from the retained invocation. -- **Batch digests.** The batch rewrites and composition digests hash one - identity tuple per applied member. A Tick of fixed-profile Actions alone - keeps today's recipes and `v1` domains byte for byte. A Tick that contains - a bridge Action uses `echo:operation-action-batch-rewrites:v2\0` and - `echo:operation-action-batch-composition:v2\0`, with a one-byte member kind - before each member (fixed `1`, bridge `2`), so the two kinds of tuple can - never alias. A fixed member contributes exactly its V1 tuple. A bridge +- **Batch digests.** The batch rewrites and composition digests, under + `echo:operation-action-batch-rewrites:v1\0` and + `echo:operation-action-batch-composition:v1\0`, hash one identity tuple per + applied member, each after a one-byte member kind (fixed `1`, bridge `2`), so + the two kinds of tuple can never alias. A fixed member contributes its + identity tuple. A bridge member contributes its proposal identity where a fixed member has its `preparation_id`, then its candidate patch digest, its `result_identity`, its invocation's evaluation-basis identity, and its write-set digest as its actual footprint. The rewrites digest takes the first element of each - tuple, as V1 takes `preparation_id`. + tuple, as a fixed member's is its `preparation_id`. - **Durability** is unchanged in kind: the decided-Tick WAL record is the only persistence. Proposals are never retained apart from their Tick. The record gains a new layout version for the new evidence (see Evidence). -- **Package version.** `ExecutableOperationPackageV1` embeds - `EchoOperationSemanticClosureV1`, which requires `lawpack_coordinate` and - `lawpack_identity`, and `EchoOperationProgramV1`, which has only the two - fixed profiles. A source-compiled bridge package therefore uses - `ExecutableOperationPackageV2`. Fixed-profile programs and invocations are - not re-encoded: they keep their V1 maps, schema strings, and identities - byte for byte. V2 adds exactly this: - - **Package.** The V1 package map with `schema` set to - `echo.operation-package/v2`, `semantic_closure` set to a - `closure-v2`, and `program` set to an effect program. Every other key, - and its value shape, is as in V1, except that the optional +- **Package format.** There is one package, program, and invocation format, + `echo.operation-package/v1`, `echo.operation-program/v1`, and + `echo.operation-invocation/v1`, with their `v1` identity domains. A program's + `kind` selects compare-and-set, create-if-absent, or an authored effect + program (`edict-effect-program/v1`); an invocation carries the same `kind`. + Fixed-profile packages, programs, and invocations keep their bytes and + identities. A source-compiled package differs from a fixed-profile one in + exactly these ways: + - **Package.** `program` is an effect program, and the optional `application_result_projection` must be absent: an effect program's - application result is its own output, which `result_identity` binds, - so no projection can compete with it. Where V1 derives a package identity - field from its program, V2 fixes it for the effect program, using V1's - `profile_digest` (BLAKE3 over `echo:operation-profile:v1\0` and the - length-prefixed label), and admission requires each equality: + application result is its own output, which `result_identity` binds, so + no projection can compete with it. The package carries no + `obstruction_coordinate`, which a fixed-profile package requires: an + effect program can return several obstructions, all declared in its + embedded exports and checked arm by arm. Where a fixed program derives a + package identity field from its profile, the effect program fixes it, + using `profile_digest` (BLAKE3 over `echo:operation-profile:v1\0` and + the length-prefixed label), and admission requires each equality: - `target_profile_identity` is the profile digest of `echo.operation-target.edict-effect/v1`; - `application_basis_schema_identity` is the profile digest of @@ -1136,28 +1136,27 @@ Consequences: `echo.operation.result-interpretation.edict-effect/v1` and `echo.operation.obstruction-interpretation.edict-effect/v1`; - `evaluation_basis_schema_identity` is the profile digest of - `echo.operation.evaluation-basis/v1`, as in V1; - - `footprint_contract_identity` is the `v2` footprint-law digest - below. + `echo.operation.evaluation-basis/v1`, as for fixed programs; + - `footprint_contract_identity` is the footprint-law digest below. Together with the profile identities the program carries, this fixes - every identity field of the package. `obstruction_coordinate` and - `operation_coordinate` remain the application's text, as in V1. - - - **Semantic closure.** The V1 closure map, with `lawpack_coordinate` and - `lawpack_identity` both omitted. A V2 package carries only an effect - program, which requires their absence, so no sentinel lawpack is - fabricated. A fixed-profile program stays in a V1 package with its - lawpack, as ADR 0023 requires today. The closure has no identity of its - own; its bytes are part of the package's. - - **Effect program.** A new map, `program-v2-effect` below. Like a V1 - program, it carries `interpreter_profile_identity` and - `intrinsic_profile_identity`. Admission requires each to equal the - package's value of the same name and the profile Echo implements for - effect programs: the profile digests of - `echo.operation-interpreter.edict-effect/v1` and - `echo.operation-intrinsics.edict-effect/v1`. Any other value is - refused as an unsupported profile, as in V1. Its other entries are: + every identity field of the package. `operation_coordinate` remains the + application's text. + + - **Semantic closure.** The closure map with `lawpack_coordinate` and + `lawpack_identity` both omitted: an effect package carries only an + effect program, which requires their absence, so no sentinel lawpack is + fabricated. A fixed-profile package requires both, as ADR 0023 requires. + The closure has no identity of its own; its bytes are part of the + package's. + - **Effect program.** The program map with `kind` + `edict-effect-program/v1`. Like a fixed program, it carries + `interpreter_profile_identity` and `intrinsic_profile_identity`. + Admission requires each to equal the package's value of the same name + and the profile Echo implements for effect programs: the profile digests + of `echo.operation-interpreter.edict-effect/v1` and + `echo.operation-intrinsics.edict-effect/v1`. Any other value is refused + as an unsupported profile. Its other entries are: - `compiled_program`, the compiled bytes; - `target_ir_identity`, which must equal the closure's; - `identity_law`, an unsigned integer naming an @@ -1169,91 +1168,56 @@ Consequences: longer than `max_result_bytes` obstructs evaluation with `BudgetExceeded`, posture `NoProposal`; - `max_allocated_bytes`, the program's declared allocation ceiling, - at most `EFFECT_ALLOCATION_CEILING_V1`, which bounds evaluation and - sizes the scheduler's evaluation arena; + at most `EFFECT_READ_STORAGE_CEILING_V1` (16 MiB), which bounds + evaluation and sizes the scheduler's evaluation arena; - `output_type`, the compiler-declared output type coordinate of the program's application result, which `result_identity` hashes. Admission requires it to be nonempty UTF-8 of at most 1,024 bytes, - the bound V1 applies to result coordinates. + the bound fixed programs apply to result coordinates. The graph schema is the executable graph contract, a different artifact from the application's own schema declaration. The closure's `application_schema_identity` keeps naming that declaration, with its codecs and migration posture, as ADR 0023 requires. The graph schema is bound by the program identity, whose bytes contain it, so no - equality between the two is required. The package's inherited + equality between the two is required. The package's `footprint_contract_identity` must equal BLAKE3 over - `echo:operation-footprint-law:v2\0` and the `footprint_law` bytes, so + `echo:operation-footprint-law:v1\0` and the `footprint_law` bytes, so the attested footprint contract is exactly the law validation executes. The `graph_schema` is a map that Echo executes, not only a - digest. `node_types` maps each - admitted node type to its addressing, which the - [identity law](#identity-laws) reads, and its atom types. `edge_types` - maps each admitted edge type to its (source node type, target node - type) pairs. Validation step 5 reads these relations directly. No - artifact is attested under one identity and executed under another. - - - **Effect invocation.** A new map, `invocation-v2-effect` below. It - carries the V1 invocation's common entries other than the anchored - node and replacement (`authority_grant_identity`, `delegated_budget`, - `evaluation_basis`, `operation_coordinate`, `package_id`), plus the - required `application_input_bytes`, the program's input in - `edict.canonical-cbor/v1`. Its `evaluation_basis` names - the frontier exactly as V1 does, and V1's basis check resolves it - there. Its application basis is the input itself: - `application_basis_schema_identity` is the package's value above, and - `application_basis_value_identity` is BLAKE3 over + digest. `node_types` maps each admitted node type to its addressing, + which the [identity law](#identity-laws) reads, and its atom types. + `edge_types` maps each admitted edge type to its (source node type, + target node type) pairs. Validation step 5 reads these relations + directly. No artifact is attested under one identity and executed + under another. + + - **Effect invocation.** The invocation map with `kind` + `edict-effect-program/v1`. It carries the common entries + (`authority_grant_identity`, `delegated_budget`, `evaluation_basis`, + `operation_coordinate`, `package_id`) and the required + `application_input_bytes`, the program's input in + `edict.canonical-cbor/v1`, and none of the anchored node, replacement, + or observation entries a fixed invocation has. Its `evaluation_basis` + names the frontier exactly as a fixed invocation's does, and the same + basis check resolves it there. Its application basis is the input + itself: `application_basis_schema_identity` is the package's value + above, and `application_basis_value_identity` is BLAKE3 over `echo:operation-edict-effect-basis:v1\0`, the input's length as a little-endian `u64`, and the input bytes. Admission recomputes both and refuses a mismatch, so the basis evidence is bound to the input the - program evaluates. Admission checks only what it can check - without a schema language: that the bytes are canonical and no longer - than the program's `max_input_bytes`. The input's shape is the compiled + program evaluates. Admission checks only what it can check without a + schema language: that the bytes are canonical and no longer than the + program's `max_input_bytes`. The input's shape is the compiled program's to check, as its first act; a malformed input obstructs evaluation with the authored `InputRejected`, posture `NoProposal`. An effect program admits only effect invocations, and a fixed profile only - its own V1 invocations, so no invocation crosses between them. - - ```text - closure-v2 = { - "application_schema_coordinate": tstr, "application_schema_identity": id, - "canonical_meaning_identity": id, "core_identity": id, - "edict_source_identity": id, "target_ir_identity": id, - } - program-v2-effect = { - "schema": "echo.operation-program/v2", "kind": "edict-effect-program/v1", - "interpreter_profile_identity": id, "intrinsic_profile_identity": id, - "compiled_program": bstr, "target_ir_identity": id, - "identity_law": uint, "graph_schema": bstr, "footprint_law": bstr, - "max_input_bytes": uint, "max_result_bytes": uint, - "max_allocated_bytes": uint, "output_type": tstr, - } - invocation-v2-effect = { - "schema": "echo.operation-invocation/v2", - "authority_grant_identity": id, "delegated_budget": budget-v1, - "evaluation_basis": evaluation-basis-v1, "operation_coordinate": tstr, - "package_id": id, "application_input_bytes": bstr, - } - ; budget-v1 and evaluation-basis-v1 are the V1 values, unchanged. - ``` - - No other key may appear in any of these maps. A V2 package, effect program, - and effect invocation hash their identities under - `echo:operation-package:v2\0`, `echo:operation-program:v2\0`, and - `echo:operation-invocation:v2\0`. Their invocation-bytes digest, package - admission, invocation admission, and installation identities use - `echo:operation-invocation-bytes:v2\0`, - `echo:operation-package-admission:v2\0`, - `echo:operation-invocation-admission:v2\0`, and - `echo:operation-installation:v2\0`. Every other domain is unchanged. The - retained installation record of a V2 package is the V1 installation map - (`admission_policy`, `admission_policy_id`, `installed_operation_id`, - `package_admission_id`, `package_bytes`, `package_id`, `schema`) with - `schema` set to `echo.operation-installation/v2`. Recovery dispatches on - `schema`: `v1` decodes `package_bytes` with the V1 package decoder and - identity recipes, and `v2` with the V2 ones; any other value is refused. So - V2 identities never coincide with V1 identities, and V1 encoding, - validation, and identities are unchanged. + its own invocations, so no invocation crosses between them. + + No other key may appear in any of these maps. The retained installation + record is the same `echo.operation-installation/v1` map for every program + kind, and recovery decodes its `package_bytes` with the one package + decoder. ## Evidence @@ -1924,9 +1888,9 @@ provisional detail into an obligation when its test lands. | Scoped basis change | A dependency-scoped Action evaluated at its origin obstructs with `BasisChanged`, posture `Proposed` with `NotDerived`, and keeps its proposal, read set, and changed dependency when a dependency changed, even if its proposal would also fail validation; an unchanged dependency set lets it commit. | | Scoped evaluation obstruction | A dependency-scoped Action whose evaluation exhausts its budget, or fails an authored guard, keeps that obstruction with `NoProposal` even when one of its reads changed after the origin; recovery reproduces it. | | Receipt format | `EPRV0003` records recover byte for byte through the unchanged decoder; a Tick containing a bridge Action is written as `EPRV0004`, with its fixed-profile Actions as all-zero `NotBridge` rows; a Tick of fixed-profile Actions alone stays `EPRV0003` and receipt digest version 4; a history mixing both formats recovers. | -| Package version | A source-compiled package encodes as `ExecutableOperationPackageV2` with a `closure-v2` (no lawpack keys) and a `program-v2-effect`, and its identities use the `v2` domains; a fixed-profile package keeps its V1 bytes and identities exactly. | -| Lawpack by program | A V2 package whose closure carries `lawpack_coordinate` or `lawpack_identity`, or whose program is a fixed profile, is refused at admission. | -| Invocation version | An effect invocation (`invocation-v2-effect`) is admitted only for an effect-program package, and a V1 invocation only for its own fixed profile. | +| Package format | A source-compiled package encodes as `echo.operation-package/v1` with an effect program, a closure without lawpack keys, and no `obstruction_coordinate`; a fixed-profile package keeps its bytes and identities exactly and requires its obstruction coordinate. | +| Lawpack by program | An effect package whose closure carries `lawpack_coordinate` or `lawpack_identity`, or a fixed-profile package whose closure lacks them, is refused at admission. | +| Invocation kind | An effect invocation (`kind` `edict-effect-program/v1`) is admitted only for an effect-program package, and a fixed invocation only for its own fixed profile. | | Input bounds | An effect invocation whose `application_input_bytes` is not canonical CBOR, or is longer than `max_input_bytes`, is refused at admission; a well-formed one of the wrong shape obstructs evaluation with `InputRejected`. | | Outcome record | A bridge obstruction, including an authored one such as `BasisNotCanonical`, is retained as `EOACT004` with the same coordinate as its receipt; fixed-profile Actions still write `EOACT003`, and mixed histories recover. | | Pending recovery | An Action lost with its Tick before the WAL record is recovered as the same submission, not a new one. | @@ -1946,40 +1910,40 @@ provisional detail into an obligation when its test lands. | Undeclared range read | A range read of an edge type outside `range_edge_types` obstructs evaluation with `FootprintLawViolation`, posture `NoProposal`, before reading anything. | | Range inspection metering | A range read over a bucket of many edges of other types is charged for every edge in the bucket and obstructs when the bucket length exceeds `max_range_edges`, even when no edge matches. | | Existing edge source | An identical `EnsureEdge` whose source exists reads and charges the source record, and the edge id is recomputed under the rule its type selects. | -| V2 domains | V2 package, program, invocation, and installed-operation identities use the `v2` schema strings and domains, and differ from V1 identities over the same content. | +| One format | A package, program, invocation, or installation map with any schema string other than the current `v1` one is refused; there is no second decoder. | | Topology eligibility | An Action whose package's law admits an edge write or a content-node write commits only onto its exact evaluation basis, including when this invocation writes only atoms. | | Mutable-source alias | A creating `EnsureEdge` or a `ReplaceEdge` from a mutable source that already has another edge of the same type, under any id, obstructs with `IdentityLawViolation`. | | Beta portal | An effect or read reached only through an edge-owned portal obstructs at step 1 with `FootprintLawViolation`. | -| Program map | An effect program map whose `schema` or `kind` differs from `program-v2-effect`, that has any other key, or whose profile identities differ from the package's, is refused at admission. | +| Program map | An effect program map whose `schema` is not `echo.operation-program/v1` or whose `kind` is not `edict-effect-program/v1`, that has any other key, or whose profile identities differ from the package's, is refused at admission. | | Dangling source | A creating `EnsureEdge` whose source exists neither at the basis nor in the proposal skips the identity law and obstructs at step 8 with `DanglingReference`. | -| Graph schema binding | A V2 package whose closure names the application's own schema declaration is admitted with a different `graph_schema`; changing `graph_schema` changes the program identity. | +| Graph schema binding | An effect package whose closure names the application's own schema declaration is admitted with a different `graph_schema`; changing `graph_schema` changes the program identity. | | Output type | A committed result's `result_identity` hashes the program's `output_type`; a program map without `output_type` is refused at admission. | -| Footprint contract | A V2 package whose `footprint_contract_identity` is not the `v2` digest of its `footprint_law` bytes is refused at admission. | +| Footprint contract | An effect package whose `footprint_contract_identity` is not BLAKE3 over `echo:operation-footprint-law:v1\0` and its `footprint_law` bytes is refused at admission. | | Authored obstruction | An authored obstruction's `authored_id` follows the specified recipe from its code and canonical detail, and recovery re-derives the same identity. | | Candidate and composite patches | In a Tick with two accepted bridge Actions, each row's `patch_digest` is its own candidate patch, and the replay patch is their batch composition; recovery verifies both. | | Bridge decision coordinate | Every `EOACT004` variant carries the 152-byte decision coordinate, and recovery refuses one that does not name the sealed Tick and member index of its receipt entry. | | Footprint maxima | `max_reads` counts read-set rows, one per slot, or one per named type for an absent slot probed under several types, so two typed probes of one absent slot exceed `max_reads = 1`; `max_writes` counts distinct resolved write slots after Ensure resolution, so an identical `EnsureNode` adds no write; one range read is one row. | -| Effect profiles | A V2 package whose `target_profile_identity` or `application_basis_schema_identity` is not the specified profile digest is refused at admission. | +| Effect profiles | An effect package whose `target_profile_identity` or `application_basis_schema_identity` is not the specified profile digest is refused at admission. | | Effect basis binding | An effect invocation whose `application_basis_value_identity` is not the digest of its own `application_input_bytes` is refused at admission. | | Outcome row projection | An `EOACT004` record whose `OutcomeRow` differs from the projection of its receipt-bound `BridgeEntryRow` is refused by recovery. | | Allocation ceiling | An effect program map without `max_allocated_bytes` is refused; evaluation is bounded by that declaration alone, never by a host's own ceiling. | -| Inherited identities | A V2 package whose input, result, obstruction, interpretation, or evaluation-basis identity differs from its specified profile digest is refused at admission. | +| Inherited identities | An effect package whose input, result, obstruction, interpretation, or evaluation-basis identity differs from its specified profile digest is refused at admission. | | Evaluation read charges | Node, absent, and atom reads charge one step and 64 `read_bytes`, plus the atom length when present; a read the budget cannot cover obstructs before reading, uncharged. | | Read types | A present read is authorized by the record's actual type in `read_type`, an absent one by the type it named; recovery reproduces both decisions from the read set. | | Orphan bucket | Creating a node whose id already has an outgoing bucket left by a non-bridge writer obstructs with `PreconditionMismatch`; a law admitting node creation keeps the whole-frontier rule. | | Supported profiles | An effect program and package that agree on any interpreter or intrinsic profile other than the specified effect profiles are refused at admission. | | Id-keyed maps | A graph schema whose `node_types` or `edge_types` keys are out of bytewise order, or repeat an id, is refused at admission. | | Repeated absent reads | Reading one absent slot under a permitted and a forbidden type yields two rows and `FootprintLawViolation`; reading it twice under one type yields one row. | -| V2 installation record | A retained V2 installation uses schema `echo.operation-installation/v2` and recovers through the V2 decoder; a record with any other schema is refused. | +| Installation record | A retained installation of any program kind uses schema `echo.operation-installation/v1` and recovers through the one package decoder; a record with any other schema is refused. | | Bucket scan charge | A bucket scan charges one step and 128 `read_bytes` per edge for the whole bucket before inspecting any; a scan the budget cannot cover obstructs uncharged. | | Bridge scope | A bridge Action's receipt entry has the synthetic scope (root warp, invocation identity) and its `scope_hash`; recovery recomputes both from the retained invocation. | -| Mixed batch digests | A Tick of fixed-profile Actions alone keeps its V1 rewrites and composition digests byte for byte; a Tick with a bridge Action uses the `v2` recipes with member kinds, and recovery reproduces them. | +| Batch digests | A Tick's rewrites and composition digests hash each member after its one-byte member kind, so a fixed and a bridge member never alias, and recovery reproduces them. | | Evaluation-only proposal cost | The same evaluation at the same origin has the same proposal identity whether validation stops at the history check or runs to the end. | | Allocation ceiling profile | A program declaring more than `EFFECT_ALLOCATION_CEILING_V1` is refused at admission; a host that cannot provide the declared bytes fails the Tick as a host fault, never obstructing the Action. | | Range-type refusal recovery | An evaluation-time `FootprintLawViolation` for an undeclared range type recovers as `NoProposal` by re-evaluation, with the same read set. | | Portal atom read | An atom read of a slot holding a portal records outcome `portal`, the owner's node type as `read_type`, and the portal's digest; it is refused unless that type is in `portal_owner_types`, decided from the row alone. | | Inherited ceilings | An effect program declaring `max_input_bytes` or `max_result_bytes` above 65,536 is refused at admission; a longer result obstructs with `BudgetExceeded`, `NoProposal`. | -| No result projection | A V2 package carrying `application_result_projection` is refused at admission. | +| No result projection | An effect package carrying `application_result_projection` is refused at admission. | | Reads authorized before use | A program that reads a node outside its law, then fails an authored guard or exhausts its budget, obstructs with `FootprintLawViolation` at that read; the forbidden value never reaches its obstruction or `authored_id`. | | Result capacity | A program with a small `max_allocated_bytes` returning input bytes up to `max_result_bytes` fits the pre-sized arena; evaluation never grows it. | | Read reachability | A read through an unreachable warp, or a chain with a forbidden portal owner, obstructs evaluation at that read with `UnreachableWarp` or `FootprintLawViolation`, `NoProposal`, before the value is visible. | diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index fefee4060..971b09350 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -13,6 +13,31 @@ no Jedit consumer emits one yet. Wesley packaging and the generated bounded-read corridor remain incomplete and must not borrow evidence from either mutation path. +## One current version and explicit migration + +Each data structure, wire format, and implementation has exactly one supported +version in the main crates. A replacement updates the current definition and +its producers and consumers together. Main crates must not retain historical +definitions, decoders, execution paths, compatibility aliases or wrappers, +feature-gated old implementations, or runtime dispatch between old and new +versions. Distinct current concepts may have distinct types; historical +versions of the same concept may not coexist in the main crates. + +Old versions may exist only in separate upgrade or migration tools. If future +persisted data needs conversion, those tools must explicitly convert and +validate it before the current runtime consumes it. This policy does not claim +that such tools already exist. Version labels on the one current format do +not authorize retaining another version in the runtime. + +The user confirmed on 2026-10-19 that there are no existing saved histories to +preserve. This decision supersedes the V1-preservation and dual-version +installation/recovery direction in proposal #889, earlier compatibility +promises, and any historical decision that would require old and new versions +of the same structure to coexist in a main crate. The implementation sections +below describe the current consolidation. Any remaining parallel historical +implementations must be replaced while updating the compiler, runtime, tests, +and consumers together. + ## Current Implementation Wesley currently emits raw `RewriteRule` builders plus generated operation, @@ -113,6 +138,31 @@ Rust helper codecs and graph-effect lowering require their own implementation and verification. Catalog-to-export nominal agreement remains a separate required check. +The authored-effect configuration carries the selected target's canonical +catalog bytes in the required `intrinsicCatalog` field, bounded to 16 MiB. +The native lowerer rejects absent, empty, oversized, malformed, or digest-mismatched +catalogs before body lowering. Catalogs use `edict.target-profile.intrinsics/v1` +and the selected profile's `echo.dpo.intrinsics/v1` owner-framed reference. +This transport check does not validate individual native signatures, authorize +execution, or enable the unfinished authored-effect producer. Existing exact +profile admission and catalog-to-export nominal checks remain required. + +The pure `effect_publication::encode_effect_publication` API constructs the +current operation-package/program wire shapes for untrusted effect proposals. +It preserves the embedded compiler bundle, derives artifact identities and +uses the current read/write/step budget fields. Native synthetic controls +round-trip through the current runtime decoder. The public lowerer does not yet +call this factory; compiler correspondence, graph-law semantics, independent +verification, authority admission and execution remain separate obligations. + +The provider's `echo-operation-package` owning root also admits this current +effect envelope. Anchored and effect envelopes share the current package field +group. Effects use a six-field semantic closure without an outer lawpack; +the compiled bundle retains the actual lawpack. Anchored envelopes retain their +projection and outer lawpack requirements. The program byte string is opaque to +metadata admission. A schema-admitted package with a malformed program still +refuses runtime preparation; metadata admission is not semantic verification. + ## Execution Corridors ### Bounded compiler-package computation @@ -224,9 +274,9 @@ operation-oriented corridor required for newly authored application mutations. The currently implemented generic runtime slice is: ```text -exact canonical ExecutableOperationPackageV1 bytes +exact canonical ExecutableOperationPackage bytes -> Echo-owned package admission under a separate policy --> installed data-only EchoOperationProgramV1 +-> installed data-only EchoOperationProgram -> installed compiler-owned application-result projection -> exact basis-bearing projected invocation with canonical application input -> canonical Action submission retained before acknowledgement @@ -259,7 +309,7 @@ The first external compiler-to-runtime singleton witness is: ```text standalone application-owned Edict source -> exact Core, lawpack, exports, target adapter, target configuration, and Target IR --> compiler-produced ExecutableOperationPackageV1 +-> compiler-produced ExecutableOperationPackage -> structurally separate accepted verification report -> exact result-projection binding and generic typed-input binding -> projected invocation retaining exact canonical application input @@ -529,6 +579,225 @@ keys survive patch identity, retained provenance encoding and native Tick metadata. This does not admit a new authored-effect profile or qualify a generated package for release. +Under `trusted_runtime`, `edict_effects::EffectRangeReadSession` provides a +host recording primitive for the effect interpreter under construction. The +host selects an immutable frontier, allowed range edge types and a whole-bucket +limit. Setup hashes the real state into a worldline/tick/state-root basis and +indexes the policy outside the per-read budget. This reading coordinate is not +an admitted commit-parent identity: the reachable state root alone does not +bind unreachable by-key data. Admission must also bind the writer head and +commit identity. A forbidden type fails before per-read state lookup, charging +or recording. An allowed read first charges one step and +64 read bytes to resolve the source node, recording presence or absence. If the +source exists, the exact bucket length is checked against the edge limit, then +charged in full at one step and 64 read bytes per edge before inspecting any +edge. Nonmatching types count. A refused charge adds nothing to consumption; +the already-observed source node remains recorded when a later scan refuses. +Successful reads retain the source node and the full source/type range, +including empty and absent ranges; matching edge records are ordered by edge +ID. These are real values observed at the selected basis, not caller claims. + +Read value-storage declarations have a fixed 16 MiB ceiling. Every attempted +node or range observation reserves 128 logical bytes; each source-bucket edge +reserves 128 bytes in each of the returned and recorded row buffers, including +nonmatches. Both buffers are provisioned before inspection, and unique edge IDs +are sorted without a temporary heap buffer. Reservations are cumulative across +repeated reads. Storage and I/O charges for each resolution succeed together +or leave both counters unchanged; earlier successful resolutions remain +recorded when a later one refuses. A host that cannot provide admitted buffer +capacity reports `HostAllocationFailed`, not `BudgetExceeded`. That host fault +must stop evaluation and must never become an Action obstruction or retained +decision. + +`EffectEvaluationFrame` connects that view to the real compiler-package pure +interpreter through one cumulative step and allocation meter. It consumes an +unused session and intersects the interpreter's ceilings with the session's +limits. Current frame declarations are bounded by 1,048,576 steps, 64 MiB of +read bytes, 16 MiB of write bytes and 16 MiB of value storage. These host-frame +bounds do not establish an admitted operation-profile contract. An already-used session cannot reenter as unspent capacity. Each read +must fit both the view's I/O limits and the shared computation/storage budget; +failed read reservations change neither meter. Each pure component retains its +own declared ceiling relative to its starting usage, while the absolute frame +ceiling continues to bind all components together. Component limits are restored +between calls without refunding work or storage. Any identity, input, +computation, read or resource failure ends the frame; later calls do no work. +The shared interpreter uses atomic failed charges. An admitted concatenation +buffer that cannot be provisioned is a host fault, never budget exhaustion. +The existing standalone pure/read APIs retain their prior accounting behavior. + +`TrustedRuntimeHost::effect_evaluation_frame_v1` can now construct that frame +against an expected full runtime parent. The host resolves the registered writer, +worldline tick, committing global tick, state root and commit identity through +the existing scheduler basis resolver, compares all runtime fields, and borrows +its own frontier. Equal reading coordinates from a different commit history are +refused before constructing a frame. The immutable host borrow prevents a +scheduler tick during the frame's lifetime. The typed application basis is +carried as a proposition; this constructor does not validate application input +or grant invocation authority. A frame constructed by this host method exposes +its full `evaluation_basis`; low-level `EffectEvaluationFrame::new` frames expose +`None` and cannot mint a runtime-bound coordinate from caller-supplied storage. +This is private host composition, not an installed or admitted effect profile. +The state-root/by-key caveat of the low-level reading coordinate and the +package-admission and recovery obligations remain as described above and below. +The optional typed mode below constrains runtime-bound reads separately. + +A runtime-bound frame can also run an unchanged +`compiler-produced-bounded-read/v1` package through `evaluate_read`. It uses the +same decoded instructions and expression interpreter as standalone +`edict_read::evaluate`, but its access adapter resolves the frame's own frontier +and records dependencies. The trusted caller supplies a sorted, unique bounded +node aperture. A key outside it is refused before graph observation. An unbound +caller-frontier frame cannot enter this recorded interpreter path. + +Each source-node resolution charges one step, 64 read bytes and 128 logical +storage bytes before lookup, and records presence or absence. A missing node +stops that read without resolving its attachment. For a present owner, resolving +its alpha attachment charges one step, 64 read bytes plus any atom payload +length, and 128 logical storage bytes plus that payload length before retention +or value delivery. The interpreter retains its existing byte-copy work and +value-storage charges. Source and attachment reservations are individually +atomic across the read and shared meters. A later failure retains the resolved +prefix, including negative observations. Wrong types, oversized atoms, missing +attachments and authored guards keep their existing obstruction coordinates; +the actual observations remain distinct from the caller's expected values. + +The frame intersects package/input/output ceilings with each requested read +component, preserves its declared local limits, and keeps work/storage consumed +by earlier reads and computations. Every refusal or host fault terminates the +frame. Failed observation reservations add no read/storage charge, and host +allocation failure while provisioning precharged atom bytes remains a host +fault rather than an authored obstruction. Standalone bounded-read accounting +and outputs are unchanged. This legacy entry point uses an untyped trusted-host +node aperture. It is refused before work or observation after a typed law is +bound to the frame. It grants no application mutation or admission authority. + +An unused runtime-bound frame can bind an `EffectFootprintLaw` through +`with_footprint_law`. The decoder requires canonical CBOR with exactly the nine +footprint-law fields: six type-id sets and `max_reads`, `max_writes`, and +`max_range_edges`. Set arrays contain distinct, strictly ascending 32-byte IDs; +unsorted or repeated entries are refused rather than normalized. Zero counts +are allowed. Read, write and whole-bucket ceilings are 4096, 256 and 65536. +Decoding is bounded to 16 MiB and occurs during trusted-host preparation. The +write permissions are validated as data but grant no write API. Binding refuses +an unbound frame, previous work, a prior failure, or replacement of an existing +law. These are host-selected permissions, not an authenticated package policy +or admission token. + +Typed mode provides `read_typed_node`, `read_typed_range`, and +`evaluate_typed_read`. Present nodes and atoms are authorized by their actual +stored types. `TypedNodeRead::absent_node_type` authorizes node absence only; +absent atoms use the requested atom type. A present, authorized atom with a +different requested type still yields the existing authored type-mismatch +obstruction. Node and atom permissions are checked before retaining or +returning their values. Raw slot readings retain authorized resolved values; +`authorized_reads` additionally retains distinct slot/type pairs, so absence +under two named types does not collapse into one authorization. `max_reads` +bounds those pairs after deduplication. Permission roles are checked on every +resolution even when a pair already exists: a portal-owner permission does not +permit an ordinary node read. Range pairs carry a zero read type, since their +edge type is already in the range key. Range edge types must pass both the +host's policy and the law before any state access. Source nodes use their +actual or absent type, and the law's bucket ceiling intersects the host limit. +Legacy untyped range and compiled-read entry points cannot bypass a bound law. + +Before a typed read resolves a descended-warp target, it follows bounded +instance metadata back to the root or a previously validated ancestor. It then +resolves and authorizes alpha portals root-first. Each portal contributes its +actual owner-node and descent-attachment observations, both authorized by +`portal_owner_types`. Edge-owned or non-alpha portals are refused before an +owner record is read. Unknown, cyclic or disconnected chains report +`UnreachableWarp`; no target value is resolved. A validated chain is reusable +within the frame's immutable parent. This cache never grants a different +permission role. The low-level untyped session retains its existing by-key +behavior. + +Typed resolutions keep the existing atomic observation and payload charges. +Each new authorization pair reserves 64 additional logical storage bytes. Each +uncached reverse-metadata hop costs one step, 64 read bytes and 256 logical +storage bytes before extending its bounded chain/cycle indexes; actual portal +owner and attachment resolutions separately cost one step, 64 read bytes and +128 observation bytes each. These are private-frame charges, not a shipped +operation-profile recipe or a physical allocator limit. Failed charges change +neither meter. Any law, reachability, resource or authored failure ends the +frame. Accepted authorization pairs can precede a later payload-budget failure; +they are host-private authorization evidence, not the persisted canonical +`ReadRow` image. The executable-operation pipeline tests exercise real bound +frontiers, compiled reads, nested alpha portals, beta refusal, distinct named +absence, role separation, whole-bucket limits and atomic failure prefixes. + +`EffectEvaluationFrame::into_read_set_image(max_bytes)` consumes a typed, +runtime-bound frame into an owned, eight-byte-aligned `EffectReadSetImage`. +Successful and failed frames can export their completed observation prefix. +Permission reservations without a completed raw value resolution are omitted; +a failed atom payload charge cannot turn into a false absence row. Unbound or +untyped frames report `MissingTypedParent`. The owned export preserves the +actual full runtime parent, cumulative work/I/O and logical allocation usage +separately from the image bytes. Consuming the frame prevents further evaluation +against an already-exported read set. + +The image is the read-set digest preimage after its domain: a little-endian +`u64` row count, the 192-byte `EffectReadRow`s, a little-endian `u64` pair count, +and the 64-byte `EffectEdgePairRow`s. There is no new retained-history record +magic. A read row contains a 104-byte `EffectSlotRow`, outcome and zero padding, +a 32-byte authorizing type, a 32-byte subject digest and a WSC range descriptor. +All rows are `repr(C, align(8))`, `Pod` and `Zeroable`, with explicit padding. +The slot classes are node `0`, attachment `1`, edge `2`, range `3`. Their wire +order is explicit and differs from native `SlotId` ordering. Slots sort by +class, warp, local ID, range edge type, attachment owner and plane; read rows +then sort by authorizing type. A present slot has one actual-type row. Only +absent slots can repeat under distinct ascending named types. + +Node subjects hash `echo.bridge.node-value.v1\0` followed by the node type. +Atom subjects hash `echo.bridge.attachment-value.v1\0`, tag `0`, atom type, +little-endian byte length and bytes. Portal subjects use that attachment domain, +tag `1` and the child warp ID; their read type is the owner's node type. Edge +subjects hash `echo.bridge.edge-value.v1\0`, edge type, source and target local +IDs. Absent subjects are zero. Range subjects and read types are zero; the range +slot already names its edge type. Matching edge/target pairs are packed in +range-row order, sorted and unique by edge local ID within each run. Separate +source-node rows distinguish an absent source from a present empty bucket. +The read-set digest hashes `echo.bridge.read-set.v1\0` and the exact image. +The parent and accounting metadata are not part of this read-set digest; an +admitted proposal/decision must separately bind them. + +`EffectReadSetView::parse` borrows rows and pairs directly from an aligned input +buffer. Its structural validator allocates no collections and checks exact +framing, checked size arithmetic, tag legality, zero padding/unused fields, +canonical order, duplicate rules, pair bounds, strict edge order and contiguous +complete pair-section coverage. A suffix, an orphan pair or overlapping range +is invalid. The fixed ceiling is 4096 read rows and 65536 pairs per range, with +an image no larger than 16 MiB. The total pair section can exceed 65536 when +individual ranges and the image limit permit it. This view proves canonical +structure only; it does not authenticate a law, parent, dependency completeness +or historical Action decision. + +Export is bounded host retention work, outside the consumed evaluation budget. +The caller's output limit intersects the fixed 16 MiB ceiling; the temporary +sorted descriptor table is bounded by the authorized-read ceiling. Export uses +only completed recorded values and does not resample the frontier. Provisioning +failure reports `HostAllocationFailed`, while an insufficient output allowance +reports `ExportLimitExceeded`; neither is an authored Action obstruction. +These buffers are separate from the interpreter's logical reservations. This +codec does not implement the single physical arena, receipt/WAL retention, +proposal admission or fresh-engine effect recovery. Independent wire fixtures +exercise malformed inputs, maximum counts, alignment and field encoding; real +runtime witnesses exercise exact node bytes/digests, atoms, portals, absence, +range packing, failed prefixes and preserved parent/resource metadata. +The codec integration target declares `trusted_runtime` in Cargo's +`required-features`; CI explicitly runs and lints it in the trusted-runtime +lane. The feature-gated-test scheduling guard rejects missing declarations or +missing feature-qualified CI execution. + +This is trusted-host composition of pinned pure packages and recorded reads. +It provides no mutation or admission path and is not a compiler-authored +effect-program format. Package/input decoding remains bounded host preparation. +Logical value-storage reservations bound the row buffers and observation count; +policy preparation, indexes and allocator overhead remain host storage. The +physical interpreter arena, package-authenticated footprint law, receipt/WAL +integration of typed read images, authored outgoing-range/effect instructions, derived effects and +installed-profile validation remain required before a new authored-effect profile can ship. +Existing package profiles and graph accessors are unchanged. + Conflicts remain explicit receipt rejections. A footprint failure must not trigger hidden retries or widen access. @@ -543,8 +812,8 @@ trigger hidden retries or widen access. material through the appropriate provider-native or Wesley compatibility record—not an app-specific engine escape hatch. - Newly authored executable operations resolve installation and invocation from - an admitted `ExecutableOperationPackageV1`; a naked - `EchoOperationProgramV1` cannot install or invoke. + an admitted `ExecutableOperationPackage`; a naked + `EchoOperationProgram` cannot install or invoke. - Newly authored executable operations expose no application matcher, executor, footprint callback, or prebuilt mutation plan. Provider-v1 callback bindings remain explicitly labeled compatibility infrastructure. @@ -555,3 +824,601 @@ trigger hidden retries or widen access. provider operation, and receipt evidence from the exact bound scheduler rule. - Runtime patches and receipts retain the generated package or rule-pack identity needed to explain execution. + +### Executable graph-schema preparation + +`EffectGraphSchema::from_canonical_bytes` decodes the graph contract required by +the current authored-effect program contract. The root has exactly `node_types` and +`edge_types`. A node-type entry has exactly `addressing` (`0` content or `1` +mutable) and `atom_types`; an edge-type entry contains source/target node-type +pairs. Every identifier is exactly 32 bytes. Set arrays are strictly ascending +and distinct, including lexicographic ordering by both endpoint types. Invalid +order or duplicates refuse rather than being normalized. + +The prepared value answers node-addressing, node/atom compatibility and edge +endpoint-type membership queries. Empty relations deny membership. This is +schema preparation, not graph-record resolution, identity-law enforcement, +package authentication or Action admission. It creates no read or mutation +capability. Input is bounded to 16 MiB and canonical decoding retains its +combined node/depth ceilings; these host preparation allocations are outside +the evaluation meter and do not implement its physical arena. + +### Current operation publication preparation + +`ExecutableOperationPackage::prepare` is a data-only check available without +`trusted_runtime` or `native_rule_bootstrap`. It uses the same current package +and embedded-program checker as trusted-host preparation. A fresh outer package +pin does not bypass compiler-bundle or semantic-closure checks. This API neither +authenticates source-to-Core compilation nor determines which release pin is +authorized. The runtime helper modules remain private without `trusted_runtime`, +and owner constructors retain their existing feature gates. Preparation grants +no installation, invocation, scheduler, WAL or mutation authority. + +`ExecutableOperationPackage::prepare` checks a trusted host's expected current +package identity against exact canonical publication bytes. There is one +`ExecutableOperationPackage`, one `EchoOperationSemanticClosure`, and one +`EchoOperationProgram` definition and codec. The program kind selects +compare-and-set, create-if-absent, or the authored-effect payload. The former +V2 package definition and decoder have been removed without aliases, wrappers, +or schema-version dispatch. + +The current package and program schema strings are `echo.operation-package/v1` +and `echo.operation-program/v1`. Their identities use the corresponding +length-prefixed `echo:operation-package:v1\0` and +`echo:operation-program:v1\0` domains. These are labels for the sole current +formats; the former V2 publication/program schemas are refused even when their +bytes have a matching current-domain pin. + +The shared package envelope binds the operation, obstruction, authority, +semantic closure, exact nested program, budget, profile identities and footprint +contract. Fixed programs require a real paired lawpack coordinate and identity. +Authored effects forbid both lawpack fields and the application-result +projection; their closure has exactly the six upstream source/meaning/Core/ +Target IR/schema fields. No sentinel lawpack is introduced. The closure and +effect program must bind the same Target IR identity. + +The authored-effect program payload has exactly thirteen fields. Its +`edict-effect-program/v1` kind requires the declared effect interpreter and +intrinsic profiles, ContentAddressV1, exact executable graph schema, exact +footprint law, compiled bytes, output type and resource ceilings. Its footprint +identity is BLAKE3 over `echo:operation-footprint-law:v1\0` followed directly by +the canonical law bytes. The package's effect target, input, result, obstruction, +interpretation and basis profiles are checked by the same package validator +that validates the fixed program kinds. + +Input and result declarations permit zero and are capped at 65,536 bytes. +Allocation permits zero and is capped at 16 MiB. The output coordinate is +nonempty UTF-8 of at most 1,024 bytes; other coordinates retain the current +nonempty-text rule. Effect package steps must be positive and byte budgets may +be zero. Preparation is outside evaluation metering and does not provision the +physical evaluation arena. + +The generated provider configuration schema recognizes the current +`edict-effect-program/v1` kind under `echo.operation-lowering-configuration/v1`. +Its declaration requires `identityLaw: 1`, encoded `graphSchema` and +`footprintLaw` byte payloads, explicit input/result/allocation limits, and an +explicit steps/read-bytes/write-bytes budget ceiling, and a nonempty +`authorityProfile` label. Input and result limits +are at most 65,536 bytes, allocation at most 16 MiB, and the encoded law/schema +payloads must be nonempty and at most 16 MiB. Zero resource declarations are +explicit values; steps must be positive. Unknown or missing fields refuse. +Edict lawpack authoring can carry those byte payloads through `$edictBytes`. + +The configuration is the only source of an effect package's requested +authority. The package's `authority_profile_identity` is the same label digest +fixed operations use: BLAKE3 over `echo:operation-profile:v1\0`, the label's +length as a little-endian `u64`, and the label bytes. It hashes a label, not a +permission manifest, so it is a request: a label does not make a package +read-only or confine its writes. What the package may do follows from the runtime +owner's independently selected admission policy, which pins the exact package, +operation, authority profile and budget, and from the admitted program, grants, +footprint law and runtime checks. + +An authored effect package carries no `obstruction_coordinate`. Its program can return several obstructions, and every one is declared in the package's embedded lawpack exports and checked arm by arm at admission: each arm's coordinate must name exactly one declared obstruction with the failure's authority class. A fixed-profile package keeps its single nonempty `obstruction_coordinate` unchanged, so its bytes and identities are the same as before; the program kind decides which shape a package has, as it already decides whether a lawpack binding is present. + +This schema crossing validates the configuration's shape and bounds. It does +not validate the embedded law/schema semantics, produce an effect package, or +provide runtime authority. The public lowerer/verifier effect branch and target +resource declarations remain unfinished. Runtime preparation and private +execution evidence above do not establish that producer path. + +The `compiled_program` payload uses one current +`echo.compiler-produced-effect-program/v1` artifact bundle with exactly +`schema`, `intent`, `core_artifact`, `target_ir_artifact`, +`lawpack_exports_artifact`, and `lowering_artifacts`. The first three artifacts +are exact canonical CBOR bytes. `lowering_artifacts` is null for a selected body +without effects. For an effect body it is an exact map containing canonical +`source_artifact`, `lawpack_artifact`, `adapter_artifact`, +`target_profile_artifact`, and `intrinsics_artifact` byte strings; +the source decodes to UTF-8 bytes. This replaces the former bundle shape; +there is one decoder and no compatibility path. Opaque program bytes, +an unbundled Core module, extra bundle fields, and noncanonical nested +encodings refuse. + +Preparation recomputes the Edict canonical SHA-256 identities for Core, Target +IR and exports and checks them against the package closure and program. The +Target IR must name the same Core coordinate and SHA-256 Core identity. Only +the current ordered Target IR schema is accepted. The selected bundle intent +must name the package operation; its Core output type must equal the program's +output type. The Core and Target IR must agree on operation profile, input +constraints, evaluation budget, optional basis and result. Instruction IDs are +nonempty and unique across step, requirement and pure-binding collections; +execution order must contain each ID exactly once. Artifact roots and selected +Core intent/body fields have exact shapes. + +The selected Core body and ordered Target IR must now correspond node for +node. `let`, terminal `require`, and `effect` nodes preserve their bindings, +values, predicates, failure reasons and inputs exactly. Added, omitted, +reordered and substituted nodes refuse, as do unknown Core node kinds and +extra Target node fields. A single validator +checks the execution order against Core; the former ID-only check was removed. + +Core local declarations use the shared current local-identity check. IDs, +complete type references and alpha names must match their declaration exactly; +the input is `arg.0` with the authored input type. Declared types resolve through +the existing bounded syntax parser. Basis and input constraints see only the +input; a local becomes available after its definition. Each obstruction binder +is available only within its own arm, every declaration is accounted for, and +an obstruction local cannot leak into the operation's result. Source arm +binders/values and lowered arm values have a one-to-one correspondence, and +lowered failure names are unique and cover the lowered arm keys exactly. +Expression syntax, type expansion and reference traversal retain the existing +combined work/depth ceilings. Unsupported control-flow and optic bodies refuse +until their instruction contracts exist. + +Effect bodies additionally require a single lawpack pinned identically by the +Core imports and Target semantic closure. The lawpack must pin the exact exports +and declarative adapter bytes. Its selected adapter entry must match the Target +profile reference and Target IR schema ID. The current bounded source preamble +requires the same package coordinate and an explicit lawpack import with that +lawpack digest and one alias. Effect names must use that alias or the fully +qualified lawpack coordinate. A uniquely matching adapter operation profile +must authorize every effect. Each lowered intrinsic name must equal its adapter +implementation. Source failure names map to the exact lowered tags and arm +bodies through the adapter; mappings are bijective and agree with exported +failure declarations. Effect result bindings and failure payload binders must +use the exported type names. Collection storage order does not replace the +explicit execution order. + +Every Core type declaration in the selected lawpack's namespace must now name +an exported type and match its canonical definition. The metadata check uses +exact Core declaration shapes, renders record fields in lexical name order, +preserves explicit byte bounds and nominal contract names, and follows exported +whole-definition aliases with bounded cycle detection. Unknown imported members, +changed field types or bounds, duplicate export coordinates and extra declaration +fields refuse even under recomputed Core/Target/package identities. It checks +all declarations in that namespace, including unused ones. Alias/reference walks +have fixed work, depth and reference-length limits. + +The metadata renderer authenticates the current primitive, string/bytes, record, +nominal, option/list/map, capability-reference and external-action-request +shapes. Unsupported shapes, including variants, refuse until implemented. +Metadata authentication does not enable evaluation of those type families; +the executable expression checker still admits its current pure subset. +Named unsigned aliases use the compiler's canonical `{"kind":"U64"}` form +and corresponding widths. The synthetic `Int`/`width` decoder was removed and +its fixture producer updated; there is no alternate decoder for that shape. + +The bundled target profile's canonical identity must equal the Target IR +reference and the lawpack adapter's selected profile reference. Its Core ABI, +coordinate and intrinsic namespace must agree, and its Target IR resource +reference must equal the adapter's complete accepted reference. The profile's +`echo.dpo.intrinsics/v1` reference must pin the exact bundled intrinsic catalog. +Only the current catalog schema is accepted, and the selected intrinsic must be +present as an effect in the declared namespace. The previous evidence shape +without profile/catalog artifacts refuses; there is no alternate decoder. + +Concrete catalog declarations must have exact fields and agree with the exported +effect's execution class, write class and supported guard posture. Free type +parameters and positional argument lists requiring unpacking refuse until +implemented. Catalog named-fault entries are indexed by the adapter's mapped +target tags; their complete tag set and authority classes must agree with +exported failures. Cost/footprint template names are present and nonempty, but +their calculus is not validated by this check. + +For an application intrinsic, the catalog's single argument type, return type +and fault payload types must equal the exported effect's input, output and +failure payload coordinates by name. This is concrete declared agreement, not +general structural or polymorphic signature unification. + +The six shared runtime intrinsics below are the one exception. Echo's provider +catalog is shared by every application, so it cannot name any application's +types. For these exact intrinsic identities, and no others, the catalog names +Echo-owned ABI coordinates, and the effect the adapter explicitly binds to the +intrinsic must declare types whose representations conform to that ABI. The +application's types keep their nominal identity: conformance establishes a +lawful representation crossing at this one boundary, not that equal +representations are equal types anywhere else. A matching shape never selects +an intrinsic; the adapter's explicit binding and its ambiguity checks still +apply. + +| Intrinsic | Catalog argument / return | Input representation | Successful result | +| -------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------- | +| `echo.dpo@1.node-atom-read` | `echo.dpo@1.node-atom-read.input` / `.result` | exactly `warpId`, `nodeId`, `typeId` | bytes, minimum zero, positive maximum at most the ceiling | +| `echo.dpo@1.ensure-node`, `echo.dpo@1.create-node` | `.input` / `.result` | exactly `warpId`, `nodeId`, `nodeType`, `atomType`, `atomBytes` | the empty record | +| `echo.dpo@1.replace-atom` | `.input` / `.result` | exactly `warpId`, `nodeId`, `expectedAtomDigest`, `atomType`, `atomBytes` | the empty record | +| `echo.dpo@1.ensure-edge` | `.input` / `.result` | exactly `warpId`, `edgeId`, `fromId`, `toId`, `edgeType` | the empty record | +| `echo.dpo@1.replace-edge` | `.input` / `.result` | exactly `warpId`, `edgeId`, `expectedToId`, `toId` | the empty record | + +Every field other than `atomBytes` has a representation of exactly 32 bytes; a +maximum-bounded or differently sized field does not conform. `atomBytes` and +the read result are bytes with minimum zero and a positive declared maximum no +greater than the 16 MiB runtime ceiling, so an application may declare a +smaller maximum. Field membership is exact: a missing, renamed, duplicated or +extra field does not conform. The mutation intrinsics declare no failures. The +read intrinsic's four failures carry `echo.dpo@1.node-atom-read.input` in the +catalog, and each exported failure must carry the effect's own declared input +type, so the fault payload stays nominally tied to that application's input. + +The catalog names the read's failures with Edict ABI identifiers, and Echo pins each one, for this intrinsic only, to the versioned tag its runtime records: + +| Catalog name | Runtime tag | Authority class | +| ----------------------- | ---------------------------------- | ---------------- | +| `atomMissing` | `echo.atom-missing/v1` | `resourceFault` | +| `atomRequired` | `echo.atom-required/v1` | `integrityFault` | +| `atomTypeMismatch` | `echo.atom-type-mismatch/v1` | `integrityFault` | +| `atomByteBoundExceeded` | `echo.atom-byte-bound-exceeded/v1` | `integrityFault` | + +The pairs belong to `(echo.dpo@1.node-atom-read, catalog name)`; there is no global dictionary and no fallback that forwards an unfamiliar string. The provider's semantic source records each pair as the failure's `runtimeTag`, so changing one changes the pinned provider; the catalog entry itself carries only `authorityClass` and `payloadType`. On the authored route an adapter maps its own failure names to these catalog names, Edict keys the lowered read step's arms by them, and the runtime translates each arm to its tag before matching the obstruction it records: a missing, unknown, repeated or tag-spelled name refuses the package. Application aliases stay in the application's adapter. Jim's `missing` maps to `atomMissing`, which Echo records as `echo.atom-missing/v1`; `missing` never appears in Echo's table. The bounded-read route keeps its existing runtime-tag mappings. + +Declarations cannot redefine the existing node/atom recording primitive's ABI. +The runtime currently recognizes `echo.dpo@1.node-atom-read` here. Its input +representation has exactly `nodeId`, `typeId` and `warpId`, each 32 bytes; its +output is a byte type with minimum zero and a positive maximum under the +runtime's logical storage ceiling. The four named atom-read fault tags and their resource +or integrity classes are fixed, and each carries the input-address type. +Mutually agreeing Core, lawpack and catalog declarations cannot change these +requirements. Five mutation-emitter ABIs are also supported as described below. +Other intrinsic names refuse until their handlers and ABI contracts exist. Static +binding does not itself execute instructions. + +The retained profile/catalog declares only the original replace fixture and a +prior Target IR resource; it does not declare node/atom read. Positive tests now +construct an explicitly synthetic read catalog and consistently repin its +profile, lawpack, Core imports, Target references and source preamble. The raw +capture remains a refusal case. Neither this synthetic chain nor matching hashes +is evidence of successful public effect compilation or verifier approval. + +The retained raw public capture has inconsistent export/adapter pins in its +lawpack and now refuses. Positive correspondence tests use an explicitly +repinned synthetic chain derived from that capture. They do not prove a +successful compiler build or verifier acceptance. + +Data-valued `let` expressions, effect inputs, operation results, basis expressions, +terminal-reason payload values, and predicates now use the shared pure-expression +type checker. Expected effect input names come from the pinned exports and +resolve through Core's current type definitions. Byte bounds, integer widths, +record fields, conditional branches and helper argument/result representations +must agree. The same helper signature collector and body checker serve pure +and effect preparation; there is no second helper authority implementation. +Every source/imported helper body is checked, including unused definitions. +Unpublished calls, effect names used as pure helpers, invalid arguments, +recursive helper cycles, and excess combined expression/call depth refuse. +These checks retain the existing pure-helper subset and bounded work limits. + +The shared inferred type model now preserves each nominal contract name and +representation. Resolved nominal declarations must name their own Core table +identity. Effect data expressions and checked helper bodies cannot substitute +another nominal contract with the same representation, implicitly unwrap a +nominal value, or construct one from an untyped literal. Helper argument/result +checks and conditional joins retain this identity. Predicates require identical +nominal identities before applying the supported scalar representation operator. +The synthetic effect fixture's numeric result is now an ordinary unsigned alias, +rather than relying on implicit construction of a nominal value. + +Nominal identity is static metadata over the existing wire representation. +Runtime value validation follows that representation and retains its existing +CBOR bounds, meter costs and physical value depth; it does not invent a nominal +wire tag or charge an extra physical frame for the static name. This does not +establish a complete static typing proof for every pure-program main/projection +path. Those paths still require full refinement-aware checking or independent +compiler/verifier authority, and explicit nominal construction operations are +not introduced by this checkpoint. + +Failure-arm values currently support declared zero-argument obstruction +constructors. Their exported authority class must equal the exported failure's +class: `domainMappable`, `resourceFault`, or `integrityFault`. A matching arm body +and fresh pins cannot turn an exported integrity failure into a resource or +domain obstruction. Parameterized or computed obstruction expressions refuse +until their contracts are implemented. Constructor payload schemas and terminal +reason authority are not yet checked by this relation. + +These checks establish artifact binding, ordered correspondence, lexical scope, +and data representation typing/helper authority within the current pure subset. +Imported declarations in the selected lawpack namespace now bind to exports, +but this does not authenticate every source/local definition or establish a +complete typing proof across every pure main/projection path. General intrinsic families, structural/polymorphic signatures and the remaining +runtime handlers remain open. Adapter +correspondence binds declared names and failure arms; it does not prove a +source-to-Core translation or an independently verified source closure. The +bounded preamble reader is not a full Edict parser. Multiple lawpacks and +ambiguous adapter profiles refuse explicitly. Complete target schema/profile +artifacts beyond the supplied profile/catalog, obstruction-taxonomy resources, +cost/footprint/configuration obligations and the complete lowering relation +remain unchecked. These checks do not establish verifier approval, +interpret instructions, or prove that an application graph schema corresponds +to compiler exports. Resource execution and the physical arena remain required. +Static preparation grants no invocation or mutation authority. + +### Private ordered authored evaluation + +`EffectEvaluationFrame::evaluate_effect` evaluates the prepared current authored +program against the runtime-selected immutable parent and typed node aperture. +The frame's exact footprint law must equal the prepared program's law. Whole +authored evaluation requires an unused frame, so it cannot adopt reads or work +from another private component. Unbound/used frames, mismatched laws and invalid +apertures refuse before new graph observations. +This is trusted-host private evaluation, without installed dispatch, invocation +grants or mutation authority. + +The checked Core body and ordered Target IR are decoded into typed `let`, +node/atom-read and terminal `require` instructions. Expressions, predicates, +helper calls and value validation reuse the existing pure evaluator. Input, +Core, program and frame ceilings intersect; computation and real recorded reads +spend the same cumulative meter. Each component retains its own declared work +and storage ceiling. Basis expressions are evaluated and must yield 32 bytes; +this does not establish the invocation's separate input-derived basis grant. + +Reads resolve the actual atom through the existing typed recording adapter. +Successful node/atom pairs must satisfy the prepared graph schema before their +values reach subsequent instructions. Actual absence and mapped read failures +retain completed dependencies. The currently supported zero-argument failure +constructors yield their authored source-context name and empty canonical +payload; false requirements evaluate their terminal payload expressions. These +are private evaluation errors, not persisted Action decisions or a complete +obstruction taxonomy/schema proof. + +The final result is validated and encoded under the intersected output ceiling. +Failure or successful whole authored evaluation ends the frame; later evaluation +calls do no work. Completed output, usage and observations remain available for +inspection or consuming retention. The result is borrowed from frame-owned +storage, so completion retention does not make an uncharged second output copy. The new runtime witnesses use the +explicitly synthetic, coherently pinned read fixture, not a genuine public +compiler/verifier output. Private whole-parent derivation is described below. +Physical arena guarantees, atomic scheduler settlement, receipts/WAL and +original-decision recovery remain required for the complete bridge. + +### Buffered compiled mutation intents + +The current interpreter recognizes five `echo.dpo@1` mutation-emitter names. +Each declares the provider effect kind that names what it does, `create` for +`create-node`, `ensure` for `ensure-node` and `ensure-edge`, and `replace` for +`replace-atom` and `replace-edge`, and declares that same value as its +`writeClass`, because Edict's intrinsic ABI has no `write` class. Its named +intrinsic failure set must be empty. They require the explicit +`continuum.profile.graph-effects/v1` Core/Target operation profile; a matching +read-only profile and adapter cannot authorize them. + +Each emitter accepts one record argument and returns an empty record. The +runtime fixes every field other than `atomBytes` to exactly 32 bytes. The byte +payload has minimum zero and a positive declared maximum within the 16 MiB +logical storage ceiling. Declaration/catalog agreement cannot redefine this ABI. + +| Intrinsic suffix | Required input fields | +| ---------------------------- | ----------------------------------------------------------------- | +| `ensure-node`, `create-node` | `warpId`, `nodeId`, `nodeType`, `atomType`, `atomBytes` | +| `replace-atom` | `warpId`, `nodeId`, `expectedAtomDigest`, `atomType`, `atomBytes` | +| `ensure-edge` | `warpId`, `edgeId`, `fromId`, `toId`, `edgeType` | +| `replace-edge` | `warpId`, `edgeId`, `expectedToId`, `toId` | + +At emission the evaluator admits one fixed step, a deterministic 280-byte row +reservation, and fixed write bytes atomically: 64 plus the atom payload length +for node/atom effects, or 128 for edge effects. The typed intent moves the already +charged expression payload without copying it. A failed row/step/write reservation +commits none of that emission charge. Earlier expression work remains consumed. It +retains separate node and atom types and both sides of replacement comparisons. +Host allocation failure remains a host error. Only a successfully completed whole authored operation exposes intents through +`EffectEvaluationFrame::buffered_mutations`, in execution order. Later +output/resource/guard failure discards the operation's entire partial buffer. +Another program cannot append to the completed operation, and prior private +component work cannot be adopted into it. The buffer is not a validated proposal. + +`EffectMutation` values are data. No public API accepts caller-constructed +values as evaluation evidence or grants a write from them. Emission does not resolve or modify graph slots. Its write-byte charge applies +even when a later Ensure resolves identically and contributes no write slots. A +whole operation may emit at most 1,024 effects, including identical targets and +intents that later become no-ops; the next emission refuses without exposing a +partial intent buffer. Completed read-image usage retains these emission costs +alongside computation and read costs. These intents are not a canonical +proposal, approved footprint, derived patch or successful Action. Reachability, +canonical ordering, duplicate targets, schema/identity/footprint rules, +preconditions and validation-read charges are handled by the private whole-parent +derivation crossing described below. Scheduler integration remains required. +Tests use explicitly synthetic pinned artifacts; genuine +compiler/verifier production and physical arena guarantees remain open. + +### Completed authored origin evidence + +`EffectEvaluationFrame::into_effect_evaluation` consumes a successful whole +operation into one `CompletedEffectEvaluation`. It retains the exact producing +program identity, canonical-input application-basis binding, actual result, +emitted intents and bounded recorded-read image. The image supplies the sole +full runtime parent and the actual usage counters. Result and intent storage move +into the completed value without copying their payloads. There is no public +constructor or setter for this completed value. + +Program identity uses the same current program identity recipe as +`EchoOperationProgram::identity`; preparation caches the identity over the exact +canonical program. Input binding reuses the current invocation-admission recipe, +not a second projection/hash rule. Completion requires that binding to equal the +runtime-selected parent's application basis. Changing otherwise valid input +under an original parent therefore cannot yield completed origin evidence. +The frame also carries the runtime owner's process-local evaluation authority +into completion. No public API constructs or replaces that authority. Matching +program and parent coordinates do not permit another owner's completion to +cross the derivation boundary: reachable-state roots exclude detached records, +so matching roots alone cannot establish equal underlying stores. This owner +binding is process-local origin evidence, not a serialized recovery proof. +Unused, failed or mixed-component frames cannot complete, and read-image export +limits/faults deliver no partial completed value. Failed typed read prefixes may +still be retained through the existing diagnostic read-image path. + +This value establishes private execution origin. It does not authenticate an +installed package or independent compiler/verifier approval, resolve mutation +preconditions, derive a patch or grant invocation authority. Proposal validation +and installed scheduler dispatch must still check the producing program and exact +parent against their own authoritative evidence. + +### Private whole-parent patch derivation + +The trusted runtime owner can consume `CompletedEffectEvaluation` through +`derive_echo_operation_effects` with an independently admitted invocation. +The owner checks both admission and completed-evaluation authority, current +installation, producing program +and exact full parent before resolving mutation intents against immutable state. +This returns a `DerivedEffectPatch` for inspection without applying its operations. +The patch binds the runtime policy, installation and invocation identities. + +Resolution retains charged validation reads alongside evaluation usage and +checks schema, content or mutable-slot identity, duplicate targets, typed write +permissions, resolved write limits, comparisons and endpoint presence. All five +mutation forms derive operations; identical Ensure intents derive no writes but +retain emission charges and must still have write permission. Attachment and +outgoing-range writes count toward the resolved write limit. + +This is a private whole-parent checkpoint. It does not provide scoped-history +admission, same-Tick composition, installed Action execution, receipt/WAL +publication or fresh-engine effect-decision recovery. The physical arena and +public compiler/verifier production proof also remain required. Synthetic +runtime fixtures do not establish those obligations. + +### Current invocation input, basis and owner grant admission + +`EchoOperationInvocation` is the sole current invocation representation and +codec. All program kinds use `echo.operation-invocation/v1` with an explicit +`kind` field. Fixed compare-and-set and create-if-absent kinds retain their +actual node anchor and optional projected input. Authored-effect invocations +require canonical application input and carry no node anchor or replacement +bytes. Unknown fields, old kind-specific invocation schemas, and the former +V2 schema refuse. Historical decoders and compatibility aliases were removed. +Observation envelopes remain a separate fixed-operation reading precondition; +nested envelopes and effect observation wrappers refuse. + +`EchoOperationInvocation::prepare` checks the selected publication identity, +operation, program kind, input contract and each package budget axis. Effect +input must fit the selected program's declared ceiling, including CBOR framing. +The effect application-basis schema is the effect-basis profile. Its value is +BLAKE3 over `echo:operation-edict-effect-basis:v1\0`, the input length as a +little-endian u64, and the exact input bytes. All kinds share the current +invocation identity domain. Static preparation establishes neither runtime +freshness nor a grant. + +`TrustedRuntimeHost::admit_echo_operation_invocation` resolves the installed +package and the actual current worldline parent. The common owner policy checks +the independent authority profile and grant identity, delegated package and +policy budgets, input binding and exact parent evidence. The admitted token +retains the current invocation, including the absence of an effect anchor. +Admission does not execute the compiled program or mutate the graph. + +Authored-effect Action execution remains unfinished. Scheduler admission and +Action recovery inspection explicitly refuse that unsupported execution path; +the direct preparation seam yields an obstruction rather than a fabricated +successful patch. Fixed-operation Action evaluation and recovery continue to +use their real anchors. Private mutation execution and whole-parent derivation +do not enable Action execution. The physical arena, scoped-history admission, +Tick integration and effect decision recovery remain required. + +### Runtime-owner package policy admission + +`TrustedRuntimeHost::admit_echo_operation_package` is the sole package-admission +crossing. It consumes the current `EchoOperationAdmissionPolicy` and exact +canonical publication bytes for any supported program kind, returning one +opaque `AdmittedExecutableOperationPackage`. The former version-specific owner +methods, effect-admission module and effect token definition were removed +without aliases or wrappers. Retained recovery reruns the same validator. + +The independently selected policy pins package identity, operation coordinate, +authority profile and maximum budget. The common validator checks the exact +pin, canonical artifact and profile contracts, operation/authority agreement, +and every budget axis. A matching pin does not replace structural validation +or independent authority policy. Both fixed and authored-effect programs cross +these same checks and produce the same current evidence type. + +The token retains the prepared package, exact publication bytes, checked +package/program identities, policy and policy identity. Its admission identity +uses `echo:operation-package-admission:v1\0` followed by the raw package and +policy identities. There is no alternative effect-admission identity recipe. +Policy identity uses the existing current +`echo:operation-admission-policy:v1\0` recipe. Different independent policies +therefore retain distinct admission evidence for otherwise identical bytes. +Installation consumes the token's checked program identity; recovery rebuilds +and checks this evidence rather than trusting retained hashes alone. + +This method is exposed only on the runtime-owner host, under the existing +`native_rule_bootstrap` and `trusted_runtime` boundary. The restricted +application handle cannot admit a package. The token has no public constructor +and is neither an installation nor an invocation grant. Installation uses one evidence type and catalog for every program kind. +Invocation admission is described above; authored-effect Action execution remains +unfinished. Package admission does not infer compiler/verifier approval from +publication identities. + +### Runtime-owned installation and retained catalog recovery + +`TrustedRuntimeHost::install_admitted_echo_operation_package` consumes the +opaque common package-admission token and installs one `InstalledEchoOperation` +in the engine's sole package catalog and operation-coordinate index. Fixed and +authored-effect program kinds use the same owner API, evidence type, preflight, +retained codec, recovery collection and restoration path. The former effect +installation module/type, second catalog/index, version-dispatch decoder and +version-specific methods were removed without aliases or wrappers. Application +handles cannot call the owner method. + +The installed object retains the exact admitted package/program/policy evidence. +A lawpack identity is `Some` for fixed programs and `None` for authored effects; +absence is not a sentinel hash. Installation grants no invocation authority and +does not validate the compiled effect instruction language. Its read-only +`retained_bytes` and `from_retained_bytes` methods reconstruct evidence without +inserting registry entries. + +Exact repeats are idempotent. A changed policy for the same package or a changed +package at an occupied operation coordinate refuses before any WAL append, +regardless of program kind. Batch recovery preflights the same single catalog +before publication. The installation identity uses +`echo:operation-installation:v1\0`, then raw package identity, package-admission +identity, length-prefixed operation coordinate, program identity and +length-prefixed exact package bytes, in that order. + +The sole retained installation record has exactly seven fields with schema +`echo.operation-installation/v1`: independent policy, policy identity, exact +package bytes and identity, package-admission identity and installation identity. +Recovery reruns the common package-policy validator, recomputes the identities +and reproduces exact record bytes. Unsupported installation schemas, including +the former V2 schema, refuse. Recomputed hashes cannot make a false independent +policy acceptable. + +With a runtime WAL configured, installation commits before the registry entry +becomes visible. Failed append/commit paths publish no entry. Uncertain +filesystem errors refresh the writer and compare the exact recovered +installation, including policy and program evidence, before reporting success. +A commit-marker sync failure leaves the live adapter closed and publishes no +entry; a fresh store reopen may establish and recover the committed history. +Without a WAL, installation is volatile; an empty WAL cannot attest an earlier +volatile installation. These checks apply to every program kind. + +The current receipt type has nullable lawpack evidence. Its canonical field +is either a 32-byte identity or null; its identity hash includes a presence byte +and, when present, the raw identity. Absence therefore differs from a present +zero hash. Receipt corroboration checks the value against the exact installed +program evidence; a structurally valid absent claim cannot replace a fixed +program's real lawpack. This updates the one current receipt recipe rather than +retaining an older decoder or hash path. + +The existing installation transaction and catalog-frontier recipes bind the +exact retained bytes. Recovery certificates use one current operation-index +recipe, under `echo:trusted-runtime-wal:executable-operation-index:v3\0`, for +every catalog and outcome shape. It hashes the preceding index root, then three +count-prefixed sections: installations, execution receipts, and Action outcomes. +Each record is length-prefixed. Installations are ordered by raw package identity +and then retained bytes; receipts and Action outcomes retain their causal order. +Action-outcome records bind the submission and ingress identities as well as the +typed outcome. Empty sections contribute their zero counts, including when all +three sections are empty. There is no older index recipe, empty-index bypass, +or recipe switch based on the presence of effect installations or Action outcomes. +The historical index recipes were removed under the one-current-version policy. + +A fresh filesystem-backed host restores all supported program kinds through +the sole installation collection and rechecks coordinate conflicts. The common +invocation model and owner grant admission are described above. Installation +recovery does not establish recovery of an authored-effect decision. Physical +allocation guarantees, scoped-history admission and authored-effect +Action/Tick/receipt recovery remain unfinished. diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index 379f3d7fc..22197b5c8 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -179,8 +179,8 @@ 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. The promoted lowerer is -493,449 bytes with SHA-256 -`b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b`; the +496,879 bytes with SHA-256 +`498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7`; the promoted verifier is 510,251 bytes with SHA-256 `e1426a80879d618cde4a8fe0f39a6d1986063efd8c6853a71b4f5ae1235ccced`. diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index fa2af64ba..a66b023f3 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -23,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 493,449 bytes with SHA-256 -`b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b`. +The checked component is 496,879 bytes with SHA-256 +`498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7`. Its sole contract attestation is the top-level custom section `edict:target-provider-contract` containing `edict:target-provider/lowerer@1.0.0`. Its only imports are the frozen WIT's diff --git a/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm b/schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm index f40a43058..2fd68b516 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/generated/v1/evidence/provenance.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json index 277972db6..6cb1b2c25 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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 4b88f8633..d1d16cc3b 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","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:ae741baed96ec6121aa06e366319d88cc8be6966e01a85f57a652173ce59b8a7","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","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:87294198ac6f94d68346fa49cd52b1c26a10cd51d91a079abacab81721eb2cb6","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file 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 8b4544430..dd8a38542 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 @@ -1174,10 +1174,10 @@ edict-source-bytes = bstr echo-operation-package = echo-anchored-operation-package / echo-compiler-produced-pure-operation-package / - echo-compiler-produced-read-operation-package + echo-compiler-produced-read-operation-package / + echo-authored-effect-operation-package -echo-anchored-operation-package = { - "application_result_projection": echo-operation-application-result-projection, +echo-current-operation-package-fields = ( "application_basis_schema_identity": bstr .size 32, "authority_profile_identity": bstr .size 32, "budget_ceiling": echo-operation-budget, @@ -1186,7 +1186,6 @@ echo-anchored-operation-package = { "interpreter_profile_identity": bstr .size 32, "input_schema_identity": bstr .size 32, "intrinsic_profile_identity": bstr .size 32, - "obstruction_coordinate": echo-nonempty-tstr, "obstruction_schema_identity": bstr .size 32, "obstruction_interpretation_identity": bstr .size 32, "operation_coordinate": echo-nonempty-tstr, @@ -1194,8 +1193,19 @@ echo-anchored-operation-package = { "result_schema_identity": bstr .size 32, "result_interpretation_identity": bstr .size 32, schema: "echo.operation-package/v1", - "semantic_closure": echo-operation-semantic-closure, "target_profile_identity": bstr .size 32, +) + +echo-anchored-operation-package = { + echo-current-operation-package-fields, + "application_result_projection": echo-operation-application-result-projection, + "obstruction_coordinate": echo-nonempty-tstr, + "semantic_closure": echo-operation-semantic-closure, +} + +echo-authored-effect-operation-package = { + echo-current-operation-package-fields, + "semantic_closure": echo-authored-effect-semantic-closure, } echo-compiler-produced-pure-operation-package = { @@ -1264,22 +1274,31 @@ echo-operation-budget = { "write_bytes": uint, } -echo-operation-semantic-closure = { +echo-operation-semantic-closure-fields = ( "application_schema_coordinate": echo-nonempty-tstr, "application_schema_identity": bstr .size 32, "canonical_meaning_identity": bstr .size 32, "core_identity": bstr .size 32, "edict_source_identity": bstr .size 32, + "target_ir_identity": bstr .size 32, +) + +echo-operation-semantic-closure = { + echo-operation-semantic-closure-fields, "lawpack_coordinate": echo-nonempty-tstr, "lawpack_identity": bstr .size 32, - "target_ir_identity": bstr .size 32, +} + +echo-authored-effect-semantic-closure = { + echo-operation-semantic-closure-fields, } echo-operation-lowering-configuration = echo-attachment-create-if-absent-lowering-configuration / echo-attachment-compare-and-set-lowering-configuration / echo-compiler-produced-bounded-pure-lowering-configuration / - echo-compiler-produced-bounded-read-lowering-configuration + echo-compiler-produced-bounded-read-lowering-configuration / + echo-authored-effect-lowering-configuration echo-compiler-produced-bounded-pure-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1", @@ -1293,6 +1312,27 @@ echo-compiler-produced-bounded-read-lowering-configuration = { maxReadBytes: 1..67108864, } +; This declares the current effect kind; schema admission grants no runtime authority. +; Payload semantics still require producer and runtime validation. +echo-authored-effect-lowering-configuration = { + apiVersion: "echo.operation-lowering-configuration/v1", + programKind: "edict-effect-program/v1", + identityLaw: 1, + intrinsicCatalog: bstr .size (1..16777216), + graphSchema: bstr .size (1..16777216), + footprintLaw: bstr .size (1..16777216), + maxInputBytes: 0..65536, + maxResultBytes: 0..65536, + maxAllocatedBytes: 0..16777216, + budgetCeiling: { + steps: 1..18446744073709551615, + readBytes: uint, + writeBytes: uint, + }, + ; The requested authority class; the runtime owner's policy grants authority. + authorityProfile: echo-nonempty-tstr, +} + echo-attachment-create-if-absent-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1" / "echo.operation-lowering-configuration/v2", authorityProfile: echo-nonempty-tstr, 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 f40a43058..2fd68b516 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/generated/evidence/provenance.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 277972db6..6cb1b2c25 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ 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 4b88f8633..d1d16cc3b 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:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b","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:ae741baed96ec6121aa06e366319d88cc8be6966e01a85f57a652173ce59b8a7","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:e2a6a539a7156296c18ff1bc02f80dcabf62b42bb4314d0737527869e43986d6"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:ce1af559551f8b8242c4b290b620c5a28323b9e43f8bc6bb161a191001b6f4b9"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:8c570362671a0b1cd1a992d8210e4a90ffd153ad06dcdf1e0cb36033cc971e2c"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:4ef3aaad0d2131ba7129a3e2ae178d10c44f7e9b062af2c1fe211de781462f34"},{"coordinate":"echo.dpo@1","digest":"sha256:1b105d1b1f6cdf5fecdef98b7adeb238525047d43581fe9fd8c44fd213e1788e"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb","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:87294198ac6f94d68346fa49cd52b1c26a10cd51d91a079abacab81721eb2cb6","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:322d9eb5eb8273108eaa602f2eb58ba591efcf829fecb10bb705717efae18852"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:8d77c84ecbd7d7e38496479b2f8346c15504a56f076170d79c0eca2a57fdb3a5"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:bc222c4a669b888a60df6f398412bae232967c11841b9a27c7ba6c18eb41e362"}]} \ No newline at end of file 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 8b4544430..dd8a38542 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 @@ -1174,10 +1174,10 @@ edict-source-bytes = bstr echo-operation-package = echo-anchored-operation-package / echo-compiler-produced-pure-operation-package / - echo-compiler-produced-read-operation-package + echo-compiler-produced-read-operation-package / + echo-authored-effect-operation-package -echo-anchored-operation-package = { - "application_result_projection": echo-operation-application-result-projection, +echo-current-operation-package-fields = ( "application_basis_schema_identity": bstr .size 32, "authority_profile_identity": bstr .size 32, "budget_ceiling": echo-operation-budget, @@ -1186,7 +1186,6 @@ echo-anchored-operation-package = { "interpreter_profile_identity": bstr .size 32, "input_schema_identity": bstr .size 32, "intrinsic_profile_identity": bstr .size 32, - "obstruction_coordinate": echo-nonempty-tstr, "obstruction_schema_identity": bstr .size 32, "obstruction_interpretation_identity": bstr .size 32, "operation_coordinate": echo-nonempty-tstr, @@ -1194,8 +1193,19 @@ echo-anchored-operation-package = { "result_schema_identity": bstr .size 32, "result_interpretation_identity": bstr .size 32, schema: "echo.operation-package/v1", - "semantic_closure": echo-operation-semantic-closure, "target_profile_identity": bstr .size 32, +) + +echo-anchored-operation-package = { + echo-current-operation-package-fields, + "application_result_projection": echo-operation-application-result-projection, + "obstruction_coordinate": echo-nonempty-tstr, + "semantic_closure": echo-operation-semantic-closure, +} + +echo-authored-effect-operation-package = { + echo-current-operation-package-fields, + "semantic_closure": echo-authored-effect-semantic-closure, } echo-compiler-produced-pure-operation-package = { @@ -1264,22 +1274,31 @@ echo-operation-budget = { "write_bytes": uint, } -echo-operation-semantic-closure = { +echo-operation-semantic-closure-fields = ( "application_schema_coordinate": echo-nonempty-tstr, "application_schema_identity": bstr .size 32, "canonical_meaning_identity": bstr .size 32, "core_identity": bstr .size 32, "edict_source_identity": bstr .size 32, + "target_ir_identity": bstr .size 32, +) + +echo-operation-semantic-closure = { + echo-operation-semantic-closure-fields, "lawpack_coordinate": echo-nonempty-tstr, "lawpack_identity": bstr .size 32, - "target_ir_identity": bstr .size 32, +} + +echo-authored-effect-semantic-closure = { + echo-operation-semantic-closure-fields, } echo-operation-lowering-configuration = echo-attachment-create-if-absent-lowering-configuration / echo-attachment-compare-and-set-lowering-configuration / echo-compiler-produced-bounded-pure-lowering-configuration / - echo-compiler-produced-bounded-read-lowering-configuration + echo-compiler-produced-bounded-read-lowering-configuration / + echo-authored-effect-lowering-configuration echo-compiler-produced-bounded-pure-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1", @@ -1293,6 +1312,27 @@ echo-compiler-produced-bounded-read-lowering-configuration = { maxReadBytes: 1..67108864, } +; This declares the current effect kind; schema admission grants no runtime authority. +; Payload semantics still require producer and runtime validation. +echo-authored-effect-lowering-configuration = { + apiVersion: "echo.operation-lowering-configuration/v1", + programKind: "edict-effect-program/v1", + identityLaw: 1, + intrinsicCatalog: bstr .size (1..16777216), + graphSchema: bstr .size (1..16777216), + footprintLaw: bstr .size (1..16777216), + maxInputBytes: 0..65536, + maxResultBytes: 0..65536, + maxAllocatedBytes: 0..16777216, + budgetCeiling: { + steps: 1..18446744073709551615, + readBytes: uint, + writeBytes: uint, + }, + ; The requested authority class; the runtime owner's policy grants authority. + authorityProfile: echo-nonempty-tstr, +} + echo-attachment-create-if-absent-lowering-configuration = { apiVersion: "echo.operation-lowering-configuration/v1" / "echo.operation-lowering-configuration/v2", authorityProfile: echo-nonempty-tstr, diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index 8aae8e290..2ccb29373 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:e82a973ba7b0031a75958e3fa01480e2aeeaad1989a914214232896f3d7a246f" + "digest": "sha256:17cca54e166fe42c6dfbcb76f4a0d9c8ff3bc873e8dd2ac1d15bcf48b3ac183e" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b" + "digest": "sha256:498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b" + "digest": "sha256:498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:2bc9bf98d4f3ff91b31359afe60b30bbf6670c94ba2d36d04baecb686eec4d38" + "digest": "sha256:b7ee6e16eb67285dd0d3ef8a7c6eef0c99854526a2c7d56a249286fb66aedfd9" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:29612fb0642a1fea047e9d348e21abb776e3467ccad61fa9e07194e945f39191" + "digest": "sha256:2ad4fd2d8bb633dac05c3bbac2aad77446462ef5bf30f904c5ec5c2188b82527" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111" + "digest": "sha256:8e4f82bb48bab1bdc11f22b3c37de49edaf87028aa365b15ef061ac07f990a10" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:3ab110f12add406a642993a40af43d1dd99b42bd61148d2d4d615c2c1c8c0bbb" } } }, diff --git a/tests/docs/test_generated_rule_truth.sh b/tests/docs/test_generated_rule_truth.sh index 56edf0ce2..113c75a56 100755 --- a/tests/docs/test_generated_rule_truth.sh +++ b/tests/docs/test_generated_rule_truth.sh @@ -76,7 +76,7 @@ require_literal "$readme" 'These corridors are not one pipeline with optional so require_literal "$readme" 'The current Wesley compatibility path is:' require_literal "$readme" 'The current Edict provider-v1 compatibility path is:' require_literal "$readme" 'The current executable-operation runtime slice is:' -require_literal "$readme" 'installed data-only EchoOperationProgramV1' +require_literal "$readme" 'installed data-only EchoOperationProgram' require_literal "$readme" 'The first two paths are callback-shaped compatibility infrastructure.' require_literal "$readme" 'The host supplies no application matcher, executor, or' require_literal "$readme" 'Deterministic runtime judgments' diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index d85776209..14bc529ad 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 = - "b52ec57c2e06a6da8ec38202885241b47483138c970376e6e11baf36bfe8014b"; + "498d2c26dfe42d32166bdcd156f34cede1cfeebb2e5481a1323d5609d02166a7"; pub(crate) const CHECKED_COMPONENT_REPOSITORY_PATH: &str = "schemas/edict-provider/components/v1/lowerer.echo-dpo.component.wasm"; diff --git a/xtask/src/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index 8743c670a..89421033a 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -23,9 +23,9 @@ use warp_core::{ echo_operation_atom_value_digest_v1, echo_operation_create_if_absent_target_profile_identity_v1, echo_operation_package_id_v1, make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, - EchoOperationActionOutcomeV1, EchoOperationAdmissionPolicyV1, + EchoOperationActionOutcomeV1, EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationResultV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, + EchoOperationInvocation, EchoOperationInvocationAdmissionPolicy, EchoOperationObstructionKindV1, EngineBuilder, GraphStore, InboxPolicy, IngressTarget, NodeId, NodeKey, NodeRecord, PlaybackMode, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeHostError, TrustedRuntimeWalConfig, TrustedRuntimeWalError, TypeId, WorldlineId, WorldlineRuntime, @@ -265,7 +265,7 @@ pub fn run(config: RunEdictOperationConfig) -> Result { b"echo:edict-operation-runner-authority-grant:v1\0", &package_id.as_hash(), ); - let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + let invocation_policy = EchoOperationInvocationAdmissionPolicy::new( package.authority_profile_identity, authority_grant_identity, package.budget, @@ -362,7 +362,7 @@ pub fn run(config: RunEdictOperationConfig) -> Result { && pending .host .engine() - .installed_echo_operation_package_v1(package_id) + .installed_echo_operation_package(package_id) .is_some() && current_state(&pending)?.state_root() == initial_state_root; if !pending_action_recovered { @@ -1088,9 +1088,9 @@ fn install_package( package_bytes: Vec, ) -> Result<()> { let admitted = host - .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + .admit_echo_operation_package( + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), &package.operation_coordinate, package.authority_profile_identity, package.budget, @@ -1098,7 +1098,7 @@ fn install_package( package_bytes, ) .context("Echo independently refused the compiler-produced package")?; - host.install_admitted_echo_operation_package_v1(admitted) + host.install_admitted_echo_operation_package(admitted) .context("Echo failed to durably install the admitted package")?; Ok(()) } @@ -1182,7 +1182,7 @@ fn invocation( let evaluation_basis = fixture .host .echo_operation_evaluation_basis_v1(fixture.head, application_basis)?; - EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( package_id, &package.operation_coordinate, evaluation_basis, diff --git a/xtask/src/run_edict_operation/session.rs b/xtask/src/run_edict_operation/session.rs index 662279125..67868448e 100644 --- a/xtask/src/run_edict_operation/session.rs +++ b/xtask/src/run_edict_operation/session.rs @@ -7,10 +7,10 @@ use super::{ echo_operation_anchored_node_creation_application_basis_v1, echo_operation_package_id_v1, fs, install_package, parse_input, parse_package, read_bounded, validate_closure, validate_package_configuration, validate_verification_report, Context, Digest, - EchoOperationActionOutcomeV1, EchoOperationAnchoredNodeOccupancyV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, HostFixture, - IngressTarget, NodeId, NodeKey, PackageMetadata, Result, RunEdictOperationConfig, Sha256, - TargetConfiguration, TrustedRuntimeWalConfig, MAX_ARTIFACT_BYTES, MAX_INPUT_BYTES, + EchoOperationActionOutcomeV1, EchoOperationAnchoredNodeOccupancyV1, EchoOperationInvocation, + EchoOperationInvocationAdmissionPolicy, HostFixture, IngressTarget, NodeId, NodeKey, + PackageMetadata, Result, RunEdictOperationConfig, Sha256, TargetConfiguration, + TrustedRuntimeWalConfig, MAX_ARTIFACT_BYTES, MAX_INPUT_BYTES, }; use serde_json::{json, Value}; use std::io::{BufRead, Read, Write}; @@ -72,7 +72,7 @@ pub fn serve(config: RunEdictOperationConfig) -> Result<()> { if fixture .host .engine() - .installed_echo_operation_package_v1(package_id) + .installed_echo_operation_package(package_id) .is_none() { install_package(&mut fixture.host, &package, package_id, package_bytes)?; @@ -84,7 +84,7 @@ pub fn serve(config: RunEdictOperationConfig) -> Result<()> { fixture .host .install_echo_operation_action_admission_policy_v1( - EchoOperationInvocationAdmissionPolicyV1::new( + EchoOperationInvocationAdmissionPolicy::new( package.authority_profile_identity, grant, package.budget, @@ -325,7 +325,7 @@ impl Session { .fixture .host .echo_operation_evaluation_basis_v1(self.fixture.head, application_basis)?; - let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent_with_application_input( + let invocation = EchoOperationInvocation::anchored_node_attachment_create_if_absent_with_application_input( self.package_id, &self.package.operation_coordinate, basis, self.grant, self.package.budget, node, input.replacement, input.canonical_bytes, );