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 54636984e..d2a2f656a 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 @@ -199,7 +199,7 @@ jobs: run: cargo clippy -p warp-core --features native_rule_bootstrap,trusted_runtime,host_test,application_root_shadow --lib --tests -- -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) @@ -376,8 +376,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 @@ -395,7 +397,7 @@ jobs: cargo test -p warp-core --features native_rule_bootstrap,trusted_runtime,application_root_shadow,delta_validate --test application_root_shadow_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 4792d9baf..ebd597bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,230 @@ ## Unreleased +### Changed + +- 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. + ### 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. + - Bounded pure evaluation executes compiler-produced U32/U64 addition with checked width bounds and ordinary work/value-storage charging. Independent native provider judgments require same-width operands and a dominating diff --git a/README.md b/README.md index d4355ad49..945b2edfb 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 @@ -250,11 +256,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 @@ -585,10 +591,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..87ddfb941 100644 --- a/crates/echo-edict-provider-lowerer/README.md +++ b/crates/echo-edict-provider-lowerer/README.md @@ -202,3 +202,23 @@ 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. +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..de913b939 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/src/effect_publication.rs @@ -0,0 +1,389 @@ +// 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, + /// Explicit obstruction coordinate declared by the exports artifact. + pub obstruction_coordinate: &'a str, + /// Proposed policy identity; this value does not grant authority. + pub authority_profile_identity: [u8; 32], + /// 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, unbounded, or undeclared. + 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, unbounded or undeclared 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, InvalidCoordinate as N, + }; + 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))?; + coordinate(&text(input.obstruction_coordinate))?; + let V::Array(obstructions) = field(&exports, "obstructions", A)? else { + return Err(A); + }; + let mut selected = 0; + for obstruction in obstructions { + if coordinate(field(obstruction, "coordinate", A)?)? == input.obstruction_coordinate { + selected += 1; + } + } + if selected != 1 { + return Err(N); + } + 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", + ], + 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); + } + 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)), + ("obstruction_coordinate", text(input.obstruction_coordinate)), + ( + "authority_profile_identity", + V::Bytes(input.authority_profile_identity.to_vec()), + ), + ( + "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..2143bf0f0 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"; + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"; /// 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", + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d", "/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..da4f7d1c2 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/current_effect_publication.rs @@ -0,0 +1,245 @@ +// 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", + obstruction_coordinate: "app.Refusal", + authority_profile_identity: [8; 32], + 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"))); + assert_eq!(get(&value, "authority_profile_identity"), &id(8)); +} + +#[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 undeclared_obstruction_and_source_domain_refuse() { + 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", + obstruction_coordinate: "app.Refusal", + authority_profile_identity: [8; 32], + configuration: &config, + }; + assert_eq!( + encode_effect_publication(EffectPublicationInput { + obstruction_coordinate: "app.Undeclared", + ..input + }), + Err(E::InvalidCoordinate) + ); + assert_eq!( + encode_effect_publication(EffectPublicationInput { + source_domain: "edict.source/v2", + ..input + }), + Err(E::InvalidArtifact) + ); +} 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..6f4462dd6 --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/configuration.hex @@ -0,0 +1 @@ +aa6a61706956657273696f6e78286563686f2e6f7065726174696f6e2d6c6f776572696e672d636f6e66696775726174696f6e2f76316b6772617068536368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026b6964656e746974794c6177016b70726f6772616d4b696e647765646963742d6566666563742d70726f6772616d2f76316c666f6f747072696e744c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806d6275646765744365696c696e67a36573746570730a697265616442797465731910006a777269746542797465731910006d6d6178496e70757442797465731a000100006e6d6178526573756c7442797465731a0001000070696e7472696e736963436174616c6f675901a2a26a61706956657273696f6e782265646963742e7461726765742d70726f66696c652e696e7472696e736963732f76316a696e7472696e73696373a1726563686f2e64706f40312e7265706c616365ab6a6566666563744b696e64677265706c6163656a72657475726e547970656d612e6240312e526563656970746a7772697465436c617373677265706c6163656c636f737454656d706c617465737461726765742e7265706c6163652e636f73746c6775617264537570706f7274f56d617267756d656e7454797065738168612e6240312e49646e6566666563744661696c75726573a16872656a6563746564a26b7061796c6f616454797065777461726765742e7265706c6163652e72656a65637465646e617574686f72697479436c6173736e646f6d61696e4d61707061626c656e696e7472696e736963436c617373666566666563746e74797065506172616d65746572738071666f6f747072696e7454656d706c61746578187461726765742e7265706c6163652e666f6f747072696e74781b63616e5061727469636970617465496e41746f6d69634775617264f5716d6178416c6c6f636174656442797465731a01000000 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..e1d192b4a 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"; + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"; /// 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", + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d", "/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..78a8a2a9a 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"; + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"; 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 424970f63..4568cf32a 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..1a6ab1cef 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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..8db719632 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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:aea35262688ec02e908a115d44f392b0c7b9451dc26b62ca2a3599c549299d8d","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..336718aa9 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, @@ -1194,8 +1194,18 @@ 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, + "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,25 @@ 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, + }, +} + 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 72f2301bf..b9c7118f6 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:36ce20c612c8c46bbe450c93e6fb2235f38e4ec2b14f04f128eee91805cdb26a" + "digest": "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c" + "digest": "sha256:d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c" + "digest": "sha256:d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:2bc9bf98d4f3ff91b31359afe60b30bbf6670c94ba2d36d04baecb686eec4d38" + "digest": "sha256:bdae385fd1cfa0463c45888387e7664dd627f9c5ece01df5f8f9a90a4c8da2d8" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:29612fb0642a1fea047e9d348e21abb776e3467ccad61fa9e07194e945f39191" + "digest": "sha256:eb306bb4f8f3e0cfe07620eb46234c4a617744a2063d2f9a7854944cb934c4b2" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111" + "digest": "sha256:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index 3a5dd794d..0909d8e45 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, @@ -262,8 +262,18 @@ 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, + "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,25 @@ 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, + }, +} + 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..08b03e967 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/README.md @@ -0,0 +1,14 @@ + + + +# Current effect publication schema control + +`package.hex` is a synthetic, zero-effect serialization control produced by +`effect_publication::encode_effect_publication` at +`2e2bbe7055d670474738b70603bc8ef51ece80c2`. Its inputs are the crate-local +`current-effect-wire` controls in `echo-edict-provider-lowerer`. + +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..03fee0c99 --- /dev/null +++ b/crates/echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex @@ -0,0 +1 @@ +b266736368656d6178196563686f2e6f7065726174696f6e2d7061636b6167652f76316770726f6772616d5907c4ad646b696e647765646963742d6566666563742d70726f6772616d2f763166736368656d6178196563686f2e6f7065726174696f6e2d70726f6772616d2f76316b6f75747075745f747970656a6170702e526573756c746c67726170685f736368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026c6964656e746974795f6c6177016d666f6f747072696e745f6c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806f6d61785f696e7075745f62797465731a0001000070636f6d70696c65645f70726f6772616d5904c9a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0706d61785f726573756c745f62797465731a00010000727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db0736d61785f616c6c6f63617465645f62797465731a01000000781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c6e6275646765745f6365696c696e67a36573746570730a6a726561645f62797465731910006b77726974655f62797465731910007073656d616e7469635f636c6f73757265a66d636f72655f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db07565646963745f736f757263655f6964656e746974795820fdf14fbb05ba25e820cd4a23b594e6648429940f7aed5218077eef841288114c781a63616e6f6e6963616c5f6d65616e696e675f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d781b6170706c69636174696f6e5f736368656d615f6964656e74697479582017ff6f37fcc8b1f81e58cc3ef5c0d94a4c3f96fb955bfb9cae7ff0bac29cb249781d6170706c69636174696f6e5f736368656d615f636f6f7264696e6174656b6170702e4578706f727473746f7065726174696f6e5f636f6f7264696e6174656c6170702e757064617465403175696e7075745f736368656d615f6964656e746974795820d277705b5ab03fcb58590bad62fb8058e075c3b4c65efc21282bb531aa22b3ad766f62737472756374696f6e5f636f6f7264696e6174656b6170702e5265667573616c76726573756c745f736368656d615f6964656e746974795820de5c0c4f0c640354c40bb4f9d4886cd820353a548caea4953269eba15fe902bc777461726765745f70726f66696c655f6964656e746974795820eaca21c05664241efd1e1c5b5636c7132fe8854f6bcaddb2fd4f014e1bd4a479781a617574686f726974795f70726f66696c655f6964656e7469747958200808080808080808080808080808080808080808080808080808080808080808781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781b666f6f747072696e745f636f6e74726163745f6964656e746974795820362b20fb18488ce180f6c1096b5189994354604fd1b1b126a3ea4ef839e5232b781b6f62737472756374696f6e5f736368656d615f6964656e746974795820b1f96fdf3ec65e84d500702951393a931b4fdd843ad02723ea3725f90a671418781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c781e726573756c745f696e746572707265746174696f6e5f6964656e74697479582042734d5e73435acc0fdd5a66b97e5bfd94278e64fce2b4890ec609f054dc969e78206576616c756174696f6e5f62617369735f736368656d615f6964656e74697479582020b6239db569aa86ae1c27596b374df90caa5ea41e8e005fd4fe00bfe5c3b38178216170706c69636174696f6e5f62617369735f736368656d615f6964656e7469747958208b294c69120a2e749c56266691b0974e7e0323c198fc42a467fd3790790f791078236f62737472756374696f6e5f696e746572707265746174696f6e5f6964656e746974795820924462071f3992b34c629f4788cc666bd14a87a0b9d0c398feba929ff4dd25f9 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..dbe400cb0 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_effect_schema.rs @@ -0,0 +1,288 @@ +// 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)), + ]), + ), + ]) +} +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()); +} + +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")), + ("obstruction_coordinate", text("")), + ("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))); + 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 c49b25d4e..1dc43cd23 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 = - "36ce20c612c8c46bbe450c93e6fb2235f38e4ec2b14f04f128eee91805cdb26a"; + "25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c"; 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 a91abf8c8..2b0aa6337 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:36ce20c612c8c46bbe450c93e6fb2235f38e4ec2b14f04f128eee91805cdb26a"; + "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "8bb6c675598fc457513a953c2aee65316f80315583df418a5ef96788321cd649"; + "f5120976f8afc38cb1e01e631134d33fb3ad35df4c48c518cbeebb48409dfbe4"; 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 ef55c4be6..6c4e3f958 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -176,6 +176,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"] @@ -232,3 +244,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 6958caff0..df15b1422 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}; @@ -5662,7 +5661,7 @@ impl SchedulerCoordinator { runtime: &mut WorldlineRuntime, provenance: &mut ProvenanceService, engine: &mut Engine, - operation_actions: &BTreeMap, + operation_actions: &BTreeMap, evaluation_authority: &EchoOperationEvaluationAuthorityV1, ) -> Result< ( @@ -5687,7 +5686,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")))] @@ -5838,8 +5837,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, @@ -5866,7 +5864,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 3782da48b..4b0043fba 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,10 @@ 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, + semantic_closure: EchoOperationSemanticClosure, target_profile_identity: Hash, interpreter_profile_identity: Hash, intrinsic_profile_identity: Hash, @@ -1802,21 +1871,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()); @@ -1830,8 +1918,8 @@ impl ExecutableOperationPackageV1 { obstruction_coordinate: 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 +1928,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, @@ -1969,7 +2057,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 +2081,7 @@ impl ExecutableOperationPackageV1 { /// Returns the executable program. #[must_use] - pub const fn program(&self) -> &EchoOperationProgramV1 { + pub const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2159,7 +2247,7 @@ impl ExecutableOperationPackageV1 { )); } 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", )?)?, @@ -2222,6 +2310,34 @@ impl ExecutableOperationPackageV1 { "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 +2377,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 +2395,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 +2406,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 +2478,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 +2502,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 +2551,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 +2569,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 +2591,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 +2607,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 +2628,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 +2694,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 +2760,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 +2782,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 +2825,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 +2869,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 +2885,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 +2895,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 +2925,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 +2950,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 +2966,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 +2976,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 +2984,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 +2993,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 +3170,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 +3212,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 +3229,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 +3262,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 +3296,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 +3324,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 +3352,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 +3502,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 +3601,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 +3629,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 +3643,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 +3718,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 +3755,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 +3788,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 +3814,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 +3858,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 +3872,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 +3894,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 +3902,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 +3923,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 +3943,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 +3957,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 +4024,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 +4032,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 +4045,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 +4063,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 +4103,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 +4167,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 +4264,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 +4297,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 +4307,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 +4320,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 +4341,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 +4357,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 +4399,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 +4417,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 +4446,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 +4472,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 +4494,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 +4524,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 +5277,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 +5303,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 +5415,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 +5471,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 +5488,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 +5508,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 +5688,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 +5717,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 +5761,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 +6080,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 +6278,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 +6394,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 +6589,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 +6644,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 +7294,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 +7450,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 +7637,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 +7664,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 +7681,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 +7693,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 +8005,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 +8037,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 +8192,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 +8208,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 +8264,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 +8281,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 +8341,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 +8356,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 +8409,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 +8425,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 +8447,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 +8457,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 +8508,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 +8518,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 +8537,18 @@ 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.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 +8566,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 +8595,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 +8612,16 @@ 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.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 +8634,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 +8669,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 +8687,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 +8700,7 @@ mod tests { .expect_err("projected CAS must bind its expected digest"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -8352,7 +8708,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 +8737,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 +8746,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 +8757,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 +8785,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 +8808,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 +8817,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 +8840,7 @@ mod tests { .expect_err("application input cannot retarget the graph consequence"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -8940,10 +9296,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 +9312,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 +9321,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 +9331,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 +9346,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 +9366,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 +9436,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 +9447,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 +9480,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 +9508,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 +9530,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 +9547,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 +9555,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 +9622,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 +9634,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 +9642,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 +9677,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 +9768,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 +9928,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 +9956,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 +9978,7 @@ mod tests { assert_eq!( operation_result_id( &installed, - &invocation, + invocation.node.expect("anchored fixture"), AnchoredNodeOperationModeV1::CompareAndSet { expected_value_digest: previous, }, @@ -9607,10 +9992,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 +10047,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 +10058,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 +10083,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 +10111,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 +10126,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 +10173,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 +10277,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 +10296,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 +10332,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 +10408,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 +10469,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..d9ef13933 --- /dev/null +++ b/crates/warp-core/src/edict_effects.rs @@ -0,0 +1,377 @@ +// 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 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..7540598ef --- /dev/null +++ b/crates/warp-core/src/edict_effects/execution.rs @@ -0,0 +1,329 @@ +// 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); + }; + let mut failures = BTreeMap::new(); + for (tag, arm) in map(field(step, "obstructionArms")?)? { + failures.insert( + text(tag)?.to_owned(), + text_field(field(arm, "value")?, "callee")?.to_owned(), + ); + } + 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/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..d0c6493f1 --- /dev/null +++ b/crates/warp-core/src/edict_effects/instruction_relation.rs @@ -0,0 +1,335 @@ +// 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 { + let actual = functions::infer( + value, + &self.available, + &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> { + functions::check_predicate( + value, + &self.available, + &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..38029a568 --- /dev/null +++ b/crates/warp-core/src/edict_effects/intrinsic_contract.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Concrete declared signature/fault agreement across the pinned catalog and lawpack. +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: &Value, + effect: &Value, + implementation: &Value, +) -> Result<(), Error> { + 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 expected_input = text_field(effect, "inputType")?; + let output = text_field(intrinsic, "returnType")?; + let expected_output = 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"])?; + for name in ["authorityClass", "payloadType"] { + if text_field(actual, name)? != text_field(expected, name)? { + 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..7ac036109 --- /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, 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..3560fc8f0 --- /dev/null +++ b/crates/warp-core/src/edict_effects/mutations.rs @@ -0,0 +1,222 @@ +// 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, + } + } + 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..1441d90d1 --- /dev/null +++ b/crates/warp-core/src/edict_effects/runtime_abi.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owned recording/emitter ABIs; declarations cannot redefine them. +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; + +pub(super) fn validate( + name: &str, + module: &Value, + effect: &Value, + intrinsic: &Value, +) -> Result<(), Error> { + let mutation = super::mutations::MutationKind::from_intrinsic(name); + if name != "echo.dpo@1.node-atom-read" && mutation.is_none() { + 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", "write")?; + require_text(intrinsic, "writeClass", "write")?; + 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); + } + let failures = field(intrinsic, "effectFailures")?; + if map(failures)?.len() != 4 { + return Err(Error::InvalidArtifact); + } + for (name, authority) in [ + ("echo.atom-missing/v1", "resourceFault"), + ("echo.atom-required/v1", "integrityFault"), + ("echo.atom-type-mismatch/v1", "integrityFault"), + ("echo.atom-byte-bound-exceeded/v1", "integrityFault"), + ] { + let failure = field(failures, name)?; + require_text(failure, "authorityClass", authority)?; + require_text(failure, "payloadType", input_name)?; + } + 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 d8f0fbfc6..882d07bc3 100644 --- a/crates/warp-core/src/edict_pure.rs +++ b/crates/warp-core/src/edict_pure.rs @@ -21,6 +21,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, @@ -66,6 +70,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, } @@ -81,6 +88,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); } @@ -91,7 +108,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 c139b44e1..093e6160d 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) => @@ -561,6 +621,7 @@ mod tests { limits, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( meter.copy_bytes(&[0, 128, 255])?, @@ -574,6 +635,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( work.copy_bytes(&[0, 128, 255]), @@ -587,6 +649,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; assert_eq!( storage.copy_bytes(&[0, 128, 255]), @@ -607,6 +670,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; let length = Expr::ByteLength { min: 0, @@ -635,6 +699,7 @@ mod tests { }, steps: 0, allocated: 0, + atomic_charges: false, }; let subtract = Expr::UnsignedSubtract { max: u64::MAX, @@ -663,6 +728,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 292784970..60f0cc38e 100644 --- a/crates/warp-core/src/edict_pure/functions.rs +++ b/crates/warp-core/src/edict_pure/functions.rs @@ -12,18 +12,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")?; @@ -82,7 +83,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 @@ -101,6 +102,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. @@ -209,7 +219,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)?; @@ -220,7 +231,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 @@ -252,7 +263,7 @@ fn join(left: RuntimeType, right: RuntimeType) -> Result { } type Scope<'a> = BTreeMap<&'a str, &'a Value>; -fn infer( +pub(crate) fn infer( value: &Value, scope: &Scope<'_>, definitions: &BTreeMap>, @@ -477,7 +488,7 @@ fn unsigned_literal(value: &Value) -> Result { } number(field(field(value, "value")?, "value")?) } -fn check_predicate( +pub(crate) fn check_predicate( value: &Value, scope: &Scope<'_>, definitions: &BTreeMap>, @@ -529,6 +540,20 @@ fn check_predicate( 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(()), @@ -700,6 +725,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 a826b5f14..74ff33501 100644 --- a/crates/warp-core/src/edict_pure/model.rs +++ b/crates/warp-core/src/edict_pure/model.rs @@ -78,6 +78,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 cc24469c6..fa4fe9fcb 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 75bdf9b6c..584aee80a 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) @@ -1394,10 +1394,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) } @@ -1405,14 +1405,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(()) } @@ -1421,14 +1421,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; @@ -1439,9 +1439,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..7fd19299e --- /dev/null +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -0,0 +1,540 @@ +// 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(|| { + captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/01-lawpack-adapter.hex" + )) + }); + 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") + }; + ( + tag.clone(), + get(get(effect, "effectFailures"), source).clone(), + ) + }) + .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![get(effect, "inputType").clone()]), + ), + ("returnType", get(effect, "outputType").clone()), + ("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")), + ("obstruction_coordinate", text("app.Refusal")), + ("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" + ); +} 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..22812512f --- /dev/null +++ b/crates/warp-core/tests/current_operation_publication_tests.rs @@ -0,0 +1,107 @@ +// 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); +} + +#[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..989d4da72 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -0,0 +1,1708 @@ +// 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" + )); + 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"), + "echo.atom-required/v1", + 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"), + "echo.atom-required/v1", + ); + 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/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/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..9f54ef83c --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs @@ -0,0 +1,571 @@ +// 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", +]; +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"), + } +} +pub(super) fn fixture(name: &str, profile: &str) -> (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("MutationInput")); + set(effect, "outputType", text("MutationUnit")); + 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("write")); + 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("MutationInput")]), + ); + set(&mut intrinsic, "returnType", text("MutationUnit")); + set(&mut intrinsic, "effectKind", text("write")); + set(&mut intrinsic, "writeClass", text("write")); + 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 = layout(name); + 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"), + "MutationInput", + input_type, + ); + add( + value_field_mut(&mut core, "types"), + "MutationUnit", + map([("kind", text("Record")), ("fields", map([]))]), + ); + let arg = map([ + ("id", text("arg.0")), + ("type", text("MutationInput")), + ("alphaName", text("$arg0")), + ]); + let local = map([ + ("id", text("local.0")), + ("type", text("MutationUnit")), + ("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("MutationInput")); + set(intent, "output", text("MutationUnit")); + 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 3419eeb81..441343c9c 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, @@ -117,12 +117,24 @@ fn fixture_host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { fn fixture_host_with_independent_nodes( node_count: usize, +) -> (TrustedRuntimeHost, WriterHeadKey, Vec) { + fixture_host_with_attachment( + node_count, + Some(AttachmentValue::Atom(AtomPayload::new( + make_type_id("operation-fixture-atom"), + Bytes::from_static(b"before"), + ))), + ) +} + +fn fixture_host_with_attachment( + node_count: usize, + attachment: Option, ) -> (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, @@ -136,16 +148,15 @@ fn fixture_host_with_independent_nodes( 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 = 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, @@ -179,7 +190,7 @@ fn fixture_host_with_independent_nodes( .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) @@ -424,8 +435,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"), @@ -440,7 +451,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) } @@ -448,15 +459,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, @@ -467,15 +478,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, @@ -483,14 +494,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"), @@ -500,9 +511,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), @@ -510,11 +521,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"), @@ -523,9 +534,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), @@ -533,7 +544,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") } @@ -548,8 +559,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), @@ -557,14 +568,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, @@ -580,7 +591,7 @@ fn canonical_invocation( fn action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, current: &[u8], @@ -650,24 +661,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, @@ -683,13 +694,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 { @@ -834,9 +845,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), @@ -845,7 +856,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(); @@ -853,7 +864,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, @@ -865,8 +876,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), @@ -934,7 +945,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) @@ -1005,7 +1016,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"); @@ -1021,7 +1032,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) @@ -1074,9 +1085,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), @@ -1086,14 +1097,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), @@ -1103,17 +1114,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, @@ -1122,9 +1133,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), @@ -1152,9 +1163,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), @@ -1176,9 +1187,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), @@ -1195,9 +1206,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), @@ -1207,13 +1218,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), @@ -1223,17 +1234,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, @@ -1267,16 +1278,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), @@ -1286,12 +1297,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), @@ -1301,7 +1312,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( @@ -1316,11 +1327,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( @@ -1335,11 +1346,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( @@ -1354,14 +1365,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, @@ -1377,11 +1388,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 @@ -1405,11 +1416,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 ); } @@ -1467,7 +1478,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) @@ -1489,11 +1500,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 @@ -1530,21 +1541,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) @@ -1595,7 +1606,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) @@ -1802,9 +1813,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), @@ -1813,12 +1824,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, @@ -1830,8 +1841,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), @@ -1857,7 +1868,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, @@ -1871,8 +1882,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), @@ -1926,7 +1937,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() @@ -1979,7 +1990,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, @@ -1991,13 +2002,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 { @@ -2060,7 +2071,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, @@ -2072,13 +2083,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 { @@ -2128,7 +2139,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, @@ -2141,17 +2152,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 ); } @@ -2180,7 +2191,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, @@ -2192,13 +2203,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 { @@ -2230,7 +2241,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, @@ -2242,13 +2253,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 { @@ -2280,7 +2291,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, @@ -2291,8 +2302,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), @@ -2368,7 +2379,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, @@ -2379,8 +2390,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), @@ -2437,7 +2448,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" @@ -2474,7 +2485,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, @@ -2486,13 +2497,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", @@ -2542,7 +2553,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, @@ -2554,17 +2565,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 ); } @@ -2593,7 +2604,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, @@ -2605,13 +2616,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 { @@ -2650,7 +2661,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, @@ -2663,13 +2674,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 { @@ -3083,7 +3094,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] { @@ -3255,7 +3266,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), @@ -3359,7 +3370,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, @@ -3400,7 +3411,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, @@ -3738,7 +3749,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, @@ -3789,7 +3800,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) @@ -3816,7 +3827,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() @@ -3859,7 +3870,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, @@ -3934,22 +3945,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, @@ -4571,7 +4581,7 @@ fn submit_action( fn creation_action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, payload: &[u8], @@ -4582,7 +4592,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, @@ -4935,186 +4945,2040 @@ fn an_action_with_a_forged_origin_keeps_the_whole_frontier_rule() { ); } -// --------------------------------------------------------------------------- -// 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 +// 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, + } } -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_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!( + 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 + ); } -/// 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) - .expect("the fixture head changes eligibility"); - TrustedRuntimeHost::from_parts(parts) +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) } -fn accept_action( - host: &mut TrustedRuntimeHost, - head_key: WriterHeadKey, - invocation: Vec, -) -> warp_core::Hash { - host.app() - .submit_intent( - warp_core::echo_operation_action_envelope_v1( - IngressTarget::ExactHead { key: head_key }, - invocation, - ) - .expect("the invocation is one canonical Action"), +#[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("the Action is accepted") - .submission_id -} - -#[derive(Clone, Copy)] -enum HeldInterval { - /// No other work. - Idle, - /// The held Action's target is written and restored. - TargetRestored, - /// An unrelated node is written and restored: same tick count, no - /// dependency interference. - UnrelatedRestored, + .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()); } -struct HeldActionDecision { - invocation: Vec, - root_before: warp_core::Hash, - tick_before: u64, - records_before: ApplicationRecords, - committed: bool, - basis_changed: bool, - root_after: warp_core::Hash, +#[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))); } -/// Accepts one Action built at tick 0, holds it undecided through `interval`, -/// then lets the scheduler decide it. -fn decide_a_held_action(interval: HeldInterval) -> HeldActionDecision { - let (mut host, writer, holder, nodes) = fixture_host_with_holding_head(); +#[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 invocation = action_invocation(&host, &installed, holder, nodes[0], b"before", b"held"); - let held = accept_action(&mut host, holder, invocation.clone()); - host = with_head_eligibility(host, holder, HeadEligibility::Dormant); + 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 + )) + )); +} - let written = match interval { - HeldInterval::Idle => None, - HeldInterval::TargetRestored => Some(nodes[0]), - HeldInterval::UnrelatedRestored => Some(nodes[1]), - }; - if let Some(node) = written { - for (current, replacement) in [(&b"before"[..], &b"between"[..]), (b"between", b"before")] { - let write = action_invocation(&host, &installed, writer, node, current, replacement); - let write = accept_action(&mut host, writer, write); - host.tick_once().expect("the intervening write is decided"); - assert!(outcome_is_committed(&host, &write)); - } - } +#[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 + )) + )); assert_eq!( - host.echo_operation_action_outcome_v1(&held), - None, - "the held Action stays undecided through the interval" + host.echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("unchanged parent"), + basis ); - assert_eq!(host.runtime().pending_witnessed_submission_count(), 1); +} - let (root_before, tick_before) = state_root_and_tick(&host, writer); - let records_before = application_records(&host, writer); - host = with_head_eligibility(host, holder, HeadEligibility::Admitted); - host.tick_once().expect("the held Action is decided"); +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()); +} + +// --------------------------------------------------------------------------- +// 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) + .expect("the fixture head changes eligibility"); + TrustedRuntimeHost::from_parts(parts) +} + +fn accept_action( + host: &mut TrustedRuntimeHost, + head_key: WriterHeadKey, + invocation: Vec, +) -> warp_core::Hash { + host.app() + .submit_intent( + warp_core::echo_operation_action_envelope_v1( + IngressTarget::ExactHead { key: head_key }, + invocation, + ) + .expect("the invocation is one canonical Action"), + ) + .expect("the Action is accepted") + .submission_id +} + +#[derive(Clone, Copy)] +enum HeldInterval { + /// No other work. + Idle, + /// The held Action's target is written and restored. + TargetRestored, + /// An unrelated node is written and restored: same tick count, no + /// dependency interference. + UnrelatedRestored, +} + +struct HeldActionDecision { + invocation: Vec, + root_before: warp_core::Hash, + tick_before: u64, + records_before: ApplicationRecords, + committed: bool, + basis_changed: bool, + root_after: warp_core::Hash, +} + +/// Accepts one Action built at tick 0, holds it undecided through `interval`, +/// then lets the scheduler decide it. +fn decide_a_held_action(interval: HeldInterval) -> HeldActionDecision { + let (mut host, writer, holder, nodes) = fixture_host_with_holding_head(); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + let invocation = action_invocation(&host, &installed, holder, nodes[0], b"before", b"held"); + let held = accept_action(&mut host, holder, invocation.clone()); + host = with_head_eligibility(host, holder, HeadEligibility::Dormant); + + let written = match interval { + HeldInterval::Idle => None, + HeldInterval::TargetRestored => Some(nodes[0]), + HeldInterval::UnrelatedRestored => Some(nodes[1]), + }; + if let Some(node) = written { + for (current, replacement) in [(&b"before"[..], &b"between"[..]), (b"between", b"before")] { + let write = action_invocation(&host, &installed, writer, node, current, replacement); + let write = accept_action(&mut host, writer, write); + host.tick_once().expect("the intervening write is decided"); + assert!(outcome_is_committed(&host, &write)); + } + } + assert_eq!( + host.echo_operation_action_outcome_v1(&held), + None, + "the held Action stays undecided through the interval" + ); + assert_eq!(host.runtime().pending_witnessed_submission_count(), 1); + + let (root_before, tick_before) = state_root_and_tick(&host, writer); + let records_before = application_records(&host, writer); + host = with_head_eligibility(host, holder, HeadEligibility::Admitted); + host.tick_once().expect("the held Action is decided"); let (root_after, _) = state_root_and_tick(&host, writer); HeldActionDecision { invocation, @@ -5160,3 +7024,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..1dd047bf2 --- /dev/null +++ b/crates/warp-core/tests/fixtures/current-effect-publication/README.md @@ -0,0 +1,11 @@ + + + +# Current effect publication control + +This is the synthetic zero-effect serialization control retained by the provider +schema tests at Echo commit `09239c7c`. 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..03fee0c99 --- /dev/null +++ b/crates/warp-core/tests/fixtures/current-effect-publication/package.hex @@ -0,0 +1 @@ +b266736368656d6178196563686f2e6f7065726174696f6e2d7061636b6167652f76316770726f6772616d5907c4ad646b696e647765646963742d6566666563742d70726f6772616d2f763166736368656d6178196563686f2e6f7065726174696f6e2d70726f6772616d2f76316b6f75747075745f747970656a6170702e526573756c746c67726170685f736368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026c6964656e746974795f6c6177016d666f6f747072696e745f6c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806f6d61785f696e7075745f62797465731a0001000070636f6d70696c65645f70726f6772616d5904c9a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0706d61785f726573756c745f62797465731a00010000727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db0736d61785f616c6c6f63617465645f62797465731a01000000781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c6e6275646765745f6365696c696e67a36573746570730a6a726561645f62797465731910006b77726974655f62797465731910007073656d616e7469635f636c6f73757265a66d636f72655f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db07565646963745f736f757263655f6964656e746974795820fdf14fbb05ba25e820cd4a23b594e6648429940f7aed5218077eef841288114c781a63616e6f6e6963616c5f6d65616e696e675f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d781b6170706c69636174696f6e5f736368656d615f6964656e74697479582017ff6f37fcc8b1f81e58cc3ef5c0d94a4c3f96fb955bfb9cae7ff0bac29cb249781d6170706c69636174696f6e5f736368656d615f636f6f7264696e6174656b6170702e4578706f727473746f7065726174696f6e5f636f6f7264696e6174656c6170702e757064617465403175696e7075745f736368656d615f6964656e746974795820d277705b5ab03fcb58590bad62fb8058e075c3b4c65efc21282bb531aa22b3ad766f62737472756374696f6e5f636f6f7264696e6174656b6170702e5265667573616c76726573756c745f736368656d615f6964656e746974795820de5c0c4f0c640354c40bb4f9d4886cd820353a548caea4953269eba15fe902bc777461726765745f70726f66696c655f6964656e746974795820eaca21c05664241efd1e1c5b5636c7132fe8854f6bcaddb2fd4f014e1bd4a479781a617574686f726974795f70726f66696c655f6964656e7469747958200808080808080808080808080808080808080808080808080808080808080808781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781b666f6f747072696e745f636f6e74726163745f6964656e746974795820362b20fb18488ce180f6c1096b5189994354604fd1b1b126a3ea4ef839e5232b781b6f62737472756374696f6e5f736368656d615f6964656e746974795820b1f96fdf3ec65e84d500702951393a931b4fdd843ad02723ea3725f90a671418781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c781e726573756c745f696e746572707265746174696f6e5f6964656e74697479582042734d5e73435acc0fdd5a66b97e5bfd94278e64fce2b4890ec609f054dc969e78206576616c756174696f6e5f62617369735f736368656d615f6964656e74697479582020b6239db569aa86ae1c27596b374df90caa5ea41e8e005fd4fe00bfe5c3b38178216170706c69636174696f6e5f62617369735f736368656d615f6964656e7469747958208b294c69120a2e749c56266691b0974e7e0323c198fc42a467fd3790790f791078236f62737472756374696f6e5f696e746572707265746174696f6e5f6964656e746974795820924462071f3992b34c629f4788cc666bd14a87a0b9d0c398feba929ff4dd25f9 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 208a407c9..b3127ea96 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/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index c84d20e4f..32951afe8 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 @@ -196,9 +246,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 @@ -231,7 +281,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 @@ -411,6 +461,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. @@ -425,8 +694,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. @@ -437,3 +706,543 @@ 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. 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`. + +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 single input coordinate and result coordinate, 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, +payload coordinates and authority classes must agree with exported failures. +This is concrete declared agreement, not general structural or polymorphic +signature unification. Cost/footprint template names are present and nonempty, +but their calculus is not validated by this check. + +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. +Their declared intrinsic `effectKind` and `writeClass` must both be `write`, and +their 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 958c9489f..2be7538b4 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -181,7 +181,7 @@ 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 485,983 bytes with SHA-256 -`cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c`; the +`d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0`; the promoted verifier is 504,431 bytes with SHA-256 `3451e95a7d084d4874c30833f90f5c21012e8341f35ed0f0888d4d268a768b11`. diff --git a/schemas/edict-provider/components/v1/README.md b/schemas/edict-provider/components/v1/README.md index d17349a3f..a4c5303fb 100644 --- a/schemas/edict-provider/components/v1/README.md +++ b/schemas/edict-provider/components/v1/README.md @@ -24,7 +24,7 @@ The source WIT is the exact 7,392-byte Edict contract with SHA-256 `2971fe44def7e51d5271dfc0f04f3088aa58754cffdc847681a587605aac749e`. The checked component is 485,983 bytes with SHA-256 -`cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c`. +`d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0`. 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 424970f63..4568cf32a 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..1a6ab1cef 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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..8db719632 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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:aea35262688ec02e908a115d44f392b0c7b9451dc26b62ca2a3599c549299d8d","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..336718aa9 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, @@ -1194,8 +1194,18 @@ 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, + "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,25 @@ 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, + }, +} + 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 424970f63..4568cf32a 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..1a6ab1cef 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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..8db719632 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:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343","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:aea35262688ec02e908a115d44f392b0c7b9451dc26b62ca2a3599c549299d8d","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..336718aa9 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, @@ -1194,8 +1194,18 @@ 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, + "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,25 @@ 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, + }, +} + 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 72f2301bf..b9c7118f6 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:36ce20c612c8c46bbe450c93e6fb2235f38e4ec2b14f04f128eee91805cdb26a" + "digest": "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -87,13 +87,13 @@ "artifactKind": "lowerer", "resource": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c" + "digest": "sha256:d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0" }, "source": { "kind": "component", "component": { "coordinate": "echo.dpo.lowerer/component@1", - "digest": "sha256:cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c" + "digest": "sha256:d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:2bc9bf98d4f3ff91b31359afe60b30bbf6670c94ba2d36d04baecb686eec4d38" + "digest": "sha256:bdae385fd1cfa0463c45888387e7664dd627f9c5ece01df5f8f9a90a4c8da2d8" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:29612fb0642a1fea047e9d348e21abb776e3467ccad61fa9e07194e945f39191" + "digest": "sha256:eb306bb4f8f3e0cfe07620eb46234c4a617744a2063d2f9a7854944cb934c4b2" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:0a4e7ddf2f4087ef53fd3116db1623097c0df08658d9ddc7a80e02da04fba111" + "digest": "sha256:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b3b575626164e4639545e3d25b31602274f9458e9dafc4f8076c832a72aa262b" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, 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 49cc91a95..8ac420dfd 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 = - "cd81874f971f290f74987b39479ef89ddb5060e56df54044d2359e6e89b2430c"; + "d9b82fa467f2a5988aa03d8d2e9cd5d2cbc476d740eadaaf9607cb9e0d4450e0"; 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, );