From 7eb3ea2446b02eac9c7b4b918c7b0e5f08627b67 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:24:58 -0700 Subject: [PATCH 01/43] Record typed outgoing-range reads against immutable frontiers --- CHANGELOG.md | 8 + crates/warp-core/src/echo_operation.rs | 8 +- crates/warp-core/src/edict_effects.rs | 150 ++++++++++ crates/warp-core/src/lib.rs | 2 + .../tests/edict_effect_range_reads.rs | 266 ++++++++++++++++++ docs/topics/GeneratedRules.md | 25 ++ 6 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 crates/warp-core/src/edict_effects.rs create mode 100644 crates/warp-core/tests/edict_effect_range_reads.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f23b13de..6189e008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ ## Unreleased +### Added + +- 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. + ### Fixed - Outgoing-edge bucket iterators expose their exact remaining length without diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 9eda59fe..81778ffb 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -525,20 +525,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 +556,7 @@ impl EchoOperationBudgetMeterV1 { true } - const fn consumed(self) -> EchoOperationBudgetV1 { + pub(crate) const fn consumed(self) -> EchoOperationBudgetV1 { self.consumed } } diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs new file mode 100644 index 00000000..71fc3100 --- /dev/null +++ b/crates/warp-core/src/edict_effects.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Trusted-host recording reads for the authored-effect interpreter under construction. +//! +//! This is not an installed program profile or admission capability. The host +//! selects the immutable frontier and policy. No mutation or caller callback is +//! available. Preparing the basis and policy is outside the per-read budget. +//! The interpreter, complete footprint law, bounded arena and portal authority +//! validation still need to be connected before an authored-effect profile ships. +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, +}; + +/// A read refusal, without an Action outcome or any mutation authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectRangeReadError { + /// The selected edge type is outside the host's range-read policy. + #[error("range edge type is outside the footprint law")] + FootprintLawViolation, + /// The whole source bucket cannot be scanned within the remaining limits. + #[error("range read exceeds its budget")] + BudgetExceeded, +} + +/// 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, + readings: BTreeMap, + footprint: Footprint, +} + +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 { + frontier, + basis: ReadBasis::at(frontier), + allowed_types: allowed_types.iter().copied().collect(), + max_range_edges, + meter: EchoOperationBudgetMeterV1::new(budget), + readings: BTreeMap::new(), + footprint: Footprint::default(), + } + } + + /// 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. + pub fn read( + &mut self, + range: OutgoingEdgeRange, + ) -> Result>, EffectRangeReadError> { + if !self.allowed_types.contains(&range.edge_type) { + return Err(EffectRangeReadError::FootprintLawViolation); + } + if !self.meter.charge(1, 64, 0) { + return Err(EffectRangeReadError::BudgetExceeded); + } + let store = self.frontier.state().store(&range.source.warp_id); + let node = store.and_then(|store| store.node(&range.source.local_id)); + self.readings + .insert(SlotId::Node(range.source), SlotReading::Node(node.cloned())); + self.footprint.n_read.insert(range.source); + let value = if let (Some(store), Some(_)) = (store, node) { + let bucket = store.edges_from(&range.source.local_id); + let count = + u64::try_from(bucket.len()).map_err(|_| EffectRangeReadError::BudgetExceeded)?; + let bytes = count + .checked_mul(64) + .ok_or(EffectRangeReadError::BudgetExceeded)?; + if count > self.max_range_edges || !self.meter.charge(count, bytes, 0) { + return Err(EffectRangeReadError::BudgetExceeded); + } + let mut rows: Vec<_> = bucket + .filter(|edge| edge.ty == range.edge_type) + .cloned() + .collect(); + rows.sort_by_key(|edge| edge.id); + Some(rows) + } else { + None + }; + self.readings.insert( + SlotId::OutgoingEdges(range), + SlotReading::OutgoingEdges(value.clone()), + ); + self.footprint.r_read.insert(range); + Ok(value) + } + + /// 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/lib.rs b/crates/warp-core/src/lib.rs index 1dd517ba..1a908b76 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -68,6 +68,8 @@ pub mod domain; )] mod echo_operation; #[cfg(feature = "trusted_runtime")] +pub mod edict_effects; +#[cfg(feature = "trusted_runtime")] pub mod edict_pure; #[cfg(feature = "trusted_runtime")] pub mod edict_read; 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 00000000..d2b1ac83 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_range_reads.rs @@ -0,0 +1,266 @@ +// 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()); +} + +#[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.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()); + } +} + +#[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); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index c9cbc446..3b7e824c 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -343,6 +343,31 @@ 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. + +This trusted-host primitive has no mutation or admission path. Its returned +values and transcript use host-owned materializations, not the future bounded +interpreter arena. Complete node/atom footprint law, portal-chain authority, +allocation accounting, interpreted read 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. From 03e00112949419e5936c052a002a2556ef4edf35 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:31:53 -0700 Subject: [PATCH 02/43] Run effect range read witnesses in feature-qualified CI lanes --- .github/workflows/ci.yml | 4 ++-- crates/warp-core/Cargo.toml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7df04cf7..9e2d5327 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,7 +194,7 @@ jobs: cargo clippy -p warp-core --features host_test --test causal_wal_hardening_tests -- -D warnings -D missing_docs - 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_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_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_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) @@ -382,7 +382,7 @@ jobs: cargo test -p warp-core --features host_test --test causal_wal_hardening_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_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_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_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/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 4c2072ff..8e455bcc 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -160,6 +160,10 @@ 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_byte_concat_tests" required-features = ["trusted_runtime"] From b374bdb78830d83e93575375242d7efb740f183b Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 22:46:54 -0700 Subject: [PATCH 03/43] Bound effect read storage with atomic resource reservations --- CHANGELOG.md | 3 +- crates/warp-core/src/edict_effects.rs | 146 +++++++++++++++--- .../tests/edict_effect_range_reads.rs | 119 ++++++++++++++ docs/topics/GeneratedRules.md | 26 +++- 4 files changed, 267 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6189e008..4cd1962c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ 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. + installation or mutation authority. Read storage reservations now have a + fixed ceiling, atomic charging with I/O and explicit host-allocation faults. ### Fixed diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 71fc3100..64b89aa2 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -16,7 +16,22 @@ use crate::{ WorldlineFrontier, }; -/// A read refusal, without an Action outcome or any mutation authority. +/// 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; +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 { /// The selected edge type is outside the host's range-read policy. @@ -25,6 +40,12 @@ pub enum EffectRangeReadError { /// The whole source bucket cannot be scanned within the remaining limits. #[error("range read exceeds its budget")] BudgetExceeded, + /// Host storage could not be provisioned despite an admitted 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 range storage")] + HostAllocationFailed, } /// Immutable frontier plus an owned, runtime-recorded dependency transcript. @@ -39,6 +60,8 @@ pub struct EffectRangeReadSession<'a> { allowed_types: BTreeSet, max_range_edges: u64, meter: EchoOperationBudgetMeterV1, + max_allocated_bytes: u64, + allocated_bytes: u64, readings: BTreeMap, footprint: Footprint, } @@ -54,6 +77,46 @@ impl<'a> EffectRangeReadSession<'a> { 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, @@ -61,6 +124,8 @@ impl<'a> EffectRangeReadSession<'a> { 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(), } @@ -78,6 +143,8 @@ impl<'a> EffectRangeReadSession<'a> { /// 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, @@ -85,7 +152,7 @@ impl<'a> EffectRangeReadSession<'a> { if !self.allowed_types.contains(&range.edge_type) { return Err(EffectRangeReadError::FootprintLawViolation); } - if !self.meter.charge(1, 64, 0) { + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES) { return Err(EffectRangeReadError::BudgetExceeded); } let store = self.frontier.state().store(&range.source.warp_id); @@ -93,33 +160,72 @@ impl<'a> EffectRangeReadSession<'a> { self.readings .insert(SlotId::Node(range.source), SlotReading::Node(node.cloned())); self.footprint.n_read.insert(range.source); - let value = if let (Some(store), Some(_)) = (store, node) { - let bucket = store.edges_from(&range.source.local_id); - let count = - u64::try_from(bucket.len()).map_err(|_| EffectRangeReadError::BudgetExceeded)?; - let bytes = count - .checked_mul(64) - .ok_or(EffectRangeReadError::BudgetExceeded)?; - if count > self.max_range_edges || !self.meter.charge(count, bytes, 0) { - return Err(EffectRangeReadError::BudgetExceeded); - } - let mut rows: Vec<_> = bucket - .filter(|edge| edge.ty == range.edge_type) - .cloned() - .collect(); - rows.sort_by_key(|edge| edge.id); - Some(rows) + 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)?; + if count > self.max_range_edges || !self.charge(count, bytes, storage) { + return Err(EffectRangeReadError::BudgetExceeded); + } + 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, None) }; self.readings.insert( SlotId::OutgoingEdges(range), - SlotReading::OutgoingEdges(value.clone()), + SlotReading::OutgoingEdges(observed), ); self.footprint.r_read.insert(range); Ok(value) } + fn charge(&mut self, steps: u64, read_bytes: u64, storage: u64) -> 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; + } + 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 diff --git a/crates/warp-core/tests/edict_effect_range_reads.rs b/crates/warp-core/tests/edict_effect_range_reads.rs index d2b1ac83..6b5c42ff 100644 --- a/crates/warp-core/tests/edict_effect_range_reads.rs +++ b/crates/warp-core/tests/edict_effect_range_reads.rs @@ -62,6 +62,7 @@ fn forbidden_type_is_refused_before_read_or_charge() { ); assert_eq!(session.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); assert!(session.readings().is_empty()); + assert_eq!(session.allocated_bytes(), 0); } #[test] @@ -76,6 +77,7 @@ fn nonmatching_edges_count_toward_the_preinspection_budget() { ); 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))); @@ -193,6 +195,7 @@ fn exhausted_node_budget_records_nothing_and_failed_charges_are_atomic() { 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); } } @@ -264,3 +267,119 @@ fn repeated_reads_charge_each_attempt_without_duplicating_observations() { 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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 3b7e824c..d13639e2 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -361,12 +361,26 @@ 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. -This trusted-host primitive has no mutation or admission path. Its returned -values and transcript use host-owned materializations, not the future bounded -interpreter arena. Complete node/atom footprint law, portal-chain authority, -allocation accounting, interpreted read 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. +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. + +This trusted-host primitive has no mutation or admission path. Logical +value-storage reservations bound the row buffers and observation count; policy +preparation, indexes and allocator overhead remain host storage. This is not +the future shared interpreter arena. Complete node/atom footprint law, +portal-chain authority, shared interpreter allocation accounting, interpreted +read 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. From b46455c38fab89ad1fcddc27251f97806e423cd9 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 18 Oct 2026 23:15:11 -0700 Subject: [PATCH 04/43] Share resource meters between compiled computation and graph reads --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 3 + crates/warp-core/Cargo.toml | 4 + crates/warp-core/src/echo_operation.rs | 4 + crates/warp-core/src/edict_effects.rs | 33 +- crates/warp-core/src/edict_effects/frame.rs | 152 ++++++++ crates/warp-core/src/edict_pure.rs | 15 +- crates/warp-core/src/edict_pure/evaluate.rs | 80 +++- .../tests/edict_effect_frame_tests.rs | 355 ++++++++++++++++++ docs/topics/GeneratedRules.md | 33 +- 10 files changed, 652 insertions(+), 31 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/frame.rs create mode 100644 crates/warp-core/tests/edict_effect_frame_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e2d5327..05412932 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,7 +194,7 @@ jobs: cargo clippy -p warp-core --features host_test --test causal_wal_hardening_tests -- -D warnings -D missing_docs - 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_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_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_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_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) @@ -382,7 +382,7 @@ jobs: cargo test -p warp-core --features host_test --test causal_wal_hardening_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_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_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_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_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/CHANGELOG.md b/CHANGELOG.md index 4cd1962c..c9ec8480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ 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. ### Fixed diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 8e455bcc..6bdabc38 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -164,6 +164,10 @@ required-features = ["trusted_runtime"] name = "edict_effect_range_reads" required-features = ["trusted_runtime"] +[[test]] +name = "edict_effect_frame_tests" +required-features = ["trusted_runtime"] + [[test]] name = "edict_byte_concat_tests" required-features = ["trusted_runtime"] diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 81778ffb..bf79f1a0 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -556,6 +556,10 @@ impl EchoOperationBudgetMeterV1 { true } + pub(crate) const fn delegated(self) -> EchoOperationBudgetV1 { + self.delegated + } + pub(crate) const fn consumed(self) -> EchoOperationBudgetV1 { self.consumed } diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 64b89aa2..8ea537c7 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -7,6 +7,10 @@ //! available. Preparing the basis and policy is outside the per-read budget. //! The interpreter, complete footprint law, bounded arena and portal authority //! validation still need to be connected before an authored-effect profile ships. +mod frame; +pub use frame::{EffectEvaluationFrame, EffectFrameError, EffectFrameSetupError}; + +use crate::edict_pure::evaluate::Meter; use std::collections::{BTreeMap, BTreeSet}; use crate::echo_operation::EchoOperationBudgetMeterV1; @@ -40,7 +44,7 @@ pub enum EffectRangeReadError { /// The whole source bucket cannot be scanned within the remaining limits. #[error("range read exceeds its budget")] BudgetExceeded, - /// Host storage could not be provisioned despite an admitted reservation. + /// 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. @@ -148,11 +152,19 @@ impl<'a> EffectRangeReadSession<'a> { pub fn read( &mut self, range: OutgoingEdgeRange, + ) -> Result>, EffectRangeReadError> { + self.read_with_meter(range, None) + } + + fn read_with_meter( + &mut self, + range: OutgoingEdgeRange, + mut shared: Option<&mut Meter>, ) -> Result>, EffectRangeReadError> { if !self.allowed_types.contains(&range.edge_type) { return Err(EffectRangeReadError::FootprintLawViolation); } - if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES) { + if !self.charge(1, 64, OBSERVATION_STORAGE_BYTES, shared.as_deref_mut()) { return Err(EffectRangeReadError::BudgetExceeded); } let store = self.frontier.state().store(&range.source.warp_id); @@ -172,7 +184,7 @@ impl<'a> EffectRangeReadSession<'a> { .checked_mul(2 * EDGE_RECORD_STORAGE_BYTES) .and_then(|rows| rows.checked_add(OBSERVATION_STORAGE_BYTES)) .ok_or(EffectRangeReadError::BudgetExceeded)?; - if count > self.max_range_edges || !self.charge(count, bytes, storage) { + if count > self.max_range_edges || !self.charge(count, bytes, storage, shared) { return Err(EffectRangeReadError::BudgetExceeded); } let (value, observed) = if let Some(bucket) = bucket { @@ -202,7 +214,13 @@ impl<'a> EffectRangeReadSession<'a> { Ok(value) } - fn charge(&mut self, steps: u64, read_bytes: u64, storage: u64) -> bool { + 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; }; @@ -210,6 +228,13 @@ impl<'a> EffectRangeReadSession<'a> { 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 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 00000000..df8acbaf --- /dev/null +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -0,0 +1,152 @@ +// 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, 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 { + /// Already-observed resources cannot be imported without accounting for them. + #[error("read session has already been used")] + ReadSessionAlreadyUsed, + /// 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 { + /// Failure from the unchanged compiler-package interpreter. + Evaluation(EvaluationError), + /// Read refusal or host fault; host allocation faults must not become Actions. + Read(EffectRangeReadError), + /// A prior failure ended this frame; no further work or reads are allowed. + Terminated, +} + +/// 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, +} + +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, + }) + } + + /// 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; + let result = self + .reads + .read_with_meter(range, Some(&mut self.meter)) + .map_err(EffectFrameError::Read); + self.terminated = result.is_err(); + result + } + + /// 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 + } + + /// Global computation plus read steps, and the view's I/O consumption. + #[must_use] + pub fn consumed(&self) -> EchoOperationBudgetV1 { + EchoOperationBudgetV1::new(self.meter.usage().0, self.reads.consumed().read_bytes(), 0) + } + + /// 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_pure.rs b/crates/warp-core/src/edict_pure.rs index d8f0fbfc..cb48479c 100644 --- a/crates/warp-core/src/edict_pure.rs +++ b/crates/warp-core/src/edict_pure.rs @@ -66,6 +66,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 +84,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 +104,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 bd3651f3..84eb5872 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) } } @@ -37,10 +47,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 +62,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 +102,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 +139,32 @@ 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 { + let saved = meter.limits; + // Each component retains its declared ceiling while the frame's absolute + // limits continue to bind cumulative work across all components and reads. + meter.limits.max_steps = saved + .max_steps + .min(meter.steps.saturating_add(program.limits.max_steps)); + meter.limits.max_allocated_bytes = saved.max_allocated_bytes.min( + meter + .allocated + .saturating_add(program.limits.max_allocated_bytes), + ); + let result = run_body(program, input, meter); + meter.limits = saved; + result +} + +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 +174,17 @@ pub(super) fn run(program: &Program, input: Value) -> Result +//! 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); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index d13639e2..e3c0b096 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -373,14 +373,31 @@ capacity reports `HostAllocationFailed`, not `BudgetExceeded`. That host fault must stop evaluation and must never become an Action obstruction or retained decision. -This trusted-host primitive has no mutation or admission path. Logical -value-storage reservations bound the row buffers and observation count; policy -preparation, indexes and allocator overhead remain host storage. This is not -the future shared interpreter arena. Complete node/atom footprint law, -portal-chain authority, shared interpreter allocation accounting, interpreted -read 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. +`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. + +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, complete node/atom footprint law, portal-chain +authority, interpreted read 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. From 784c61aaf39f72be2a9d4a9281d0c7bb0ce4ff61 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 01:53:00 -0700 Subject: [PATCH 05/43] Bind private effect frames to the runtime commit parent --- CHANGELOG.md | 2 + crates/warp-core/src/edict_effects/frame.rs | 39 +++- crates/warp-core/src/trusted_runtime_host.rs | 48 ++++ .../tests/edict_effect_frame_tests.rs | 8 + .../executable_operation_pipeline_tests.rs | 208 ++++++++++++++++++ docs/topics/GeneratedRules.md | 16 ++ 6 files changed, 320 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ec8480..f0595790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added +- 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 diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index df8acbaf..313207ac 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -4,12 +4,18 @@ 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, EdgeRecord, Footprint, OutgoingEdgeRange, SlotId, SlotReading}; +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, @@ -40,6 +46,7 @@ pub struct EffectEvaluationFrame<'a> { meter: Meter, limits: EvaluationLimits, terminated: bool, + evaluation_basis: Option, } impl<'a> EffectEvaluationFrame<'a> { @@ -73,9 +80,39 @@ impl<'a> EffectEvaluationFrame<'a> { meter: Meter::new_atomic(limits), limits, terminated: false, + evaluation_basis: 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, + ) -> 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); + Ok(frame) + } + + /// 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 diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index d59ce69a..60b7af08 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -104,6 +104,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), @@ -1160,6 +1163,51 @@ 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, + )?) + } + /// Independently admits a canonical installed-operation invocation. /// /// Admission checks exact installed package identity, public operation diff --git a/crates/warp-core/tests/edict_effect_frame_tests.rs b/crates/warp-core/tests/edict_effect_frame_tests.rs index bb91eec5..675babb9 100644 --- a/crates/warp-core/tests/edict_effect_frame_tests.rs +++ b/crates/warp-core/tests/edict_effect_frame_tests.rs @@ -353,3 +353,11 @@ fn component_caps_are_relative_and_the_global_cap_is_restored_between_calls() { 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); +} diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index a7523d3d..06d4d773 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -4934,3 +4934,211 @@ fn an_action_with_a_forged_origin_keeps_the_whole_frontier_rule() { "an origin that history does not hold cannot earn scoped validity" ); } + +// 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, + } +} + +#[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 + ); +} + +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) +} + +#[test] +fn effect_frame_refuses_equal_reading_coordinates_from_another_commit_history() { + let (first, head) = effect_frame_host_after_round_trip(b"first-intermediate"); + let (second, second_head) = effect_frame_host_after_round_trip(b"second-intermediate"); + assert_eq!(head, second_head); + let expected = first + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("first basis"); + let actual = second + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("second basis"); + assert_eq!(expected.state_root(), actual.state_root()); + assert_eq!(expected.worldline_tick(), actual.worldline_tick()); + assert_eq!(expected.commit_global_tick(), actual.commit_global_tick()); + assert_ne!(expected.commit_id(), actual.commit_id()); + assert!(matches!( + second.effect_evaluation_frame_v1( + expected, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits() + ), + Err(TrustedRuntimeHostError::EffectFrameSetup( + warp_core::edict_effects::EffectFrameSetupError::BasisMismatch + )) + )); + let frame = second + .effect_evaluation_frame_v1( + actual, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits(), + ) + .expect("current exact history succeeds"); + assert_eq!(frame.evaluation_basis(), Some(actual)); + assert_eq!(frame.consumed(), EchoOperationBudgetV1::new(0, 0, 0)); + assert!(frame.readings().is_empty()); +} + +#[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))); +} + +#[test] +fn effect_frame_refuses_a_stale_parent_after_a_decided_tick() { + let (mut host, head, node) = fixture_host(); + host.enable_runtime_wal(TrustedRuntimeWalConfig::in_memory()) + .expect("WAL"); + let installed = install_fixture_operation(&mut host); + host.install_echo_operation_action_admission_policy_v1(invocation_policy()); + let stale = host + .echo_operation_evaluation_basis_v1(head, application_basis()) + .expect("original parent"); + let invocation = action_invocation(&host, &installed, head, node, b"before", b"after"); + let id = submit_action(&mut host, head, invocation); + host.tick_once().expect("decided tick"); + assert!(outcome_is_committed(&host, &id)); + assert!(matches!( + host.effect_evaluation_frame_v1( + stale, + &[], + 0, + EchoOperationBudgetV1::new(10000, 1024, 0), + effect_frame_limits() + ), + Err(TrustedRuntimeHostError::EffectFrameSetup( + warp_core::edict_effects::EffectFrameSetupError::BasisMismatch + )) + )); +} + +#[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_evaluation_basis_v1(head, application_basis()) + .expect("unchanged parent"), + basis + ); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index e3c0b096..b0f80d2e 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -389,6 +389,22 @@ 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, complete +node/atom/portal authorization and recovery obligations remain as described +above and below. + 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. From da04e125966d7ed7fff73c924c460cc885e32454 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 02:13:53 -0700 Subject: [PATCH 06/43] Interpret compiled atom reads through the recorded effect frame --- CHANGELOG.md | 2 + crates/warp-core/src/edict_effects.rs | 2 + crates/warp-core/src/edict_effects/frame.rs | 74 +++ .../warp-core/src/edict_effects/node_reads.rs | 90 ++++ crates/warp-core/src/edict_pure/evaluate.rs | 46 +- crates/warp-core/src/edict_read.rs | 29 +- crates/warp-core/src/edict_read/access.rs | 38 ++ crates/warp-core/src/edict_read/evaluate.rs | 62 ++- crates/warp-core/src/edict_read/view.rs | 2 +- .../tests/edict_effect_frame_tests.rs | 46 ++ .../executable_operation_pipeline_tests.rs | 503 +++++++++++++++++- docs/topics/GeneratedRules.md | 35 +- 12 files changed, 885 insertions(+), 44 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/node_reads.rs create mode 100644 crates/warp-core/src/edict_read/access.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f0595790..8d1c7fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added +- 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 diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 8ea537c7..d7e263dd 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -8,7 +8,9 @@ //! The interpreter, complete footprint law, bounded arena and portal authority //! validation still need to be connected before an authored-effect profile ships. mod frame; +mod node_reads; pub use frame::{EffectEvaluationFrame, EffectFrameError, EffectFrameSetupError}; +pub(crate) use node_reads::RecordedNodeAccess; use crate::edict_pure::evaluate::Meter; use std::collections::{BTreeMap, BTreeSet}; diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 313207ac..0e32fa01 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -27,6 +27,10 @@ pub enum EffectFrameSetupError { /// 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. @@ -35,6 +39,17 @@ pub enum EffectFrameError { Terminated, } +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 @@ -163,6 +178,65 @@ impl<'a> EffectEvaluationFrame<'a> { 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], + 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 + } + /// Global computation plus read steps, and the view's I/O consumption. #[must_use] pub fn consumed(&self) -> EchoOperationBudgetV1 { 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 00000000..63dd3d2c --- /dev/null +++ b/crates/warp-core/src/edict_effects/node_reads.rs @@ -0,0 +1,90 @@ +// 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: &'r [NodeKey], +} + +impl NodeAtomAccess for RecordedNodeAccess<'_, '_> { + type Error = EffectFrameError; + fn basis(&self) -> ReadBasis { + self.reads.basis() + } + fn contains(&self, node: NodeKey) -> bool { + self.aperture.binary_search(&node).is_ok() + } + fn atom( + &mut self, + node: NodeKey, + expected: TypeId, + max_bytes: u64, + meter: &mut Meter, + ) -> Result, Self::Error> { + self.reads.record_atom(node, 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), + }) + } +} diff --git a/crates/warp-core/src/edict_pure/evaluate.rs b/crates/warp-core/src/edict_pure/evaluate.rs index 84eb5872..364a8dbe 100644 --- a/crates/warp-core/src/edict_pure/evaluate.rs +++ b/crates/warp-core/src/edict_pure/evaluate.rs @@ -39,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); @@ -147,20 +178,7 @@ pub(crate) fn run_with_meter( input: Value, meter: &mut Meter, ) -> Result { - let saved = meter.limits; - // Each component retains its declared ceiling while the frame's absolute - // limits continue to bind cumulative work across all components and reads. - meter.limits.max_steps = saved - .max_steps - .min(meter.steps.saturating_add(program.limits.max_steps)); - meter.limits.max_allocated_bytes = saved.max_allocated_bytes.min( - meter - .allocated - .saturating_add(program.limits.max_allocated_bytes), - ); - let result = run_body(program, input, meter); - meter.limits = saved; - result + meter.with_component(program.limits, |meter| run_body(program, input, meter)) } fn run_body(program: &Program, input: Value, meter: &mut Meter) -> Result { diff --git a/crates/warp-core/src/edict_read.rs b/crates/warp-core/src/edict_read.rs index 35c11246..fd8b5fcd 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: &[NodeKey], + meter: &mut crate::edict_pure::evaluate::Meter, + limits: ReadLimits, +) -> Result { + ReadView::validate_aperture(aperture)?; + 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 00000000..747bf90a --- /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 36a0f309..987e5175 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 db8f92cc..a3e4e845 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(super) 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/tests/edict_effect_frame_tests.rs b/crates/warp-core/tests/edict_effect_frame_tests.rs index 675babb9..bf55a34b 100644 --- a/crates/warp-core/tests/edict_effect_frame_tests.rs +++ b/crates/warp-core/tests/edict_effect_frame_tests.rs @@ -361,3 +361,49 @@ fn a_low_level_frame_does_not_claim_a_runtime_parent() { 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/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 06d4d773..760191d4 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -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,13 +148,7 @@ 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"); @@ -5142,3 +5148,484 @@ fn effect_frame_bound_parent_retains_the_resource_ceiling() { basis ); } + +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)); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index b0f80d2e..1bec4210 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -405,14 +405,45 @@ The state-root/by-key caveat of the low-level reading coordinate, complete node/atom/portal authorization and recovery obligations remain as described above and below. +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 path still uses a trusted-host node aperture; +it does not implement the complete typed node/atom/portal footprint law or +interpret outgoing-range instructions. It grants no application mutation or +admission authority. + 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, complete node/atom footprint law, portal-chain -authority, interpreted read instructions, derived effects and installed-profile -validation remain required before a new authored-effect profile can ship. +authority, 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 From e61e835f5bb9b0639898708214eee797fcbc6c7d Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 03:02:21 -0700 Subject: [PATCH 07/43] feat: authorize recorded reads under typed footprint laws --- CHANGELOG.md | 7 + crates/warp-core/src/edict_effects.rs | 79 +- crates/warp-core/src/edict_effects/frame.rs | 148 +++ crates/warp-core/src/edict_effects/law.rs | 107 +++ .../warp-core/src/edict_effects/node_reads.rs | 118 ++- .../src/edict_effects/typed_reads.rs | 172 ++++ crates/warp-core/src/edict_read.rs | 4 +- crates/warp-core/src/edict_read/view.rs | 2 +- .../executable_operation_pipeline_tests.rs | 890 +++++++++++++++++- docs/topics/GeneratedRules.md | 71 +- 10 files changed, 1574 insertions(+), 24 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/law.rs create mode 100644 crates/warp-core/src/edict_effects/typed_reads.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad14add..a15f8516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Added +- 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. diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index d7e263dd..9bafedf3 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -5,12 +5,17 @@ //! This is not an installed program profile or admission capability. The host //! selects the immutable frontier and policy. No mutation or caller callback is //! available. Preparing the basis and policy is outside the per-read budget. -//! The interpreter, complete footprint law, bounded arena and portal authority -//! validation still need to be connected before an authored-effect profile ships. +//! Optional typed laws authorize runtime-bound node, atom, portal and range reads. +//! Authored effect instructions, package-authenticated policy, canonical read +//! images, physical arena and admission still precede a shipped effect profile. mod frame; +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::RecordedNodeAccess; +pub(crate) use node_reads::{NodeAperture, RecordedNodeAccess}; use crate::edict_pure::evaluate::Meter; use std::collections::{BTreeMap, BTreeSet}; @@ -40,17 +45,20 @@ pub enum EffectReadSetupError { /// A read failure, without an Action outcome or any mutation authority. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum EffectRangeReadError { - /// The selected edge type is outside the host's range-read policy. + /// A resolved type or distinct read count is outside the selected read law. #[error("range edge type is outside the footprint law")] FootprintLawViolation, - /// The whole source bucket cannot be scanned within the remaining limits. + /// 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 range storage")] + #[error("host could not provision reserved read storage")] HostAllocationFailed, } @@ -70,6 +78,9 @@ pub struct EffectRangeReadSession<'a> { allocated_bytes: u64, readings: BTreeMap, footprint: Footprint, + law: Option, + authorized_reads: BTreeSet<(SlotId, TypeId)>, + validated_warps: BTreeSet, } impl<'a> EffectRangeReadSession<'a> { @@ -134,6 +145,9 @@ impl<'a> EffectRangeReadSession<'a> { allocated_bytes: 0, readings: BTreeMap::new(), footprint: Footprint::default(), + law: None, + authorized_reads: BTreeSet::new(), + validated_warps: BTreeSet::new(), } } @@ -159,18 +173,53 @@ impl<'a> EffectRangeReadSession<'a> { } 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 store = self.frontier.state().store(&range.source.warp_id); + 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); @@ -186,9 +235,23 @@ impl<'a> EffectRangeReadSession<'a> { .checked_mul(2 * EDGE_RECORD_STORAGE_BYTES) .and_then(|rows| rows.checked_add(OBSERVATION_STORAGE_BYTES)) .ok_or(EffectRangeReadError::BudgetExceeded)?; - if count > self.max_range_edges || !self.charge(count, bytes, storage, shared) { + 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. diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 0e32fa01..3067f7d3 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -19,6 +19,9 @@ pub enum EffectFrameSetupError { /// 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, @@ -39,6 +42,12 @@ pub enum EffectFrameError { Terminated, } +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) @@ -119,6 +128,60 @@ impl<'a> EffectEvaluationFrame<'a> { 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 @@ -141,6 +204,9 @@ impl<'a> EffectEvaluationFrame<'a> { 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)) @@ -149,6 +215,33 @@ impl<'a> EffectEvaluationFrame<'a> { 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 + } + /// Evaluates exact compiler-package bytes using the same cumulative meter. /// /// Package/input decoding is bounded host preparation. This is private @@ -197,6 +290,61 @@ impl<'a> EffectEvaluationFrame<'a> { 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 { 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 00000000..ab88956a --- /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, including currently unused write fields. +/// 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, + _write_nodes: BTreeSet, + _write_edges: BTreeSet, + _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: 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/node_reads.rs b/crates/warp-core/src/edict_effects/node_reads.rs index 63dd3d2c..d47e35b2 100644 --- a/crates/warp-core/src/edict_effects/node_reads.rs +++ b/crates/warp-core/src/edict_effects/node_reads.rs @@ -10,7 +10,7 @@ use crate::{AttachmentKey, AttachmentValue, NodeKey, SlotId, SlotReading, TypeId pub(crate) struct RecordedNodeAccess<'r, 'f> { pub(crate) reads: &'r mut EffectRangeReadSession<'f>, - pub(crate) aperture: &'r [NodeKey], + pub(crate) aperture: NodeAperture<'r>, } impl NodeAtomAccess for RecordedNodeAccess<'_, '_> { @@ -19,7 +19,7 @@ impl NodeAtomAccess for RecordedNodeAccess<'_, '_> { self.reads.basis() } fn contains(&self, node: NodeKey) -> bool { - self.aperture.binary_search(&node).is_ok() + self.aperture.absent_type(node).is_some() } fn atom( &mut self, @@ -28,7 +28,17 @@ impl NodeAtomAccess for RecordedNodeAccess<'_, '_> { max_bytes: u64, meter: &mut Meter, ) -> Result, Self::Error> { - self.reads.record_atom(node, expected, max_bytes, meter) + 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) + } + } } } @@ -88,3 +98,105 @@ impl EffectRangeReadSession<'_> { }) } } + +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/typed_reads.rs b/crates/warp-core/src/edict_effects/typed_reads.rs new file mode 100644 index 00000000..91218fe2 --- /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_read.rs b/crates/warp-core/src/edict_read.rs index fd8b5fcd..7d9e5c58 100644 --- a/crates/warp-core/src/edict_read.rs +++ b/crates/warp-core/src/edict_read.rs @@ -137,11 +137,11 @@ pub(crate) fn evaluate_recorded( pin: [u8; 32], input: &[u8], reads: &mut crate::edict_effects::EffectRangeReadSession<'_>, - aperture: &[NodeKey], + aperture: crate::edict_effects::NodeAperture<'_>, meter: &mut crate::edict_pure::evaluate::Meter, limits: ReadLimits, ) -> Result { - ReadView::validate_aperture(aperture)?; + 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/view.rs b/crates/warp-core/src/edict_read/view.rs index a3e4e845..e2a99bb3 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> { }) } - pub(super) 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/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 760191d4..af13e302 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -152,6 +152,11 @@ fn fixture_host_with_attachment( } 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, @@ -185,7 +190,7 @@ fn fixture_host_with_attachment( .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) @@ -5629,3 +5634,886 @@ fn recorded_effect_frame_refuses_invalid_apertures_before_reading() { 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()); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index fd2ac4d9..86b969c4 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -410,9 +410,9 @@ 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, complete -node/atom/portal authorization and recovery obligations remain as described -above and below. +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 @@ -440,18 +440,71 @@ 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 path still uses a trusted-host node aperture; -it does not implement the complete typed node/atom/portal footprint law or -interpret outgoing-range instructions. It grants no application mutation or -admission authority. +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. 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, complete node/atom footprint law, portal-chain -authority, authored outgoing-range/effect instructions, derived effects and +physical interpreter arena, package-authenticated footprint law, persisted 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. From 9b7db0ce69bd6f922befa26e108ccf028cd51f82 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 03:34:55 -0700 Subject: [PATCH 08/43] feat: export canonical typed read-set images --- CHANGELOG.md | 7 + crates/warp-core/src/edict_effects.rs | 10 +- crates/warp-core/src/edict_effects/frame.rs | 30 ++ .../warp-core/src/edict_effects/read_image.rs | 82 +++++ .../src/edict_effects/read_image/encode.rs | 187 ++++++++++++ .../src/edict_effects/read_image/rows.rs | 143 +++++++++ .../src/edict_effects/read_image/validate.rs | 120 ++++++++ .../tests/edict_effect_read_image_tests.rs | 225 ++++++++++++++ .../executable_operation_pipeline_tests.rs | 282 ++++++++++++++++++ docs/topics/GeneratedRules.md | 63 +++- 10 files changed, 1145 insertions(+), 4 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/read_image.rs create mode 100644 crates/warp-core/src/edict_effects/read_image/encode.rs create mode 100644 crates/warp-core/src/edict_effects/read_image/rows.rs create mode 100644 crates/warp-core/src/edict_effects/read_image/validate.rs create mode 100644 crates/warp-core/tests/edict_effect_read_image_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a15f8516..474e7423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Added +- 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. diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 9bafedf3..be4cb5a5 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -6,9 +6,15 @@ //! selects the immutable frontier and policy. No mutation or caller callback is //! available. Preparing the basis and policy is outside the per-read budget. //! Optional typed laws authorize runtime-bound node, atom, portal and range reads. -//! Authored effect instructions, package-authenticated policy, canonical read -//! images, physical arena and admission still precede a shipped effect profile. +//! Completed reads can be exported as bounded canonical images. Authored effect +//! instructions, package-authenticated policy, receipt/WAL integration, a physical +//! arena and admission still precede a shipped effect profile. mod frame; +mod read_image; +pub use read_image::{ + EffectEdgePairRow, EffectReadImageError, EffectReadRow, EffectReadSetImage, EffectReadSetView, + EffectSlotRow, +}; mod law; mod typed_reads; pub use law::{EffectFootprintLaw, EffectLawError}; diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 3067f7d3..80d0f0ed 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -385,6 +385,36 @@ impl<'a> EffectEvaluationFrame<'a> { 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 plus read steps, and the view's I/O consumption. #[must_use] pub fn consumed(&self) -> EchoOperationBudgetV1 { 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 00000000..f5cc40ba --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image.rs @@ -0,0 +1,82 @@ +// 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"; + +/// 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 00000000..e5a6297a --- /dev/null +++ b/crates/warp-core/src/edict_effects/read_image/encode.rs @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +use super::super::EffectRangeReadSession; +use super::{ + EffectEdgePairRow, EffectReadImageError as Error, EffectReadRow, EffectReadSetImage, + EffectReadSetView, EffectSlotRow, +}; +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( + u64::try_from(rows.len()) + .map_err(|_| Error::ExportLimitExceeded)? + .to_le(), + ); + 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(cursor.to_le()); + 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 => + { + 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( + &u64::try_from(atom.bytes.len()) + .map_err(|_| Error::InconsistentObservation)? + .to_le_bytes(), + ); + hash.update(&atom.bytes); + row.subject = hash.finalize().into(); + } + ( + 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 00000000..b8dd106e --- /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(super) 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(super) 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 00000000..00d70a0c --- /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/tests/edict_effect_read_image_tests.rs b/crates/warp-core/tests/edict_effect_read_image_tests.rs new file mode 100644 index 00000000..9c1e4062 --- /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/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index af13e302..b2e26916 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -6517,3 +6517,285 @@ fn typed_effect_compiled_apertures_refuse_duplicate_and_unordered_declarations_b 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()); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 86b969c4..b9f511b2 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -498,13 +498,72 @@ they are host-private authorization evidence, not the persisted canonical 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. + 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, persisted typed -read images, authored outgoing-range/effect instructions, derived effects and +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. From 6107b355e8c6234bba30c4b5924f824d84d4fcbd Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 03:42:36 -0700 Subject: [PATCH 09/43] fix: guarantee read-image buffer alignment across targets --- crates/warp-core/src/edict_effects/read_image.rs | 8 +++++++- .../src/edict_effects/read_image/encode.rs | 14 +++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/warp-core/src/edict_effects/read_image.rs b/crates/warp-core/src/edict_effects/read_image.rs index f5cc40ba..c64a323c 100644 --- a/crates/warp-core/src/edict_effects/read_image.rs +++ b/crates/warp-core/src/edict_effects/read_image.rs @@ -10,6 +10,12 @@ 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 { @@ -41,7 +47,7 @@ pub enum EffectReadImageError { /// installation, policy-authentication or historical-decision capability. #[derive(Debug)] pub struct EffectReadSetImage { - words: Vec, + words: Vec, basis: EchoOperationEvaluationBasisV1, consumed: crate::EchoOperationBudgetV1, allocated_bytes: u64, diff --git a/crates/warp-core/src/edict_effects/read_image/encode.rs b/crates/warp-core/src/edict_effects/read_image/encode.rs index e5a6297a..cbbfa06e 100644 --- a/crates/warp-core/src/edict_effects/read_image/encode.rs +++ b/crates/warp-core/src/edict_effects/read_image/encode.rs @@ -3,7 +3,7 @@ use super::super::EffectRangeReadSession; use super::{ EffectEdgePairRow, EffectReadImageError as Error, EffectReadRow, EffectReadSetImage, - EffectReadSetView, EffectSlotRow, + EffectReadSetView, EffectSlotRow, ImageWord, }; use crate::{ AttachmentKey, AttachmentOwner, AttachmentValue, EchoOperationEvaluationBasisV1, EdgeRecord, @@ -56,11 +56,11 @@ pub(in crate::edict_effects) fn export( words .try_reserve_exact(size / 8) .map_err(|_| Error::HostAllocationFailed)?; - words.push( + words.push(ImageWord( u64::try_from(rows.len()) .map_err(|_| Error::ExportLimitExceeded)? - .to_le(), - ); + .to_le_bytes(), + )); let mut cursor = 0_u64; for (row, pairs) in &mut rows { if let Some(pairs) = pairs { @@ -73,7 +73,7 @@ pub(in crate::edict_effects) fn export( } append(&mut words, row)?; } - words.push(cursor.to_le()); + words.push(ImageWord(cursor.to_le_bytes())); for (_, pairs) in rows { for edge in pairs.into_iter().flatten() { append( @@ -93,9 +93,9 @@ pub(in crate::edict_effects) fn export( allocated_bytes, }) } -fn append(words: &mut Vec, row: &T) -> Result<(), Error> { +fn append(words: &mut Vec, row: &T) -> Result<(), Error> { words.extend_from_slice( - bytemuck::try_cast_slice::(std::slice::from_ref(row)) + bytemuck::try_cast_slice::(std::slice::from_ref(row)) .map_err(|_| Error::InvalidImage)?, ); Ok(()) From 57afb57ceb5a1fefdac4ef3e360c153ba676ae3d Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 04:00:44 -0700 Subject: [PATCH 10/43] fix: schedule read-image codec tests in runtime CI --- .github/workflows/ci.yml | 4 ++-- crates/warp-core/Cargo.toml | 4 ++++ docs/topics/GeneratedRules.md | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05412932..8a44f76b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -194,7 +194,7 @@ jobs: cargo clippy -p warp-core --features host_test --test causal_wal_hardening_tests -- -D warnings -D missing_docs - 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_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_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_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_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) @@ -382,7 +382,7 @@ jobs: cargo test -p warp-core --features host_test --test causal_wal_hardening_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_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_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_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_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/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 6bdabc38..021c3f7f 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -168,6 +168,10 @@ required-features = ["trusted_runtime"] 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"] diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index b9f511b2..268e33fa 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -556,6 +556,10 @@ 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 From b9ee20b4dcea1b90bb388f7b5e16ff1ff8457e27 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 06:47:23 -0700 Subject: [PATCH 11/43] Decode strict executable graph-schema relations for authored effects --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 5 + crates/warp-core/Cargo.toml | 5 + crates/warp-core/src/edict_effects.rs | 2 + crates/warp-core/src/edict_effects/schema.rs | 131 +++++++++++ .../tests/edict_effect_graph_schema_tests.rs | 206 ++++++++++++++++++ docs/topics/GeneratedRules.md | 19 ++ 7 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/schema.rs create mode 100644 crates/warp-core/tests/edict_effect_graph_schema_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d1be360..41bbf5c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_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_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) @@ -395,7 +395,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_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_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_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/CHANGELOG.md b/CHANGELOG.md index d8c5b3a5..335a3be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- 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 diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index bb721f7b..cf2025fc 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -244,3 +244,8 @@ 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"] diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index be4cb5a5..6773d1a9 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -15,6 +15,8 @@ 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}; 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 00000000..925435f4 --- /dev/null +++ b/crates/warp-core/src/edict_effects/schema.rs @@ -0,0 +1,131 @@ +// 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))) + } + 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/tests/edict_effect_graph_schema_tests.rs b/crates/warp-core/tests/edict_effect_graph_schema_tests.rs new file mode 100644 index 00000000..6efe827f --- /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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 2c7af366..04dbeb6c 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -635,3 +635,22 @@ 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 authored-effect V2 program direction. 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. From 4eacdc696eb88b798590f84ccf095ce9983d2421 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:14:02 -0700 Subject: [PATCH 12/43] Bind V2 effect publications to exact program schema and footprint law --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 5 + crates/warp-core/Cargo.toml | 4 + crates/warp-core/src/edict_effects.rs | 2 + crates/warp-core/src/edict_effects/package.rs | 295 +++++++++++++ .../tests/edict_effect_package_tests.rs | 408 ++++++++++++++++++ docs/topics/GeneratedRules.md | 34 ++ 7 files changed, 750 insertions(+), 2 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/package.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41bbf5c0..b120a9e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_tests --test edict_effect_graph_schema_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 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) @@ -395,7 +395,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_effect_range_reads --test edict_effect_frame_tests --test edict_effect_read_image_tests --test edict_effect_graph_schema_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 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/CHANGELOG.md b/CHANGELOG.md index 335a3be7..c4f43a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- V2 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; V1 package encodings and identities are unchanged. + - 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 diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index cf2025fc..5dc85fa7 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -249,3 +249,7 @@ required-features = ["delta_validate"] 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"] diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 6773d1a9..254d45c4 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,6 +9,8 @@ //! Completed reads can be exported as bounded canonical images. Authored effect //! instructions, package-authenticated policy, receipt/WAL integration, a physical //! arena and admission still precede a shipped effect profile. +mod package; +pub use package::{EffectPackageError, ExecutableOperationPackageV2}; mod frame; mod read_image; pub use read_image::{ 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 00000000..178391c7 --- /dev/null +++ b/crates/warp-core/src/edict_effects/package.rs @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Exact V2 publication binding, separate from interpreter and admission. +use super::{EffectFootprintLaw, EffectGraphSchema, EFFECT_READ_STORAGE_CEILING_V1}; +use crate::edict_pure::{ + values::{bytes, exact_fields, field, number, require_text, text}, + EvaluationError, +}; +use crate::EchoOperationBudgetV1; +use echo_edict_canonical::{decode_canonical_cbor_v1, 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 V2 package identity. + #[error("effect package identity mismatch")] + IdentityMismatch, + /// The publication has malformed or inconsistent fields or profiles. + #[error("invalid effect package")] + InvalidPackage, +} +/// Prepared publication, without installation or mutation authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecutableOperationPackageV2 { + canonical: Vec, + program_identity: [u8; 32], + compiled: Vec, + schema: EffectGraphSchema, + law: EffectFootprintLaw, + output_type: String, + max_input_bytes: u64, + max_result_bytes: u64, + max_allocated_bytes: u64, + budget: EchoOperationBudgetV1, +} +impl ExecutableOperationPackageV2 { + /// Checks exact pinned publication bytes and their executable contracts. + /// # Errors + /// Refuses substitutions, malformed maps and inconsistent contracts. + pub fn prepare(bytes: &[u8], expected: [u8; 32]) -> Result { + if domain_hash(b"echo:operation-package:v2\0", bytes) != expected { + return Err(EffectPackageError::IdentityMismatch); + } + let value = + decode_canonical_cbor_v1(bytes).map_err(|_| EffectPackageError::InvalidPackage)?; + Self::parse(&value, bytes).map_err(|_| EffectPackageError::InvalidPackage) + } + /// Exact bytes bound by the requested V2 package identity. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical + } + /// Identity of the exact nested program bytes under the V2 domain. + #[must_use] + pub const fn program_identity(&self) -> [u8; 32] { + self.program_identity + } + /// Compiler bytes, not yet validated by the effect interpreter. + #[must_use] + pub fn compiled_program(&self) -> &[u8] { + &self.compiled + } + /// Executable schema decoded from this publication's exact program. + #[must_use] + pub const fn graph_schema(&self) -> &EffectGraphSchema { + &self.schema + } + /// Footprint permissions decoded from the same pinned program as the schema. + #[must_use] + pub const fn footprint_law(&self) -> &EffectFootprintLaw { + &self.law + } + /// Compiler-declared result coordinate, bounded to 1,024 UTF-8 bytes. + #[must_use] + pub fn output_type(&self) -> &str { + &self.output_type + } + /// Declared application input ceiling, at most 65,536 bytes. + #[must_use] + pub const fn max_input_bytes(&self) -> u64 { + self.max_input_bytes + } + /// Declared application result ceiling, at most 65,536 bytes. + #[must_use] + pub const fn max_result_bytes(&self) -> u64 { + self.max_result_bytes + } + /// Declared evaluation allocation ceiling, at most 16 MiB. + #[must_use] + pub const fn max_allocated_bytes(&self) -> u64 { + self.max_allocated_bytes + } + /// Publication's budget ceiling; delegation is checked separately at admission. + #[must_use] + pub const fn budget_ceiling(&self) -> EchoOperationBudgetV1 { + self.budget + } + + fn parse(value: &Value, canonical: &[u8]) -> Result { + exact_fields( + value, + &[ + "schema", + "operation_coordinate", + "obstruction_coordinate", + "authority_profile_identity", + "semantic_closure", + "program", + "budget_ceiling", + "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", + ], + )?; + require_text(value, "schema", "echo.operation-package/v2")?; + coordinate(field(value, "operation_coordinate")?)?; + coordinate(field(value, "obstruction_coordinate")?)?; + hash(field(value, "authority_profile_identity")?)?; + let closure = field(value, "semantic_closure")?; + exact_fields( + closure, + &[ + "application_schema_coordinate", + "application_schema_identity", + "canonical_meaning_identity", + "core_identity", + "edict_source_identity", + "target_ir_identity", + ], + )?; + coordinate(field(closure, "application_schema_coordinate")?)?; + for name in [ + "application_schema_identity", + "canonical_meaning_identity", + "core_identity", + "edict_source_identity", + ] { + hash(field(closure, name)?)?; + } + let target_ir = hash(field(closure, "target_ir_identity")?)?; + let program_bytes = bytes(field(value, "program")?)?; + let program = decode_canonical_cbor_v1(program_bytes) + .map_err(|_| EvaluationError::InvalidArtifact)?; + exact_fields( + &program, + &[ + "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(&program, "schema", "echo.operation-program/v2")?; + require_text(&program, "kind", "edict-effect-program/v1")?; + if number(field(&program, "identity_law")?)? != 1 + || hash(field(&program, "target_ir_identity")?)? != target_ir + { + return Err(EvaluationError::InvalidArtifact); + } + for (name, label) in [ + ( + "target_profile_identity", + "echo.operation-target.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", + ), + ( + "interpreter_profile_identity", + "echo.operation-interpreter.edict-effect/v1", + ), + ( + "intrinsic_profile_identity", + "echo.operation-intrinsics.edict-effect/v1", + ), + ] { + let expected = domain_hash(b"echo:operation-profile:v1\0", label.as_bytes()); + if hash(field(value, name)?)? != expected { + return Err(EvaluationError::InvalidArtifact); + } + } + for name in ["interpreter_profile_identity", "intrinsic_profile_identity"] { + if hash(field(&program, name)?)? != hash(field(value, name)?)? { + return Err(EvaluationError::InvalidArtifact); + } + } + let schema = + EffectGraphSchema::from_canonical_bytes(bytes(field(&program, "graph_schema")?)?) + .map_err(|_| EvaluationError::InvalidArtifact)?; + let law_bytes = bytes(field(&program, "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:v2\0"); + footprint.update(law_bytes); + if hash(field(value, "footprint_contract_identity")?)? != *footprint.finalize().as_bytes() { + return Err(EvaluationError::InvalidArtifact); + } + let max_input_bytes = number(field(&program, "max_input_bytes")?)?; + let max_result_bytes = number(field(&program, "max_result_bytes")?)?; + let max_allocated_bytes = number(field(&program, "max_allocated_bytes")?)?; + if max_input_bytes > 65_536 + || max_result_bytes > 65_536 + || max_allocated_bytes > EFFECT_READ_STORAGE_CEILING_V1 + { + return Err(EvaluationError::InvalidArtifact); + } + if coordinate(field(&program, "output_type")?)?.len() > 1024 { + return Err(EvaluationError::InvalidArtifact); + } + let budget = field(value, "budget_ceiling")?; + exact_fields(budget, &["steps", "read_bytes", "write_bytes"])?; + let budget = EchoOperationBudgetV1::new( + number(field(budget, "steps")?)?, + number(field(budget, "read_bytes")?)?, + number(field(budget, "write_bytes")?)?, + ); + if budget.steps() == 0 { + return Err(EvaluationError::InvalidArtifact); + } + Ok(Self { + canonical: canonical.to_vec(), + program_identity: domain_hash(b"echo:operation-program:v2\0", program_bytes), + compiled: bytes(field(&program, "compiled_program")?)?.to_vec(), + schema, + law, + output_type: coordinate(field(&program, "output_type")?)?.to_owned(), + max_input_bytes, + max_result_bytes, + max_allocated_bytes, + budget, + }) + } +} +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) +} +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/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs new file mode 100644 index 00000000..4e16f5e8 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! V2 byte binding and executable-law validation, before installation. +#![cfg(feature = "trusted_runtime")] +#![allow(clippy::expect_used, clippy::panic)] +use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V}; +use warp_core::edict_effects::{EffectPackageError, ExecutableOperationPackageV2}; +use warp_core::TypeId; +fn map(fields: impl IntoIterator) -> V { + V::Map( + fields + .into_iter() + .map(|(k, v)| (V::Text(k.into()), v)) + .collect(), + ) +} +fn text(s: &str) -> V { + V::Text(s.into()) +} +fn id(n: u8) -> V { + V::Bytes(vec![n; 32]) +} +fn encoded(v: &V) -> Vec { + encode(v).expect("fixture") +} +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() +} +fn profile(label: &str) -> V { + V::Bytes(hash(b"echo:operation-profile:v1\0", label.as_bytes()).to_vec()) +} +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![])), + ]) +} +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)), + ]) +} +fn footprint(v: &V) -> V { + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-footprint-law:v2\0"); + h.update(&encoded(v)); + V::Bytes(h.finalize().as_bytes().to_vec()) +} +fn program() -> V { + map([ + ("schema", text("echo.operation-program/v2")), + ("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(vec![0xa0])), + ("target_ir_identity", id(4)), + ("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")), + ]) +} +fn package(program: &V) -> V { + map([ + ("schema", text("echo.operation-package/v2")), + ("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", id(9)), + ("canonical_meaning_identity", id(3)), + ("core_identity", id(2)), + ("edict_source_identity", id(1)), + ("target_ir_identity", id(4)), + ]), + ), + ("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())), + ]) +} +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; +} +fn add(v: &mut V, k: &str, x: V) { + let V::Map(m) = v else { panic!("map") }; + m.push((text(k), x)); +} +fn prepare(v: &V) -> Result { + let b = encoded(v); + let expected = hash(b"echo:operation-package:v2\0", &b); + ExecutableOperationPackageV2::prepare(&b, expected) +} +fn refused(v: &V) { + assert!( + prepare(v).is_err(), + "must refuse malformed or inconsistent package" + ); +} +#[test] +fn exact_v2_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.canonical_bytes(), b); + assert_eq!( + package.program_identity(), + hash(b"echo:operation-program:v2\0", &encoded(&p)) + ); + assert!(package + .graph_schema() + .allows_atom(TypeId([1; 32]), TypeId([2; 32]))); + assert!(!package + .graph_schema() + .allows_atom(TypeId([2; 32]), TypeId([1; 32]))); + assert_eq!(package.compiled_program(), [0xa0]); +} +#[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:v2\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!(ExecutableOperationPackageV2::prepare(&encoded(&v), pin).is_err()); + } + assert!(ExecutableOperationPackageV2::prepare( + &original, + hash(b"echo:operation-package:v1\0", &original) + ) + .is_err()); +} +#[test] +fn exact_package_and_closure_shapes_refuse_v1_lawpacks_projection_and_extra_fields() { + for (k, x) in [ + ("schema", text("echo.operation-package/v1")), + ("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/v1")), + ("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:v2\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!( + ExecutableOperationPackageV2::prepare(&b, hash(b"echo:operation-package:v2\0", &b)) + .is_err() + ); +} +#[test] +fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_v1_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 v = package(&p); + set(&mut v, "operation_coordinate", text(&"x".repeat(1025))); + let prepared = prepare(&v).expect("zero byte limits and legacy package coordinate shape"); + assert_eq!(prepared.max_input_bytes(), 0); + assert_eq!(prepared.max_result_bytes(), 0); + assert_eq!(prepared.max_allocated_bytes(), 0); + assert_eq!(prepared.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(), + prepare(&package(&program())) + .expect("original") + .program_identity() + ); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 04dbeb6c..8d6d213c 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -654,3 +654,37 @@ 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. + +### V2 effect publication preparation + +`edict_effects::ExecutableOperationPackageV2::prepare` checks a trusted host's +expected V2 package identity against the exact canonical publication bytes. +This is byte binding and contract preparation, not independent compiler +verification, package admission, installation or permission to execute. + +The V2 package has the V1 required package fields with its versioned schema, +no `application_result_projection`, and a six-field semantic closure that omits +both lawpack keys. Its nested canonical program has exactly the thirteen +`edict-effect-program/v1` fields. The decoder requires the effect target, +interpreter, intrinsic, input, result, obstruction, interpretation and basis +profiles, equality of the closure/program Target IR identities, and +ContentAddressV1 as the identity law. It decodes and retains the exact program's +executable graph schema and footprint permissions together. The footprint +contract is BLAKE3 over `echo:operation-footprint-law:v2\0` followed directly by +the exact canonical law bytes. Package/program identities use their V2 domains +and the existing length-prefixed artifact recipe; those recipes are distinct. + +Input and result declarations permit zero and are capped at 65,536 bytes. +Allocation permits zero and is capped at 16 MiB; these declarations do not +provision a physical arena. The output coordinate is nonempty UTF-8 of at most +1,024 bytes. Other package/closure coordinates retain V1's nonempty-text rule. +The package budget requires at least one step and permits zero byte budgets. +Canonical preparation uses the existing decoder's node/depth ceilings and the +schema/law preparation bounds. It occurs outside evaluation metering. + +The compiler bytes and upstream source, meaning, Core and Target IR identities +remain publication evidence. This preparation does not infer verifier approval +or validate the compiled instruction language. Invocation/authority admission, +interpreter execution, graph identity enforcement, atomic effects and retained +recovery must separately consume and validate this prepared contract. V1 +package decoding and identity recipes remain unchanged. From d163d06b9bfb8578bcc38af5667b31d1cddf7fd5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:17:21 -0700 Subject: [PATCH 13/43] Bind V2 effect invocation input to its package and evaluation basis --- CHANGELOG.md | 5 + crates/warp-core/src/echo_operation.rs | 4 +- crates/warp-core/src/edict_effects.rs | 2 + .../warp-core/src/edict_effects/invocation.rs | 135 ++++++++++++ crates/warp-core/src/edict_effects/package.rs | 19 +- .../tests/edict_effect_package_tests.rs | 2 + .../edict_effect_package_tests/invocation.rs | 200 ++++++++++++++++++ docs/topics/GeneratedRules.md | 21 ++ 8 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/invocation.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests/invocation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f43a8d..833f32b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- 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. + - V2 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 diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 60acfdcf..3ec64642 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -2968,7 +2968,9 @@ impl EchoOperationEvaluationBasisV1 { ]) } - fn from_value(value: CanonicalValueV1) -> Result { + pub(crate) fn from_value( + value: CanonicalValueV1, + ) -> Result { let mut fields = exact_text_map( value, &[ diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 254d45c4..e160422e 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,6 +9,8 @@ //! Completed reads can be exported as bounded canonical images. Authored effect //! instructions, package-authenticated policy, receipt/WAL integration, a physical //! arena and admission still precede a shipped effect profile. +mod invocation; +pub use invocation::EffectInvocationV2; mod package; pub use package::{EffectPackageError, ExecutableOperationPackageV2}; mod frame; diff --git a/crates/warp-core/src/edict_effects/invocation.rs b/crates/warp-core/src/edict_effects/invocation.rs new file mode 100644 index 00000000..b3c11e4e --- /dev/null +++ b/crates/warp-core/src/edict_effects/invocation.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Static V2 input/basis binding, without installed authority or freshness. +use super::package::{domain_hash, hash}; +use super::{EffectPackageError, ExecutableOperationPackageV2}; +use crate::edict_pure::{ + values::{bytes, exact_fields, field, number, require_text, text}, + EvaluationError, +}; +use crate::{EchoOperationBudgetV1, EchoOperationEvaluationBasisV1}; +use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; +/// Prepared invocation bytes, not an admitted Action or verified grant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectInvocationV2 { + canonical: Vec, + input: Vec, + basis: EchoOperationEvaluationBasisV1, + grant: [u8; 32], + identity: [u8; 32], + budget: EchoOperationBudgetV1, +} +impl EffectInvocationV2 { + /// Checks canonical invocation input against its pinned package contract. + /// + /// This checks static byte/basis consistency. Scheduler admission must still + /// verify the authority grant and installation, and evaluation must resolve + /// the retained parent against the actual runtime frontier. + /// # Errors + /// Refuses malformed or inconsistent input, budget, package and basis fields. + pub fn prepare( + bytes: &[u8], + package: &ExecutableOperationPackageV2, + ) -> Result { + let value = + decode_canonical_cbor_v1(bytes).map_err(|_| EffectPackageError::InvalidInvocation)?; + Self::parse(&value, bytes, package).map_err(|_| EffectPackageError::InvalidInvocation) + } + fn parse( + value: &Value, + canonical: &[u8], + package: &ExecutableOperationPackageV2, + ) -> Result { + exact_fields( + value, + &[ + "schema", + "authority_grant_identity", + "delegated_budget", + "evaluation_basis", + "operation_coordinate", + "package_id", + "application_input_bytes", + ], + )?; + require_text(value, "schema", "echo.operation-invocation/v2")?; + if hash(field(value, "package_id")?)? != package.identity() + || text(field(value, "operation_coordinate")?)? != package.operation_coordinate() + { + return Err(EvaluationError::InvalidArtifact); + } + let input = bytes(field(value, "application_input_bytes")?)?; + if u64::try_from(input.len()).map_err(|_| EvaluationError::InvalidArtifact)? + > package.max_input_bytes() + { + return Err(EvaluationError::InvalidArtifact); + } + decode_canonical_cbor_v1(input).map_err(|_| EvaluationError::InvalidArtifact)?; + let basis = + EchoOperationEvaluationBasisV1::from_value(field(value, "evaluation_basis")?.clone()) + .map_err(|_| EvaluationError::InvalidArtifact)?; + if basis.application_basis().schema_identity() + != domain_hash( + b"echo:operation-profile:v1\0", + b"echo.operation.basis.edict-effect/v1", + ) + || basis.application_basis().value_identity() + != domain_hash(b"echo:operation-edict-effect-basis:v1\0", input) + { + return Err(EvaluationError::InvalidArtifact); + } + let budget = field(value, "delegated_budget")?; + exact_fields(budget, &["steps", "read_bytes", "write_bytes"])?; + let budget = EchoOperationBudgetV1::new( + number(field(budget, "steps")?)?, + number(field(budget, "read_bytes")?)?, + number(field(budget, "write_bytes")?)?, + ); + let ceiling = package.budget_ceiling(); + if budget.steps() == 0 + || budget.steps() > ceiling.steps() + || budget.read_bytes() > ceiling.read_bytes() + || budget.write_bytes() > ceiling.write_bytes() + { + return Err(EvaluationError::InvalidArtifact); + } + Ok(Self { + canonical: canonical.to_vec(), + input: input.to_vec(), + basis, + grant: hash(field(value, "authority_grant_identity")?)?, + identity: domain_hash(b"echo:operation-invocation:v2\0", canonical), + budget, + }) + } + /// Requested resource allowance, checked only against the publication ceiling. + #[must_use] + pub const fn delegated_budget(&self) -> EchoOperationBudgetV1 { + self.budget + } + /// Exact canonical invocation bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical + } + /// Exact application input whose digest is retained in the basis. + #[must_use] + pub fn application_input_bytes(&self) -> &[u8] { + &self.input + } + /// Claimed exact V1 parent basis; freshness has not been established. + #[must_use] + pub const fn evaluation_basis(&self) -> EchoOperationEvaluationBasisV1 { + self.basis + } + /// Claimed grant identity; no grant authority is conferred by decoding it. + #[must_use] + pub const fn authority_grant_identity(&self) -> [u8; 32] { + self.grant + } + /// Identity of exact invocation bytes under the distinct V2 domain. + #[must_use] + pub const fn identity(&self) -> [u8; 32] { + self.identity + } +} diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index 178391c7..8ef412e4 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -18,11 +18,15 @@ pub enum EffectPackageError { /// The publication has malformed or inconsistent fields or profiles. #[error("invalid effect package")] InvalidPackage, + /// The invocation has malformed or inconsistent package, input or basis fields. + #[error("invalid effect invocation")] + InvalidInvocation, } /// Prepared publication, without installation or mutation authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExecutableOperationPackageV2 { canonical: Vec, + operation_coordinate: String, program_identity: [u8; 32], compiled: Vec, schema: EffectGraphSchema, @@ -50,6 +54,16 @@ impl ExecutableOperationPackageV2 { pub fn canonical_bytes(&self) -> &[u8] { &self.canonical } + /// Identity of these exact publication bytes under the V2 package domain. + #[must_use] + pub fn identity(&self) -> [u8; 32] { + domain_hash(b"echo:operation-package:v2\0", &self.canonical) + } + /// Application-owned operation coordinate named by this publication. + #[must_use] + pub fn operation_coordinate(&self) -> &str { + &self.operation_coordinate + } /// Identity of the exact nested program bytes under the V2 domain. #[must_use] pub const fn program_identity(&self) -> [u8; 32] { @@ -262,6 +276,7 @@ impl ExecutableOperationPackageV2 { } Ok(Self { canonical: canonical.to_vec(), + operation_coordinate: coordinate(field(value, "operation_coordinate")?)?.to_owned(), program_identity: domain_hash(b"echo:operation-program:v2\0", program_bytes), compiled: bytes(field(&program, "compiled_program")?)?.to_vec(), schema, @@ -274,7 +289,7 @@ impl ExecutableOperationPackageV2 { }) } } -fn hash(value: &Value) -> Result<[u8; 32], EvaluationError> { +pub(super) fn hash(value: &Value) -> Result<[u8; 32], EvaluationError> { bytes(value)? .try_into() .map_err(|_| EvaluationError::InvalidArtifact) @@ -286,7 +301,7 @@ fn coordinate(value: &Value) -> Result<&str, EvaluationError> { } Ok(value) } -fn domain_hash(domain: &[u8], bytes: &[u8]) -> [u8; 32] { +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()); diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 4e16f5e8..7ea2cb82 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -406,3 +406,5 @@ fn a_new_pin_can_bind_changed_laws_but_cannot_reuse_the_old_contract_identity() .program_identity() ); } +#[path = "edict_effect_package_tests/invocation.rs"] +mod invocation; 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 00000000..b55f2731 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Invocation byte/basis binding without runtime authority admission. +use super::*; +use warp_core::edict_effects::EffectInvocationV2; +fn invocation(input: Vec) -> V { + let package_bytes = encoded(&package(&program())); + map([ + ("schema", text("echo.operation-invocation/v2")), + ("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:v2\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 { + EffectInvocationV2::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 V2 invocation"); + assert_eq!(prepared.canonical_bytes(), bytes); + assert_eq!( + prepared.identity(), + hash(b"echo:operation-invocation:v2\0", &bytes) + ); + assert_eq!(prepared.application_input_bytes(), encoded(&V::Integer(42))); + 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/v1")), + ("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", "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(selected.identity().to_vec()), + ); + assert_eq!( + EffectInvocationV2::prepare(&encoded(&value), &selected).is_ok(), + accepts + ); + } + let mut bytes = encoded(&invocation(encoded(&V::Integer(42)))); + bytes.push(0); + assert!( + EffectInvocationV2::prepare(&bytes, &prepare(&package(&program())).expect("original")) + .is_err() + ); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 8d6d213c..68377559 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -688,3 +688,24 @@ or validate the compiled instruction language. Invocation/authority admission, interpreter execution, graph identity enforcement, atomic effects and retained recovery must separately consume and validate this prepared contract. V1 package decoding and identity recipes remain unchanged. + +### V2 invocation input and basis preparation + +`EffectInvocationV2::prepare` checks an exact canonical V2 invocation against a +prepared V2 publication. Only its seven specified fields are accepted. The +package identity and operation coordinate must match the publication. The +application input must itself be canonical CBOR and fit that program's declared +input ceiling, including the input's CBOR framing bytes. The delegated budget +requires at least one step and cannot exceed any package budget axis. + +The invocation retains the existing V1 evaluation-basis map. Its application +basis schema is the effect-basis profile, and its application basis value is +BLAKE3 over `echo:operation-edict-effect-basis:v1\0`, the input byte length as a +little-endian u64, and the exact input bytes. An input change requires a new +basis digest and changes the versioned V2 invocation identity. The decoder +reuses V1's basis parser without changing its encoding or identity recipe. + +This is static preparation. It retains a claimed grant identity and parent; +it does not verify a grant, establish installation, resolve the current +frontier or admit an Action. Runtime-owned authority checks and evaluation at +the named parent remain required before effects can be accepted. From fb53a3381d042c8ba6a4f7c0b6a80b3394d9003b Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:24:17 -0700 Subject: [PATCH 14/43] Admit V2 effect publications under runtime-owner package policy --- .github/workflows/ci.yml | 9 +- CHANGELOG.md | 5 + crates/warp-core/Cargo.toml | 4 + crates/warp-core/src/edict_effects.rs | 8 + .../warp-core/src/edict_effects/admission.rs | 127 ++++++++++ crates/warp-core/src/edict_effects/package.rs | 7 + crates/warp-core/src/trusted_runtime_host.rs | 25 ++ .../tests/common/edict_effect_publication.rs | 181 +++++++++++++++ .../tests/edict_effect_admission_tests.rs | 216 ++++++++++++++++++ .../tests/edict_effect_package_tests.rs | 180 +-------------- docs/topics/GeneratedRules.md | 26 +++ 11 files changed, 608 insertions(+), 180 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/admission.rs create mode 100644 crates/warp-core/tests/common/edict_effect_publication.rs create mode 100644 crates/warp-core/tests/edict_effect_admission_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b120a9e2..8bc5983b 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 @@ -376,8 +376,9 @@ 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 - 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 833f32b7..a1187ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- Runtime-owner V2 package admission checks independently pinned publication, + operation, authority profile and budget policy, retaining distinct versioned + admission evidence. 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 diff --git a/crates/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 5dc85fa7..0501b156 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -253,3 +253,7 @@ 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"] diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index e160422e..4d336164 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,6 +9,14 @@ //! Completed reads can be exported as bounded canonical images. Authored effect //! instructions, package-authenticated policy, receipt/WAL integration, a physical //! arena and admission still precede a shipped effect profile. +#[cfg(feature = "native_rule_bootstrap")] +mod admission; +#[cfg(feature = "native_rule_bootstrap")] +pub(crate) use admission::admit as admit_effect_package_v2; +#[cfg(feature = "native_rule_bootstrap")] +pub use admission::{ + AdmittedEffectPackageV2, EffectAdmissionError, EffectPackageAdmissionPolicyV2, +}; mod invocation; pub use invocation::EffectInvocationV2; mod package; diff --git a/crates/warp-core/src/edict_effects/admission.rs b/crates/warp-core/src/edict_effects/admission.rs new file mode 100644 index 00000000..e239da04 --- /dev/null +++ b/crates/warp-core/src/edict_effects/admission.rs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owner package policy evidence, separate from installation. +use super::{EffectPackageError, ExecutableOperationPackageV2}; +use crate::EchoOperationBudgetV1; +/// Independently selected package-admission pins and budget ceiling. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectPackageAdmissionPolicyV2 { + pin: [u8; 32], + operation: String, + authority: [u8; 32], + maximum: EchoOperationBudgetV1, +} +impl EffectPackageAdmissionPolicyV2 { + /// Pins exact publication bytes, operation, authority profile and budget. + #[must_use] + pub fn exact( + pin: [u8; 32], + operation: impl Into, + authority: [u8; 32], + maximum: EchoOperationBudgetV1, + ) -> Self { + Self { + pin, + operation: operation.into(), + authority, + maximum, + } + } + /// Policy identity using the existing V1 policy recipe and V2 package pin. + #[must_use] + pub fn identity(&self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-admission-policy:v1\0"); + h.update(&self.pin); + h.update(&(self.operation.len() as u64).to_le_bytes()); + h.update(self.operation.as_bytes()); + h.update(&self.authority); + h.update(&self.maximum.steps().to_le_bytes()); + h.update(&self.maximum.read_bytes().to_le_bytes()); + h.update(&self.maximum.write_bytes().to_le_bytes()); + h.finalize().into() + } +} +/// Stable V2 package-admission refusal category. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum EffectAdmissionError { + /// Exact bytes differ from the independent package pin. + #[error("effect package identity differs from policy")] + PackageIdentityMismatch, + /// Canonical artifact structure or profile bindings are invalid. + #[error("invalid effect publication")] + ArtifactInvalid, + /// The package operation coordinate differs from policy. + #[error("effect operation differs from policy")] + OperationCoordinateMismatch, + /// The package authority profile differs from policy. + #[error("effect authority profile differs from policy")] + AuthorityProfileMismatch, + /// The publication requires more resources than policy permits. + #[error("effect package budget exceeds policy")] + BudgetExceedsPolicy, +} +/// Opaque evidence of runtime-owner admission, without installation or a grant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AdmittedEffectPackageV2 { + package: ExecutableOperationPackageV2, + policy_id: [u8; 32], + id: [u8; 32], + policy: EffectPackageAdmissionPolicyV2, +} +impl AdmittedEffectPackageV2 { + /// Independent policy retained alongside its identity. + #[must_use] + pub const fn admission_policy(&self) -> &EffectPackageAdmissionPolicyV2 { + &self.policy + } + /// Exact publication admitted by the runtime owner. + #[must_use] + pub const fn package(&self) -> &ExecutableOperationPackageV2 { + &self.package + } + /// Identity of the independently supplied admission policy. + #[must_use] + pub const fn admission_policy_identity(&self) -> [u8; 32] { + self.policy_id + } + /// Admission identity under the distinct V2 domain. + #[must_use] + pub const fn identity(&self) -> [u8; 32] { + self.id + } +} +pub(crate) fn admit( + policy: &EffectPackageAdmissionPolicyV2, + bytes: Vec, +) -> Result { + let package = + ExecutableOperationPackageV2::prepare(&bytes, policy.pin).map_err(|error| match error { + EffectPackageError::IdentityMismatch => EffectAdmissionError::PackageIdentityMismatch, + _ => EffectAdmissionError::ArtifactInvalid, + })?; + if package.operation_coordinate() != policy.operation { + return Err(EffectAdmissionError::OperationCoordinateMismatch); + } + if package.authority_profile_identity() != policy.authority { + return Err(EffectAdmissionError::AuthorityProfileMismatch); + } + let budget = package.budget_ceiling(); + if budget.steps() > policy.maximum.steps() + || budget.read_bytes() > policy.maximum.read_bytes() + || budget.write_bytes() > policy.maximum.write_bytes() + { + return Err(EffectAdmissionError::BudgetExceedsPolicy); + } + let policy_id = policy.identity(); + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-package-admission:v2\0"); + h.update(&package.identity()); + h.update(&policy_id); + Ok(AdmittedEffectPackageV2 { + package, + policy_id, + id: h.finalize().into(), + policy: policy.clone(), + }) +} diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index 8ef412e4..f61863f2 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -27,6 +27,7 @@ pub enum EffectPackageError { pub struct ExecutableOperationPackageV2 { canonical: Vec, operation_coordinate: String, + authority_profile_identity: [u8; 32], program_identity: [u8; 32], compiled: Vec, schema: EffectGraphSchema, @@ -59,6 +60,11 @@ impl ExecutableOperationPackageV2 { pub fn identity(&self) -> [u8; 32] { domain_hash(b"echo:operation-package:v2\0", &self.canonical) } + /// Claimed authority profile, checked independently by runtime-owner policy. + #[must_use] + pub const fn authority_profile_identity(&self) -> [u8; 32] { + self.authority_profile_identity + } /// Application-owned operation coordinate named by this publication. #[must_use] pub fn operation_coordinate(&self) -> &str { @@ -276,6 +282,7 @@ impl ExecutableOperationPackageV2 { } Ok(Self { canonical: canonical.to_vec(), + authority_profile_identity: hash(field(value, "authority_profile_identity")?)?, operation_coordinate: coordinate(field(value, "operation_coordinate")?)?.to_owned(), program_identity: domain_hash(b"echo:operation-program:v2\0", program_bytes), compiled: bytes(field(&program, "compiled_program")?)?.to_vec(), diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 60b7af08..1c198d31 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -1095,6 +1095,31 @@ impl TrustedRuntimeHost { .copied() } + /// Admits V2 effect publication bytes under independently supplied owner policy. + /// + /// This returns opaque policy evidence. It neither installs the program nor + /// validates its compiled instruction language or authorizes an invocation. + /// Application-facing handles cannot call this runtime-owner method: + /// + /// ```compile_fail,E0599 + /// use warp_core::{TrustedRuntimeApp, edict_effects::EffectPackageAdmissionPolicyV2}; + /// fn application_cannot_admit( + /// app: &TrustedRuntimeApp<'_>, policy: &EffectPackageAdmissionPolicyV2, bytes: Vec, + /// ) { + /// app.admit_echo_operation_package_v2(policy, bytes); + /// } + /// ``` + pub fn admit_echo_operation_package_v2( + &self, + policy: &crate::edict_effects::EffectPackageAdmissionPolicyV2, + canonical_package_bytes: Vec, + ) -> Result< + crate::edict_effects::AdmittedEffectPackageV2, + crate::edict_effects::EffectAdmissionError, + > { + crate::edict_effects::admit_effect_package_v2(policy, canonical_package_bytes) + } + /// Admits exact executable-operation package bytes under independent policy. /// /// A successful result is an opaque admission token. It does not install 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 00000000..5fb61aec --- /dev/null +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -0,0 +1,181 @@ +// 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, ExecutableOperationPackageV2}; +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:v2\0"); + h.update(&encoded(v)); + V::Bytes(h.finalize().as_bytes().to_vec()) +} +pub fn program() -> V { + map([ + ("schema", text("echo.operation-program/v2")), + ("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(vec![0xa0])), + ("target_ir_identity", id(4)), + ("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/v2")), + ("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", id(9)), + ("canonical_meaning_identity", id(3)), + ("core_identity", id(2)), + ("edict_source_identity", id(1)), + ("target_ir_identity", id(4)), + ]), + ), + ("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:v2\0", &b); + ExecutableOperationPackageV2::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/edict_effect_admission_tests.rs b/crates/warp-core/tests/edict_effect_admission_tests.rs new file mode 100644 index 00000000..ac128718 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -0,0 +1,216 @@ +// 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::edict_effects::{EffectAdmissionError, EffectPackageAdmissionPolicyV2}; +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, +) -> EffectPackageAdmissionPolicyV2 { + EffectPackageAdmissionPolicyV2::exact( + hash(b"echo:operation-package:v2\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_v2(&policy, bytes.clone()) + .expect("exact policy admits"); + assert_eq!(admitted.package().canonical_bytes(), bytes); + assert_eq!( + admitted.package().identity(), + hash(b"echo:operation-package:v2\0", &bytes) + ); + let mut oracle = blake3::Hasher::new(); + oracle.update(b"echo:operation-admission-policy:v1\0"); + oracle.update(&hash(b"echo:operation-package:v2\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:v2\0"); + id.update(&hash(b"echo:operation-package:v2\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), + EffectAdmissionError::OperationCoordinateMismatch, + ), + ( + "app.update@1", + [9; 32], + EchoOperationBudgetV1::new(1, 0, 0), + EffectAdmissionError::AuthorityProfileMismatch, + ), + ( + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(0, 0, 0), + EffectAdmissionError::BudgetExceedsPolicy, + ), + ] { + assert_eq!( + host.admit_echo_operation_package_v2( + &policy(&bytes, operation, authority, budget), + bytes.clone() + ) + .expect_err("independent policy refuses"), + 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/v1")); + let bytes = encoded(&value); + assert_eq!( + host.admit_echo_operation_package_v2( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0) + ), + bytes + ) + .expect_err("pinned but unsupported"), + EffectAdmissionError::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_v2(&pin, bytes) + .expect_err("wrong pin"), + EffectAdmissionError::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_v2( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0) + ), + bytes + ) + .expect_err("ceiling exceeds policy"), + EffectAdmissionError::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_v2(&narrow, bytes.clone()) + .expect("narrow policy"); + let second = host + .admit_echo_operation_package_v2(&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:v1\0"); + wrong_domain.update(&first.package().identity()); + wrong_domain.update(&first.admission_policy_identity()); + assert_ne!(first.identity(), *wrong_domain.finalize().as_bytes()); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 7ea2cb82..0a125470 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -3,184 +3,12 @@ //! V2 byte binding and executable-law validation, before installation. #![cfg(feature = "trusted_runtime")] #![allow(clippy::expect_used, clippy::panic)] -use echo_edict_canonical::{encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V}; +use echo_edict_canonical::CanonicalValueV1 as V; use warp_core::edict_effects::{EffectPackageError, ExecutableOperationPackageV2}; use warp_core::TypeId; -fn map(fields: impl IntoIterator) -> V { - V::Map( - fields - .into_iter() - .map(|(k, v)| (V::Text(k.into()), v)) - .collect(), - ) -} -fn text(s: &str) -> V { - V::Text(s.into()) -} -fn id(n: u8) -> V { - V::Bytes(vec![n; 32]) -} -fn encoded(v: &V) -> Vec { - encode(v).expect("fixture") -} -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() -} -fn profile(label: &str) -> V { - V::Bytes(hash(b"echo:operation-profile:v1\0", label.as_bytes()).to_vec()) -} -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![])), - ]) -} -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)), - ]) -} -fn footprint(v: &V) -> V { - let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-footprint-law:v2\0"); - h.update(&encoded(v)); - V::Bytes(h.finalize().as_bytes().to_vec()) -} -fn program() -> V { - map([ - ("schema", text("echo.operation-program/v2")), - ("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(vec![0xa0])), - ("target_ir_identity", id(4)), - ("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")), - ]) -} -fn package(program: &V) -> V { - map([ - ("schema", text("echo.operation-package/v2")), - ("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", id(9)), - ("canonical_meaning_identity", id(3)), - ("core_identity", id(2)), - ("edict_source_identity", id(1)), - ("target_ir_identity", id(4)), - ]), - ), - ("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())), - ]) -} -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; -} -fn add(v: &mut V, k: &str, x: V) { - let V::Map(m) = v else { panic!("map") }; - m.push((text(k), x)); -} -fn prepare(v: &V) -> Result { - let b = encoded(v); - let expected = hash(b"echo:operation-package:v2\0", &b); - ExecutableOperationPackageV2::prepare(&b, expected) -} -fn refused(v: &V) { - assert!( - prepare(v).is_err(), - "must refuse malformed or inconsistent package" - ); -} +#[path = "common/edict_effect_publication.rs"] +mod fixture; +use fixture::*; #[test] fn exact_v2_package_binds_program_and_executes_its_schema_relations() { let p = program(); diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 68377559..4d79c1ac 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -709,3 +709,29 @@ This is static preparation. It retains a claimed grant identity and parent; it does not verify a grant, establish installation, resolve the current frontier or admit an Action. Runtime-owned authority checks and evaluation at the named parent remain required before effects can be accepted. + +### Runtime-owner V2 package policy admission + +`TrustedRuntimeHost::admit_echo_operation_package_v2` crosses the independent +package-policy boundary. Its `EffectPackageAdmissionPolicyV2` pins the exact +V2 publication, operation coordinate, authority profile and maximum budget. +The host prepares the canonical package, checks all four independent policy +conditions, and returns opaque `AdmittedEffectPackageV2` evidence. Every budget +axis is checked separately. A matching package pin alone cannot substitute for +canonical artifact/profile validation or for the independent authority policy. + +The evidence retains the exact package and policy. Policy identity uses the +existing `echo:operation-admission-policy:v1\0` recipe with the V2 package pin. +The package-admission identity uses `echo:operation-package-admission:v2\0` +followed by the package and policy identities. Two policies admitting the same +program therefore produce distinct policy/admission evidence while retaining +the same package identity. V1 identities and policy admission are unchanged. + +This method is exposed only on the runtime-owner host, under its existing +`native_rule_bootstrap` and `trusted_runtime` feature boundary. The restricted +application handle cannot admit a package. The opaque token has no public +constructor and is not an installation, invocation grant or scheduler Action. +Compiled effect instruction validation, V2 installation, grant admission, +evaluation and retained recovery remain necessary before this program can +execute. This policy crossing does not infer compiler/verifier approval from +publication identities or confer graph mutation authority. From 9a6a501871347130dccc62cc44705a193de7c73a Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:40:49 -0700 Subject: [PATCH 15/43] Install and recover V2 effect publications through the runtime WAL catalog --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 5 + crates/warp-core/src/echo_operation.rs | 12 +- crates/warp-core/src/edict_effects.rs | 23 +- .../warp-core/src/edict_effects/admission.rs | 47 ++ .../src/edict_effects/installation.rs | 188 +++++++ crates/warp-core/src/engine_impl.rs | 86 ++++ crates/warp-core/src/trusted_runtime_host.rs | 178 ++++++- .../tests/edict_effect_admission_tests.rs | 2 + .../installation.rs | 460 ++++++++++++++++++ docs/topics/GeneratedRules.md | 45 ++ 11 files changed, 1010 insertions(+), 37 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/installation.rs create mode 100644 crates/warp-core/tests/edict_effect_admission_tests/installation.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc5983b..4292dcf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -379,6 +379,7 @@ jobs: 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a1187ded..101c9a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- Runtime-owned V2 installation retains exact policy and package evidence in the + existing WAL catalog, restores it in a fresh filesystem-backed host, and shares + coordinate-conflict checks with V1. Conflicts and failed WAL appends publish no + V2 registry entry. Installation recovery does not authorize or execute effects. + - Runtime-owner V2 package admission checks independently pinned publication, operation, authority profile and budget policy, retaining distinct versioned admission evidence. Restricted application handles cannot admit packages. diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 3ec64642..5c9b2c98 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -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")?, @@ -7687,7 +7689,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() @@ -7842,7 +7844,7 @@ fn artifact_error( } } -fn invalid_structure(detail: impl Into) -> EchoOperationArtifactErrorV1 { +pub(crate) fn invalid_structure(detail: impl Into) -> EchoOperationArtifactErrorV1 { artifact_error(EchoOperationArtifactErrorKindV1::InvalidStructure, detail) } @@ -7867,7 +7869,7 @@ fn invocation_admission_error( } } -fn installation_error( +pub(crate) fn installation_error( kind: EchoOperationInstallationErrorKindV1, detail: impl Into, ) -> EchoOperationInstallationErrorV1 { diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 4d336164..a0a38aeb 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -1,22 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Trusted-host recording reads for the authored-effect interpreter under construction. +//! Authored-effect preparation, runtime-owned catalog and private recording reads. //! -//! This is not an installed program profile or admission capability. The host -//! selects the immutable frontier and policy. No mutation or caller callback is -//! available. Preparing the basis and policy is outside the per-read budget. -//! Optional typed laws authorize runtime-bound node, atom, portal and range reads. -//! Completed reads can be exported as bounded canonical images. Authored effect -//! instructions, package-authenticated policy, receipt/WAL integration, a physical -//! arena and admission still precede a shipped effect profile. +//! Exact V2 publications and static invocations can be prepared, and the runtime +//! owner can admit package policy and retain/install its catalog evidence. None +//! of those crossings grants invocation rights or enables effect execution. +//! Private frames bind immutable runtime frontiers, typed read laws and shared +//! logical meters. Compiled effect instructions, a physical evaluation arena, +//! derived mutations and Action/receipt/WAL decision recovery remain unfinished. #[cfg(feature = "native_rule_bootstrap")] mod admission; #[cfg(feature = "native_rule_bootstrap")] +mod installation; +#[cfg(feature = "native_rule_bootstrap")] pub(crate) use admission::admit as admit_effect_package_v2; #[cfg(feature = "native_rule_bootstrap")] pub use admission::{ AdmittedEffectPackageV2, EffectAdmissionError, EffectPackageAdmissionPolicyV2, }; +#[cfg(feature = "native_rule_bootstrap")] +pub use installation::InstalledEffectOperationV2; +#[cfg(feature = "native_rule_bootstrap")] +pub(crate) use installation::{ + install_recovered_v2, recover_versioned_installation, RecoveredOperationInstallation, +}; mod invocation; pub use invocation::EffectInvocationV2; mod package; diff --git a/crates/warp-core/src/edict_effects/admission.rs b/crates/warp-core/src/edict_effects/admission.rs index e239da04..c461534a 100644 --- a/crates/warp-core/src/edict_effects/admission.rs +++ b/crates/warp-core/src/edict_effects/admission.rs @@ -41,6 +41,53 @@ impl EffectPackageAdmissionPolicyV2 { h.update(&self.maximum.write_bytes().to_le_bytes()); h.finalize().into() } + pub(super) fn to_value(&self) -> echo_edict_canonical::CanonicalValueV1 { + use echo_edict_canonical::CanonicalValueV1 as V; + crate::echo_operation::map_value([ + ("expected_package_id", V::Bytes(self.pin.to_vec())), + ( + "expected_operation_coordinate", + V::Text(self.operation.clone()), + ), + ( + "expected_authority_profile_identity", + V::Bytes(self.authority.to_vec()), + ), + ("maximum_budget", self.maximum.to_value()), + ]) + } + pub(super) fn from_value( + value: &echo_edict_canonical::CanonicalValueV1, + ) -> Result { + use super::package::hash; + use crate::edict_pure::values::{exact_fields, field, text}; + let read = || { + exact_fields( + value, + &[ + "expected_package_id", + "expected_operation_coordinate", + "expected_authority_profile_identity", + "maximum_budget", + ], + )?; + Ok::<_, crate::edict_pure::EvaluationError>(( + hash(field(value, "expected_package_id")?)?, + text(field(value, "expected_operation_coordinate")?)?.to_owned(), + hash(field(value, "expected_authority_profile_identity")?)?, + field(value, "maximum_budget")?.clone(), + )) + }; + let (pin, operation, authority, budget) = read() + .map_err(|_| crate::echo_operation::invalid_structure("invalid V2 admission policy"))?; + let maximum = EchoOperationBudgetV1::from_value(budget)?; + if operation.is_empty() || maximum.steps() == 0 { + return Err(crate::echo_operation::invalid_structure( + "invalid V2 policy coordinate or budget", + )); + } + Ok(Self::exact(pin, operation, authority, maximum)) + } } /// Stable V2 package-admission refusal category. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs new file mode 100644 index 00000000..e8ea96ea --- /dev/null +++ b/crates/warp-core/src/edict_effects/installation.rs @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Versioned effect installation evidence; invocation authority remains separate. +use super::package::hash; +use super::{ + AdmittedEffectPackageV2, EffectPackageAdmissionPolicyV2, ExecutableOperationPackageV2, +}; +use crate::echo_operation::{ + installation_error, invalid_structure, map_value, recover_installation_v1, +}; +use crate::edict_pure::values::{bytes, exact_fields, field, require_text, text}; +use crate::{ + EchoOperationArtifactErrorV1, EchoOperationInstallationErrorKindV1, + EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, InstalledEchoOperationV1, +}; +use echo_edict_canonical::{ + decode_canonical_cbor_v1 as decode, encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V, +}; +use std::collections::BTreeMap; +/// Exact installation evidence; registry membership remains runtime-owned. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InstalledEffectOperationV2 { + admitted: AdmittedEffectPackageV2, + id: [u8; 32], +} +impl InstalledEffectOperationV2 { + pub(crate) fn from_admitted(admitted: AdmittedEffectPackageV2) -> Self { + let package = admitted.package(); + let mut h = blake3::Hasher::new(); + h.update(b"echo:operation-installation:v2\0"); + h.update(&package.identity()); + h.update(&admitted.identity()); + h.update(&(package.operation_coordinate().len() as u64).to_le_bytes()); + h.update(package.operation_coordinate().as_bytes()); + h.update(&package.program_identity()); + h.update(&(package.canonical_bytes().len() as u64).to_le_bytes()); + h.update(package.canonical_bytes()); + Self { + admitted, + id: h.finalize().into(), + } + } + /// Exact publication named by this installation. + #[must_use] + pub const fn package(&self) -> &ExecutableOperationPackageV2 { + self.admitted.package() + } + /// Versioned installation identity, without invocation rights. + #[must_use] + pub const fn identity(&self) -> [u8; 32] { + self.id + } + /// Canonical retained installation evidence. + /// # Errors + /// Refuses invalid canonical material. + pub fn retained_bytes(&self) -> Result, EchoOperationArtifactErrorV1> { + encode(&map_value([ + ("schema", V::Text("echo.operation-installation/v2".into())), + ( + "admission_policy", + self.admitted.admission_policy().to_value(), + ), + ( + "admission_policy_id", + V::Bytes(self.admitted.admission_policy_identity().to_vec()), + ), + ("package_id", V::Bytes(self.package().identity().to_vec())), + ( + "package_bytes", + V::Bytes(self.package().canonical_bytes().to_vec()), + ), + ( + "package_admission_id", + V::Bytes(self.admitted.identity().to_vec()), + ), + ("installed_operation_id", V::Bytes(self.id.to_vec())), + ])) + .map_err(|_| invalid_structure("invalid V2 installation encoding")) + } + /// Reconstructs exact retained evidence; this does not install it in an engine. + /// # Errors + /// Refuses altered policy, publication, identity or version evidence. + pub fn from_retained_bytes(canonical: &[u8]) -> Result { + let value = + decode(canonical).map_err(|_| invalid_structure("invalid V2 retained bytes"))?; + let read = || { + exact_fields( + &value, + &[ + "schema", + "admission_policy", + "admission_policy_id", + "package_id", + "package_bytes", + "package_admission_id", + "installed_operation_id", + ], + )?; + require_text(&value, "schema", "echo.operation-installation/v2")?; + Ok::<_, crate::edict_pure::EvaluationError>(( + field(&value, "admission_policy")?, + hash(field(&value, "admission_policy_id")?)?, + hash(field(&value, "package_id")?)?, + bytes(field(&value, "package_bytes")?)?, + hash(field(&value, "package_admission_id")?)?, + hash(field(&value, "installed_operation_id")?)?, + )) + }; + let (policy, policy_id, package_id, package_bytes, admission_id, installed_id) = + read().map_err(|_| invalid_structure("invalid V2 installation fields"))?; + let policy = EffectPackageAdmissionPolicyV2::from_value(policy)?; + let admitted = super::admission::admit(&policy, package_bytes.to_vec()) + .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; + if policy.identity() != policy_id + || admitted.package().identity() != package_id + || admitted.identity() != admission_id + { + return Err(invalid_structure("V2 retained admission identities differ")); + } + let installed = Self::from_admitted(admitted); + if installed.id != installed_id || installed.retained_bytes()? != canonical { + return Err(invalid_structure( + "V2 retained installation identity differs", + )); + } + Ok(installed) + } +} +pub(crate) enum RecoveredOperationInstallation { + V1(InstalledEchoOperationV1), + V2(InstalledEffectOperationV2), +} +impl RecoveredOperationInstallation { + pub(crate) fn package_identity(&self) -> [u8; 32] { + match self { + Self::V1(v) => v.package_id().as_hash(), + Self::V2(v) => v.package().identity(), + } + } +} +pub(crate) fn recover_versioned_installation( + canonical: &[u8], +) -> Result { + let value = + decode(canonical).map_err(|_| invalid_structure("invalid retained installation"))?; + match field(&value, "schema") + .and_then(text) + .map_err(|_| invalid_structure("missing installation version"))? + { + "echo.operation-installation/v1" => { + recover_installation_v1(canonical).map(RecoveredOperationInstallation::V1) + } + "echo.operation-installation/v2" => { + InstalledEffectOperationV2::from_retained_bytes(canonical) + .map(RecoveredOperationInstallation::V2) + } + _ => Err(invalid_structure("unsupported installation version")), + } +} +pub(crate) fn install_recovered_v2( + packages: &mut BTreeMap<[u8; 32], InstalledEffectOperationV2>, + operations: &mut BTreeMap, + v1_operations: &BTreeMap, + installed: InstalledEffectOperationV2, +) -> Result<(), EchoOperationInstallationErrorV1> { + let pin = installed.package().identity(); + let coordinate = installed.package().operation_coordinate(); + if let Some(existing) = packages.get(&pin) { + if existing == &installed { + return Ok(()); + } + return Err(installation_error( + EchoOperationInstallationErrorKindV1::PackageIdentityConflict, + "V2 installation policy conflicts with existing package", + )); + } + if v1_operations.contains_key(coordinate) + || operations.get(coordinate).is_some_and(|id| id != &pin) + { + return Err(installation_error( + EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, + "V2 installation operation coordinate already exists", + )); + } + operations.insert(coordinate.to_owned(), pin); + packages.insert(pin, installed); + Ok(()) +} diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 75bdf9b6..c51eaa29 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -456,6 +456,11 @@ pub struct Engine { installed_contract_packages: BTreeMap, installed_echo_operation_packages: BTreeMap, + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_packages: + BTreeMap, + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_operations_by_coordinate: BTreeMap, #[cfg_attr( not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) @@ -983,6 +988,10 @@ impl Engine { canonical_cmd_rules: Vec::new(), installed_contract_packages: BTreeMap::new(), installed_echo_operation_packages: BTreeMap::new(), + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_packages: BTreeMap::new(), + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_operations_by_coordinate: BTreeMap::new(), installed_echo_operations_by_coordinate: BTreeMap::new(), #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] installed_provider_contract_packages: BTreeMap::new(), @@ -1189,6 +1198,10 @@ impl Engine { canonical_cmd_rules: Vec::new(), installed_contract_packages: BTreeMap::new(), installed_echo_operation_packages: BTreeMap::new(), + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_packages: BTreeMap::new(), + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + installed_echo_effect_operations_by_coordinate: BTreeMap::new(), installed_echo_operations_by_coordinate: BTreeMap::new(), #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] installed_provider_contract_packages: BTreeMap::new(), @@ -1392,6 +1405,59 @@ impl Engine { Ok(prepared.record) } + /// Returns an installed V2 effect publication by its exact versioned pin. + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + #[must_use] + pub fn installed_echo_operation_package_v2( + &self, + package_id: Hash, + ) -> Option<&crate::edict_effects::InstalledEffectOperationV2> { + self.installed_echo_effect_packages.get(&package_id) + } + + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + pub(crate) fn preflight_echo_effect_installations( + &self, + recovered: &[crate::edict_effects::InstalledEffectOperationV2], + ) -> Result<(), EchoOperationInstallationErrorV1> { + let mut packages = self.installed_echo_effect_packages.clone(); + let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); + for installed in recovered { + crate::edict_effects::install_recovered_v2( + &mut packages, + &mut operations, + &self.installed_echo_operations_by_coordinate, + installed.clone(), + )?; + } + Ok(()) + } + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + pub(crate) fn restore_echo_effect_installations( + &mut self, + recovered: &[crate::edict_effects::InstalledEffectOperationV2], + ) -> Result<(), EchoOperationInstallationErrorV1> { + let mut packages = self.installed_echo_effect_packages.clone(); + let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); + for installed in recovered { + crate::edict_effects::install_recovered_v2( + &mut packages, + &mut operations, + &self.installed_echo_operations_by_coordinate, + installed.clone(), + )?; + } + self.installed_echo_effect_packages = packages; + self.installed_echo_effect_operations_by_coordinate = operations; + Ok(()) + } + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + pub(crate) fn installed_echo_effect_packages( + &self, + ) -> impl Iterator { + self.installed_echo_effect_packages.values() + } + /// Returns one installed executable operation package by exact package id. #[must_use] pub fn installed_echo_operation_package_v1( @@ -1412,6 +1478,16 @@ impl Engine { let mut packages = self.installed_echo_operation_packages.clone(); let mut operations = self.installed_echo_operations_by_coordinate.clone(); for installed in recovered { + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + if self + .installed_echo_effect_operations_by_coordinate + .contains_key(installed.operation_coordinate()) + { + return Err(crate::echo_operation::installation_error( + crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, + "V1 operation conflicts with installed V2 meaning", + )); + } install_recovered_v1(&mut packages, &mut operations, installed.clone())?; } Ok(()) @@ -1428,6 +1504,16 @@ impl Engine { let mut packages = self.installed_echo_operation_packages.clone(); let mut operations = self.installed_echo_operations_by_coordinate.clone(); for installed in recovered { + #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] + if self + .installed_echo_effect_operations_by_coordinate + .contains_key(installed.operation_coordinate()) + { + return Err(crate::echo_operation::installation_error( + crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, + "V1 operation conflicts with installed V2 meaning", + )); + } install_recovered_v1(&mut packages, &mut operations, installed.clone())?; } self.installed_echo_operation_packages = packages; diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 1c198d31..fd88a932 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -22,6 +22,10 @@ pub use observed_context::EchoOperationContextErrorV1; use crate::causal_anchor::prepare_causal_anchor_admission; use crate::coordinator::{SuperTickUndo, TicketedIngressStagingRollback}; +use crate::edict_effects::{ + install_recovered_v2, recover_versioned_installation, InstalledEffectOperationV2, + RecoveredOperationInstallation, +}; use crate::{ causal_wal::{ affected_frontiers_root, build_causal_anchor_admission_transaction, @@ -56,7 +60,7 @@ use crate::{ 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, + recover_action_outcome_v1, recover_committed_execution_receipt_v1, retain_action_outcome_v1, retain_committed_execution_v1, retain_installation_v1, validate_receipt_application_result_v1, validate_receipt_installation_v1, EchoOperationEvaluationAuthorityV1, SchedulerEchoOperationCandidateV1, @@ -517,6 +521,8 @@ pub struct TrustedRuntimeWalRecovery { pub causal_anchor_history: Vec, /// Exact executable meaning reconstructed from retained installation records. pub installed_echo_operations: Vec, + /// Exact V2 effect installations reconstructed from retained records. + pub installed_echo_effect_operations: Vec, /// Typed receipts reconstructed from executable-operation tick records. pub echo_operation_receipts: Vec, /// Typed executable-operation Action outcomes reconstructed from @@ -710,6 +716,7 @@ impl TrustedRuntimeWalRecovery { missing_runtime_state_deltas: &self.missing_runtime_state_deltas, causal_anchor_history: &self.causal_anchor_history, installed_echo_operations: &self.installed_echo_operations, + installed_echo_effect_operations: &self.installed_echo_effect_operations, echo_operation_receipts: &self.echo_operation_receipts, echo_operation_action_outcomes: &self.echo_operation_action_outcomes, }) @@ -1133,6 +1140,44 @@ impl TrustedRuntimeHost { admit_package_v1(policy, canonical_package_bytes) } + /// Installs an admitted V2 publication without granting invocation authority. + /// + /// With a WAL configured, the exact installation is durable before it becomes + /// visible in the engine. Application handles cannot call this owner method: + /// + /// ```compile_fail,E0599 + /// use warp_core::{TrustedRuntimeApp, edict_effects::AdmittedEffectPackageV2}; + /// fn application_cannot_install(app: &mut TrustedRuntimeApp<'_>, admitted: AdmittedEffectPackageV2) { + /// app.install_admitted_echo_operation_package_v2(admitted); + /// } + /// ``` + pub fn install_admitted_echo_operation_package_v2( + &mut self, + admitted: crate::edict_effects::AdmittedEffectPackageV2, + ) -> Result { + let installed = InstalledEffectOperationV2::from_admitted(admitted); + self.engine + .preflight_echo_effect_installations(core::slice::from_ref(&installed))?; + if self + .engine + .installed_echo_operation_package_v2(installed.package().identity()) + == Some(&installed) + { + return Ok(installed); + } + if let Some(wal) = self.runtime_wal.as_mut() { + let retained = installed.retained_bytes()?; + if let Err(error) = wal.record_executable_operation_installation(&retained) { + if !wal.recover_filesystem_effect_installation_after_error(&installed) { + return Err(error.into()); + } + } + } + self.engine + .restore_echo_effect_installations(core::slice::from_ref(&installed))?; + Ok(installed) + } + /// Installs exact executable meaning carried by an admitted operation package. /// /// Installation stores no native callback and still does not authorize an @@ -1489,6 +1534,8 @@ impl TrustedRuntimeHost { )?; self.engine .preflight_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; + self.engine + .preflight_echo_effect_installations(&recovery.installed_echo_effect_operations)?; let mut restored_runtime = forked_runtime; let mut restored_provenance = forked_provenance; @@ -1514,6 +1561,8 @@ impl TrustedRuntimeHost { self.engine .restore_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; + self.engine + .restore_echo_effect_installations(&recovery.installed_echo_effect_operations)?; self.echo_operation_action_outcomes = recovery .echo_operation_action_outcomes .iter() @@ -2390,6 +2439,20 @@ fn ensure_runtime_authority_is_durable( engine: &Engine, recovery: &TrustedRuntimeWalRecovery, ) -> Result<(), TrustedRuntimeWalError> { + let recovered_effects = recovery + .installed_echo_effect_operations + .iter() + .map(|installed| (installed.package().identity(), installed)) + .collect::>(); + if engine.installed_echo_effect_packages().any(|installed| { + recovered_effects + .get(&installed.package().identity()) + .is_none_or(|recovered| **recovered != *installed) + }) { + return Err(TrustedRuntimeWalError::RuntimeAuthorityNotDurable { + gap: RuntimeWalActivationGap::ExecutableOperationInstallation, + }); + } let recovered_operations = recovery .installed_echo_operations .iter() @@ -3080,6 +3143,7 @@ impl TrustedRuntimeWal { runtime_state.echo_operation_action_installation_order; let missing_runtime_state_deltas = runtime_state.missing_runtime_state_deltas; let installed_echo_operations = operation_material.installations; + let installed_echo_effect_operations = operation_material.effect_installations; let echo_operation_receipts = operation_material.receipts; validate_recovered_causal_parent_evidence(&witnessed_submissions, &receipt_correlations)?; validate_recovered_echo_operation_action_outcomes( @@ -3102,6 +3166,7 @@ impl TrustedRuntimeWal { missing_runtime_state_deltas: &missing_runtime_state_deltas, causal_anchor_history: &causal_anchor_history, installed_echo_operations: &installed_echo_operations, + installed_echo_effect_operations: &installed_echo_effect_operations, echo_operation_receipts: &echo_operation_receipts, echo_operation_action_outcomes: &echo_operation_action_outcomes, }, @@ -3117,6 +3182,7 @@ impl TrustedRuntimeWal { receipt_correlations, causal_anchor_history, installed_echo_operations, + installed_echo_effect_operations, echo_operation_receipts, echo_operation_action_outcomes, echo_operation_action_decisions, @@ -3431,6 +3497,21 @@ impl TrustedRuntimeWal { .flatten() } + fn recover_filesystem_effect_installation_after_error( + &mut self, + expected: &InstalledEffectOperationV2, + ) -> bool { + if !self.uses_filesystem_store() || self.refresh_cursor_from_store_for_writer().is_err() { + return false; + } + self.recover_read_only().is_ok_and(|recovery| { + recovery + .installed_echo_effect_operations + .iter() + .any(|installed| installed == expected) + }) + } + fn recover_filesystem_executable_operation_installation_after_error( &mut self, package_id: crate::EchoOperationPackageIdV1, @@ -3669,10 +3750,10 @@ impl TrustedRuntimeWal { &mut self, retained_installation_bytes: &[u8], ) -> Result { - let installed = recover_installation_v1(retained_installation_bytes)?; + let installed = recover_versioned_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( @@ -3681,7 +3762,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, )), ), @@ -4329,7 +4410,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( @@ -4562,6 +4643,7 @@ fn restore_provenance_entries( struct RecoveredEchoOperationMaterial { installations: Vec, + effect_installations: Vec, receipts: Vec, } @@ -4571,6 +4653,8 @@ fn recover_echo_operation_material( ) -> Result { let mut installations = BTreeMap::new(); let mut operations = BTreeMap::new(); + let mut effects = BTreeMap::new(); + let mut effect_operations = BTreeMap::new(); let mut receipts_by_digest = BTreeMap::new(); let mut receipts = Vec::new(); let provenance_by_coordinate = provenance_entries @@ -4582,7 +4666,20 @@ 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)?; + match installed { + RecoveredOperationInstallation::V1(installed) => { + if effect_operations.contains_key(installed.operation_coordinate()) { + return Err(crate::echo_operation::installation_error(crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict,"recovered V1 coordinate conflicts with V2 meaning").into()); + } + install_recovered_v1(&mut installations, &mut operations, installed)?; + } + RecoveredOperationInstallation::V2(installed) => install_recovered_v2( + &mut effects, + &mut effect_operations, + &operations, + installed, + )?, + } } WalTransactionKind::ExecutableOperationTick => { let (receipt, state_delta, _) = @@ -4615,6 +4712,7 @@ fn recover_echo_operation_material( Ok(RecoveredEchoOperationMaterial { installations: installations.into_values().collect(), + effect_installations: effects.into_values().collect(), receipts, }) } @@ -5804,9 +5902,11 @@ fn recover_runtime_state_delta_material( { let (installed, _) = operation_installation_from_transaction(transaction)?; let next_ordinal = installation_ordinals.len(); - installation_ordinals - .entry(installed.package_id()) - .or_insert(next_ordinal); + if let RecoveredOperationInstallation::V1(installed) = installed { + installation_ordinals + .entry(installed.package_id()) + .or_insert(next_ordinal); + } continue; } if transaction.commit.transaction_kind == WalTransactionKind::ExecutableOperationTick { @@ -6319,13 +6419,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<(RecoveredOperationInstallation, 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_versioned_installation(&retained_bytes)?; Ok((installed, retained_bytes)) } @@ -7606,7 +7706,7 @@ mod tests { installed_from_admitted(admitted).expect("the legacy-index package installs"); assert_eq!( - recovered_echo_operation_index_root([0x20; 32], &[installed], &[], &[]) + 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, @@ -8843,14 +8943,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() @@ -8867,13 +8967,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() @@ -8949,6 +9049,7 @@ struct RecoveredRuntimeWalIndexEvidence<'a> { missing_runtime_state_deltas: &'a [Hash], causal_anchor_history: &'a [WitnessedCausalAnchorAdmission], installed_echo_operations: &'a [InstalledEchoOperationV1], + installed_echo_effect_operations: &'a [InstalledEffectOperationV2], echo_operation_receipts: &'a [EchoOperationReceiptV1], echo_operation_action_outcomes: &'a [(Hash, Hash, EchoOperationActionOutcomeV1)], } @@ -8989,6 +9090,7 @@ fn recovered_runtime_wal_indexes_root( recovered_echo_operation_index_root( causal_anchor_root, indexes.installed_echo_operations, + indexes.installed_echo_effect_operations, indexes.echo_operation_receipts, indexes.echo_operation_action_outcomes, ) @@ -8997,11 +9099,16 @@ fn recovered_runtime_wal_indexes_root( fn recovered_echo_operation_index_root( base_root: Hash, installations: &[InstalledEchoOperationV1], + effect_installations: &[InstalledEffectOperationV2], receipts: &[EchoOperationReceiptV1], action_outcomes: &[(Hash, Hash, EchoOperationActionOutcomeV1)], ) -> Result { - let legacy_root = - recovered_echo_operation_legacy_index_root(base_root, installations, receipts)?; + let legacy_root = recovered_echo_operation_legacy_index_root( + base_root, + installations, + effect_installations, + receipts, + )?; if action_outcomes.is_empty() { return Ok(legacy_root); } @@ -9020,19 +9127,42 @@ fn recovered_echo_operation_index_root( fn recovered_echo_operation_legacy_index_root( base_root: Hash, installations: &[InstalledEchoOperationV1], + effect_installations: &[InstalledEffectOperationV2], receipts: &[EchoOperationReceiptV1], ) -> Result { - if installations.is_empty() && receipts.is_empty() { + if installations.is_empty() && effect_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(&base_root); - hasher.update(&(installations.len() as u64).to_le_bytes()); - for installed in installations { - let bytes = retain_installation_v1(installed)?; - hasher.update(&(bytes.len() as u64).to_le_bytes()); - hasher.update(&bytes); + if effect_installations.is_empty() { + // Preserve the original V1-only recipe, including supplied record order. + hasher.update(&(installations.len() as u64).to_le_bytes()); + for installed in installations { + let bytes = retain_installation_v1(installed)?; + hasher.update(&(bytes.len() as u64).to_le_bytes()); + hasher.update(&bytes); + } + } else { + let mut catalog = installations + .iter() + .map(|installed| { + Ok(( + installed.package_id().as_hash(), + retain_installation_v1(installed)?, + )) + }) + .collect::, TrustedRuntimeWalError>>()?; + for installed in effect_installations { + catalog.push((installed.package().identity(), installed.retained_bytes()?)); + } + catalog.sort_unstable(); + 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); + } } hasher.update(&(receipts.len() as u64).to_le_bytes()); for receipt in receipts { diff --git a/crates/warp-core/tests/edict_effect_admission_tests.rs b/crates/warp-core/tests/edict_effect_admission_tests.rs index ac128718..c145d1b2 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -214,3 +214,5 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() 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; 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 00000000..496bbaeb --- /dev/null +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -0,0 +1,460 @@ +// 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 std::sync::atomic::{AtomicU64, Ordering}; +use warp_core::edict_effects::InstalledEffectOperationV2; +use warp_core::TrustedRuntimeWalConfig; +static NEXT: AtomicU64 = AtomicU64::new(0); +struct WalDir(std::path::PathBuf); +impl WalDir { + fn new() -> Self { + let p = std::env::temp_dir().join(format!( + "echo-v2-install-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir(&p).expect("unique owned WAL directory"); + Self(p) + } +} +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::edict_effects::AdmittedEffectPackageV2 { + let bytes = encoded(&package(&program())); + host.admit_echo_operation_package_v2( + &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_v2(token.clone()) + .expect("install"); + let mut identity_oracle = blake3::Hasher::new(); + identity_oracle.update(b"echo:operation-installation:v2\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.package().program_identity()); + identity_oracle.update(&(token.package().canonical_bytes().len() as u64).to_le_bytes()); + identity_oracle.update(token.package().canonical_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_v2(installed.package().identity()), + Some(&installed) + ); + assert_eq!( + owner + .install_admitted_echo_operation_package_v2(token) + .expect("idempotent"), + installed + ); + assert_eq!( + owner.runtime_wal().expect("wal").frames().len(), + first_frames + ); + let bytes = installed.retained_bytes().expect("retained"); + assert_eq!( + InstalledEffectOperationV2::from_retained_bytes(&bytes).expect("reconstruct"), + installed + ); + let recovered = owner + .runtime_wal() + .expect("wal") + .recover_read_only() + .expect("recover"); + assert_eq!( + recovered.installed_echo_effect_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_v2(installed.package().identity()), + 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_v2(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!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + } + for schema in [ + "echo.operation-installation/v1", + "echo.operation-installation/v3", + ] { + let mut value = decode(&bytes).expect("canonical"); + set(&mut value, "schema", text(schema)); + assert!(InstalledEffectOperationV2::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!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + let mut value = decode(&bytes).expect("canonical"); + add(&mut value, "extra", V::Integer(0)); + assert!(InstalledEffectOperationV2::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!(InstalledEffectOperationV2::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_v2(admitted(&owner)) + .expect("install"); + let before = owner.runtime_wal().expect("wal").frames().len(); + let mut p = program(); + set(&mut p, "compiled_program", V::Bytes(vec![0xa1])); + let bytes = encoded(&package(&p)); + let token = owner + .admit_echo_operation_package_v2( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ), + bytes, + ) + .expect("new pinned meaning"); + assert!(owner + .install_admitted_echo_operation_package_v2(token) + .is_err()); + let bytes = encoded(&package(&program())); + let token = owner + .admit_echo_operation_package_v2( + &policy( + &bytes, + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(2, 0, 0), + ), + bytes, + ) + .expect("new policy"); + assert!(owner + .install_admitted_echo_operation_package_v2(token) + .is_err()); + assert_eq!( + owner + .engine() + .installed_echo_operation_package_v2(installed.package().identity()), + Some(&installed) + ); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); +} +#[test] +fn activating_empty_wal_cannot_attest_an_earlier_volatile_v2_installation() { + let mut owner = host(); + owner + .install_admitted_echo_operation_package_v2(admitted(&owner)) + .expect("volatile install"); + assert!(owner.enable_in_memory_runtime_wal().is_err()); +} +fn install_v1( + owner: &mut TrustedRuntimeHost, + coordinate: &str, +) -> Result { + use warp_core::{ + EchoOperationAdmissionPolicyV1, EchoOperationProgramV1, EchoOperationSemanticClosureV1, + ExecutableOperationPackageV1, + }; + let budget = EchoOperationBudgetV1::new(16, 4096, 4096); + let legacy = ExecutableOperationPackageV1::new( + coordinate, + "app.LegacyRefusal", + EchoOperationSemanticClosureV1::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, + EchoOperationProgramV1::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 = EchoOperationAdmissionPolicyV1::exact( + warp_core::echo_operation_package_id_v1(&bytes), + coordinate, + [8; 32], + budget, + ); + let admitted = owner + .admit_echo_operation_package_v1(&policy, bytes) + .expect("V1 policy"); + owner.install_admitted_echo_operation_package_v1(admitted) +} +#[test] +fn v1_and_v2_share_coordinate_exclusion_in_both_installation_orders() { + for v2_first in [false, true] { + let mut owner = host(); + owner.enable_in_memory_runtime_wal().expect("wal"); + if v2_first { + owner + .install_admitted_echo_operation_package_v2(admitted(&owner)) + .expect("V2 first"); + let before = owner.runtime_wal().expect("wal").frames().len(); + assert!(install_v1(&mut owner, "app.update@1").is_err()); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); + } else { + install_v1(&mut owner, "app.update@1").expect("V1 first"); + let before = owner.runtime_wal().expect("wal").frames().len(); + assert!(owner + .install_admitted_echo_operation_package_v2(admitted(&owner)) + .is_err()); + assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); + } + } +} +#[test] +fn mixed_catalog_recovers_both_versions_and_certificate_binds_v2_installations() { + let dir = WalDir::new(); + let mut owner = host(); + owner + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) + .expect("wal"); + let legacy = install_v1(&mut owner, "app.legacy@1").expect("V1"); + let effect = owner + .install_admitted_echo_operation_package_v2(admitted(&owner)) + .expect("V2"); + let mut recovered = owner + .runtime_wal() + .expect("wal") + .recover_read_only() + .expect("mixed recovery"); + assert_eq!(recovered.installed_echo_operations, vec![legacy.clone()]); + assert_eq!( + recovered.recomputed_indexes_root().expect("root"), + recovered.certificate.recovered_indexes_root + ); + recovered.installed_echo_effect_operations.clear(); + 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_v1(legacy.package_id()), + Some(&legacy) + ); + assert_eq!( + fresh + .engine() + .installed_echo_operation_package_v2(effect.package().identity()), + Some(&effect) + ); +} +#[cfg(feature = "host_test")] +#[test] +fn failed_wal_append_and_commit_do_not_publish_v2_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_v2(token) + .is_err()); + assert!(owner + .engine() + .installed_echo_operation_package_v2(pin) + .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_v2(pin) + .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_v2(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:v2\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:v2\0"); + h.update(&pin); + h.update(&admission_id); + h.update(&12u64.to_le_bytes()); + h.update(b"app.update@1"); + h.update(&token.package().program_identity()); + h.update(&(token.package().canonical_bytes().len() as u64).to_le_bytes()); + h.update(token.package().canonical_bytes()); + set( + &mut value, + "installed_operation_id", + V::Bytes(h.finalize().as_bytes().to_vec()), + ); + assert!(InstalledEffectOperationV2::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 package = token.package().clone(); + owner + .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next( + FilesystemWalFaultTarget::CommitMarkerSync, + )) + .expect("sync fault"); + assert!(owner + .install_admitted_echo_operation_package_v2(token) + .is_err()); + assert!(owner + .engine() + .installed_echo_operation_package_v2(pin) + .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_v2(pin) + .expect("recovered installation") + .package(), + &package + ); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 4d79c1ac..b18be344 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -735,3 +735,48 @@ Compiled effect instruction validation, V2 installation, grant admission, evaluation and retained recovery remain necessary before this program can execute. This policy crossing does not infer compiler/verifier approval from publication identities or confer graph mutation authority. + +### Runtime-owned V2 installation and retained catalog recovery + +`TrustedRuntimeHost::install_admitted_echo_operation_package_v2` consumes opaque +package-policy admission evidence and installs the exact publication in the +engine's V2 catalog. Installation remains separate from invocation authority +and compiled effect instruction validation. The application handle cannot call +this owner-only method. Reconstructing an `InstalledEffectOperationV2` from +retained bytes produces evidence only; it does not insert a registry entry. + +The engine shares operation-coordinate exclusion across V1 and V2. An exact +repeat is idempotent; a changed package at the same coordinate or a changed +policy for an installed package refuses before any WAL append. Batch recovery +preflights both catalogs. V1's package and installation identity recipes remain +unchanged. V2 installation identity uses `echo:operation-installation:v2\0`, +then package identity, package-admission identity, length-prefixed operation +coordinate, program identity and length-prefixed exact package bytes, in that +order, as the V1 recipe does under its original domain. + +The retained V2 record has exactly the existing seven installation fields with +`schema` set to `echo.operation-installation/v2`. It retains the full independent +policy, policy identity, exact package bytes and identity, admission identity and +installation identity. WAL recovery dispatches by installation schema; unknown +versions refuse. V2 reconstruction reruns package-policy admission, recomputes +every identity and reproduces the exact record bytes. Recomputed hashes cannot +make an invalid independent authority policy acceptable. + +With a runtime WAL configured, the installation record commits before the V2 +registry entry becomes visible. A failed append or commit publishes no entry. +The uncertain filesystem-error path refreshes the writer and checks the exact +recovered installation before returning success. A commit-marker sync failure +keeps the live adapter closed to read-only recovery and publishes no registry +entry; a fresh store reopen can establish and recover that retained commit. Without a WAL, installation +is volatile; activating an empty WAL cannot attest that earlier volatile +installation as durable authority. + +The existing installation transaction and catalog-frontier recipes bind the +versioned retained bytes. Recovery certificates include both catalogs. V1-only +index commitments retain the original recipe and order; mixed catalogs order +exact retained records by raw package identity, with retained bytes breaking a +tie. A fresh filesystem-backed host restores both installation versions and +rechecks coordinate conflicts. This is installation recovery, not recovery of +an authored-effect decision. Invocation grant admission, compiled execution, +physical allocation, derived effects and Action/Tick/receipt recovery remain +unfinished. From d27c7c7ae890203c3ab8964d65c6060ffd514d36 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:50:08 -0700 Subject: [PATCH 16/43] Keep V2 WAL fixtures within determinism boundaries --- .ban-nondeterminism-allowlist | 2 ++ crates/warp-core/src/edict_effects.rs | 2 +- .../src/edict_effects/installation.rs | 2 +- crates/warp-core/src/engine_impl.rs | 4 ++-- crates/warp-core/src/trusted_runtime_host.rs | 4 ++-- .../installation.rs | 20 ++++++++++--------- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.ban-nondeterminism-allowlist b/.ban-nondeterminism-allowlist index 1e582926..a08c0312 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/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index a0a38aeb..06339172 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -22,7 +22,7 @@ pub use admission::{ pub use installation::InstalledEffectOperationV2; #[cfg(feature = "native_rule_bootstrap")] pub(crate) use installation::{ - install_recovered_v2, recover_versioned_installation, RecoveredOperationInstallation, + recover_versioned_installation, restore_effect_catalog_entry, RecoveredOperationInstallation, }; mod invocation; pub use invocation::EffectInvocationV2; diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs index e8ea96ea..061e4de7 100644 --- a/crates/warp-core/src/edict_effects/installation.rs +++ b/crates/warp-core/src/edict_effects/installation.rs @@ -157,7 +157,7 @@ pub(crate) fn recover_versioned_installation( _ => Err(invalid_structure("unsupported installation version")), } } -pub(crate) fn install_recovered_v2( +pub(crate) fn restore_effect_catalog_entry( packages: &mut BTreeMap<[u8; 32], InstalledEffectOperationV2>, operations: &mut BTreeMap, v1_operations: &BTreeMap, diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index c51eaa29..9cd66179 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -1423,7 +1423,7 @@ impl Engine { let mut packages = self.installed_echo_effect_packages.clone(); let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); for installed in recovered { - crate::edict_effects::install_recovered_v2( + crate::edict_effects::restore_effect_catalog_entry( &mut packages, &mut operations, &self.installed_echo_operations_by_coordinate, @@ -1440,7 +1440,7 @@ impl Engine { let mut packages = self.installed_echo_effect_packages.clone(); let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); for installed in recovered { - crate::edict_effects::install_recovered_v2( + crate::edict_effects::restore_effect_catalog_entry( &mut packages, &mut operations, &self.installed_echo_operations_by_coordinate, diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index fd88a932..9622a44d 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -23,7 +23,7 @@ use crate::causal_anchor::prepare_causal_anchor_admission; use crate::coordinator::{SuperTickUndo, TicketedIngressStagingRollback}; use crate::edict_effects::{ - install_recovered_v2, recover_versioned_installation, InstalledEffectOperationV2, + recover_versioned_installation, restore_effect_catalog_entry, InstalledEffectOperationV2, RecoveredOperationInstallation, }; use crate::{ @@ -4673,7 +4673,7 @@ fn recover_echo_operation_material( } install_recovered_v1(&mut installations, &mut operations, installed)?; } - RecoveredOperationInstallation::V2(installed) => install_recovered_v2( + RecoveredOperationInstallation::V2(installed) => restore_effect_catalog_entry( &mut effects, &mut effect_operations, &operations, diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs index 496bbaeb..7b51e789 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -3,20 +3,22 @@ //! 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 std::sync::atomic::{AtomicU64, Ordering}; use warp_core::edict_effects::InstalledEffectOperationV2; use warp_core::TrustedRuntimeWalConfig; -static NEXT: AtomicU64 = AtomicU64::new(0); struct WalDir(std::path::PathBuf); impl WalDir { fn new() -> Self { - let p = std::env::temp_dir().join(format!( - "echo-v2-install-{}-{}", - std::process::id(), - NEXT.fetch_add(1, Ordering::Relaxed) - )); - std::fs::create_dir(&p).expect("unique owned WAL directory"); - Self(p) + 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-v2-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 { From 48d2d8acb92edb153bccb1a7ab0cc4d4f402b149 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 07:57:57 -0700 Subject: [PATCH 17/43] Require one current format in main crates --- AGENTS.md | 14 ++++++++++++++ README.md | 11 +++++++---- docs/topics/GeneratedRules.md | 26 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7ba7fe2f..ce660f99 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/README.md b/README.md index d4355ad4..43eea1e0 100644 --- a/README.md +++ b/README.md @@ -585,10 +585,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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index b18be344..4e5db457 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -13,6 +13,32 @@ 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 branch's current behavior, including its still-present +V1/V2 duplication; that duplication does not satisfy this policy. A completed +authored-effect bridge must replace it with one current format and update +the compiler, runtime, tests, and consumers together. + ## Current Implementation Wesley currently emits raw `RewriteRule` builders plus generated operation, From ca78d85ef39a41583fd0a5756830ac5c6f4b961b Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 08:08:00 -0700 Subject: [PATCH 18/43] Unify operation package admission policy and refusals --- CHANGELOG.md | 9 ++ .../tests/action_preparation_allocation.rs | 6 +- .../warp-core/benches/support/action_scene.rs | 14 +- crates/warp-core/src/echo_operation.rs | 150 +++++++++-------- crates/warp-core/src/edict_effects.rs | 4 +- .../warp-core/src/edict_effects/admission.rs | 152 +++--------------- .../src/edict_effects/installation.rs | 7 +- crates/warp-core/src/lib.rs | 4 +- crates/warp-core/src/trusted_runtime_host.rs | 23 ++- .../tests/edict_effect_admission_tests.rs | 58 +++++-- .../installation.rs | 6 +- .../tests/edict_projected_cas_tests.rs | 12 +- .../executable_operation_pipeline_tests.rs | 62 +++---- docs/topics/GeneratedRules.md | 21 ++- xtask/src/run_edict_operation.rs | 6 +- 15 files changed, 246 insertions(+), 288 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 101c9a69..73021824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ ## Unreleased +### Changed + +- 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 V2 installation retains exact policy and package evidence in the diff --git a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs index 32550453..8b0e0411 100644 --- a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs +++ b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs @@ -25,7 +25,7 @@ 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, + AtomPayload, AttachmentValue, EchoOperationAdmissionPolicy, EchoOperationBudgetV1, EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EchoOperationPreparationV1, EngineBuilder, GraphStore, InboxPolicy, NodeId, NodeKey, NodeRecord, PlaybackMode, SchedulerKind, TrustedRuntimeHost, TypeId, WorldlineId, @@ -183,8 +183,8 @@ fn preparing_an_action_does_not_copy_its_installed_package() { let package_len = bytes.len(); let admitted = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&bytes), + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), diff --git a/crates/warp-core/benches/support/action_scene.rs b/crates/warp-core/benches/support/action_scene.rs index 62be1973..636f90f5 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, EchoOperationInvocationAdmissionPolicyV1, + EchoOperationInvocationV1, 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"; @@ -227,8 +227,8 @@ impl Scene { let package = package(); let admitted = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&package), + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&package).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 5c9b2c98..37a284d4 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -2368,18 +2368,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, @@ -2392,17 +2392,50 @@ 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) const fn expected_package_identity(&self) -> Hash { + self.expected_package_id + } + + 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", @@ -2412,15 +2445,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, &[ @@ -2431,10 +2463,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, @@ -2456,7 +2485,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. @@ -2472,16 +2501,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 } @@ -2500,7 +2529,7 @@ pub struct AdmittedExecutableOperationPackageV1 { package_id: EchoOperationPackageIdV1, admission_policy_id: Hash, admission_id: EchoOperationPackageAdmissionIdV1, - admission_policy: EchoOperationAdmissionPolicyV1, + admission_policy: EchoOperationAdmissionPolicy, } /// Installed executable meaning. Installation does not itself authorize an invocation. @@ -2525,7 +2554,7 @@ pub struct InstalledEchoOperationV1 { Option>, canonical_package_bytes: std::sync::Arc<[u8]>, admission_policy_id: Hash, - admission_policy: std::sync::Arc, + admission_policy: std::sync::Arc, } impl InstalledEchoOperationV1 { @@ -2586,7 +2615,7 @@ impl InstalledEchoOperationV1 { /// 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 } @@ -2623,40 +2652,27 @@ impl EchoOperationInstallationErrorV1 { } pub(crate) fn admit_package_v1( - policy: &EchoOperationAdmissionPolicyV1, + 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, + .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 admission_policy_id = policy.identity(); Ok(AdmittedExecutableOperationPackageV1 { package, @@ -2750,7 +2766,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( @@ -7848,11 +7864,11 @@ pub(crate) fn invalid_structure(detail: impl Into) -> EchoOperationArtif 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, @@ -8100,8 +8116,8 @@ mod tests { let package_id = echo_operation_package_id_v1(&package_bytes); let installed = installed_from_admitted( admit_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile, EchoOperationBudgetV1::new(8, 1_024, 1_024), @@ -8219,8 +8235,8 @@ 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, @@ -8252,7 +8268,7 @@ mod tests { .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(), @@ -8287,8 +8303,8 @@ 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, @@ -8325,7 +8341,7 @@ mod tests { .expect_err("an unusable digest path must refuse before installation"); assert_eq!( error.kind(), - EchoOperationAdmissionErrorKindV1::ArtifactInvalid, + EchoOperationAdmissionErrorKind::ArtifactInvalid, "{path:?}" ); assert_eq!( @@ -8974,8 +8990,8 @@ mod tests { let package_id = echo_operation_package_id_v1(&package_bytes); let installed = installed_from_admitted( admit_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile, EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -9209,8 +9225,8 @@ 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), @@ -9297,8 +9313,8 @@ 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), diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 06339172..95b29925 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -15,9 +15,7 @@ mod installation; #[cfg(feature = "native_rule_bootstrap")] pub(crate) use admission::admit as admit_effect_package_v2; #[cfg(feature = "native_rule_bootstrap")] -pub use admission::{ - AdmittedEffectPackageV2, EffectAdmissionError, EffectPackageAdmissionPolicyV2, -}; +pub use admission::AdmittedEffectPackageV2; #[cfg(feature = "native_rule_bootstrap")] pub use installation::InstalledEffectOperationV2; #[cfg(feature = "native_rule_bootstrap")] diff --git a/crates/warp-core/src/edict_effects/admission.rs b/crates/warp-core/src/edict_effects/admission.rs index c461534a..6224367a 100644 --- a/crates/warp-core/src/edict_effects/admission.rs +++ b/crates/warp-core/src/edict_effects/admission.rs @@ -2,124 +2,22 @@ // © James Ross Ω FLYING•ROBOTS //! Runtime-owner package policy evidence, separate from installation. use super::{EffectPackageError, ExecutableOperationPackageV2}; -use crate::EchoOperationBudgetV1; -/// Independently selected package-admission pins and budget ceiling. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectPackageAdmissionPolicyV2 { - pin: [u8; 32], - operation: String, - authority: [u8; 32], - maximum: EchoOperationBudgetV1, -} -impl EffectPackageAdmissionPolicyV2 { - /// Pins exact publication bytes, operation, authority profile and budget. - #[must_use] - pub fn exact( - pin: [u8; 32], - operation: impl Into, - authority: [u8; 32], - maximum: EchoOperationBudgetV1, - ) -> Self { - Self { - pin, - operation: operation.into(), - authority, - maximum, - } - } - /// Policy identity using the existing V1 policy recipe and V2 package pin. - #[must_use] - pub fn identity(&self) -> [u8; 32] { - let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-admission-policy:v1\0"); - h.update(&self.pin); - h.update(&(self.operation.len() as u64).to_le_bytes()); - h.update(self.operation.as_bytes()); - h.update(&self.authority); - h.update(&self.maximum.steps().to_le_bytes()); - h.update(&self.maximum.read_bytes().to_le_bytes()); - h.update(&self.maximum.write_bytes().to_le_bytes()); - h.finalize().into() - } - pub(super) fn to_value(&self) -> echo_edict_canonical::CanonicalValueV1 { - use echo_edict_canonical::CanonicalValueV1 as V; - crate::echo_operation::map_value([ - ("expected_package_id", V::Bytes(self.pin.to_vec())), - ( - "expected_operation_coordinate", - V::Text(self.operation.clone()), - ), - ( - "expected_authority_profile_identity", - V::Bytes(self.authority.to_vec()), - ), - ("maximum_budget", self.maximum.to_value()), - ]) - } - pub(super) fn from_value( - value: &echo_edict_canonical::CanonicalValueV1, - ) -> Result { - use super::package::hash; - use crate::edict_pure::values::{exact_fields, field, text}; - let read = || { - exact_fields( - value, - &[ - "expected_package_id", - "expected_operation_coordinate", - "expected_authority_profile_identity", - "maximum_budget", - ], - )?; - Ok::<_, crate::edict_pure::EvaluationError>(( - hash(field(value, "expected_package_id")?)?, - text(field(value, "expected_operation_coordinate")?)?.to_owned(), - hash(field(value, "expected_authority_profile_identity")?)?, - field(value, "maximum_budget")?.clone(), - )) - }; - let (pin, operation, authority, budget) = read() - .map_err(|_| crate::echo_operation::invalid_structure("invalid V2 admission policy"))?; - let maximum = EchoOperationBudgetV1::from_value(budget)?; - if operation.is_empty() || maximum.steps() == 0 { - return Err(crate::echo_operation::invalid_structure( - "invalid V2 policy coordinate or budget", - )); - } - Ok(Self::exact(pin, operation, authority, maximum)) - } -} -/// Stable V2 package-admission refusal category. -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -pub enum EffectAdmissionError { - /// Exact bytes differ from the independent package pin. - #[error("effect package identity differs from policy")] - PackageIdentityMismatch, - /// Canonical artifact structure or profile bindings are invalid. - #[error("invalid effect publication")] - ArtifactInvalid, - /// The package operation coordinate differs from policy. - #[error("effect operation differs from policy")] - OperationCoordinateMismatch, - /// The package authority profile differs from policy. - #[error("effect authority profile differs from policy")] - AuthorityProfileMismatch, - /// The publication requires more resources than policy permits. - #[error("effect package budget exceeds policy")] - BudgetExceedsPolicy, -} +use crate::echo_operation::admission_error; +use crate::{ + EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, +}; /// Opaque evidence of runtime-owner admission, without installation or a grant. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AdmittedEffectPackageV2 { package: ExecutableOperationPackageV2, policy_id: [u8; 32], id: [u8; 32], - policy: EffectPackageAdmissionPolicyV2, + policy: EchoOperationAdmissionPolicy, } impl AdmittedEffectPackageV2 { /// Independent policy retained alongside its identity. #[must_use] - pub const fn admission_policy(&self) -> &EffectPackageAdmissionPolicyV2 { + pub const fn admission_policy(&self) -> &EchoOperationAdmissionPolicy { &self.policy } /// Exact publication admitted by the runtime owner. @@ -139,27 +37,27 @@ impl AdmittedEffectPackageV2 { } } pub(crate) fn admit( - policy: &EffectPackageAdmissionPolicyV2, + policy: &EchoOperationAdmissionPolicy, bytes: Vec, -) -> Result { - let package = - ExecutableOperationPackageV2::prepare(&bytes, policy.pin).map_err(|error| match error { - EffectPackageError::IdentityMismatch => EffectAdmissionError::PackageIdentityMismatch, - _ => EffectAdmissionError::ArtifactInvalid, +) -> Result { + let package = ExecutableOperationPackageV2::prepare(&bytes, policy.expected_package_identity()) + .map_err(|error| { + let kind = match error { + EffectPackageError::IdentityMismatch => { + EchoOperationAdmissionErrorKind::PackageIdentityMismatch + } + _ => EchoOperationAdmissionErrorKind::ArtifactInvalid, + }; + admission_error( + kind, + "effect publication differs from its pin or canonical contract", + ) })?; - if package.operation_coordinate() != policy.operation { - return Err(EffectAdmissionError::OperationCoordinateMismatch); - } - if package.authority_profile_identity() != policy.authority { - return Err(EffectAdmissionError::AuthorityProfileMismatch); - } - let budget = package.budget_ceiling(); - if budget.steps() > policy.maximum.steps() - || budget.read_bytes() > policy.maximum.read_bytes() - || budget.write_bytes() > policy.maximum.write_bytes() - { - return Err(EffectAdmissionError::BudgetExceedsPolicy); - } + policy.validate_contract( + package.operation_coordinate(), + package.authority_profile_identity(), + package.budget_ceiling(), + )?; let policy_id = policy.identity(); let mut h = blake3::Hasher::new(); h.update(b"echo:operation-package-admission:v2\0"); diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs index 061e4de7..5b38d6ec 100644 --- a/crates/warp-core/src/edict_effects/installation.rs +++ b/crates/warp-core/src/edict_effects/installation.rs @@ -2,13 +2,12 @@ // © James Ross Ω FLYING•ROBOTS //! Versioned effect installation evidence; invocation authority remains separate. use super::package::hash; -use super::{ - AdmittedEffectPackageV2, EffectPackageAdmissionPolicyV2, ExecutableOperationPackageV2, -}; +use super::{AdmittedEffectPackageV2, ExecutableOperationPackageV2}; use crate::echo_operation::{ installation_error, invalid_structure, map_value, recover_installation_v1, }; use crate::edict_pure::values::{bytes, exact_fields, field, require_text, text}; +use crate::EchoOperationAdmissionPolicy; use crate::{ EchoOperationArtifactErrorV1, EchoOperationInstallationErrorKindV1, EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, InstalledEchoOperationV1, @@ -108,7 +107,7 @@ impl InstalledEffectOperationV2 { }; let (policy, policy_id, package_id, package_bytes, admission_id, installed_id) = read().map_err(|_| invalid_structure("invalid V2 installation fields"))?; - let policy = EffectPackageAdmissionPolicyV2::from_value(policy)?; + let policy = EchoOperationAdmissionPolicy::from_value(policy.clone())?; let admitted = super::admission::admit(&policy, package_bytes.to_vec()) .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; if policy.identity() != policy_id diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index ac742a85..8c7d0057 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -261,8 +261,8 @@ pub use echo_operation::{ 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, + EchoOperationActionOutcomeV1, EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, + EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, EchoOperationApplicationResultV1, EchoOperationArtifactErrorKindV1, EchoOperationArtifactErrorV1, EchoOperationBudgetV1, EchoOperationCommitErrorV1, EchoOperationEvaluationBasisIdV1, EchoOperationEvaluationBasisV1, diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 9622a44d..a2afe0e0 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -72,8 +72,8 @@ use crate::{ 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, @@ -1109,21 +1109,18 @@ impl TrustedRuntimeHost { /// Application-facing handles cannot call this runtime-owner method: /// /// ```compile_fail,E0599 - /// use warp_core::{TrustedRuntimeApp, edict_effects::EffectPackageAdmissionPolicyV2}; + /// use warp_core::{TrustedRuntimeApp, EchoOperationAdmissionPolicy}; /// fn application_cannot_admit( - /// app: &TrustedRuntimeApp<'_>, policy: &EffectPackageAdmissionPolicyV2, bytes: Vec, + /// app: &TrustedRuntimeApp<'_>, policy: &EchoOperationAdmissionPolicy, bytes: Vec, /// ) { /// app.admit_echo_operation_package_v2(policy, bytes); /// } /// ``` pub fn admit_echo_operation_package_v2( &self, - policy: &crate::edict_effects::EffectPackageAdmissionPolicyV2, + policy: &crate::EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, - ) -> Result< - crate::edict_effects::AdmittedEffectPackageV2, - crate::edict_effects::EffectAdmissionError, - > { + ) -> Result { crate::edict_effects::admit_effect_package_v2(policy, canonical_package_bytes) } @@ -1134,9 +1131,9 @@ impl TrustedRuntimeHost { /// operation coordinate, invocability, or authority. pub fn admit_echo_operation_package_v1( &self, - policy: &EchoOperationAdmissionPolicyV1, + policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, - ) -> Result { + ) -> Result { admit_package_v1(policy, canonical_package_bytes) } @@ -7693,8 +7690,8 @@ mod tests { .expect("the legacy-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, + &crate::EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, authority_profile_identity, budget, diff --git a/crates/warp-core/tests/edict_effect_admission_tests.rs b/crates/warp-core/tests/edict_effect_admission_tests.rs index c145d1b2..b25cde6a 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -6,7 +6,8 @@ #[path = "common/edict_effect_publication.rs"] mod fixture; use fixture::*; -use warp_core::edict_effects::{EffectAdmissionError, EffectPackageAdmissionPolicyV2}; +use warp_core::EchoOperationAdmissionErrorKind; +use warp_core::EchoOperationAdmissionPolicy; use warp_core::{ make_node_id, make_type_id, EchoOperationBudgetV1, EngineBuilder, GraphStore, NodeRecord, TrustedRuntimeHost, WorldlineRuntime, @@ -28,8 +29,8 @@ fn policy( operation: &str, authority: [u8; 32], budget: EchoOperationBudgetV1, -) -> EffectPackageAdmissionPolicyV2 { - EffectPackageAdmissionPolicyV2::exact( +) -> EchoOperationAdmissionPolicy { + EchoOperationAdmissionPolicy::exact( hash(b"echo:operation-package:v2\0", bytes), operation, authority, @@ -82,19 +83,19 @@ fn runtime_policy_refuses_operation_authority_and_budget_substitution() { "app.other@1", [8; 32], EchoOperationBudgetV1::new(1, 0, 0), - EffectAdmissionError::OperationCoordinateMismatch, + EchoOperationAdmissionErrorKind::OperationCoordinateMismatch, ), ( "app.update@1", [9; 32], EchoOperationBudgetV1::new(1, 0, 0), - EffectAdmissionError::AuthorityProfileMismatch, + EchoOperationAdmissionErrorKind::AuthorityProfileMismatch, ), ( "app.update@1", [8; 32], EchoOperationBudgetV1::new(0, 0, 0), - EffectAdmissionError::BudgetExceedsPolicy, + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy, ), ] { assert_eq!( @@ -102,7 +103,8 @@ fn runtime_policy_refuses_operation_authority_and_budget_substitution() { &policy(&bytes, operation, authority, budget), bytes.clone() ) - .expect_err("independent policy refuses"), + .expect_err("independent policy refuses") + .kind(), want ); } @@ -123,8 +125,9 @@ fn pin_identity_does_not_substitute_for_supported_artifact_validation() { ), bytes ) - .expect_err("pinned but unsupported"), - EffectAdmissionError::ArtifactInvalid + .expect_err("pinned but unsupported") + .kind(), + EchoOperationAdmissionErrorKind::ArtifactInvalid ); let bytes = encoded(&package(&program())); let pin = policy( @@ -135,8 +138,9 @@ fn pin_identity_does_not_substitute_for_supported_artifact_validation() { ); assert_eq!( host.admit_echo_operation_package_v2(&pin, bytes) - .expect_err("wrong pin"), - EffectAdmissionError::PackageIdentityMismatch + .expect_err("wrong pin") + .kind(), + EchoOperationAdmissionErrorKind::PackageIdentityMismatch ); } #[test] @@ -173,8 +177,9 @@ fn each_package_budget_axis_is_subject_to_the_independent_policy() { ), bytes ) - .expect_err("ceiling exceeds policy"), - EffectAdmissionError::BudgetExceedsPolicy + .expect_err("ceiling exceeds policy") + .kind(), + EchoOperationAdmissionErrorKind::BudgetExceedsPolicy ); } } @@ -216,3 +221,30 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() } #[path = "edict_effect_admission_tests/installation.rs"] mod installation; + +#[test] +fn all_operation_publications_use_one_policy_and_one_refusal_contract() { + let owner = host(); + let pin = policy( + b"independent pin", + "app.update@1", + [8; 32], + EchoOperationBudgetV1::new(1, 0, 0), + ); + // The array requires both host entry points to return the same error type. + // Neither decoder may accept a publication that differs from this pin. + let refusals = [ + owner + .admit_echo_operation_package_v1(&pin, vec![0xa0]) + .map(|_| ()), + owner + .admit_echo_operation_package_v2(&pin, vec![0xa0]) + .map(|_| ()), + ]; + for refusal in refusals { + assert_eq!( + refusal.expect_err("wrong pin").kind(), + warp_core::EchoOperationAdmissionErrorKind::PackageIdentityMismatch, + ); + } +} diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs index 7b51e789..d8657033 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -212,7 +212,7 @@ fn install_v1( coordinate: &str, ) -> Result { use warp_core::{ - EchoOperationAdmissionPolicyV1, EchoOperationProgramV1, EchoOperationSemanticClosureV1, + EchoOperationAdmissionPolicy, EchoOperationProgramV1, EchoOperationSemanticClosureV1, ExecutableOperationPackageV1, }; let budget = EchoOperationBudgetV1::new(16, 4096, 4096); @@ -239,8 +239,8 @@ fn install_v1( ), ); let bytes = legacy.to_canonical_bytes().expect("V1 fixture"); - let policy = EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&bytes), + let policy = EchoOperationAdmissionPolicy::exact( + warp_core::echo_operation_package_id_v1(&bytes).as_hash(), coordinate, [8; 32], budget, diff --git a/crates/warp-core/tests/edict_projected_cas_tests.rs b/crates/warp-core/tests/edict_projected_cas_tests.rs index d70d8c23..7e824e68 100644 --- a/crates/warp-core/tests/edict_projected_cas_tests.rs +++ b/crates/warp-core/tests/edict_projected_cas_tests.rs @@ -19,7 +19,7 @@ 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, + EchoOperationAdmissionPolicy, EchoOperationBudgetV1, EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, EngineBuilder, GraphStore, InboxPolicy, IngressTarget, NodeId, NodeKey, NodeRecord, @@ -163,8 +163,8 @@ impl Fixture { let admitted = self .host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - echo_operation_package_id_v1(&bytes), + &EchoOperationAdmissionPolicy::exact( + echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, profile("cas.echo.authority.local-demo/v1"), budget(), @@ -827,8 +827,8 @@ fn compiler_cas_refuses_aliased_projection_before_installation() { let error = fixture .host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - id, + &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(), diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 0d22249a..ab160d49 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -22,9 +22,9 @@ 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, + EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, + EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, + EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, EchoOperationProgramV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, @@ -512,8 +512,8 @@ fn install_fixture_operation_at( let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let admitted = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), operation_coordinate, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -535,8 +535,8 @@ fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> Installe let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); let admitted = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), CREATE_OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -661,8 +661,8 @@ 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), @@ -846,8 +846,8 @@ fn a_prepared_operation_cannot_commit_after_its_parent_basis_changes() { 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, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1086,8 +1086,8 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by let digest_mismatch = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(b"different package bytes"), + &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), @@ -1097,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), + &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), @@ -1114,7 +1114,7 @@ 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( @@ -1134,8 +1134,8 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by .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), + &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), @@ -1164,8 +1164,8 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by ); let unsupported_program = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&unsupported_program_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), @@ -1188,8 +1188,8 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by ); let unsupported_schema = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - warp_core::echo_operation_package_id_v1(&unsupported_schema_bytes), + &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), @@ -1207,8 +1207,8 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by let authority_mismatch = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("different-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), @@ -1218,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, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(15, 4_096, 4_096), @@ -1234,7 +1234,7 @@ 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( @@ -1814,8 +1814,8 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { .expect("the fresh filesystem WAL opens"); let admitted_package = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), OPERATION_COORDINATE, digest("fixture-authority-profile"), EchoOperationBudgetV1::new(16, 4_096, 4_096), diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 4e5db457..30b47e7c 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -739,8 +739,16 @@ the named parent remain required before effects can be accepted. ### Runtime-owner V2 package policy admission `TrustedRuntimeHost::admit_echo_operation_package_v2` crosses the independent -package-policy boundary. Its `EffectPackageAdmissionPolicyV2` pins the exact -V2 publication, operation coordinate, authority profile and maximum budget. +package-policy boundary. The single `EchoOperationAdmissionPolicy` pins exact +publication identity bytes, operation coordinate, authority profile and maximum +budget. Both current package admission entry points consume this definition, +its retained codec and its shared operation/authority/budget checks, and return +the same `EchoOperationAdmissionError` / `EchoOperationAdmissionErrorKind` +refusal contract. The old version-specific policy and refusal definitions have +been removed without compatibility aliases. This is one completed part of the +format cutover; the separate package and installation definitions below still +require replacement under the one-current-version policy. + The host prepares the canonical package, checks all four independent policy conditions, and returns opaque `AdmittedEffectPackageV2` evidence. Every budget axis is checked separately. A matching package pin alone cannot substitute for @@ -751,15 +759,16 @@ existing `echo:operation-admission-policy:v1\0` recipe with the V2 package pin. The package-admission identity uses `echo:operation-package-admission:v2\0` followed by the package and policy identities. Two policies admitting the same program therefore produce distinct policy/admission evidence while retaining -the same package identity. V1 identities and policy admission are unchanged. +the same package identity. A policy pins a raw 32-byte identity; the package +decoder, rather than the policy type, selects the publication identity domain. This method is exposed only on the runtime-owner host, under its existing `native_rule_bootstrap` and `trusted_runtime` feature boundary. The restricted application handle cannot admit a package. The opaque token has no public constructor and is not an installation, invocation grant or scheduler Action. -Compiled effect instruction validation, V2 installation, grant admission, -evaluation and retained recovery remain necessary before this program can -execute. This policy crossing does not infer compiler/verifier approval from +Compiled effect instruction validation, grant admission, evaluation and +retained decision recovery remain necessary before this program can execute. +This policy crossing does not infer compiler/verifier approval from publication identities or confer graph mutation authority. ### Runtime-owned V2 installation and retained catalog recovery diff --git a/xtask/src/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index 8743c670..c65f79fd 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -23,7 +23,7 @@ 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, EchoOperationObstructionKindV1, EngineBuilder, GraphStore, InboxPolicy, IngressTarget, NodeId, @@ -1089,8 +1089,8 @@ fn install_package( ) -> Result<()> { let admitted = host .admit_echo_operation_package_v1( - &EchoOperationAdmissionPolicyV1::exact( - package_id, + &EchoOperationAdmissionPolicy::exact( + package_id.as_hash(), &package.operation_coordinate, package.authority_profile_identity, package.budget, From 4d615b21fbaf8f0665617585903e41304fa12a9e Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 08:16:22 -0700 Subject: [PATCH 19/43] Use one current operation recovery index --- CHANGELOG.md | 6 + crates/warp-core/src/trusted_runtime_host.rs | 151 +++++++++--------- .../executable_operation_pipeline_tests.rs | 86 ++++++++++ docs/topics/GeneratedRules.md | 21 ++- 4 files changed, 184 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73021824..0fd8a1bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ ### Changed +- 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, diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index a2afe0e0..f2e988c3 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -7658,36 +7658,34 @@ 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) -> InstalledEchoOperationV1 { let authority_profile_identity = [0x17; 32]; let budget = crate::EchoOperationBudgetV1::new(7, 1_024, 1_024); let package = crate::ExecutableOperationPackageV1::new( operation_coordinate, - "echo.test.LegacyRecoveryIndex.Obstruction.v1", + "echo.test.RecoveryIndex.Obstruction.v1", crate::EchoOperationSemanticClosureV1::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::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::EchoOperationAdmissionPolicy::exact( @@ -7698,21 +7696,57 @@ mod tests { ), 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).expect("the recovery-index package installs") + } + #[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_installation_v1(&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 { @@ -9100,66 +9134,27 @@ fn recovered_echo_operation_index_root( receipts: &[EchoOperationReceiptV1], action_outcomes: &[(Hash, Hash, EchoOperationActionOutcomeV1)], ) -> Result { - let legacy_root = recovered_echo_operation_legacy_index_root( - base_root, - installations, - effect_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); + let mut catalog = installations + .iter() + .map(|installed| { + Ok(( + installed.package_id().as_hash(), + retain_installation_v1(installed)?, + )) + }) + .collect::, TrustedRuntimeWalError>>()?; + for installed in effect_installations { + catalog.push((installed.package().identity(), installed.retained_bytes()?)); } - Ok(hasher.finalize().into()) -} + catalog.sort_unstable(); -fn recovered_echo_operation_legacy_index_root( - base_root: Hash, - installations: &[InstalledEchoOperationV1], - effect_installations: &[InstalledEffectOperationV2], - receipts: &[EchoOperationReceiptV1], -) -> Result { - if installations.is_empty() && effect_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); - if effect_installations.is_empty() { - // Preserve the original V1-only recipe, including supplied record order. - hasher.update(&(installations.len() as u64).to_le_bytes()); - for installed in installations { - let bytes = retain_installation_v1(installed)?; - hasher.update(&(bytes.len() as u64).to_le_bytes()); - hasher.update(&bytes); - } - } else { - let mut catalog = installations - .iter() - .map(|installed| { - Ok(( - installed.package_id().as_hash(), - retain_installation_v1(installed)?, - )) - }) - .collect::, TrustedRuntimeWalError>>()?; - for installed in effect_installations { - catalog.push((installed.package().identity(), installed.retained_bytes()?)); - } - catalog.sort_unstable(); - 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); - } + 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); } hasher.update(&(receipts.len() as u64).to_le_bytes()); for receipt in receipts { @@ -9167,6 +9162,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/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index ab160d49..0abde4ad 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -7025,3 +7025,89 @@ 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 + ); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 30b47e7c..6814ff5f 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -807,11 +807,22 @@ is volatile; activating an empty WAL cannot attest that earlier volatile installation as durable authority. The existing installation transaction and catalog-frontier recipes bind the -versioned retained bytes. Recovery certificates include both catalogs. V1-only -index commitments retain the original recipe and order; mixed catalogs order -exact retained records by raw package identity, with retained bytes breaking a -tie. A fresh filesystem-backed host restores both installation versions and -rechecks coordinate conflicts. This is installation recovery, not recovery of +versioned 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. + +The package and installation definitions still have the parallel versions +described above; the index cutover does not complete their replacement. A fresh +filesystem-backed host currently restores those installations and rechecks +coordinate conflicts. This is installation recovery, not recovery of an authored-effect decision. Invocation grant admission, compiled execution, physical allocation, derived effects and Action/Tick/receipt recovery remain unfinished. From 84e36df93ff6696ad27b09effa815f236fd7eecd Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 08:34:19 -0700 Subject: [PATCH 20/43] Consolidate operation publication and program definitions --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 13 +- README.md | 8 +- .../tests/executable_operation_package.rs | 16 +- crates/warp-core/Cargo.toml | 4 + crates/warp-core/src/echo_operation.rs | 310 ++++++++++++------ crates/warp-core/src/edict_effects.rs | 4 +- .../warp-core/src/edict_effects/admission.rs | 68 +++- .../src/edict_effects/installation.rs | 50 ++- .../warp-core/src/edict_effects/invocation.rs | 19 +- crates/warp-core/src/edict_effects/package.rs | 297 +++++------------ crates/warp-core/src/lib.rs | 8 +- crates/warp-core/src/trusted_runtime_host.rs | 61 ++-- .../tests/common/edict_effect_publication.rs | 15 +- .../current_operation_publication_tests.rs | 108 ++++++ .../tests/edict_effect_admission_tests.rs | 16 +- .../installation.rs | 38 +-- .../tests/edict_effect_package_tests.rs | 101 ++++-- .../edict_effect_package_tests/invocation.rs | 10 +- .../executable_operation_pipeline_tests.rs | 30 +- docs/topics/GeneratedRules.md | 110 ++++--- 21 files changed, 762 insertions(+), 528 deletions(-) create mode 100644 crates/warp-core/tests/current_operation_publication_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4292dcf5..d2a2f656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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_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 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) @@ -397,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_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 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/CHANGELOG.md b/CHANGELOG.md index 0fd8a1bb..30524cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ ### Changed +- 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, admission-token and installation/catalog consolidation + remains separate unfinished work. + + - 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 @@ -37,10 +46,10 @@ ceilings. It retains the V1 parent format without granting authority or establishing frontier freshness. -- V2 effect publication preparation binds exact package/program bytes, validates +- 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; V1 package encodings and identities are unchanged. + 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 diff --git a/README.md b/README.md index 43eea1e0..a682cf75 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,9 @@ runtime authority. Echo separately retains the Wesley compatibility path for host-constructed `InstalledContractPackage` values. 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 +250,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 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 cbd19daa..ed9f0663 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/warp-core/Cargo.toml b/crates/warp-core/Cargo.toml index 0501b156..6c4e3f95 100644 --- a/crates/warp-core/Cargo.toml +++ b/crates/warp-core/Cargo.toml @@ -257,3 +257,7 @@ 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/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 37a284d4..b676c5f0 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -576,18 +576,13 @@ 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. + #[cfg(feature = "trusted_runtime")] + EdictEffect(Arc), + /// Compare and replace one anchored typed node attachment. AnchoredNodeAttachmentCompareAndSet { /// Required skeleton node type. @@ -608,7 +603,32 @@ pub enum EchoOperationProgramV1 { }, } -impl EchoOperationProgramV1 { +impl EchoOperationProgram { + /// Returns the authored-effect payload when this is that program kind. + #[cfg(feature = "trusted_runtime")] + #[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 { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation-interpreter.edict-effect/v1", + _ => INTERPRETER_PROFILE, + } + } + const fn intrinsic_profile(&self) -> &'static str { + match self { + #[cfg(feature = "trusted_runtime")] + 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( @@ -651,19 +671,21 @@ 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) } + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(program) => program.footprint_identity(), } } const fn minimum_budget(&self) -> EchoOperationBudgetV1 { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => EchoOperationBudgetV1::new(1, 0, 0), Self::AnchoredNodeAttachmentCompareAndSet { .. } => { EchoOperationBudgetV1::new(4, 64, 32) } @@ -679,6 +701,8 @@ impl EchoOperationProgramV1 { const fn input_schema(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.input.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => INPUT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_INPUT_SCHEMA, } @@ -686,6 +710,8 @@ impl EchoOperationProgramV1 { const fn result_schema(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.result.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_SCHEMA, } @@ -693,6 +719,8 @@ impl EchoOperationProgramV1 { const fn obstruction_schema(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.obstruction.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_SCHEMA, } @@ -700,6 +728,8 @@ impl EchoOperationProgramV1 { const fn result_interpretation(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.result-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_INTERPRETATION, } @@ -707,6 +737,8 @@ impl EchoOperationProgramV1 { const fn obstruction_interpretation(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.obstruction-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_INTERPRETATION, } @@ -714,6 +746,8 @@ impl EchoOperationProgramV1 { const fn application_basis_schema(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation.basis.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => APPLICATION_BASIS_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_APPLICATION_BASIS_SCHEMA, } @@ -721,6 +755,8 @@ impl EchoOperationProgramV1 { const fn target_profile(&self) -> &'static str { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => "echo.operation-target.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => TARGET_PROFILE, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_TARGET_PROFILE, } @@ -741,6 +777,8 @@ impl EchoOperationProgramV1 { fn validate_supported_profile(&self) -> Result<(), EchoOperationArtifactErrorV1> { match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(_) => Ok(()), Self::AnchoredNodeAttachmentCompareAndSet { max_replacement_bytes, .. @@ -760,6 +798,8 @@ impl EchoOperationProgramV1 { fn to_value(&self) -> CanonicalValueV1 { let (kind, required_node_type, required_attachment_type, max_replacement_bytes) = match self { + #[cfg(feature = "trusted_runtime")] + Self::EdictEffect(program) => return program.to_value(), Self::AnchoredNodeAttachmentCompareAndSet { required_node_type, required_attachment_type, @@ -803,6 +843,12 @@ impl EchoOperationProgramV1 { fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; + #[cfg(feature = "trusted_runtime")] + 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, &[ @@ -858,18 +904,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] @@ -890,8 +936,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), } } @@ -903,12 +949,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", )); @@ -917,7 +969,7 @@ impl EchoOperationSemanticClosureV1 { } fn to_value(&self) -> CanonicalValueV1 { - map_value([ + let mut fields = vec![ ( "application_schema_coordinate", text_value(&self.application_schema_coordinate), @@ -935,26 +987,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")?, @@ -962,8 +1024,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) @@ -1791,10 +1861,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, @@ -1808,21 +1878,36 @@ 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. + /// # Errors + /// Refuses a different pin or an unsupported canonical publication contract. + #[cfg(feature = "trusted_runtime")] + 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()); @@ -1836,8 +1921,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, @@ -1846,7 +1931,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, @@ -1975,7 +2060,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 } @@ -1999,7 +2084,7 @@ impl ExecutableOperationPackageV1 { /// Returns the executable program. #[must_use] - pub const fn program(&self) -> &EchoOperationProgramV1 { + pub const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2165,7 +2250,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")?, @@ -2176,7 +2261,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", )?)?, @@ -2228,6 +2313,27 @@ impl ExecutableOperationPackageV1 { "package step budget must be nonzero", )); } + self.semantic_closure.validate()?; + let requires_lawpack = match &self.program { + #[cfg(feature = "trusted_runtime")] + EchoOperationProgram::EdictEffect(_) => false, + _ => true, + }; + if requires_lawpack != self.semantic_closure.lawpack_identity.is_some() { + return Err(invalid_structure( + "lawpack binding does not match the program kind", + )); + } + #[cfg(feature = "trusted_runtime")] + if let Some(effect) = self.program.as_effect_program() { + 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", @@ -2267,7 +2373,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 @@ -2285,8 +2391,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, @@ -2296,7 +2402,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 @@ -2523,8 +2629,8 @@ impl EchoOperationAdmissionError { /// Opaque evidence that Echo admitted exact package bytes under separate policy. #[derive(Clone, Debug)] -pub struct AdmittedExecutableOperationPackageV1 { - package: ExecutableOperationPackageV1, +pub struct AdmittedExecutableOperationPackage { + package: ExecutableOperationPackage, canonical_package_bytes: Vec, package_id: EchoOperationPackageIdV1, admission_policy_id: Hash, @@ -2549,7 +2655,7 @@ 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]>, @@ -2588,7 +2694,7 @@ impl InstalledEchoOperationV1 { self.program_id } - pub(crate) const fn program(&self) -> &EchoOperationProgramV1 { + pub(crate) const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2654,7 +2760,7 @@ impl EchoOperationInstallationErrorV1 { pub(crate) fn admit_package_v1( policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, -) -> Result { +) -> Result { let package_id = echo_operation_package_id_v1(&canonical_package_bytes); if package_id.as_hash() != policy.expected_package_id { return Err(admission_error( @@ -2662,7 +2768,7 @@ pub(crate) fn admit_package_v1( "exact package bytes differ from the independently pinned package id", )); } - let package = ExecutableOperationPackageV1::from_canonical_bytes(&canonical_package_bytes) + let package = ExecutableOperationPackage::from_canonical_bytes(&canonical_package_bytes) .map_err(|artifact| EchoOperationAdmissionError { kind: EchoOperationAdmissionErrorKind::ArtifactInvalid, detail: artifact.to_string(), @@ -2674,7 +2780,7 @@ pub(crate) fn admit_package_v1( package.budget_ceiling, )?; let admission_policy_id = policy.identity(); - Ok(AdmittedExecutableOperationPackageV1 { + Ok(AdmittedExecutableOperationPackage { package, canonical_package_bytes, package_id, @@ -2685,16 +2791,19 @@ pub(crate) fn admit_package_v1( } pub(crate) fn installed_from_admitted( - admitted: AdmittedExecutableOperationPackageV1, + admitted: AdmittedExecutableOperationPackage, ) -> Result { let package = admitted.package; + let lawpack_identity = package.semantic_closure.lawpack_identity.ok_or_else(|| { + invalid_structure("authored effects require the effect installation crossing") + })?; let mut installed = InstalledEchoOperationV1 { installed_operation_id: InstalledEchoOperationIdV1([0; 32]), package_id: admitted.package_id, package_admission_id: admitted.admission_id, operation_coordinate: package.operation_coordinate.into(), semantic_identity: package.semantic_closure.canonical_meaning_identity, - lawpack_identity: package.semantic_closure.lawpack_identity, + lawpack_identity, target_profile_identity: package.target_profile_identity, interpreter_profile_identity: package.interpreter_profile_identity, intrinsic_profile_identity: package.intrinsic_profile_identity, @@ -3788,7 +3897,12 @@ fn current_application_basis( ) }; match installed.program { - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } => { + #[cfg(feature = "trusted_runtime")] + EchoOperationProgram::EdictEffect(_) => Err(invocation_admission_error( + EchoOperationInvocationAdmissionErrorKindV1::OperationProfileMismatch, + "fixed invocation does not match an authored-effect program", + )), + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } => { let store = state .store(&invocation.node.warp_id) .ok_or_else(|| basis_mismatch("application-basis warp is unavailable"))?; @@ -3827,7 +3941,7 @@ fn current_application_basis( &atom.bytes, )) } - EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } => { let store = state .store(&invocation.node.warp_id) .ok_or_else(|| basis_mismatch("application-basis warp is unavailable"))?; @@ -3876,8 +3990,8 @@ pub(crate) fn scoped_action_basis_holds_v1( let origin = &invocation.evaluation_basis; let supported = matches!( installed.program, - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } - | EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } + EchoOperationProgram::AnchoredNodeAttachmentCompareAndSet { .. } + | EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { .. } ); supported && origin.writer_head == current_head @@ -5163,7 +5277,7 @@ 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, @@ -5180,7 +5294,7 @@ pub(crate) fn prepare_operation_v1( }, ), ( - EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { required_node_type, required_attachment_type, max_replacement_bytes, @@ -7932,11 +8046,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), @@ -7949,7 +8063,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, @@ -8077,10 +8191,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), @@ -8093,7 +8207,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, @@ -8206,18 +8320,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, @@ -8281,16 +8395,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, @@ -8355,7 +8469,7 @@ 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, @@ -8376,7 +8490,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, @@ -8964,10 +9078,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), @@ -8980,7 +9094,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, @@ -9149,7 +9263,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, @@ -9201,10 +9315,10 @@ mod tests { } fn retained_fixture_installation() -> InstalledEchoOperationV1 { - let package = ExecutableOperationPackageV1::new( + let package = ExecutableOperationPackage::new( "echo.fixture.Retention.v1", "echo.fixture.Retention.Obstruction/v1", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( digest(1), digest(2), digest(3), @@ -9217,7 +9331,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, @@ -9634,7 +9748,7 @@ mod tests { fn legacy_compare_and_set_program_and_invocation_bytes_remain_fixed() { 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, diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 95b29925..f47b7d78 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -2,7 +2,7 @@ // © James Ross Ω FLYING•ROBOTS //! Authored-effect preparation, runtime-owned catalog and private recording reads. //! -//! Exact V2 publications and static invocations can be prepared, and the runtime +//! 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 crossings grants invocation rights or enables effect execution. //! Private frames bind immutable runtime frontiers, typed read laws and shared @@ -25,7 +25,7 @@ pub(crate) use installation::{ mod invocation; pub use invocation::EffectInvocationV2; mod package; -pub use package::{EffectPackageError, ExecutableOperationPackageV2}; +pub use package::{EffectPackageError, EffectProgram}; mod frame; mod read_image; pub use read_image::{ diff --git a/crates/warp-core/src/edict_effects/admission.rs b/crates/warp-core/src/edict_effects/admission.rs index 6224367a..8a862c33 100644 --- a/crates/warp-core/src/edict_effects/admission.rs +++ b/crates/warp-core/src/edict_effects/admission.rs @@ -1,20 +1,40 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS //! Runtime-owner package policy evidence, separate from installation. -use super::{EffectPackageError, ExecutableOperationPackageV2}; +use super::EffectPackageError; use crate::echo_operation::admission_error; +use crate::ExecutableOperationPackage; use crate::{ EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, }; /// Opaque evidence of runtime-owner admission, without installation or a grant. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AdmittedEffectPackageV2 { - package: ExecutableOperationPackageV2, + package: ExecutableOperationPackage, + canonical: Vec, + package_id: [u8; 32], + program_id: [u8; 32], policy_id: [u8; 32], id: [u8; 32], policy: EchoOperationAdmissionPolicy, } impl AdmittedEffectPackageV2 { + /// Exact publication identity admitted by the owner. + #[must_use] + pub const fn package_identity(&self) -> [u8; 32] { + self.package_id + } + /// Exact current program identity. + #[must_use] + pub const fn program_identity(&self) -> [u8; 32] { + self.program_id + } + /// Exact canonical publication retained at admission. + #[must_use] + pub fn canonical_package_bytes(&self) -> &[u8] { + &self.canonical + } + /// Independent policy retained alongside its identity. #[must_use] pub const fn admission_policy(&self) -> &EchoOperationAdmissionPolicy { @@ -22,7 +42,7 @@ impl AdmittedEffectPackageV2 { } /// Exact publication admitted by the runtime owner. #[must_use] - pub const fn package(&self) -> &ExecutableOperationPackageV2 { + pub const fn package(&self) -> &ExecutableOperationPackage { &self.package } /// Identity of the independently supplied admission policy. @@ -40,19 +60,36 @@ pub(crate) fn admit( policy: &EchoOperationAdmissionPolicy, bytes: Vec, ) -> Result { - let package = ExecutableOperationPackageV2::prepare(&bytes, policy.expected_package_identity()) + let package = ExecutableOperationPackage::prepare(&bytes, policy.expected_package_identity()) .map_err(|error| { - let kind = match error { - EffectPackageError::IdentityMismatch => { - EchoOperationAdmissionErrorKind::PackageIdentityMismatch - } - _ => EchoOperationAdmissionErrorKind::ArtifactInvalid, - }; + let kind = match error { + EffectPackageError::IdentityMismatch => { + EchoOperationAdmissionErrorKind::PackageIdentityMismatch + } + _ => EchoOperationAdmissionErrorKind::ArtifactInvalid, + }; + admission_error( + kind, + "effect publication differs from its pin or canonical contract", + ) + })?; + if package.program().as_effect_program().is_none() { + return Err(admission_error( + EchoOperationAdmissionErrorKind::ArtifactInvalid, + "effect admission requires an authored-effect program", + )); + } + let package_id = policy.expected_package_identity(); + let program_id = package + .program() + .identity() + .map_err(|_| { admission_error( - kind, - "effect publication differs from its pin or canonical contract", + EchoOperationAdmissionErrorKind::ArtifactInvalid, + "invalid current program", ) - })?; + })? + .as_hash(); policy.validate_contract( package.operation_coordinate(), package.authority_profile_identity(), @@ -61,10 +98,13 @@ pub(crate) fn admit( let policy_id = policy.identity(); let mut h = blake3::Hasher::new(); h.update(b"echo:operation-package-admission:v2\0"); - h.update(&package.identity()); + h.update(&package_id); h.update(&policy_id); Ok(AdmittedEffectPackageV2 { package, + canonical: bytes, + package_id, + program_id, policy_id, id: h.finalize().into(), policy: policy.clone(), diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs index 5b38d6ec..3f270e17 100644 --- a/crates/warp-core/src/edict_effects/installation.rs +++ b/crates/warp-core/src/edict_effects/installation.rs @@ -2,12 +2,13 @@ // © James Ross Ω FLYING•ROBOTS //! Versioned effect installation evidence; invocation authority remains separate. use super::package::hash; -use super::{AdmittedEffectPackageV2, ExecutableOperationPackageV2}; +use super::AdmittedEffectPackageV2; use crate::echo_operation::{ installation_error, invalid_structure, map_value, recover_installation_v1, }; use crate::edict_pure::values::{bytes, exact_fields, field, require_text, text}; use crate::EchoOperationAdmissionPolicy; +use crate::ExecutableOperationPackage; use crate::{ EchoOperationArtifactErrorV1, EchoOperationInstallationErrorKindV1, EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, InstalledEchoOperationV1, @@ -23,17 +24,33 @@ pub struct InstalledEffectOperationV2 { id: [u8; 32], } impl InstalledEffectOperationV2 { + /// Identity of the current package definition. + #[must_use] + pub const fn package_identity(&self) -> [u8; 32] { + self.admitted.package_identity() + } + /// Identity of the current program definition. + #[must_use] + pub const fn program_identity(&self) -> [u8; 32] { + self.admitted.program_identity() + } + /// Exact current publication bytes retained by installation. + #[must_use] + pub fn canonical_package_bytes(&self) -> &[u8] { + self.admitted.canonical_package_bytes() + } + pub(crate) fn from_admitted(admitted: AdmittedEffectPackageV2) -> Self { let package = admitted.package(); let mut h = blake3::Hasher::new(); h.update(b"echo:operation-installation:v2\0"); - h.update(&package.identity()); + h.update(&admitted.package_identity()); h.update(&admitted.identity()); h.update(&(package.operation_coordinate().len() as u64).to_le_bytes()); h.update(package.operation_coordinate().as_bytes()); - h.update(&package.program_identity()); - h.update(&(package.canonical_bytes().len() as u64).to_le_bytes()); - h.update(package.canonical_bytes()); + h.update(&admitted.program_identity()); + h.update(&(admitted.canonical_package_bytes().len() as u64).to_le_bytes()); + h.update(admitted.canonical_package_bytes()); Self { admitted, id: h.finalize().into(), @@ -41,7 +58,7 @@ impl InstalledEffectOperationV2 { } /// Exact publication named by this installation. #[must_use] - pub const fn package(&self) -> &ExecutableOperationPackageV2 { + pub const fn package(&self) -> &ExecutableOperationPackage { self.admitted.package() } /// Versioned installation identity, without invocation rights. @@ -63,10 +80,10 @@ impl InstalledEffectOperationV2 { "admission_policy_id", V::Bytes(self.admitted.admission_policy_identity().to_vec()), ), - ("package_id", V::Bytes(self.package().identity().to_vec())), + ("package_id", V::Bytes(self.package_identity().to_vec())), ( "package_bytes", - V::Bytes(self.package().canonical_bytes().to_vec()), + V::Bytes(self.canonical_package_bytes().to_vec()), ), ( "package_admission_id", @@ -111,7 +128,7 @@ impl InstalledEffectOperationV2 { let admitted = super::admission::admit(&policy, package_bytes.to_vec()) .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; if policy.identity() != policy_id - || admitted.package().identity() != package_id + || admitted.package_identity() != package_id || admitted.identity() != admission_id { return Err(invalid_structure("V2 retained admission identities differ")); @@ -126,14 +143,14 @@ impl InstalledEffectOperationV2 { } } pub(crate) enum RecoveredOperationInstallation { - V1(InstalledEchoOperationV1), - V2(InstalledEffectOperationV2), + V1(Box), + V2(Box), } impl RecoveredOperationInstallation { pub(crate) fn package_identity(&self) -> [u8; 32] { match self { Self::V1(v) => v.package_id().as_hash(), - Self::V2(v) => v.package().identity(), + Self::V2(v) => v.package_identity(), } } } @@ -146,12 +163,11 @@ pub(crate) fn recover_versioned_installation( .and_then(text) .map_err(|_| invalid_structure("missing installation version"))? { - "echo.operation-installation/v1" => { - recover_installation_v1(canonical).map(RecoveredOperationInstallation::V1) - } + "echo.operation-installation/v1" => recover_installation_v1(canonical) + .map(|installed| RecoveredOperationInstallation::V1(Box::new(installed))), "echo.operation-installation/v2" => { InstalledEffectOperationV2::from_retained_bytes(canonical) - .map(RecoveredOperationInstallation::V2) + .map(|installed| RecoveredOperationInstallation::V2(Box::new(installed))) } _ => Err(invalid_structure("unsupported installation version")), } @@ -162,7 +178,7 @@ pub(crate) fn restore_effect_catalog_entry( v1_operations: &BTreeMap, installed: InstalledEffectOperationV2, ) -> Result<(), EchoOperationInstallationErrorV1> { - let pin = installed.package().identity(); + let pin = installed.package_identity(); let coordinate = installed.package().operation_coordinate(); if let Some(existing) = packages.get(&pin) { if existing == &installed { diff --git a/crates/warp-core/src/edict_effects/invocation.rs b/crates/warp-core/src/edict_effects/invocation.rs index b3c11e4e..3aa248fb 100644 --- a/crates/warp-core/src/edict_effects/invocation.rs +++ b/crates/warp-core/src/edict_effects/invocation.rs @@ -2,11 +2,12 @@ // © James Ross Ω FLYING•ROBOTS //! Static V2 input/basis binding, without installed authority or freshness. use super::package::{domain_hash, hash}; -use super::{EffectPackageError, ExecutableOperationPackageV2}; +use super::EffectPackageError; use crate::edict_pure::{ values::{bytes, exact_fields, field, number, require_text, text}, EvaluationError, }; +use crate::ExecutableOperationPackage; use crate::{EchoOperationBudgetV1, EchoOperationEvaluationBasisV1}; use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; /// Prepared invocation bytes, not an admitted Action or verified grant. @@ -29,7 +30,7 @@ impl EffectInvocationV2 { /// Refuses malformed or inconsistent input, budget, package and basis fields. pub fn prepare( bytes: &[u8], - package: &ExecutableOperationPackageV2, + package: &ExecutableOperationPackage, ) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(|_| EffectPackageError::InvalidInvocation)?; @@ -38,7 +39,7 @@ impl EffectInvocationV2 { fn parse( value: &Value, canonical: &[u8], - package: &ExecutableOperationPackageV2, + package: &ExecutableOperationPackage, ) -> Result { exact_fields( value, @@ -53,14 +54,22 @@ impl EffectInvocationV2 { ], )?; require_text(value, "schema", "echo.operation-invocation/v2")?; - if hash(field(value, "package_id")?)? != package.identity() + let publication = package + .to_canonical_bytes() + .map_err(|_| EvaluationError::InvalidArtifact)?; + let effect = package + .program() + .as_effect_program() + .ok_or(EvaluationError::InvalidArtifact)?; + if hash(field(value, "package_id")?)? + != domain_hash(b"echo:operation-package:v1\0", &publication) || text(field(value, "operation_coordinate")?)? != package.operation_coordinate() { return Err(EvaluationError::InvalidArtifact); } let input = bytes(field(value, "application_input_bytes")?)?; if u64::try_from(input.len()).map_err(|_| EvaluationError::InvalidArtifact)? - > package.max_input_bytes() + > effect.max_input_bytes() { return Err(EvaluationError::InvalidArtifact); } diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index f61863f2..153406c0 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -1,18 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Exact V2 publication binding, separate from interpreter and admission. +//! 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 crate::EchoOperationBudgetV1; -use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; +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 V2 package identity. + /// 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. @@ -22,155 +21,24 @@ pub enum EffectPackageError { #[error("invalid effect invocation")] InvalidInvocation, } -/// Prepared publication, without installation or mutation authority. +/// Prepared authored-effect program payload, without execution authority. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ExecutableOperationPackageV2 { - canonical: Vec, - operation_coordinate: String, - authority_profile_identity: [u8; 32], - program_identity: [u8; 32], +pub struct EffectProgram { + value: Value, compiled: Vec, 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, - budget: EchoOperationBudgetV1, } -impl ExecutableOperationPackageV2 { - /// Checks exact pinned publication bytes and their executable contracts. - /// # Errors - /// Refuses substitutions, malformed maps and inconsistent contracts. - pub fn prepare(bytes: &[u8], expected: [u8; 32]) -> Result { - if domain_hash(b"echo:operation-package:v2\0", bytes) != expected { - return Err(EffectPackageError::IdentityMismatch); - } - let value = - decode_canonical_cbor_v1(bytes).map_err(|_| EffectPackageError::InvalidPackage)?; - Self::parse(&value, bytes).map_err(|_| EffectPackageError::InvalidPackage) - } - /// Exact bytes bound by the requested V2 package identity. - #[must_use] - pub fn canonical_bytes(&self) -> &[u8] { - &self.canonical - } - /// Identity of these exact publication bytes under the V2 package domain. - #[must_use] - pub fn identity(&self) -> [u8; 32] { - domain_hash(b"echo:operation-package:v2\0", &self.canonical) - } - /// Claimed authority profile, checked independently by runtime-owner policy. - #[must_use] - pub const fn authority_profile_identity(&self) -> [u8; 32] { - self.authority_profile_identity - } - /// Application-owned operation coordinate named by this publication. - #[must_use] - pub fn operation_coordinate(&self) -> &str { - &self.operation_coordinate - } - /// Identity of the exact nested program bytes under the V2 domain. - #[must_use] - pub const fn program_identity(&self) -> [u8; 32] { - self.program_identity - } - /// Compiler bytes, not yet validated by the effect interpreter. - #[must_use] - pub fn compiled_program(&self) -> &[u8] { - &self.compiled - } - /// Executable schema decoded from this publication's exact program. - #[must_use] - pub const fn graph_schema(&self) -> &EffectGraphSchema { - &self.schema - } - /// Footprint permissions decoded from the same pinned program as the schema. - #[must_use] - pub const fn footprint_law(&self) -> &EffectFootprintLaw { - &self.law - } - /// Compiler-declared result coordinate, bounded to 1,024 UTF-8 bytes. - #[must_use] - pub fn output_type(&self) -> &str { - &self.output_type - } - /// Declared application input ceiling, at most 65,536 bytes. - #[must_use] - pub const fn max_input_bytes(&self) -> u64 { - self.max_input_bytes - } - /// Declared application result ceiling, at most 65,536 bytes. - #[must_use] - pub const fn max_result_bytes(&self) -> u64 { - self.max_result_bytes - } - /// Declared evaluation allocation ceiling, at most 16 MiB. - #[must_use] - pub const fn max_allocated_bytes(&self) -> u64 { - self.max_allocated_bytes - } - /// Publication's budget ceiling; delegation is checked separately at admission. - #[must_use] - pub const fn budget_ceiling(&self) -> EchoOperationBudgetV1 { - self.budget - } - - fn parse(value: &Value, canonical: &[u8]) -> Result { +impl EffectProgram { + pub(crate) fn from_value(value: &Value) -> Result { exact_fields( value, - &[ - "schema", - "operation_coordinate", - "obstruction_coordinate", - "authority_profile_identity", - "semantic_closure", - "program", - "budget_ceiling", - "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", - ], - )?; - require_text(value, "schema", "echo.operation-package/v2")?; - coordinate(field(value, "operation_coordinate")?)?; - coordinate(field(value, "obstruction_coordinate")?)?; - hash(field(value, "authority_profile_identity")?)?; - let closure = field(value, "semantic_closure")?; - exact_fields( - closure, - &[ - "application_schema_coordinate", - "application_schema_identity", - "canonical_meaning_identity", - "core_identity", - "edict_source_identity", - "target_ir_identity", - ], - )?; - coordinate(field(closure, "application_schema_coordinate")?)?; - for name in [ - "application_schema_identity", - "canonical_meaning_identity", - "core_identity", - "edict_source_identity", - ] { - hash(field(closure, name)?)?; - } - let target_ir = hash(field(closure, "target_ir_identity")?)?; - let program_bytes = bytes(field(value, "program")?)?; - let program = decode_canonical_cbor_v1(program_bytes) - .map_err(|_| EvaluationError::InvalidArtifact)?; - exact_fields( - &program, &[ "schema", "kind", @@ -187,46 +55,12 @@ impl ExecutableOperationPackageV2 { "output_type", ], )?; - require_text(&program, "schema", "echo.operation-program/v2")?; - require_text(&program, "kind", "edict-effect-program/v1")?; - if number(field(&program, "identity_law")?)? != 1 - || hash(field(&program, "target_ir_identity")?)? != target_ir - { + 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 (name, label) in [ - ( - "target_profile_identity", - "echo.operation-target.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", - ), + for (key, label) in [ ( "interpreter_profile_identity", "echo.operation-interpreter.edict-effect/v1", @@ -236,65 +70,88 @@ impl ExecutableOperationPackageV2 { "echo.operation-intrinsics.edict-effect/v1", ), ] { - let expected = domain_hash(b"echo:operation-profile:v1\0", label.as_bytes()); - if hash(field(value, name)?)? != expected { + if hash(field(value, key)?)? + != domain_hash(b"echo:operation-profile:v1\0", label.as_bytes()) + { return Err(EvaluationError::InvalidArtifact); } } - for name in ["interpreter_profile_identity", "intrinsic_profile_identity"] { - if hash(field(&program, name)?)? != hash(field(value, name)?)? { - return Err(EvaluationError::InvalidArtifact); - } - } - let schema = - EffectGraphSchema::from_canonical_bytes(bytes(field(&program, "graph_schema")?)?) - .map_err(|_| EvaluationError::InvalidArtifact)?; - let law_bytes = bytes(field(&program, "footprint_law")?)?; + 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:v2\0"); + footprint.update(b"echo:operation-footprint-law:v1\0"); footprint.update(law_bytes); - if hash(field(value, "footprint_contract_identity")?)? != *footprint.finalize().as_bytes() { - return Err(EvaluationError::InvalidArtifact); - } - let max_input_bytes = number(field(&program, "max_input_bytes")?)?; - let max_result_bytes = number(field(&program, "max_result_bytes")?)?; - let max_allocated_bytes = number(field(&program, "max_allocated_bytes")?)?; - if max_input_bytes > 65_536 - || max_result_bytes > 65_536 + 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); } - if coordinate(field(&program, "output_type")?)?.len() > 1024 { - return Err(EvaluationError::InvalidArtifact); - } - let budget = field(value, "budget_ceiling")?; - exact_fields(budget, &["steps", "read_bytes", "write_bytes"])?; - let budget = EchoOperationBudgetV1::new( - number(field(budget, "steps")?)?, - number(field(budget, "read_bytes")?)?, - number(field(budget, "write_bytes")?)?, - ); - if budget.steps() == 0 { - return Err(EvaluationError::InvalidArtifact); - } Ok(Self { - canonical: canonical.to_vec(), - authority_profile_identity: hash(field(value, "authority_profile_identity")?)?, - operation_coordinate: coordinate(field(value, "operation_coordinate")?)?.to_owned(), - program_identity: domain_hash(b"echo:operation-program:v2\0", program_bytes), - compiled: bytes(field(&program, "compiled_program")?)?.to_vec(), + value: value.clone(), + compiled: bytes(field(value, "compiled_program")?)?.to_vec(), schema, law, - output_type: coordinate(field(&program, "output_type")?)?.to_owned(), + 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, - budget, }) } + 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 + } + /// Compiler bytes whose instruction language still requires validation. + #[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)? diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 8c7d0057..1013c47f 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -260,7 +260,7 @@ pub use echo_operation::{ 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, + AdmittedExecutableOperationPackage, EchoOperationActionDecisionCoordinateV1, EchoOperationActionOutcomeV1, EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, EchoOperationApplicationResultV1, @@ -274,9 +274,9 @@ pub use echo_operation::{ EchoOperationInvocationV1, EchoOperationObservationV1, EchoOperationObstructionIdV1, EchoOperationObstructionKindV1, EchoOperationObstructionV1, EchoOperationPackageAdmissionIdV1, EchoOperationPackageIdV1, EchoOperationPreparationV1, EchoOperationPrivateEvaluationIdV1, - EchoOperationProgramIdV1, EchoOperationProgramV1, EchoOperationReceiptV1, - EchoOperationResultIdV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, - ExecutableOperationPackageV1, InstalledEchoOperationIdV1, InstalledEchoOperationV1, + EchoOperationProgram, EchoOperationProgramIdV1, EchoOperationReceiptV1, + EchoOperationResultIdV1, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, + ExecutableOperationPackage, InstalledEchoOperationIdV1, InstalledEchoOperationV1, 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 f2e988c3..7a758826 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -66,7 +66,7 @@ use crate::{ EchoOperationEvaluationAuthorityV1, SchedulerEchoOperationCandidateV1, }, provider_contract::admit_provider_contract_package_v1, - AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackageV1, + AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackage, AdmittedProviderContractPackageV1, CausalAnchorAdmissionRequest, CausalAnchorClaim, CausalAnchorError, CausalAnchorId, CausalAnchorRootSupportPolicy, CausalAnchorSupportError, CausalFrontierRef, CausalReceiptHistoryObstruction, CausalReceiptHistoryPage, @@ -1133,7 +1133,7 @@ impl TrustedRuntimeHost { &self, policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, - ) -> Result { + ) -> Result { admit_package_v1(policy, canonical_package_bytes) } @@ -1157,7 +1157,7 @@ impl TrustedRuntimeHost { .preflight_echo_effect_installations(core::slice::from_ref(&installed))?; if self .engine - .installed_echo_operation_package_v2(installed.package().identity()) + .installed_echo_operation_package_v2(installed.package_identity()) == Some(&installed) { return Ok(installed); @@ -1182,7 +1182,7 @@ impl TrustedRuntimeHost { /// this runtime-owner method. pub fn install_admitted_echo_operation_package_v1( &mut self, - admitted: AdmittedExecutableOperationPackageV1, + admitted: AdmittedExecutableOperationPackage, ) -> Result { let installed = installed_from_admitted(admitted)?; self.engine @@ -2439,11 +2439,11 @@ fn ensure_runtime_authority_is_durable( let recovered_effects = recovery .installed_echo_effect_operations .iter() - .map(|installed| (installed.package().identity(), installed)) + .map(|installed| (installed.package_identity(), installed)) .collect::>(); if engine.installed_echo_effect_packages().any(|installed| { recovered_effects - .get(&installed.package().identity()) + .get(&installed.package_identity()) .is_none_or(|recovered| **recovered != *installed) }) { return Err(TrustedRuntimeWalError::RuntimeAuthorityNotDurable { @@ -4668,13 +4668,13 @@ fn recover_echo_operation_material( if effect_operations.contains_key(installed.operation_coordinate()) { return Err(crate::echo_operation::installation_error(crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict,"recovered V1 coordinate conflicts with V2 meaning").into()); } - install_recovered_v1(&mut installations, &mut operations, installed)?; + install_recovered_v1(&mut installations, &mut operations, *installed)?; } RecoveredOperationInstallation::V2(installed) => restore_effect_catalog_entry( &mut effects, &mut effect_operations, &operations, - installed, + *installed, )?, } } @@ -4725,10 +4725,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, .. @@ -4754,7 +4755,7 @@ fn operation_patch_scope_v1( } _ => None, }, - crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + crate::EchoOperationProgram::AnchoredNodeAttachmentCreateIfAbsent { required_node_type, required_attachment_type, max_replacement_bytes, @@ -4833,7 +4834,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)?; @@ -4843,7 +4844,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() { @@ -4871,7 +4872,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; @@ -4892,7 +4893,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)?; @@ -4910,13 +4911,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) } @@ -7661,10 +7663,10 @@ mod tests { fn recovery_index_installation(operation_coordinate: &str) -> InstalledEchoOperationV1 { 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.RecoveryIndex.Obstruction.v1", - crate::EchoOperationSemanticClosureV1::new( + crate::EchoOperationSemanticClosure::new( [0x10; 32], [0x11; 32], [0x12; 32], @@ -7677,7 +7679,7 @@ mod tests { crate::echo_operation_target_profile_identity_v1(), authority_profile_identity, budget, - crate::EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + crate::EchoOperationProgram::anchored_node_attachment_compare_and_set( crate::make_type_id("recovery-index-node"), crate::make_type_id("recovery-index-attachment"), 128, @@ -7754,17 +7756,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!( @@ -8057,7 +8058,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, @@ -9144,7 +9145,7 @@ fn recovered_echo_operation_index_root( }) .collect::, TrustedRuntimeWalError>>()?; for installed in effect_installations { - catalog.push((installed.package().identity(), installed.retained_bytes()?)); + catalog.push((installed.package_identity(), installed.retained_bytes()?)); } catalog.sort_unstable(); diff --git a/crates/warp-core/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index 5fb61aec..8bff9ad1 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -3,7 +3,8 @@ //! 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, ExecutableOperationPackageV2}; +use warp_core::edict_effects::EffectPackageError; +use warp_core::ExecutableOperationPackage; pub fn map(fields: impl IntoIterator) -> V { V::Map( fields @@ -61,13 +62,13 @@ pub fn law() -> V { } pub fn footprint(v: &V) -> V { let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-footprint-law:v2\0"); + h.update(b"echo:operation-footprint-law:v1\0"); h.update(&encoded(v)); V::Bytes(h.finalize().as_bytes().to_vec()) } pub fn program() -> V { map([ - ("schema", text("echo.operation-program/v2")), + ("schema", text("echo.operation-program/v1")), ("kind", text("edict-effect-program/v1")), ( "interpreter_profile_identity", @@ -90,7 +91,7 @@ pub fn program() -> V { } pub fn package(program: &V) -> V { map([ - ("schema", text("echo.operation-package/v2")), + ("schema", text("echo.operation-package/v1")), ("operation_coordinate", text("app.update@1")), ("obstruction_coordinate", text("app.Refusal")), ("authority_profile_identity", id(8)), @@ -168,10 +169,10 @@ 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 { +pub fn prepare(v: &V) -> Result { let b = encoded(v); - let expected = hash(b"echo:operation-package:v2\0", &b); - ExecutableOperationPackageV2::prepare(&b, expected) + let expected = hash(b"echo:operation-package:v1\0", &b); + ExecutableOperationPackage::prepare(&b, expected) } pub fn refused(v: &V) { assert!( 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 00000000..625c411a --- /dev/null +++ b/crates/warp-core/tests/current_operation_publication_tests.rs @@ -0,0 +1,108 @@ +// 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_eq!(effect.compiled_program(), [0xa0]); + 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 index b25cde6a..2c422a9e 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -31,7 +31,7 @@ fn policy( budget: EchoOperationBudgetV1, ) -> EchoOperationAdmissionPolicy { EchoOperationAdmissionPolicy::exact( - hash(b"echo:operation-package:v2\0", bytes), + hash(b"echo:operation-package:v1\0", bytes), operation, authority, budget, @@ -50,14 +50,14 @@ fn runtime_owner_admits_exact_publication_and_retains_separate_policy_evidence() let admitted = host .admit_echo_operation_package_v2(&policy, bytes.clone()) .expect("exact policy admits"); - assert_eq!(admitted.package().canonical_bytes(), bytes); + assert_eq!(admitted.canonical_package_bytes(), bytes); assert_eq!( - admitted.package().identity(), - hash(b"echo:operation-package:v2\0", &bytes) + 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:v2\0", &bytes)); + 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]); @@ -70,7 +70,7 @@ fn runtime_owner_admits_exact_publication_and_retains_separate_policy_evidence() ); let mut id = blake3::Hasher::new(); id.update(b"echo:operation-package-admission:v2\0"); - id.update(&hash(b"echo:operation-package:v2\0", &bytes)); + id.update(&hash(b"echo:operation-package:v1\0", &bytes)); id.update(oracle.finalize().as_bytes()); assert_eq!(admitted.identity(), *id.finalize().as_bytes()); } @@ -113,7 +113,7 @@ fn runtime_policy_refuses_operation_authority_and_budget_substitution() { 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/v1")); + set(&mut value, "schema", text("echo.operation-package/v2")); let bytes = encoded(&value); assert_eq!( host.admit_echo_operation_package_v2( @@ -215,7 +215,7 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() assert_ne!(first.identity(), second.identity()); let mut wrong_domain = blake3::Hasher::new(); wrong_domain.update(b"echo:operation-package-admission:v1\0"); - wrong_domain.update(&first.package().identity()); + wrong_domain.update(&first.package_identity()); wrong_domain.update(&first.admission_policy_identity()); assert_ne!(first.identity(), *wrong_domain.finalize().as_bytes()); } diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs index d8657033..dc2a1c2a 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -52,19 +52,19 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact .expect("install"); let mut identity_oracle = blake3::Hasher::new(); identity_oracle.update(b"echo:operation-installation:v2\0"); - identity_oracle.update(&token.package().identity()); + 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.package().program_identity()); - identity_oracle.update(&(token.package().canonical_bytes().len() as u64).to_le_bytes()); - identity_oracle.update(token.package().canonical_bytes()); + 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_v2(installed.package().identity()), + .installed_echo_operation_package_v2(installed.package_identity()), Some(&installed) ); assert_eq!( @@ -99,7 +99,7 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact assert_eq!( fresh .engine() - .installed_echo_operation_package_v2(installed.package().identity()), + .installed_echo_operation_package_v2(installed.package_identity()), Some(&installed) ); } @@ -194,7 +194,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen assert_eq!( owner .engine() - .installed_echo_operation_package_v2(installed.package().identity()), + .installed_echo_operation_package_v2(installed.package_identity()), Some(&installed) ); assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); @@ -212,14 +212,14 @@ fn install_v1( coordinate: &str, ) -> Result { use warp_core::{ - EchoOperationAdmissionPolicy, EchoOperationProgramV1, EchoOperationSemanticClosureV1, - ExecutableOperationPackageV1, + EchoOperationAdmissionPolicy, EchoOperationProgram, EchoOperationSemanticClosure, + ExecutableOperationPackage, }; let budget = EchoOperationBudgetV1::new(16, 4096, 4096); - let legacy = ExecutableOperationPackageV1::new( + let legacy = ExecutableOperationPackage::new( coordinate, "app.LegacyRefusal", - EchoOperationSemanticClosureV1::new( + EchoOperationSemanticClosure::new( [1; 32], [2; 32], [3; 32], @@ -232,7 +232,7 @@ fn install_v1( warp_core::echo_operation_target_profile_identity_v1(), [8; 32], budget, - EchoOperationProgramV1::anchored_node_attachment_compare_and_set( + EchoOperationProgram::anchored_node_attachment_compare_and_set( make_type_id("legacy-node"), make_type_id("legacy-atom"), 1024, @@ -312,7 +312,7 @@ fn mixed_catalog_recovers_both_versions_and_certificate_binds_v2_installations() assert_eq!( fresh .engine() - .installed_echo_operation_package_v2(effect.package().identity()), + .installed_echo_operation_package_v2(effect.package_identity()), Some(&effect) ); } @@ -330,7 +330,7 @@ fn failed_wal_append_and_commit_do_not_publish_v2_registry_entries() { .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) .expect("wal"); let token = admitted(&owner); - let pin = token.package().identity(); + let pin = token.package_identity(); owner .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next(target)) .expect("fault"); @@ -372,7 +372,7 @@ fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission( "expected_authority_profile_identity", id(99), ); - let pin = token.package().identity(); + let pin = token.package_identity(); let mut h = blake3::Hasher::new(); h.update(b"echo:operation-admission-policy:v1\0"); h.update(&pin); @@ -404,9 +404,9 @@ fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission( h.update(&admission_id); h.update(&12u64.to_le_bytes()); h.update(b"app.update@1"); - h.update(&token.package().program_identity()); - h.update(&(token.package().canonical_bytes().len() as u64).to_le_bytes()); - h.update(token.package().canonical_bytes()); + 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", @@ -424,7 +424,7 @@ fn readable_unsynced_installation_is_not_published_until_a_fresh_store_reopens() .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(&dir.0)) .expect("wal"); let token = admitted(&owner); - let pin = token.package().identity(); + let pin = token.package_identity(); let package = token.package().clone(); owner .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next( diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 0a125470..ce8f5568 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -4,34 +4,52 @@ #![cfg(feature = "trusted_runtime")] #![allow(clippy::expect_used, clippy::panic)] use echo_edict_canonical::CanonicalValueV1 as V; -use warp_core::edict_effects::{EffectPackageError, ExecutableOperationPackageV2}; +use warp_core::edict_effects::EffectPackageError; +use warp_core::ExecutableOperationPackage; use warp_core::TypeId; #[path = "common/edict_effect_publication.rs"] mod fixture; use fixture::*; #[test] -fn exact_v2_package_binds_program_and_executes_its_schema_relations() { +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.canonical_bytes(), b); + assert_eq!(package.to_canonical_bytes().expect("canonical package"), b); assert_eq!( - package.program_identity(), - hash(b"echo:operation-program:v2\0", &encoded(&p)) + 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.compiled_program(), [0xa0]); + assert_eq!( + package + .program() + .as_effect_program() + .expect("effect program") + .compiled_program(), + [0xa0] + ); } #[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:v2\0", &original); + let pin = hash(b"echo:operation-package:v1\0", &original); for mode in 0..4 { let mut p = program(); let mut v = package(&p); @@ -59,18 +77,18 @@ fn substituted_program_schema_law_and_closure_cannot_keep_the_original_pin() { } _ => set(&mut v, "operation_coordinate", text("app.other@1")), } - assert!(ExecutableOperationPackageV2::prepare(&encoded(&v), pin).is_err()); + assert!(ExecutableOperationPackage::prepare(&encoded(&v), pin).is_err()); } - assert!(ExecutableOperationPackageV2::prepare( + assert!(ExecutableOperationPackage::prepare( &original, - hash(b"echo:operation-package:v1\0", &original) + hash(b"echo:operation-package:v2\0", &original) ) .is_err()); } #[test] -fn exact_package_and_closure_shapes_refuse_v1_lawpacks_projection_and_extra_fields() { +fn exact_package_and_closure_shapes_refuse_effect_lawpacks_projection_and_extra_fields() { for (k, x) in [ - ("schema", text("echo.operation-package/v1")), + ("schema", text("echo.operation-package/v2")), ("program", program()), ] { let mut v = package(&program()); @@ -127,7 +145,7 @@ fn all_package_profile_equalities_and_target_ir_binding_are_enforced() { #[test] fn program_refuses_unknown_profiles_laws_shapes_and_unbounded_declarations() { for (k, x) in [ - ("schema", text("echo.operation-program/v1")), + ("schema", text("echo.operation-program/v2")), ("kind", text("other")), ("identity_law", V::Integer(0)), ("identity_law", V::Integer(2)), @@ -177,7 +195,7 @@ fn nested_canonical_bytes_and_exact_footprint_law_digest_are_required() { set( &mut v, "footprint_contract_identity", - V::Bytes(hash(b"echo:operation-footprint-law:v2\0", &encoded(&law())).to_vec()), + V::Bytes(hash(b"echo:operation-footprint-law:v1\0", &encoded(&law())).to_vec()), ); refused(&v); let mut v = package(&program()); @@ -188,12 +206,11 @@ fn nested_canonical_bytes_and_exact_footprint_law_digest_are_required() { let mut b = encoded(&package(&program())); b.push(0); assert!( - ExecutableOperationPackageV2::prepare(&b, hash(b"echo:operation-package:v2\0", &b)) - .is_err() + ExecutableOperationPackage::prepare(&b, hash(b"echo:operation-package:v1\0", &b)).is_err() ); } #[test] -fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_v1_coordinate_rules() { +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)); @@ -201,11 +218,40 @@ fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_v1_coordina set(&mut p, "output_type", text(&"é".repeat(512))); let mut v = package(&p); set(&mut v, "operation_coordinate", text(&"x".repeat(1025))); - let prepared = prepare(&v).expect("zero byte limits and legacy package coordinate shape"); - assert_eq!(prepared.max_input_bytes(), 0); - assert_eq!(prepared.max_result_bytes(), 0); - assert_eq!(prepared.max_allocated_bytes(), 0); - assert_eq!(prepared.output_type().len(), 1024); + 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); @@ -228,10 +274,17 @@ fn a_new_pin_can_bind_changed_laws_but_cannot_reuse_the_old_contract_identity() ); let prepared = prepare(&v).expect("new pin binds the new exact law"); assert_ne!( - prepared.program_identity(), + prepared + .program() + .identity() + .expect("program identity") + .as_hash(), prepare(&package(&program())) .expect("original") - .program_identity() + .program() + .identity() + .expect("program identity") + .as_hash() ); } #[path = "edict_effect_package_tests/invocation.rs"] diff --git a/crates/warp-core/tests/edict_effect_package_tests/invocation.rs b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs index b55f2731..99900cef 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/invocation.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs @@ -19,7 +19,7 @@ fn invocation(input: Vec) -> V { ("operation_coordinate", text("app.update@1")), ( "package_id", - V::Bytes(hash(b"echo:operation-package:v2\0", &package_bytes).to_vec()), + V::Bytes(hash(b"echo:operation-package:v1\0", &package_bytes).to_vec()), ), ("application_input_bytes", V::Bytes(input.clone())), ( @@ -184,7 +184,13 @@ fn input_uses_the_selected_packages_smaller_ceiling_and_exact_outer_bytes() { set( &mut value, "package_id", - V::Bytes(selected.identity().to_vec()), + V::Bytes( + hash( + b"echo:operation-package:v1\0", + &selected.to_canonical_bytes().expect("selected publication"), + ) + .to_vec(), + ), ); assert_eq!( EffectInvocationV2::prepare(&encoded(&value), &selected).is_ok(), diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 0abde4ad..69df9850 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -27,8 +27,8 @@ use warp_core::{ EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, - EchoOperationProgramV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, - EngineBuilder, ExecutableOperationPackageV1, GraphStore, HeadEligibility, InboxPolicy, + EchoOperationProgram, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, + EngineBuilder, ExecutableOperationPackage, GraphStore, HeadEligibility, InboxPolicy, IngressTarget, InstalledEchoOperationV1, NodeKey, NodeRecord, PlaybackMode, RuntimeError, RuntimeWalActivationGap, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeHostError, TrustedRuntimeWalConfig, TrustedRuntimeWalError, WorldlineId, WorldlineRuntime, WorldlineState, @@ -435,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"), @@ -451,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) } @@ -459,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, @@ -478,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, @@ -1117,14 +1117,14 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by 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, @@ -1237,14 +1237,14 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by 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, diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 6814ff5f..f1039666 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -201,9 +201,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 @@ -236,7 +236,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 @@ -649,8 +649,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. @@ -666,7 +666,7 @@ trigger hidden retries or widen access. ### Executable graph-schema preparation `EffectGraphSchema::from_canonical_bytes` decodes the graph contract required by -the authored-effect V2 program direction. The root has exactly `node_types` and +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 @@ -681,44 +681,57 @@ 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. -### V2 effect publication preparation - -`edict_effects::ExecutableOperationPackageV2::prepare` checks a trusted host's -expected V2 package identity against the exact canonical publication bytes. -This is byte binding and contract preparation, not independent compiler -verification, package admission, installation or permission to execute. - -The V2 package has the V1 required package fields with its versioned schema, -no `application_result_projection`, and a six-field semantic closure that omits -both lawpack keys. Its nested canonical program has exactly the thirteen -`edict-effect-program/v1` fields. The decoder requires the effect target, -interpreter, intrinsic, input, result, obstruction, interpretation and basis -profiles, equality of the closure/program Target IR identities, and -ContentAddressV1 as the identity law. It decodes and retains the exact program's -executable graph schema and footprint permissions together. The footprint -contract is BLAKE3 over `echo:operation-footprint-law:v2\0` followed directly by -the exact canonical law bytes. Package/program identities use their V2 domains -and the existing length-prefixed artifact recipe; those recipes are distinct. +### Current operation publication preparation + +`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; these declarations do not -provision a physical arena. The output coordinate is nonempty UTF-8 of at most -1,024 bytes. Other package/closure coordinates retain V1's nonempty-text rule. -The package budget requires at least one step and permits zero byte budgets. -Canonical preparation uses the existing decoder's node/depth ceilings and the -schema/law preparation bounds. It occurs outside evaluation metering. - -The compiler bytes and upstream source, meaning, Core and Target IR identities -remain publication evidence. This preparation does not infer verifier approval -or validate the compiled instruction language. Invocation/authority admission, -interpreter execution, graph identity enforcement, atomic effects and retained -recovery must separately consume and validate this prepared contract. V1 -package decoding and identity recipes remain unchanged. +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. + +This is byte binding and contract preparation. Compiler bytes and upstream +identities do not infer verifier approval or validate the compiled instruction +language. The separate invocation/admission-token/installation definitions and +catalogs described below still require consolidation under the one-current- +version policy. Preparation does not grant invocation or mutation authority. ### V2 invocation input and basis preparation `EffectInvocationV2::prepare` checks an exact canonical V2 invocation against a -prepared V2 publication. Only its seven specified fields are accepted. The +prepared current publication. Only its seven specified fields are accepted. The package identity and operation coordinate must match the publication. The application input must itself be canonical CBOR and fit that program's declared input ceiling, including the input's CBOR framing bytes. The delegated budget @@ -746,8 +759,8 @@ its retained codec and its shared operation/authority/budget checks, and return the same `EchoOperationAdmissionError` / `EchoOperationAdmissionErrorKind` refusal contract. The old version-specific policy and refusal definitions have been removed without compatibility aliases. This is one completed part of the -format cutover; the separate package and installation definitions below still -require replacement under the one-current-version policy. +format cutover; the remaining invocation, admission-token and installation +definitions still require replacement under the one-current-version policy. The host prepares the canonical package, checks all four independent policy conditions, and returns opaque `AdmittedEffectPackageV2` evidence. Every budget @@ -755,7 +768,7 @@ axis is checked separately. A matching package pin alone cannot substitute for canonical artifact/profile validation or for the independent authority policy. The evidence retains the exact package and policy. Policy identity uses the -existing `echo:operation-admission-policy:v1\0` recipe with the V2 package pin. +existing `echo:operation-admission-policy:v1\0` recipe with the current package pin. The package-admission identity uses `echo:operation-package-admission:v2\0` followed by the package and policy identities. Two policies admitting the same program therefore produce distinct policy/admission evidence while retaining @@ -780,11 +793,14 @@ and compiled effect instruction validation. The application handle cannot call this owner-only method. Reconstructing an `InstalledEffectOperationV2` from retained bytes produces evidence only; it does not insert a registry entry. -The engine shares operation-coordinate exclusion across V1 and V2. An exact -repeat is idempotent; a changed package at the same coordinate or a changed +The engine still has separate fixed-program and effect installation catalogs, +both consuming the sole current package/program definitions. It shares operation- +coordinate exclusion across them. An exact repeat is idempotent; a changed package at the same coordinate or a changed policy for an installed package refuses before any WAL append. Batch recovery -preflights both catalogs. V1's package and installation identity recipes remain -unchanged. V2 installation identity uses `echo:operation-installation:v2\0`, +preflights both catalogs. These two installation definitions and their versioned +retained records remain unfinished consolidation work, not a compatibility +exception. Package/program identities use the shared recipe above. The effect +installation identity still uses `echo:operation-installation:v2\0`, then package identity, package-admission identity, length-prefixed operation coordinate, program identity and length-prefixed exact package bytes, in that order, as the V1 recipe does under its original domain. @@ -819,8 +835,8 @@ 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. -The package and installation definitions still have the parallel versions -described above; the index cutover does not complete their replacement. A fresh +The installation and admission-token definitions still have parallel versions; +the shared package/program and index cutovers do not complete their replacement. A fresh filesystem-backed host currently restores those installations and rechecks coordinate conflicts. This is installation recovery, not recovery of an authored-effect decision. Invocation grant admission, compiled execution, From 24c59c3f3f6d296d7114651177b5482d88b2fdc0 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 08:48:17 -0700 Subject: [PATCH 21/43] Unify package admission evidence and owner API --- CHANGELOG.md | 13 +- .../tests/action_preparation_allocation.rs | 2 +- .../warp-core/benches/support/action_scene.rs | 2 +- crates/warp-core/src/echo_operation.rs | 76 +++++++++--- crates/warp-core/src/edict_effects.rs | 6 - .../warp-core/src/edict_effects/admission.rs | 112 ------------------ .../src/edict_effects/installation.rs | 24 ++-- crates/warp-core/src/trusted_runtime_host.rs | 61 ++++------ .../tests/edict_effect_admission_tests.rs | 93 ++++++++++----- .../installation.rs | 12 +- .../tests/edict_projected_cas_tests.rs | 4 +- .../executable_operation_pipeline_tests.rs | 24 ++-- docs/topics/GeneratedRules.md | 76 ++++++------ xtask/src/run_edict_operation.rs | 2 +- 14 files changed, 237 insertions(+), 270 deletions(-) delete mode 100644 crates/warp-core/src/edict_effects/admission.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 30524cc6..ff122b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Changed +- 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; @@ -36,9 +43,9 @@ coordinate-conflict checks with V1. Conflicts and failed WAL appends publish no V2 registry entry. Installation recovery does not authorize or execute effects. -- Runtime-owner V2 package admission checks independently pinned publication, - operation, authority profile and budget policy, retaining distinct versioned - admission evidence. Restricted application handles cannot admit packages. +- 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 diff --git a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs index 8b0e0411..f4bda0dd 100644 --- a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs +++ b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs @@ -182,7 +182,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, diff --git a/crates/warp-core/benches/support/action_scene.rs b/crates/warp-core/benches/support/action_scene.rs index 636f90f5..594cd02b 100644 --- a/crates/warp-core/benches/support/action_scene.rs +++ b/crates/warp-core/benches/support/action_scene.rs @@ -226,7 +226,7 @@ impl Scene { }; let package = package(); let admitted = host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( echo_operation_package_id_v1(&package).as_hash(), OPERATION, diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index b676c5f0..0f2880dd 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -2510,10 +2510,6 @@ impl EchoOperationAdmissionPolicy { hasher.finalize().into() } - pub(crate) const fn expected_package_identity(&self) -> Hash { - self.expected_package_id - } - pub(crate) fn validate_contract( &self, operation_coordinate: &str, @@ -2628,16 +2624,55 @@ impl EchoOperationAdmissionError { } /// Opaque evidence that Echo admitted exact package bytes under separate policy. -#[derive(Clone, Debug)] +#[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: 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 { @@ -2757,7 +2792,7 @@ impl EchoOperationInstallationErrorV1 { } } -pub(crate) fn admit_package_v1( +pub(crate) fn admit_operation_package( policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, ) -> Result { @@ -2779,9 +2814,19 @@ pub(crate) fn admit_package_v1( 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(AdmittedExecutableOperationPackage { package, + program_id, canonical_package_bytes, package_id, admission_policy_id, @@ -2810,7 +2855,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 @@ -2891,7 +2936,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}" )) @@ -8229,7 +8274,7 @@ 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( + admit_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), operation_coordinate, @@ -8356,7 +8401,7 @@ mod tests { 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") @@ -8378,7 +8423,7 @@ 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(), @@ -8424,7 +8469,8 @@ mod tests { 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()); @@ -8451,7 +8497,7 @@ 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(), @@ -9103,7 +9149,7 @@ 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( + admit_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), operation_coordinate, @@ -9345,7 +9391,7 @@ mod tests { digest(7), EchoOperationBudgetV1::new(8, 512, 512), ); - installed_from_admitted(admit_package_v1(&policy, bytes).expect("package admits")) + installed_from_admitted(admit_operation_package(&policy, bytes).expect("package admits")) .expect("an admitted program remains canonically encodable") } diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index f47b7d78..fd684c94 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,14 +9,8 @@ //! logical meters. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. #[cfg(feature = "native_rule_bootstrap")] -mod admission; -#[cfg(feature = "native_rule_bootstrap")] mod installation; #[cfg(feature = "native_rule_bootstrap")] -pub(crate) use admission::admit as admit_effect_package_v2; -#[cfg(feature = "native_rule_bootstrap")] -pub use admission::AdmittedEffectPackageV2; -#[cfg(feature = "native_rule_bootstrap")] pub use installation::InstalledEffectOperationV2; #[cfg(feature = "native_rule_bootstrap")] pub(crate) use installation::{ diff --git a/crates/warp-core/src/edict_effects/admission.rs b/crates/warp-core/src/edict_effects/admission.rs deleted file mode 100644 index 8a862c33..00000000 --- a/crates/warp-core/src/edict_effects/admission.rs +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS -//! Runtime-owner package policy evidence, separate from installation. -use super::EffectPackageError; -use crate::echo_operation::admission_error; -use crate::ExecutableOperationPackage; -use crate::{ - EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, -}; -/// Opaque evidence of runtime-owner admission, without installation or a grant. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AdmittedEffectPackageV2 { - package: ExecutableOperationPackage, - canonical: Vec, - package_id: [u8; 32], - program_id: [u8; 32], - policy_id: [u8; 32], - id: [u8; 32], - policy: EchoOperationAdmissionPolicy, -} -impl AdmittedEffectPackageV2 { - /// Exact publication identity admitted by the owner. - #[must_use] - pub const fn package_identity(&self) -> [u8; 32] { - self.package_id - } - /// Exact current program identity. - #[must_use] - pub const fn program_identity(&self) -> [u8; 32] { - self.program_id - } - /// Exact canonical publication retained at admission. - #[must_use] - pub fn canonical_package_bytes(&self) -> &[u8] { - &self.canonical - } - - /// Independent policy retained alongside its identity. - #[must_use] - pub const fn admission_policy(&self) -> &EchoOperationAdmissionPolicy { - &self.policy - } - /// Exact publication admitted by the runtime owner. - #[must_use] - pub const fn package(&self) -> &ExecutableOperationPackage { - &self.package - } - /// Identity of the independently supplied admission policy. - #[must_use] - pub const fn admission_policy_identity(&self) -> [u8; 32] { - self.policy_id - } - /// Admission identity under the distinct V2 domain. - #[must_use] - pub const fn identity(&self) -> [u8; 32] { - self.id - } -} -pub(crate) fn admit( - policy: &EchoOperationAdmissionPolicy, - bytes: Vec, -) -> Result { - let package = ExecutableOperationPackage::prepare(&bytes, policy.expected_package_identity()) - .map_err(|error| { - let kind = match error { - EffectPackageError::IdentityMismatch => { - EchoOperationAdmissionErrorKind::PackageIdentityMismatch - } - _ => EchoOperationAdmissionErrorKind::ArtifactInvalid, - }; - admission_error( - kind, - "effect publication differs from its pin or canonical contract", - ) - })?; - if package.program().as_effect_program().is_none() { - return Err(admission_error( - EchoOperationAdmissionErrorKind::ArtifactInvalid, - "effect admission requires an authored-effect program", - )); - } - let package_id = policy.expected_package_identity(); - let program_id = package - .program() - .identity() - .map_err(|_| { - admission_error( - EchoOperationAdmissionErrorKind::ArtifactInvalid, - "invalid current program", - ) - })? - .as_hash(); - policy.validate_contract( - package.operation_coordinate(), - package.authority_profile_identity(), - package.budget_ceiling(), - )?; - let policy_id = policy.identity(); - let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-package-admission:v2\0"); - h.update(&package_id); - h.update(&policy_id); - Ok(AdmittedEffectPackageV2 { - package, - canonical: bytes, - package_id, - program_id, - policy_id, - id: h.finalize().into(), - policy: policy.clone(), - }) -} diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs index 3f270e17..0ab4c4a0 100644 --- a/crates/warp-core/src/edict_effects/installation.rs +++ b/crates/warp-core/src/edict_effects/installation.rs @@ -2,11 +2,11 @@ // © James Ross Ω FLYING•ROBOTS //! Versioned effect installation evidence; invocation authority remains separate. use super::package::hash; -use super::AdmittedEffectPackageV2; use crate::echo_operation::{ installation_error, invalid_structure, map_value, recover_installation_v1, }; use crate::edict_pure::values::{bytes, exact_fields, field, require_text, text}; +use crate::AdmittedExecutableOperationPackage; use crate::EchoOperationAdmissionPolicy; use crate::ExecutableOperationPackage; use crate::{ @@ -20,7 +20,7 @@ use std::collections::BTreeMap; /// Exact installation evidence; registry membership remains runtime-owned. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InstalledEffectOperationV2 { - admitted: AdmittedEffectPackageV2, + admitted: AdmittedExecutableOperationPackage, id: [u8; 32], } impl InstalledEffectOperationV2 { @@ -40,7 +40,14 @@ impl InstalledEffectOperationV2 { self.admitted.canonical_package_bytes() } - pub(crate) fn from_admitted(admitted: AdmittedEffectPackageV2) -> Self { + pub(crate) fn from_admitted( + admitted: AdmittedExecutableOperationPackage, + ) -> Result { + if admitted.package().program().as_effect_program().is_none() { + return Err(invalid_structure( + "effect catalog requires an authored-effect program", + )); + } let package = admitted.package(); let mut h = blake3::Hasher::new(); h.update(b"echo:operation-installation:v2\0"); @@ -51,10 +58,10 @@ impl InstalledEffectOperationV2 { h.update(&admitted.program_identity()); h.update(&(admitted.canonical_package_bytes().len() as u64).to_le_bytes()); h.update(admitted.canonical_package_bytes()); - Self { + Ok(Self { admitted, id: h.finalize().into(), - } + }) } /// Exact publication named by this installation. #[must_use] @@ -125,15 +132,16 @@ impl InstalledEffectOperationV2 { let (policy, policy_id, package_id, package_bytes, admission_id, installed_id) = read().map_err(|_| invalid_structure("invalid V2 installation fields"))?; let policy = EchoOperationAdmissionPolicy::from_value(policy.clone())?; - let admitted = super::admission::admit(&policy, package_bytes.to_vec()) - .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; + let admitted = + crate::echo_operation::admit_operation_package(&policy, package_bytes.to_vec()) + .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; if policy.identity() != policy_id || admitted.package_identity() != package_id || admitted.identity() != admission_id { return Err(invalid_structure("V2 retained admission identities differ")); } - let installed = Self::from_admitted(admitted); + let installed = Self::from_admitted(admitted)?; if installed.id != installed_id || installed.retained_bytes()? != canonical { return Err(invalid_structure( "V2 retained installation identity differs", diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 7a758826..f5f5a4c9 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -54,14 +54,15 @@ 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, - retain_action_outcome_v1, retain_committed_execution_v1, retain_installation_v1, + 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, + 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, retain_action_outcome_v1, + retain_committed_execution_v1, retain_installation_v1, validate_receipt_application_result_v1, validate_receipt_installation_v1, EchoOperationEvaluationAuthorityV1, SchedulerEchoOperationCandidateV1, }, @@ -1102,57 +1103,43 @@ impl TrustedRuntimeHost { .copied() } - /// Admits V2 effect publication bytes under independently supplied owner policy. + /// Admits exact executable-operation package bytes under independent policy. /// - /// This returns opaque policy evidence. It neither installs the program nor - /// validates its compiled instruction language or authorizes an invocation. - /// Application-facing handles cannot call this runtime-owner method: + /// 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. + /// 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_v2(policy, bytes); + /// fn application_cannot_admit(app: &TrustedRuntimeApp<'_>, policy: &EchoOperationAdmissionPolicy, bytes: Vec) { + /// app.admit_echo_operation_package(policy, bytes); /// } /// ``` - pub fn admit_echo_operation_package_v2( - &self, - policy: &crate::EchoOperationAdmissionPolicy, - canonical_package_bytes: Vec, - ) -> Result { - crate::edict_effects::admit_effect_package_v2(policy, canonical_package_bytes) - } - - /// Admits exact executable-operation package bytes under independent policy. - /// - /// 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( + pub fn admit_echo_operation_package( &self, policy: &EchoOperationAdmissionPolicy, canonical_package_bytes: Vec, ) -> Result { - admit_package_v1(policy, canonical_package_bytes) + admit_operation_package(policy, canonical_package_bytes) } - /// Installs an admitted V2 publication without granting invocation authority. + /// Installs an admitted effect program without granting invocation authority. /// /// With a WAL configured, the exact installation is durable before it becomes /// visible in the engine. Application handles cannot call this owner method: /// /// ```compile_fail,E0599 - /// use warp_core::{TrustedRuntimeApp, edict_effects::AdmittedEffectPackageV2}; - /// fn application_cannot_install(app: &mut TrustedRuntimeApp<'_>, admitted: AdmittedEffectPackageV2) { + /// use warp_core::{TrustedRuntimeApp, AdmittedExecutableOperationPackage}; + /// fn application_cannot_install(app: &mut TrustedRuntimeApp<'_>, admitted: AdmittedExecutableOperationPackage) { /// app.install_admitted_echo_operation_package_v2(admitted); /// } /// ``` pub fn install_admitted_echo_operation_package_v2( &mut self, - admitted: crate::edict_effects::AdmittedEffectPackageV2, + admitted: crate::AdmittedExecutableOperationPackage, ) -> Result { - let installed = InstalledEffectOperationV2::from_admitted(admitted); + let installed = InstalledEffectOperationV2::from_admitted(admitted)?; self.engine .preflight_echo_effect_installations(core::slice::from_ref(&installed))?; if self @@ -7689,7 +7676,7 @@ mod tests { .to_canonical_bytes() .expect("the recovery-index package is canonical"); let package_id = crate::echo_operation_package_id_v1(&package_bytes); - let admitted = admit_package_v1( + let admitted = admit_operation_package( &crate::EchoOperationAdmissionPolicy::exact( package_id.as_hash(), operation_coordinate, diff --git a/crates/warp-core/tests/edict_effect_admission_tests.rs b/crates/warp-core/tests/edict_effect_admission_tests.rs index 2c422a9e..c3c12d3c 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests.rs @@ -48,7 +48,7 @@ fn runtime_owner_admits_exact_publication_and_retains_separate_policy_evidence() ); let host = host(); let admitted = host - .admit_echo_operation_package_v2(&policy, bytes.clone()) + .admit_echo_operation_package(&policy, bytes.clone()) .expect("exact policy admits"); assert_eq!(admitted.canonical_package_bytes(), bytes); assert_eq!( @@ -69,7 +69,7 @@ fn runtime_owner_admits_exact_publication_and_retains_separate_policy_evidence() *oracle.finalize().as_bytes() ); let mut id = blake3::Hasher::new(); - id.update(b"echo:operation-package-admission:v2\0"); + 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()); @@ -99,7 +99,7 @@ fn runtime_policy_refuses_operation_authority_and_budget_substitution() { ), ] { assert_eq!( - host.admit_echo_operation_package_v2( + host.admit_echo_operation_package( &policy(&bytes, operation, authority, budget), bytes.clone() ) @@ -116,7 +116,7 @@ fn pin_identity_does_not_substitute_for_supported_artifact_validation() { set(&mut value, "schema", text("echo.operation-package/v2")); let bytes = encoded(&value); assert_eq!( - host.admit_echo_operation_package_v2( + host.admit_echo_operation_package( &policy( &bytes, "app.update@1", @@ -137,7 +137,7 @@ fn pin_identity_does_not_substitute_for_supported_artifact_validation() { EchoOperationBudgetV1::new(1, 0, 0), ); assert_eq!( - host.admit_echo_operation_package_v2(&pin, bytes) + host.admit_echo_operation_package(&pin, bytes) .expect_err("wrong pin") .kind(), EchoOperationAdmissionErrorKind::PackageIdentityMismatch @@ -168,7 +168,7 @@ fn each_package_budget_axis_is_subject_to_the_independent_policy() { ); let bytes = encoded(&value); assert_eq!( - host.admit_echo_operation_package_v2( + host.admit_echo_operation_package( &policy( &bytes, "app.update@1", @@ -200,10 +200,10 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() EchoOperationBudgetV1::new(10, 20, 30), ); let first = host - .admit_echo_operation_package_v2(&narrow, bytes.clone()) + .admit_echo_operation_package(&narrow, bytes.clone()) .expect("narrow policy"); let second = host - .admit_echo_operation_package_v2(&wide, bytes) + .admit_echo_operation_package(&wide, bytes) .expect("wide policy"); assert_eq!(first.package(), second.package()); assert_eq!(first.admission_policy(), &narrow); @@ -214,7 +214,7 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() ); assert_ne!(first.identity(), second.identity()); let mut wrong_domain = blake3::Hasher::new(); - wrong_domain.update(b"echo:operation-package-admission:v1\0"); + 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()); @@ -223,28 +223,65 @@ fn admission_evidence_changes_with_policy_even_when_exact_program_bytes_do_not() mod installation; #[test] -fn all_operation_publications_use_one_policy_and_one_refusal_contract() { - let owner = host(); - let pin = policy( - b"independent pin", +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(1, 0, 0), - ); - // The array requires both host entry points to return the same error type. - // Neither decoder may accept a publication that differs from this pin. - let refusals = [ - owner - .admit_echo_operation_package_v1(&pin, vec![0xa0]) - .map(|_| ()), - owner - .admit_echo_operation_package_v2(&pin, vec![0xa0]) - .map(|_| ()), - ]; - for refusal in refusals { + 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!( - refusal.expect_err("wrong pin").kind(), - warp_core::EchoOperationAdmissionErrorKind::PackageIdentityMismatch, + 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 index dc2a1c2a..045ff364 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -26,9 +26,9 @@ impl Drop for WalDir { std::fs::remove_dir_all(&self.0).expect("remove owned WAL fixture"); } } -fn admitted(host: &TrustedRuntimeHost) -> warp_core::edict_effects::AdmittedEffectPackageV2 { +fn admitted(host: &TrustedRuntimeHost) -> warp_core::AdmittedExecutableOperationPackage { let bytes = encoded(&package(&program())); - host.admit_echo_operation_package_v2( + host.admit_echo_operation_package( &policy( &bytes, "app.update@1", @@ -163,7 +163,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen set(&mut p, "compiled_program", V::Bytes(vec![0xa1])); let bytes = encoded(&package(&p)); let token = owner - .admit_echo_operation_package_v2( + .admit_echo_operation_package( &policy( &bytes, "app.update@1", @@ -178,7 +178,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen .is_err()); let bytes = encoded(&package(&program())); let token = owner - .admit_echo_operation_package_v2( + .admit_echo_operation_package( &policy( &bytes, "app.update@1", @@ -246,7 +246,7 @@ fn install_v1( budget, ); let admitted = owner - .admit_echo_operation_package_v1(&policy, bytes) + .admit_echo_operation_package(&policy, bytes) .expect("V1 policy"); owner.install_admitted_echo_operation_package_v1(admitted) } @@ -389,7 +389,7 @@ fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission( V::Bytes(policy_id.to_vec()), ); let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-package-admission:v2\0"); + h.update(b"echo:operation-package-admission:v1\0"); h.update(&pin); h.update(&policy_id); let admission_id = *h.finalize().as_bytes(); diff --git a/crates/warp-core/tests/edict_projected_cas_tests.rs b/crates/warp-core/tests/edict_projected_cas_tests.rs index 7e824e68..76a9a4bb 100644 --- a/crates/warp-core/tests/edict_projected_cas_tests.rs +++ b/crates/warp-core/tests/edict_projected_cas_tests.rs @@ -162,7 +162,7 @@ impl Fixture { let bytes = package(); let admitted = self .host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( echo_operation_package_id_v1(&bytes).as_hash(), OPERATION, @@ -826,7 +826,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( id.as_hash(), OPERATION, diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 69df9850..e0709f60 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -511,7 +511,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), operation_coordinate, @@ -534,7 +534,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), CREATE_OPERATION_COORDINATE, @@ -668,7 +668,7 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { 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) @@ -845,7 +845,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), OPERATION_COORDINATE, @@ -1085,7 +1085,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( warp_core::echo_operation_package_id_v1(b"different package bytes").as_hash(), OPERATION_COORDINATE, @@ -1102,7 +1102,7 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by let malformed_bytes = vec![0xff]; let malformed = host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( warp_core::echo_operation_package_id_v1(&malformed_bytes).as_hash(), OPERATION_COORDINATE, @@ -1133,7 +1133,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( warp_core::echo_operation_package_id_v1(&unsupported_target_bytes).as_hash(), OPERATION_COORDINATE, @@ -1163,7 +1163,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( warp_core::echo_operation_package_id_v1(&unsupported_program_package).as_hash(), OPERATION_COORDINATE, @@ -1187,7 +1187,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( warp_core::echo_operation_package_id_v1(&unsupported_schema_bytes).as_hash(), OPERATION_COORDINATE, @@ -1206,7 +1206,7 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by ); let authority_mismatch = host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), OPERATION_COORDINATE, @@ -1222,7 +1222,7 @@ fn package_admission_rejects_malformed_unsupported_mismatched_and_over_budget_by ); let over_budget = host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), OPERATION_COORDINATE, @@ -1813,7 +1813,7 @@ 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( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), OPERATION_COORDINATE, diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index f1039666..785964b7 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -724,8 +724,8 @@ physical evaluation arena. This is byte binding and contract preparation. Compiler bytes and upstream identities do not infer verifier approval or validate the compiled instruction -language. The separate invocation/admission-token/installation definitions and -catalogs described below still require consolidation under the one-current- +language. The separate invocation/installation definitions and catalogs described below +still require consolidation under the one-current- version policy. Preparation does not grant invocation or mutation authority. ### V2 invocation input and basis preparation @@ -749,40 +749,40 @@ it does not verify a grant, establish installation, resolve the current frontier or admit an Action. Runtime-owned authority checks and evaluation at the named parent remain required before effects can be accepted. -### Runtime-owner V2 package policy admission - -`TrustedRuntimeHost::admit_echo_operation_package_v2` crosses the independent -package-policy boundary. The single `EchoOperationAdmissionPolicy` pins exact -publication identity bytes, operation coordinate, authority profile and maximum -budget. Both current package admission entry points consume this definition, -its retained codec and its shared operation/authority/budget checks, and return -the same `EchoOperationAdmissionError` / `EchoOperationAdmissionErrorKind` -refusal contract. The old version-specific policy and refusal definitions have -been removed without compatibility aliases. This is one completed part of the -format cutover; the remaining invocation, admission-token and installation -definitions still require replacement under the one-current-version policy. - -The host prepares the canonical package, checks all four independent policy -conditions, and returns opaque `AdmittedEffectPackageV2` evidence. Every budget -axis is checked separately. A matching package pin alone cannot substitute for -canonical artifact/profile validation or for the independent authority policy. - -The evidence retains the exact package and policy. Policy identity uses the -existing `echo:operation-admission-policy:v1\0` recipe with the current package pin. -The package-admission identity uses `echo:operation-package-admission:v2\0` -followed by the package and policy identities. Two policies admitting the same -program therefore produce distinct policy/admission evidence while retaining -the same package identity. A policy pins a raw 32-byte identity; the package -decoder, rather than the policy type, selects the publication identity domain. - -This method is exposed only on the runtime-owner host, under its existing -`native_rule_bootstrap` and `trusted_runtime` feature boundary. The restricted -application handle cannot admit a package. The opaque token has no public -constructor and is not an installation, invocation grant or scheduler Action. -Compiled effect instruction validation, grant admission, evaluation and -retained decision recovery remain necessary before this program can execute. -This policy crossing does not infer compiler/verifier approval from -publication identities or confer graph mutation authority. +### 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. The still-separate +fixed and effect installation paths enforce their supported program kinds; +their definitions/catalogs remain unfinished consolidation work. Invocation +admission and compiled effect execution also remain unfinished. Package +admission does not infer compiler/verifier approval from publication identities. ### Runtime-owned V2 installation and retained catalog recovery @@ -835,8 +835,8 @@ 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. -The installation and admission-token definitions still have parallel versions; -the shared package/program and index cutovers do not complete their replacement. A fresh +The installation definitions still have parallel versions; the shared package/ +program, admission-token and index cutovers do not complete their replacement. A fresh filesystem-backed host currently restores those installations and rechecks coordinate conflicts. This is installation recovery, not recovery of an authored-effect decision. Invocation grant admission, compiled execution, diff --git a/xtask/src/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index c65f79fd..237124c4 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -1088,7 +1088,7 @@ fn install_package( package_bytes: Vec, ) -> Result<()> { let admitted = host - .admit_echo_operation_package_v1( + .admit_echo_operation_package( &EchoOperationAdmissionPolicy::exact( package_id.as_hash(), &package.operation_coordinate, From 66571a78209a13c6393503eaef09f59a77afb1c4 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 09:13:53 -0700 Subject: [PATCH 22/43] Unify operation installation records and catalog --- CHANGELOG.md | 19 +- .../tests/action_preparation_allocation.rs | 4 +- .../warp-core/benches/support/action_scene.rs | 2 +- crates/warp-core/src/coordinator.rs | 3 +- crates/warp-core/src/echo_operation.rs | 173 +++++++++----- crates/warp-core/src/edict_effects.rs | 10 +- .../src/edict_effects/installation.rs | 211 ----------------- crates/warp-core/src/engine_impl.rs | 112 ++------- crates/warp-core/src/lib.rs | 2 +- crates/warp-core/src/trusted_runtime_host.rs | 219 +++++------------- .../installation.rs | 168 +++++++++----- .../tests/edict_projected_cas_tests.rs | 8 +- .../executable_operation_pipeline_tests.rs | 110 +++++++-- docs/topics/GeneratedRules.md | 106 +++++---- xtask/src/run_edict_operation.rs | 4 +- xtask/src/run_edict_operation/session.rs | 2 +- 16 files changed, 479 insertions(+), 674 deletions(-) delete mode 100644 crates/warp-core/src/edict_effects/installation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ff122b51..f9d4155a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ ### Changed +- 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 @@ -19,8 +27,7 @@ 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, admission-token and installation/catalog consolidation - remains separate unfinished work. + effects. Invocation consolidation and authored-effect execution remain unfinished. - Recovery certificates now use one current operation-index recipe for empty, @@ -38,10 +45,10 @@ ### Added -- Runtime-owned V2 installation retains exact policy and package evidence in the - existing WAL catalog, restores it in a fresh filesystem-backed host, and shares - coordinate-conflict checks with V1. Conflicts and failed WAL appends publish no - V2 registry entry. Installation recovery does not authorize or execute effects. +- 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 diff --git a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs index f4bda0dd..4a89740f 100644 --- a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs +++ b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs @@ -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()); diff --git a/crates/warp-core/benches/support/action_scene.rs b/crates/warp-core/benches/support/action_scene.rs index 594cd02b..b8b141ee 100644 --- a/crates/warp-core/benches/support/action_scene.rs +++ b/crates/warp-core/benches/support/action_scene.rs @@ -236,7 +236,7 @@ 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( diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 6958caff..4f7b128e 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -5838,8 +5838,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, diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 0f2880dd..ecea8441 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -169,6 +169,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)] @@ -2675,14 +2681,14 @@ impl AdmittedExecutableOperationPackage { /// 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, @@ -2698,7 +2704,34 @@ pub struct InstalledEchoOperationV1 { 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 { @@ -2729,7 +2762,9 @@ impl InstalledEchoOperationV1 { self.program_id } - pub(crate) const fn program(&self) -> &EchoOperationProgram { + /// Returns the installed current program kind and metadata. + #[must_use] + pub const fn program(&self) -> &EchoOperationProgram { &self.program } @@ -2749,7 +2784,7 @@ 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 } @@ -2837,18 +2872,15 @@ pub(crate) fn admit_operation_package( pub(crate) fn installed_from_admitted( admitted: AdmittedExecutableOperationPackage, -) -> Result { +) -> InstalledEchoOperation { let package = admitted.package; - let lawpack_identity = package.semantic_closure.lawpack_identity.ok_or_else(|| { - invalid_structure("authored effects require the effect installation crossing") - })?; - 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, operation_coordinate: package.operation_coordinate.into(), semantic_identity: package.semantic_closure.canonical_meaning_identity, - lawpack_identity, + lawpack_identity: package.semantic_closure.lawpack_identity, target_profile_identity: package.target_profile_identity, interpreter_profile_identity: package.interpreter_profile_identity, intrinsic_profile_identity: package.intrinsic_profile_identity, @@ -2865,11 +2897,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()), @@ -2895,9 +2927,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, @@ -2946,7 +2978,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 { @@ -2954,7 +2986,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", @@ -2963,11 +2995,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()); @@ -3732,7 +3764,7 @@ impl AdmittedEchoOperationInvocationV1 { } pub(crate) fn admit_invocation_v1( - installed: Option<&InstalledEchoOperationV1>, + installed: Option<&InstalledEchoOperation>, policy: EchoOperationInvocationAdmissionPolicyV1, canonical_invocation_bytes: &[u8], current_basis: EchoOperationEvaluationBasisV1, @@ -3783,7 +3815,7 @@ 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>, + installed: Option<&InstalledEchoOperation>, policy: EchoOperationInvocationAdmissionPolicyV1, canonical_invocation_bytes: &[u8], evaluation_authority: EchoOperationEvaluationAuthorityV1, @@ -3811,11 +3843,11 @@ pub(crate) fn admit_action_invocation_v1( } fn admit_invocation_static_v1<'a>( - installed: Option<&'a InstalledEchoOperationV1>, + installed: Option<&'a InstalledEchoOperation>, policy: EchoOperationInvocationAdmissionPolicyV1, canonical_invocation_bytes: &[u8], ) -> Result< - (EchoOperationInvocationV1, &'a InstalledEchoOperationV1), + (EchoOperationInvocationV1, &'a InstalledEchoOperation), EchoOperationInvocationAdmissionErrorV1, > { let invocation = EchoOperationInvocationV1::from_canonical_bytes(canonical_invocation_bytes) @@ -3901,7 +3933,7 @@ fn admit_invocation_static_v1<'a>( fn build_admitted_invocation_v1( invocation: EchoOperationInvocationV1, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, policy: EchoOperationInvocationAdmissionPolicyV1, canonical_invocation_bytes: &[u8], evaluation_authority: EchoOperationEvaluationAuthorityV1, @@ -3931,7 +3963,7 @@ fn build_admitted_invocation_v1( } fn current_application_basis( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, invocation: &EchoOperationInvocationV1, state: &WorldlineState, ) -> Result { @@ -4026,7 +4058,7 @@ 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, + installed: &InstalledEchoOperation, invocation: &EchoOperationInvocationV1, current_head: WriterHeadKey, current_tick: WorldlineTick, @@ -4183,7 +4215,7 @@ pub(crate) fn recovered_origin_corroborated_v1( /// [`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, @@ -4197,7 +4229,7 @@ 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 { @@ -4213,7 +4245,7 @@ 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, @@ -4256,7 +4288,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, @@ -4303,7 +4335,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, @@ -5129,7 +5161,7 @@ pub enum EchoOperationPreparationV1 { /// Complete private evaluation, bound to all substitution and resource evidence. #[derive(Clone, Debug)] pub struct PreparedEchoOperationV1 { - installed: InstalledEchoOperationV1, + installed: InstalledEchoOperation, invocation: EchoOperationInvocationV1, invocation_id: EchoOperationInvocationIdV1, canonical_invocation_bytes: Vec, @@ -5154,7 +5186,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 } @@ -5266,7 +5298,7 @@ impl PreparedEchoOperationV1 { } pub(crate) fn prepare_operation_v1( - installed: Option<&InstalledEchoOperationV1>, + installed: Option<&InstalledEchoOperation>, admitted: AdmittedEchoOperationInvocationV1, current_basis: EchoOperationEvaluationBasisV1, state: &WorldlineState, @@ -5609,7 +5641,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, @@ -5928,7 +5960,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), @@ -6122,7 +6158,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")?, @@ -6433,7 +6469,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 @@ -6488,7 +6524,7 @@ 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)?; @@ -7294,7 +7330,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); @@ -7473,7 +7517,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()); @@ -7500,7 +7544,7 @@ fn invocation_admission_id( } fn operation_result_id( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, invocation: &EchoOperationInvocationV1, mode: AnchoredNodeOperationModeV1, replacement_value_digest: Hash, @@ -8168,7 +8212,7 @@ mod tests { pub(super) fn projected_create_fixture( max_output_bytes: u64, ) -> ( - InstalledEchoOperationV1, + InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, EchoOperationInvocationAdmissionPolicyV1, @@ -8183,7 +8227,7 @@ mod tests { key: &str, key_bound: Option, ) -> ( - InstalledEchoOperationV1, + InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, EchoOperationInvocationAdmissionPolicyV1, @@ -8284,8 +8328,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"); @@ -9159,8 +9202,7 @@ mod tests { package_bytes, ) .expect("package admits"), - ) - .expect("package installs"); + ); let writer_head = WriterHeadKey { worldline_id: crate::WorldlineId::from_bytes(digest(47)), @@ -9360,7 +9402,7 @@ mod tests { .expect_err("an invalid invocation must not mint a stable identity"); } - fn retained_fixture_installation() -> InstalledEchoOperationV1 { + fn retained_fixture_installation() -> InstalledEchoOperation { let package = ExecutableOperationPackage::new( "echo.fixture.Retention.v1", "echo.fixture.Retention.Obstruction/v1", @@ -9392,7 +9434,6 @@ mod tests { EchoOperationBudgetV1::new(8, 512, 512), ); installed_from_admitted(admit_operation_package(&policy, bytes).expect("package admits")) - .expect("an admitted program remains canonically encodable") } fn replace_map_field(bytes: &[u8], field_name: &str, replacement: CanonicalValueV1) -> Vec { @@ -9453,7 +9494,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", @@ -9465,7 +9506,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(), @@ -9481,7 +9522,7 @@ mod tests { ); 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(), @@ -9599,6 +9640,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); diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index fd684c94..1c9400af 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Authored-effect preparation, runtime-owned catalog and private recording reads. +//! 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 @@ -8,14 +8,6 @@ //! Private frames bind immutable runtime frontiers, typed read laws and shared //! logical meters. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. -#[cfg(feature = "native_rule_bootstrap")] -mod installation; -#[cfg(feature = "native_rule_bootstrap")] -pub use installation::InstalledEffectOperationV2; -#[cfg(feature = "native_rule_bootstrap")] -pub(crate) use installation::{ - recover_versioned_installation, restore_effect_catalog_entry, RecoveredOperationInstallation, -}; mod invocation; pub use invocation::EffectInvocationV2; mod package; diff --git a/crates/warp-core/src/edict_effects/installation.rs b/crates/warp-core/src/edict_effects/installation.rs deleted file mode 100644 index 0ab4c4a0..00000000 --- a/crates/warp-core/src/edict_effects/installation.rs +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS -//! Versioned effect installation evidence; invocation authority remains separate. -use super::package::hash; -use crate::echo_operation::{ - installation_error, invalid_structure, map_value, recover_installation_v1, -}; -use crate::edict_pure::values::{bytes, exact_fields, field, require_text, text}; -use crate::AdmittedExecutableOperationPackage; -use crate::EchoOperationAdmissionPolicy; -use crate::ExecutableOperationPackage; -use crate::{ - EchoOperationArtifactErrorV1, EchoOperationInstallationErrorKindV1, - EchoOperationInstallationErrorV1, EchoOperationPackageIdV1, InstalledEchoOperationV1, -}; -use echo_edict_canonical::{ - decode_canonical_cbor_v1 as decode, encode_canonical_cbor_v1 as encode, CanonicalValueV1 as V, -}; -use std::collections::BTreeMap; -/// Exact installation evidence; registry membership remains runtime-owned. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InstalledEffectOperationV2 { - admitted: AdmittedExecutableOperationPackage, - id: [u8; 32], -} -impl InstalledEffectOperationV2 { - /// Identity of the current package definition. - #[must_use] - pub const fn package_identity(&self) -> [u8; 32] { - self.admitted.package_identity() - } - /// Identity of the current program definition. - #[must_use] - pub const fn program_identity(&self) -> [u8; 32] { - self.admitted.program_identity() - } - /// Exact current publication bytes retained by installation. - #[must_use] - pub fn canonical_package_bytes(&self) -> &[u8] { - self.admitted.canonical_package_bytes() - } - - pub(crate) fn from_admitted( - admitted: AdmittedExecutableOperationPackage, - ) -> Result { - if admitted.package().program().as_effect_program().is_none() { - return Err(invalid_structure( - "effect catalog requires an authored-effect program", - )); - } - let package = admitted.package(); - let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-installation:v2\0"); - h.update(&admitted.package_identity()); - h.update(&admitted.identity()); - h.update(&(package.operation_coordinate().len() as u64).to_le_bytes()); - h.update(package.operation_coordinate().as_bytes()); - h.update(&admitted.program_identity()); - h.update(&(admitted.canonical_package_bytes().len() as u64).to_le_bytes()); - h.update(admitted.canonical_package_bytes()); - Ok(Self { - admitted, - id: h.finalize().into(), - }) - } - /// Exact publication named by this installation. - #[must_use] - pub const fn package(&self) -> &ExecutableOperationPackage { - self.admitted.package() - } - /// Versioned installation identity, without invocation rights. - #[must_use] - pub const fn identity(&self) -> [u8; 32] { - self.id - } - /// Canonical retained installation evidence. - /// # Errors - /// Refuses invalid canonical material. - pub fn retained_bytes(&self) -> Result, EchoOperationArtifactErrorV1> { - encode(&map_value([ - ("schema", V::Text("echo.operation-installation/v2".into())), - ( - "admission_policy", - self.admitted.admission_policy().to_value(), - ), - ( - "admission_policy_id", - V::Bytes(self.admitted.admission_policy_identity().to_vec()), - ), - ("package_id", V::Bytes(self.package_identity().to_vec())), - ( - "package_bytes", - V::Bytes(self.canonical_package_bytes().to_vec()), - ), - ( - "package_admission_id", - V::Bytes(self.admitted.identity().to_vec()), - ), - ("installed_operation_id", V::Bytes(self.id.to_vec())), - ])) - .map_err(|_| invalid_structure("invalid V2 installation encoding")) - } - /// Reconstructs exact retained evidence; this does not install it in an engine. - /// # Errors - /// Refuses altered policy, publication, identity or version evidence. - pub fn from_retained_bytes(canonical: &[u8]) -> Result { - let value = - decode(canonical).map_err(|_| invalid_structure("invalid V2 retained bytes"))?; - let read = || { - exact_fields( - &value, - &[ - "schema", - "admission_policy", - "admission_policy_id", - "package_id", - "package_bytes", - "package_admission_id", - "installed_operation_id", - ], - )?; - require_text(&value, "schema", "echo.operation-installation/v2")?; - Ok::<_, crate::edict_pure::EvaluationError>(( - field(&value, "admission_policy")?, - hash(field(&value, "admission_policy_id")?)?, - hash(field(&value, "package_id")?)?, - bytes(field(&value, "package_bytes")?)?, - hash(field(&value, "package_admission_id")?)?, - hash(field(&value, "installed_operation_id")?)?, - )) - }; - let (policy, policy_id, package_id, package_bytes, admission_id, installed_id) = - read().map_err(|_| invalid_structure("invalid V2 installation fields"))?; - let policy = EchoOperationAdmissionPolicy::from_value(policy.clone())?; - let admitted = - crate::echo_operation::admit_operation_package(&policy, package_bytes.to_vec()) - .map_err(|_| invalid_structure("V2 retained package no longer admits"))?; - if policy.identity() != policy_id - || admitted.package_identity() != package_id - || admitted.identity() != admission_id - { - return Err(invalid_structure("V2 retained admission identities differ")); - } - let installed = Self::from_admitted(admitted)?; - if installed.id != installed_id || installed.retained_bytes()? != canonical { - return Err(invalid_structure( - "V2 retained installation identity differs", - )); - } - Ok(installed) - } -} -pub(crate) enum RecoveredOperationInstallation { - V1(Box), - V2(Box), -} -impl RecoveredOperationInstallation { - pub(crate) fn package_identity(&self) -> [u8; 32] { - match self { - Self::V1(v) => v.package_id().as_hash(), - Self::V2(v) => v.package_identity(), - } - } -} -pub(crate) fn recover_versioned_installation( - canonical: &[u8], -) -> Result { - let value = - decode(canonical).map_err(|_| invalid_structure("invalid retained installation"))?; - match field(&value, "schema") - .and_then(text) - .map_err(|_| invalid_structure("missing installation version"))? - { - "echo.operation-installation/v1" => recover_installation_v1(canonical) - .map(|installed| RecoveredOperationInstallation::V1(Box::new(installed))), - "echo.operation-installation/v2" => { - InstalledEffectOperationV2::from_retained_bytes(canonical) - .map(|installed| RecoveredOperationInstallation::V2(Box::new(installed))) - } - _ => Err(invalid_structure("unsupported installation version")), - } -} -pub(crate) fn restore_effect_catalog_entry( - packages: &mut BTreeMap<[u8; 32], InstalledEffectOperationV2>, - operations: &mut BTreeMap, - v1_operations: &BTreeMap, - installed: InstalledEffectOperationV2, -) -> Result<(), EchoOperationInstallationErrorV1> { - let pin = installed.package_identity(); - let coordinate = installed.package().operation_coordinate(); - if let Some(existing) = packages.get(&pin) { - if existing == &installed { - return Ok(()); - } - return Err(installation_error( - EchoOperationInstallationErrorKindV1::PackageIdentityConflict, - "V2 installation policy conflicts with existing package", - )); - } - if v1_operations.contains_key(coordinate) - || operations.get(coordinate).is_some_and(|id| id != &pin) - { - return Err(installation_error( - EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, - "V2 installation operation coordinate already exists", - )); - } - operations.insert(coordinate.to_owned(), pin); - packages.insert(pin, installed); - Ok(()) -} diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 9cd66179..584aee80 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,12 +455,7 @@ pub struct Engine { #[cfg_attr(not(feature = "native_rule_bootstrap"), allow(dead_code))] installed_contract_packages: BTreeMap, - installed_echo_operation_packages: BTreeMap, - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_packages: - BTreeMap, - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_operations_by_coordinate: BTreeMap, + installed_echo_operation_packages: BTreeMap, #[cfg_attr( not(all(feature = "native_rule_bootstrap", feature = "trusted_runtime")), allow(dead_code) @@ -988,10 +983,6 @@ impl Engine { canonical_cmd_rules: Vec::new(), installed_contract_packages: BTreeMap::new(), installed_echo_operation_packages: BTreeMap::new(), - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_packages: BTreeMap::new(), - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_operations_by_coordinate: BTreeMap::new(), installed_echo_operations_by_coordinate: BTreeMap::new(), #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] installed_provider_contract_packages: BTreeMap::new(), @@ -1198,10 +1189,6 @@ impl Engine { canonical_cmd_rules: Vec::new(), installed_contract_packages: BTreeMap::new(), installed_echo_operation_packages: BTreeMap::new(), - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_packages: BTreeMap::new(), - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - installed_echo_effect_operations_by_coordinate: BTreeMap::new(), installed_echo_operations_by_coordinate: BTreeMap::new(), #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] installed_provider_contract_packages: BTreeMap::new(), @@ -1405,65 +1392,12 @@ impl Engine { Ok(prepared.record) } - /// Returns an installed V2 effect publication by its exact versioned pin. - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - #[must_use] - pub fn installed_echo_operation_package_v2( - &self, - package_id: Hash, - ) -> Option<&crate::edict_effects::InstalledEffectOperationV2> { - self.installed_echo_effect_packages.get(&package_id) - } - - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - pub(crate) fn preflight_echo_effect_installations( - &self, - recovered: &[crate::edict_effects::InstalledEffectOperationV2], - ) -> Result<(), EchoOperationInstallationErrorV1> { - let mut packages = self.installed_echo_effect_packages.clone(); - let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); - for installed in recovered { - crate::edict_effects::restore_effect_catalog_entry( - &mut packages, - &mut operations, - &self.installed_echo_operations_by_coordinate, - installed.clone(), - )?; - } - Ok(()) - } - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - pub(crate) fn restore_echo_effect_installations( - &mut self, - recovered: &[crate::edict_effects::InstalledEffectOperationV2], - ) -> Result<(), EchoOperationInstallationErrorV1> { - let mut packages = self.installed_echo_effect_packages.clone(); - let mut operations = self.installed_echo_effect_operations_by_coordinate.clone(); - for installed in recovered { - crate::edict_effects::restore_effect_catalog_entry( - &mut packages, - &mut operations, - &self.installed_echo_operations_by_coordinate, - installed.clone(), - )?; - } - self.installed_echo_effect_packages = packages; - self.installed_echo_effect_operations_by_coordinate = operations; - Ok(()) - } - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - pub(crate) fn installed_echo_effect_packages( - &self, - ) -> impl Iterator { - self.installed_echo_effect_packages.values() - } - /// 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) } @@ -1471,24 +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 { - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - if self - .installed_echo_effect_operations_by_coordinate - .contains_key(installed.operation_coordinate()) - { - return Err(crate::echo_operation::installation_error( - crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, - "V1 operation conflicts with installed V2 meaning", - )); - } - install_recovered_v1(&mut packages, &mut operations, installed.clone())?; + restore_operation_catalog_entry(&mut packages, &mut operations, installed.clone())?; } Ok(()) } @@ -1497,24 +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 { - #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] - if self - .installed_echo_effect_operations_by_coordinate - .contains_key(installed.operation_coordinate()) - { - return Err(crate::echo_operation::installation_error( - crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict, - "V1 operation conflicts with installed V2 meaning", - )); - } - 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; @@ -1525,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 1013c47f..a0555216 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -276,7 +276,7 @@ pub use echo_operation::{ EchoOperationPackageIdV1, EchoOperationPreparationV1, EchoOperationPrivateEvaluationIdV1, EchoOperationProgram, EchoOperationProgramIdV1, EchoOperationReceiptV1, EchoOperationResultIdV1, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, - ExecutableOperationPackage, InstalledEchoOperationIdV1, InstalledEchoOperationV1, + 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 f5f5a4c9..704b7a8c 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -22,10 +22,6 @@ pub use observed_context::EchoOperationContextErrorV1; use crate::causal_anchor::prepare_causal_anchor_admission; use crate::coordinator::{SuperTickUndo, TicketedIngressStagingRollback}; -use crate::edict_effects::{ - recover_versioned_installation, restore_effect_catalog_entry, InstalledEffectOperationV2, - RecoveredOperationInstallation, -}; use crate::{ causal_wal::{ affected_frontiers_root, build_causal_anchor_admission_transaction, @@ -57,14 +53,15 @@ use crate::{ 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, - install_recovered_v1, installed_from_admitted, not_committed_basis_changed, + 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, retain_action_outcome_v1, - retain_committed_execution_v1, retain_installation_v1, - validate_receipt_application_result_v1, validate_receipt_installation_v1, - EchoOperationEvaluationAuthorityV1, SchedulerEchoOperationCandidateV1, + 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, AdmittedExecutableOperationPackage, @@ -81,7 +78,7 @@ use crate::{ 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, @@ -521,9 +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, - /// Exact V2 effect installations reconstructed from retained records. - pub installed_echo_effect_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 @@ -717,7 +712,6 @@ impl TrustedRuntimeWalRecovery { missing_runtime_state_deltas: &self.missing_runtime_state_deltas, causal_anchor_history: &self.causal_anchor_history, installed_echo_operations: &self.installed_echo_operations, - installed_echo_effect_operations: &self.installed_echo_effect_operations, echo_operation_receipts: &self.echo_operation_receipts, echo_operation_action_outcomes: &self.echo_operation_action_outcomes, }) @@ -1124,76 +1118,45 @@ impl TrustedRuntimeHost { admit_operation_package(policy, canonical_package_bytes) } - /// Installs an admitted effect program without granting invocation authority. + /// Installs exact executable meaning carried by an admitted operation package. /// - /// With a WAL configured, the exact installation is durable before it becomes - /// visible in the engine. Application handles cannot call this owner method: + /// Installation stores no native callback and still does not authorize an + /// invocation. Application-facing [`TrustedRuntimeApp`] handles cannot call + /// this runtime-owner method. /// /// ```compile_fail,E0599 /// use warp_core::{TrustedRuntimeApp, AdmittedExecutableOperationPackage}; /// fn application_cannot_install(app: &mut TrustedRuntimeApp<'_>, admitted: AdmittedExecutableOperationPackage) { - /// app.install_admitted_echo_operation_package_v2(admitted); + /// app.install_admitted_echo_operation_package(admitted); /// } /// ``` - pub fn install_admitted_echo_operation_package_v2( - &mut self, - admitted: crate::AdmittedExecutableOperationPackage, - ) -> Result { - let installed = InstalledEffectOperationV2::from_admitted(admitted)?; - self.engine - .preflight_echo_effect_installations(core::slice::from_ref(&installed))?; - if self - .engine - .installed_echo_operation_package_v2(installed.package_identity()) - == Some(&installed) - { - return Ok(installed); - } - if let Some(wal) = self.runtime_wal.as_mut() { - let retained = installed.retained_bytes()?; - if let Err(error) = wal.record_executable_operation_installation(&retained) { - if !wal.recover_filesystem_effect_installation_after_error(&installed) { - return Err(error.into()); - } - } - } - self.engine - .restore_echo_effect_installations(core::slice::from_ref(&installed))?; - Ok(installed) - } - - /// Installs exact executable meaning carried by an admitted operation package. - /// - /// 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( + pub fn install_admitted_echo_operation_package( &mut self, admitted: AdmittedExecutableOperationPackage, - ) -> Result { - let installed = installed_from_admitted(admitted)?; + ) -> 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) } @@ -1290,7 +1253,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, @@ -1328,7 +1291,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, @@ -1371,7 +1334,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() }) @@ -1440,7 +1403,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 { @@ -1517,9 +1480,7 @@ impl TrustedRuntimeHost { &recovery, )?; self.engine - .preflight_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; - self.engine - .preflight_echo_effect_installations(&recovery.installed_echo_effect_operations)?; + .preflight_recovered_echo_operation_packages(&recovery.installed_echo_operations)?; let mut restored_runtime = forked_runtime; let mut restored_provenance = forked_provenance; @@ -1544,9 +1505,7 @@ impl TrustedRuntimeHost { validate_recovered_tick_decisions(&restored_runtime, &recovery.receipts)?; self.engine - .restore_recovered_echo_operation_packages_v1(&recovery.installed_echo_operations)?; - self.engine - .restore_echo_effect_installations(&recovery.installed_echo_effect_operations)?; + .restore_recovered_echo_operation_packages(&recovery.installed_echo_operations)?; self.echo_operation_action_outcomes = recovery .echo_operation_action_outcomes .iter() @@ -2133,7 +2092,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(), @@ -2423,33 +2382,16 @@ fn ensure_runtime_authority_is_durable( engine: &Engine, recovery: &TrustedRuntimeWalRecovery, ) -> Result<(), TrustedRuntimeWalError> { - let recovered_effects = recovery - .installed_echo_effect_operations - .iter() - .map(|installed| (installed.package_identity(), installed)) - .collect::>(); - if engine.installed_echo_effect_packages().any(|installed| { - recovered_effects - .get(&installed.package_identity()) - .is_none_or(|recovered| **recovered != *installed) - }) { - return Err(TrustedRuntimeWalError::RuntimeAuthorityNotDurable { - gap: RuntimeWalActivationGap::ExecutableOperationInstallation, - }); - } let recovered_operations = recovery .installed_echo_operations .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, }); @@ -3127,7 +3069,6 @@ impl TrustedRuntimeWal { runtime_state.echo_operation_action_installation_order; let missing_runtime_state_deltas = runtime_state.missing_runtime_state_deltas; let installed_echo_operations = operation_material.installations; - let installed_echo_effect_operations = operation_material.effect_installations; let echo_operation_receipts = operation_material.receipts; validate_recovered_causal_parent_evidence(&witnessed_submissions, &receipt_correlations)?; validate_recovered_echo_operation_action_outcomes( @@ -3150,7 +3091,6 @@ impl TrustedRuntimeWal { missing_runtime_state_deltas: &missing_runtime_state_deltas, causal_anchor_history: &causal_anchor_history, installed_echo_operations: &installed_echo_operations, - installed_echo_effect_operations: &installed_echo_effect_operations, echo_operation_receipts: &echo_operation_receipts, echo_operation_action_outcomes: &echo_operation_action_outcomes, }, @@ -3166,7 +3106,6 @@ impl TrustedRuntimeWal { receipt_correlations, causal_anchor_history, installed_echo_operations, - installed_echo_effect_operations, echo_operation_receipts, echo_operation_action_outcomes, echo_operation_action_decisions, @@ -3481,24 +3420,9 @@ impl TrustedRuntimeWal { .flatten() } - fn recover_filesystem_effect_installation_after_error( - &mut self, - expected: &InstalledEffectOperationV2, - ) -> bool { - if !self.uses_filesystem_store() || self.refresh_cursor_from_store_for_writer().is_err() { - return false; - } - self.recover_read_only().is_ok_and(|recovery| { - recovery - .installed_echo_effect_operations - .iter() - .any(|installed| installed == expected) - }) - } - fn recover_filesystem_executable_operation_installation_after_error( &mut self, - package_id: crate::EchoOperationPackageIdV1, + expected: &InstalledEchoOperation, ) -> bool { if !self.uses_filesystem_store() { return false; @@ -3510,7 +3434,7 @@ impl TrustedRuntimeWal { recovery .installed_echo_operations .iter() - .any(|installed| installed.package_id() == package_id) + .any(|installed| installed == expected) }) } @@ -3734,7 +3658,7 @@ impl TrustedRuntimeWal { &mut self, retained_installation_bytes: &[u8], ) -> Result { - let installed = recover_versioned_installation(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_identity(), @@ -3768,7 +3692,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 { @@ -4626,8 +4550,7 @@ fn restore_provenance_entries( } struct RecoveredEchoOperationMaterial { - installations: Vec, - effect_installations: Vec, + installations: Vec, receipts: Vec, } @@ -4637,8 +4560,6 @@ fn recover_echo_operation_material( ) -> Result { let mut installations = BTreeMap::new(); let mut operations = BTreeMap::new(); - let mut effects = BTreeMap::new(); - let mut effect_operations = BTreeMap::new(); let mut receipts_by_digest = BTreeMap::new(); let mut receipts = Vec::new(); let provenance_by_coordinate = provenance_entries @@ -4650,20 +4571,7 @@ fn recover_echo_operation_material( match transaction.commit.transaction_kind { WalTransactionKind::ExecutableOperationInstallation => { let (installed, _) = operation_installation_from_transaction(transaction)?; - match installed { - RecoveredOperationInstallation::V1(installed) => { - if effect_operations.contains_key(installed.operation_coordinate()) { - return Err(crate::echo_operation::installation_error(crate::EchoOperationInstallationErrorKindV1::OperationCoordinateConflict,"recovered V1 coordinate conflicts with V2 meaning").into()); - } - install_recovered_v1(&mut installations, &mut operations, *installed)?; - } - RecoveredOperationInstallation::V2(installed) => restore_effect_catalog_entry( - &mut effects, - &mut effect_operations, - &operations, - *installed, - )?, - } + restore_operation_catalog_entry(&mut installations, &mut operations, installed)?; } WalTransactionKind::ExecutableOperationTick => { let (receipt, state_delta, _) = @@ -4696,7 +4604,6 @@ fn recover_echo_operation_material( Ok(RecoveredEchoOperationMaterial { installations: installations.into_values().collect(), - effect_installations: effects.into_values().collect(), receipts, }) } @@ -5751,7 +5658,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(); @@ -5888,11 +5795,9 @@ fn recover_runtime_state_delta_material( { let (installed, _) = operation_installation_from_transaction(transaction)?; let next_ordinal = installation_ordinals.len(); - if let RecoveredOperationInstallation::V1(installed) = installed { - installation_ordinals - .entry(installed.package_id()) - .or_insert(next_ordinal); - } + installation_ordinals + .entry(installed.package_id()) + .or_insert(next_ordinal); continue; } if transaction.commit.transaction_kind == WalTransactionKind::ExecutableOperationTick { @@ -6079,7 +5984,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, @@ -6405,13 +6310,13 @@ fn wal_tick_decision_for_action_outcome(outcome: &EchoOperationActionOutcomeV1) fn operation_installation_from_transaction( transaction: &crate::causal_wal::WalRecoveredTransaction, -) -> Result<(RecoveredOperationInstallation, 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_versioned_installation(&retained_bytes)?; + let installed = recover_operation_installation(&retained_bytes)?; Ok((installed, retained_bytes)) } @@ -7647,7 +7552,7 @@ mod tests { .expect("the recovery parent-state fixture is lawful") } - fn recovery_index_installation(operation_coordinate: &str) -> InstalledEchoOperationV1 { + 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::ExecutableOperationPackage::new( @@ -7686,14 +7591,14 @@ mod tests { package_bytes, ) .expect("the recovery-index package is admitted"); - installed_from_admitted(admitted).expect("the recovery-index package installs") + 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_installation_v1(&installed).expect("retained installation"); + 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); @@ -7703,7 +7608,7 @@ mod tests { 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(base, &[installed], &[], &[], &[]) + recovered_echo_operation_index_root(base, &[installed], &[], &[],) .expect("current index"), *expected.finalize().as_bytes(), ); @@ -7714,9 +7619,9 @@ mod tests { 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()], &[], &[], &[]) + 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], &[], &[], &[]) + let reversed = recovered_echo_operation_index_root([0x20; 32], &[b, a], &[], &[]) .expect("reversed catalog"); assert_eq!(forward, reversed); } @@ -7730,8 +7635,8 @@ mod tests { for _ in 0..3 { expected.update(&0u64.to_le_bytes()); } - let root = recovered_echo_operation_index_root(base, &[], &[], &[], &[]) - .expect("empty current index"); + let root = + recovered_echo_operation_index_root(base, &[], &[], &[]).expect("empty current index"); assert_eq!(root, *expected.finalize().as_bytes()); assert_ne!(root, base); } @@ -9067,8 +8972,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_effect_operations: &'a [InstalledEffectOperationV2], + installed_echo_operations: &'a [InstalledEchoOperation], echo_operation_receipts: &'a [EchoOperationReceiptV1], echo_operation_action_outcomes: &'a [(Hash, Hash, EchoOperationActionOutcomeV1)], } @@ -9109,7 +9013,6 @@ fn recovered_runtime_wal_indexes_root( recovered_echo_operation_index_root( causal_anchor_root, indexes.installed_echo_operations, - indexes.installed_echo_effect_operations, indexes.echo_operation_receipts, indexes.echo_operation_action_outcomes, ) @@ -9117,8 +9020,7 @@ fn recovered_runtime_wal_indexes_root( fn recovered_echo_operation_index_root( base_root: Hash, - installations: &[InstalledEchoOperationV1], - effect_installations: &[InstalledEffectOperationV2], + installations: &[InstalledEchoOperation], receipts: &[EchoOperationReceiptV1], action_outcomes: &[(Hash, Hash, EchoOperationActionOutcomeV1)], ) -> Result { @@ -9127,13 +9029,10 @@ fn recovered_echo_operation_index_root( .map(|installed| { Ok(( installed.package_id().as_hash(), - retain_installation_v1(installed)?, + retain_operation_installation(installed)?, )) }) .collect::, TrustedRuntimeWalError>>()?; - for installed in effect_installations { - catalog.push((installed.package_identity(), installed.retained_bytes()?)); - } catalog.sort_unstable(); let mut hasher = blake3::Hasher::new(); diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs index 045ff364..65642618 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -3,7 +3,7 @@ //! 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::edict_effects::InstalledEffectOperationV2; +use warp_core::InstalledEchoOperation; use warp_core::TrustedRuntimeWalConfig; struct WalDir(std::path::PathBuf); impl WalDir { @@ -11,7 +11,7 @@ impl WalDir { 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-v2-install-{ordinal}")); + 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 => {} @@ -48,10 +48,10 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact .expect("activate empty WAL"); let token = admitted(&owner); let installed = owner - .install_admitted_echo_operation_package_v2(token.clone()) + .install_admitted_echo_operation_package(token.clone()) .expect("install"); let mut identity_oracle = blake3::Hasher::new(); - identity_oracle.update(b"echo:operation-installation:v2\0"); + 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()); @@ -64,12 +64,12 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact assert_eq!( owner .engine() - .installed_echo_operation_package_v2(installed.package_identity()), + .installed_echo_operation_package((installed.package_identity()).into()), Some(&installed) ); assert_eq!( owner - .install_admitted_echo_operation_package_v2(token) + .install_admitted_echo_operation_package(token) .expect("idempotent"), installed ); @@ -79,7 +79,7 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact ); let bytes = installed.retained_bytes().expect("retained"); assert_eq!( - InstalledEffectOperationV2::from_retained_bytes(&bytes).expect("reconstruct"), + InstalledEchoOperation::from_retained_bytes(&bytes).expect("reconstruct"), installed ); let recovered = owner @@ -87,10 +87,7 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact .expect("wal") .recover_read_only() .expect("recover"); - assert_eq!( - recovered.installed_echo_effect_operations, - vec![installed.clone()] - ); + assert_eq!(recovered.installed_echo_operations, vec![installed.clone()]); drop(owner); let mut fresh = host(); fresh @@ -99,7 +96,7 @@ fn owner_installation_is_idempotent_and_fresh_filesystem_recovery_restores_exact assert_eq!( fresh .engine() - .installed_echo_operation_package_v2(installed.package_identity()), + .installed_echo_operation_package((installed.package_identity()).into()), Some(&installed) ); } @@ -108,7 +105,7 @@ 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_v2(token) + .install_admitted_echo_operation_package(token) .expect("install"); let bytes = installed.retained_bytes().expect("bytes"); for key in [ @@ -119,15 +116,15 @@ fn retained_installation_refuses_substituted_package_policy_and_evidence_ids() { ] { let mut value = decode(&bytes).expect("canonical"); set(&mut value, key, id(99)); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); } for schema in [ - "echo.operation-installation/v1", + "echo.operation-installation/v2", "echo.operation-installation/v3", ] { let mut value = decode(&bytes).expect("canonical"); set(&mut value, "schema", text(schema)); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); } let mut value = decode(&bytes).expect("canonical"); set( @@ -135,10 +132,10 @@ fn retained_installation_refuses_substituted_package_policy_and_evidence_ids() { "package_bytes", V::Bytes(encoded(&V::Integer(0))), ); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); let mut value = decode(&bytes).expect("canonical"); add(&mut value, "extra", V::Integer(0)); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + 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") @@ -149,14 +146,14 @@ fn retained_installation_refuses_substituted_package_policy_and_evidence_ids() { .expect("policy") .1; set(policy, "expected_authority_profile_identity", id(99)); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + 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_v2(admitted(&owner)) + .install_admitted_echo_operation_package(admitted(&owner)) .expect("install"); let before = owner.runtime_wal().expect("wal").frames().len(); let mut p = program(); @@ -174,7 +171,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen ) .expect("new pinned meaning"); assert!(owner - .install_admitted_echo_operation_package_v2(token) + .install_admitted_echo_operation_package(token) .is_err()); let bytes = encoded(&package(&program())); let token = owner @@ -189,28 +186,28 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen ) .expect("new policy"); assert!(owner - .install_admitted_echo_operation_package_v2(token) + .install_admitted_echo_operation_package(token) .is_err()); assert_eq!( owner .engine() - .installed_echo_operation_package_v2(installed.package_identity()), + .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_v2_installation() { +fn activating_empty_wal_cannot_attest_an_earlier_volatile_installation() { let mut owner = host(); owner - .install_admitted_echo_operation_package_v2(admitted(&owner)) + .install_admitted_echo_operation_package(admitted(&owner)) .expect("volatile install"); assert!(owner.enable_in_memory_runtime_wal().is_err()); } -fn install_v1( +fn install_fixed_program( owner: &mut TrustedRuntimeHost, coordinate: &str, -) -> Result { +) -> Result { use warp_core::{ EchoOperationAdmissionPolicy, EchoOperationProgram, EchoOperationSemanticClosure, ExecutableOperationPackage, @@ -248,52 +245,56 @@ fn install_v1( let admitted = owner .admit_echo_operation_package(&policy, bytes) .expect("V1 policy"); - owner.install_admitted_echo_operation_package_v1(admitted) + owner.install_admitted_echo_operation_package(admitted) } #[test] -fn v1_and_v2_share_coordinate_exclusion_in_both_installation_orders() { - for v2_first in [false, true] { +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 v2_first { + if effect_first { owner - .install_admitted_echo_operation_package_v2(admitted(&owner)) + .install_admitted_echo_operation_package(admitted(&owner)) .expect("V2 first"); let before = owner.runtime_wal().expect("wal").frames().len(); - assert!(install_v1(&mut owner, "app.update@1").is_err()); + assert!(install_fixed_program(&mut owner, "app.update@1").is_err()); assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); } else { - install_v1(&mut owner, "app.update@1").expect("V1 first"); + 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_v2(admitted(&owner)) + .install_admitted_echo_operation_package(admitted(&owner)) .is_err()); assert_eq!(owner.runtime_wal().expect("wal").frames().len(), before); } } } #[test] -fn mixed_catalog_recovers_both_versions_and_certificate_binds_v2_installations() { +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_v1(&mut owner, "app.legacy@1").expect("V1"); + let legacy = install_fixed_program(&mut owner, "app.legacy@1").expect("V1"); let effect = owner - .install_admitted_echo_operation_package_v2(admitted(&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, vec![legacy.clone()]); + 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_effect_operations.clear(); + 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 @@ -306,19 +307,19 @@ fn mixed_catalog_recovers_both_versions_and_certificate_binds_v2_installations() assert_eq!( fresh .engine() - .installed_echo_operation_package_v1(legacy.package_id()), + .installed_echo_operation_package(legacy.package_id()), Some(&legacy) ); assert_eq!( fresh .engine() - .installed_echo_operation_package_v2(effect.package_identity()), + .installed_echo_operation_package((effect.package_identity()).into()), Some(&effect) ); } #[cfg(feature = "host_test")] #[test] -fn failed_wal_append_and_commit_do_not_publish_v2_registry_entries() { +fn failed_wal_append_and_commit_do_not_publish_registry_entries() { use warp_core::causal_wal::{FilesystemWalFaultPlan, FilesystemWalFaultTarget}; for target in [ FilesystemWalFaultTarget::AppendFrame, @@ -335,11 +336,11 @@ fn failed_wal_append_and_commit_do_not_publish_v2_registry_entries() { .inject_runtime_wal_filesystem_fault_for_test(FilesystemWalFaultPlan::fail_next(target)) .expect("fault"); assert!(owner - .install_admitted_echo_operation_package_v2(token) + .install_admitted_echo_operation_package(token) .is_err()); assert!(owner .engine() - .installed_echo_operation_package_v2(pin) + .installed_echo_operation_package((pin).into()) .is_none()); drop(owner); let mut fresh = host(); @@ -348,7 +349,7 @@ fn failed_wal_append_and_commit_do_not_publish_v2_registry_entries() { .expect("recover incomplete transaction"); assert!(fresh .engine() - .installed_echo_operation_package_v2(pin) + .installed_echo_operation_package((pin).into()) .is_none()); } } @@ -357,7 +358,7 @@ 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_v2(token.clone()) + .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 { @@ -399,7 +400,7 @@ fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission( V::Bytes(admission_id.to_vec()), ); let mut h = blake3::Hasher::new(); - h.update(b"echo:operation-installation:v2\0"); + h.update(b"echo:operation-installation:v1\0"); h.update(&pin); h.update(&admission_id); h.update(&12u64.to_le_bytes()); @@ -412,7 +413,7 @@ fn self_consistent_retained_hashes_cannot_bypass_independent_policy_readmission( "installed_operation_id", V::Bytes(h.finalize().as_bytes().to_vec()), ); - assert!(InstalledEffectOperationV2::from_retained_bytes(&encoded(&value)).is_err()); + assert!(InstalledEchoOperation::from_retained_bytes(&encoded(&value)).is_err()); } #[cfg(feature = "host_test")] #[test] @@ -425,18 +426,18 @@ fn readable_unsynced_installation_is_not_published_until_a_fresh_store_reopens() .expect("wal"); let token = admitted(&owner); let pin = token.package_identity(); - let package = token.package().clone(); + 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_v2(token) + .install_admitted_echo_operation_package(token) .is_err()); assert!(owner .engine() - .installed_echo_operation_package_v2(pin) + .installed_echo_operation_package((pin).into()) .is_none()); assert!( owner @@ -454,9 +455,66 @@ fn readable_unsynced_installation_is_not_published_until_a_fresh_store_reopens() assert_eq!( fresh .engine() - .installed_echo_operation_package_v2(pin) + .installed_echo_operation_package((pin).into()) .expect("recovered installation") - .package(), - &package + .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_projected_cas_tests.rs b/crates/warp-core/tests/edict_projected_cas_tests.rs index 76a9a4bb..4e0e071b 100644 --- a/crates/warp-core/tests/edict_projected_cas_tests.rs +++ b/crates/warp-core/tests/edict_projected_cas_tests.rs @@ -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) { @@ -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 @@ -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); } @@ -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/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index e0709f60..66e0100a 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -29,7 +29,7 @@ use warp_core::{ EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, EchoOperationProgram, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, EngineBuilder, ExecutableOperationPackage, GraphStore, HeadEligibility, InboxPolicy, - IngressTarget, InstalledEchoOperationV1, NodeKey, NodeRecord, PlaybackMode, RuntimeError, + IngressTarget, InstalledEchoOperation, NodeKey, NodeRecord, PlaybackMode, RuntimeError, RuntimeWalActivationGap, SchedulerKind, TrustedRuntimeHost, TrustedRuntimeHostError, TrustedRuntimeWalConfig, TrustedRuntimeWalError, WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, ACTION_BATCH_CANDIDATE_LIMIT_V1, @@ -494,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"), @@ -521,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"), @@ -544,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") } @@ -568,7 +568,7 @@ fn invocation_policy() -> EchoOperationInvocationAdmissionPolicyV1 { } fn canonical_invocation( - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, basis: warp_core::EchoOperationEvaluationBasisV1, node: NodeKey, expected_value_digest: [u8; 32], @@ -591,7 +591,7 @@ fn canonical_invocation( fn action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, current: &[u8], @@ -671,7 +671,7 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { .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(); @@ -856,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(); @@ -1016,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"); @@ -1824,7 +1824,7 @@ 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) @@ -1937,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() @@ -2448,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" @@ -3094,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] { @@ -3266,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), @@ -4582,7 +4582,7 @@ fn submit_action( fn creation_action_invocation( host: &TrustedRuntimeHost, - installed: &InstalledEchoOperationV1, + installed: &InstalledEchoOperation, head_key: WriterHeadKey, node: NodeKey, payload: &[u8], @@ -7111,3 +7111,79 @@ fn recovery_index_binds_retained_sections_and_action_attribution_under_one_recip 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); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 785964b7..671b1dcc 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -724,8 +724,7 @@ physical evaluation arena. This is byte binding and contract preparation. Compiler bytes and upstream identities do not infer verifier approval or validate the compiled instruction -language. The separate invocation/installation definitions and catalogs described below -still require consolidation under the one-current- +language. The invocation definitions described below still require consolidation under the one-current- version policy. Preparation does not grant invocation or mutation authority. ### V2 invocation input and basis preparation @@ -778,52 +777,63 @@ 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. The still-separate -fixed and effect installation paths enforce their supported program kinds; -their definitions/catalogs remain unfinished consolidation work. Invocation -admission and compiled effect execution also remain unfinished. Package +and is neither an installation nor an invocation grant. Installation uses one evidence type and catalog for every program kind. +Invocation admission and compiled effect execution remain unfinished. Package admission does not infer compiler/verifier approval from publication identities. -### Runtime-owned V2 installation and retained catalog recovery - -`TrustedRuntimeHost::install_admitted_echo_operation_package_v2` consumes opaque -package-policy admission evidence and installs the exact publication in the -engine's V2 catalog. Installation remains separate from invocation authority -and compiled effect instruction validation. The application handle cannot call -this owner-only method. Reconstructing an `InstalledEffectOperationV2` from -retained bytes produces evidence only; it does not insert a registry entry. - -The engine still has separate fixed-program and effect installation catalogs, -both consuming the sole current package/program definitions. It shares operation- -coordinate exclusion across them. An exact repeat is idempotent; a changed package at the same coordinate or a changed -policy for an installed package refuses before any WAL append. Batch recovery -preflights both catalogs. These two installation definitions and their versioned -retained records remain unfinished consolidation work, not a compatibility -exception. Package/program identities use the shared recipe above. The effect -installation identity still uses `echo:operation-installation:v2\0`, -then package identity, package-admission identity, length-prefixed operation -coordinate, program identity and length-prefixed exact package bytes, in that -order, as the V1 recipe does under its original domain. - -The retained V2 record has exactly the existing seven installation fields with -`schema` set to `echo.operation-installation/v2`. It retains the full independent -policy, policy identity, exact package bytes and identity, admission identity and -installation identity. WAL recovery dispatches by installation schema; unknown -versions refuse. V2 reconstruction reruns package-policy admission, recomputes -every identity and reproduces the exact record bytes. Recomputed hashes cannot -make an invalid independent authority policy acceptable. - -With a runtime WAL configured, the installation record commits before the V2 -registry entry becomes visible. A failed append or commit publishes no entry. -The uncertain filesystem-error path refreshes the writer and checks the exact -recovered installation before returning success. A commit-marker sync failure -keeps the live adapter closed to read-only recovery and publishes no registry -entry; a fresh store reopen can establish and recover that retained commit. Without a WAL, installation -is volatile; activating an empty WAL cannot attest that earlier volatile -installation as durable authority. +### 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 -versioned retained bytes. Recovery certificates use one current operation-index +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. @@ -835,10 +845,10 @@ 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. -The installation definitions still have parallel versions; the shared package/ -program, admission-token and index cutovers do not complete their replacement. A fresh -filesystem-backed host currently restores those installations and rechecks -coordinate conflicts. This is installation recovery, not recovery of +A fresh filesystem-backed host restores all supported program kinds through +the sole installation collection and rechecks coordinate conflicts. Invocation +definitions still require consolidation under the one-current-version policy. +This is installation recovery, not recovery of an authored-effect decision. Invocation grant admission, compiled execution, physical allocation, derived effects and Action/Tick/receipt recovery remain unfinished. diff --git a/xtask/src/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index 237124c4..23677cf0 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -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 { @@ -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(()) } diff --git a/xtask/src/run_edict_operation/session.rs b/xtask/src/run_edict_operation/session.rs index 66227912..e337ecfb 100644 --- a/xtask/src/run_edict_operation/session.rs +++ b/xtask/src/run_edict_operation/session.rs @@ -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)?; From de10fb39a2d3583cd1bf98f0c8d01522262ed566 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 09:35:24 -0700 Subject: [PATCH 23/43] Unify operation invocations and admit effect grants --- CHANGELOG.md | 9 + README.md | 6 + .../tests/action_preparation_allocation.rs | 14 +- .../warp-core/benches/support/action_scene.rs | 14 +- crates/warp-core/src/coordinator.rs | 15 +- crates/warp-core/src/echo_operation.rs | 894 +++++++++++------- .../warp-core/src/echo_operation/observed.rs | 4 +- crates/warp-core/src/edict_effects.rs | 5 +- .../warp-core/src/edict_effects/invocation.rs | 144 --- crates/warp-core/src/edict_effects/package.rs | 3 - crates/warp-core/src/lib.rs | 10 +- crates/warp-core/src/trusted_runtime_host.rs | 36 +- .../trusted_runtime_host/observed_context.rs | 6 +- .../tests/edict_effect_package_tests.rs | 1 - .../edict_effect_package_tests/invocation.rs | 37 +- .../tests/edict_projected_cas_tests.rs | 40 +- .../executable_operation_pipeline_tests.rs | 423 ++++++--- .../tests/trusted_runtime_host_loop_tests.rs | 8 +- docs/topics/GeneratedRules.md | 66 +- tests/docs/test_generated_rule_truth.sh | 2 +- xtask/src/run_edict_operation.rs | 6 +- xtask/src/run_edict_operation/session.rs | 12 +- 22 files changed, 1006 insertions(+), 749 deletions(-) delete mode 100644 crates/warp-core/src/edict_effects/invocation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d4155a..6ddddf21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ ### Changed +- 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 diff --git a/README.md b/README.md index a682cf75..945b2edf 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,12 @@ 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 `ExecutableOperationPackage`, admit an exact-basis invocation under explicit authority and delegated budget, evaluate its data-only diff --git a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs index 4a89740f..daba1eea 100644 --- a/crates/echo-alloc-witness/tests/action_preparation_allocation.rs +++ b/crates/echo-alloc-witness/tests/action_preparation_allocation.rs @@ -26,10 +26,10 @@ 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, EchoOperationAdmissionPolicy, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, - EchoOperationPreparationV1, EngineBuilder, GraphStore, InboxPolicy, NodeId, NodeKey, - NodeRecord, PlaybackMode, SchedulerKind, TrustedRuntimeHost, TypeId, WorldlineId, - WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, + 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. @@ -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/warp-core/benches/support/action_scene.rs b/crates/warp-core/benches/support/action_scene.rs index b8b141ee..29438317 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, - EchoOperationAdmissionPolicy, 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"; @@ -239,7 +239,7 @@ impl Scene { 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 4f7b128e..df15b142 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")))] @@ -5865,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 ecea8441..e86c5122 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"; @@ -331,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, @@ -768,15 +762,25 @@ impl EchoOperationProgram { } } - 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 ) ) } @@ -1404,7 +1408,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)?; @@ -1450,13 +1454,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, }, ) => { @@ -3214,8 +3218,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, } @@ -3227,20 +3235,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] @@ -3260,8 +3268,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, @@ -3294,8 +3302,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, @@ -3322,8 +3330,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, @@ -3350,14 +3358,124 @@ 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(EchoOperationApplicationBasisV1::new( + profile_digest("echo.operation.basis.edict-effect/v1"), + domain_hash(b"echo:operation-edict-effect-basis:v1\0", input), + )) + } + /// Returns the canonical invocation identity. pub fn identity(&self) -> Result { Ok(EchoOperationInvocationIdV1(domain_hash( @@ -3393,67 +3511,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) } @@ -3485,7 +3610,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 @@ -3512,6 +3638,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", @@ -3521,100 +3652,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, @@ -3629,12 +3727,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 { @@ -3658,13 +3764,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( @@ -3691,11 +3797,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. @@ -3715,33 +3823,39 @@ 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 { + /// 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 } @@ -3750,7 +3864,7 @@ impl AdmittedEchoOperationInvocationV1 { self.invocation.evaluation_basis } - pub(crate) const fn scope(&self) -> NodeKey { + pub(crate) const fn scope(&self) -> Option { self.invocation.node } @@ -3765,17 +3879,17 @@ impl AdmittedEchoOperationInvocationV1 { pub(crate) fn admit_invocation_v1( installed: Option<&InstalledEchoOperation>, - policy: EchoOperationInvocationAdmissionPolicyV1, + 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", )); } @@ -3786,7 +3900,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", )); } @@ -3794,7 +3908,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", )); } @@ -3816,12 +3930,18 @@ pub(crate) fn admit_invocation_v1( /// obstruction instead of failing the host admission loop. pub(crate) fn admit_action_invocation_v1( installed: Option<&InstalledEchoOperation>, - policy: EchoOperationInvocationAdmissionPolicyV1, + 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 @@ -3829,7 +3949,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", )); } @@ -3844,71 +3964,57 @@ pub(crate) fn admit_action_invocation_v1( fn admit_invocation_static_v1<'a>( installed: Option<&'a InstalledEchoOperation>, - policy: EchoOperationInvocationAdmissionPolicyV1, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], ) -> Result< - (EchoOperationInvocationV1, &'a InstalledEchoOperation), - 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", )); } @@ -3924,7 +4030,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", )); } @@ -3932,12 +4038,12 @@ fn admit_invocation_static_v1<'a>( } fn build_admitted_invocation_v1( - invocation: EchoOperationInvocationV1, + invocation: EchoOperationInvocation, installed: &InstalledEchoOperation, - policy: EchoOperationInvocationAdmissionPolicyV1, + policy: EchoOperationInvocationAdmissionPolicy, canonical_invocation_bytes: &[u8], evaluation_authority: EchoOperationEvaluationAuthorityV1, -) -> AdmittedEchoOperationInvocationV1 { +) -> AdmittedEchoOperationInvocation { let invocation_id = EchoOperationInvocationIdV1(domain_hash( INVOCATION_ID_DOMAIN, canonical_invocation_bytes, @@ -3945,7 +4051,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(), @@ -3964,30 +4070,32 @@ fn build_admitted_invocation_v1( fn current_application_basis( installed: &InstalledEchoOperation, - invocation: &EchoOperationInvocationV1, + invocation: &EchoOperationInvocation, state: &WorldlineState, -) -> Result { +) -> Result { let basis_mismatch = |detail| { invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch, + EchoOperationInvocationAdmissionErrorKind::BasisMismatch, detail, ) }; match installed.program { #[cfg(feature = "trusted_runtime")] - EchoOperationProgram::EdictEffect(_) => Err(invocation_admission_error( - EchoOperationInvocationAdmissionErrorKindV1::OperationProfileMismatch, - "fixed invocation does not match an authored-effect program", - )), + EchoOperationProgram::EdictEffect(_) => invocation + .effect_application_basis() + .map_err(|_| basis_mismatch("effect application input is absent")), 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( @@ -3996,39 +4104,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, )) } 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, )) } } @@ -4059,11 +4169,14 @@ fn current_application_basis( /// with `BasisChanged`. `state` must be the state the Action commits onto. pub(crate) fn scoped_action_basis_holds_v1( installed: &InstalledEchoOperation, - invocation: &EchoOperationInvocationV1, + 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, @@ -4073,7 +4186,7 @@ pub(crate) fn scoped_action_basis_holds_v1( 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 @@ -4152,7 +4265,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 _; @@ -4185,7 +4298,7 @@ pub(crate) fn echo_operation_origin_corroborated_v1( )) }; retained - && untouched(invocation.node) + && invocation.node.is_some_and(untouched) && invocation .observation .as_ref() @@ -4195,7 +4308,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) @@ -4208,7 +4321,7 @@ 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), ) } @@ -4221,7 +4334,7 @@ pub(crate) fn scoped_action_basis_holds_for_bytes_v1( 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) }, @@ -4232,11 +4345,11 @@ pub(crate) fn action_application_basis_matches_state_v1( 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(), ) })?; @@ -4251,12 +4364,11 @@ pub(crate) fn action_admission_evidence_matches_v1( 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, @@ -4306,8 +4418,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, @@ -4361,12 +4473,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(), ) })?; @@ -4383,17 +4495,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, @@ -4408,15 +4525,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, @@ -5161,8 +5278,9 @@ pub enum EchoOperationPreparationV1 { /// Complete private evaluation, bound to all substitution and resource evidence. #[derive(Clone, Debug)] pub struct PreparedEchoOperationV1 { + scope: NodeKey, installed: InstalledEchoOperation, - invocation: EchoOperationInvocationV1, + invocation: EchoOperationInvocation, invocation_id: EchoOperationInvocationIdV1, canonical_invocation_bytes: Vec, invocation_admission_policy_id: Hash, @@ -5299,7 +5417,7 @@ impl PreparedEchoOperationV1 { pub(crate) fn prepare_operation_v1( installed: Option<&InstalledEchoOperation>, - admitted: AdmittedEchoOperationInvocationV1, + admitted: AdmittedEchoOperationInvocation, current_basis: EchoOperationEvaluationBasisV1, state: &WorldlineState, policy_id: u32, @@ -5359,7 +5477,7 @@ pub(crate) fn prepare_operation_v1( required_attachment_type, max_replacement_bytes, }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest, }, ) => ( @@ -5376,7 +5494,7 @@ pub(crate) fn prepare_operation_v1( required_attachment_type, max_replacement_bytes, }, - EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, + EchoOperationInvocationKind::AnchoredNodeAttachmentCreateIfAbsent, ) => ( *required_node_type, *required_attachment_type, @@ -5391,7 +5509,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 { @@ -5569,7 +5689,7 @@ pub(crate) fn prepare_operation_v1( }; let result_id = operation_result_id( installed, - &admitted.invocation, + node, mode, replacement_value_digest, patch.digest(), @@ -5598,6 +5718,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, @@ -6274,7 +6395,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, @@ -6527,7 +6648,7 @@ pub(crate) fn validate_receipt_application_result_v1( 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(), @@ -7174,7 +7295,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, @@ -7545,7 +7666,7 @@ fn invocation_admission_id( fn operation_result_id( installed: &InstalledEchoOperation, - invocation: &EchoOperationInvocationV1, + node: NodeKey, mode: AnchoredNodeOperationModeV1, replacement_value_digest: Hash, patch_digest: Hash, @@ -7561,8 +7682,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 => { @@ -7573,8 +7694,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); } } @@ -8079,10 +8200,10 @@ pub(crate) fn admission_error( } fn invocation_admission_error( - kind: EchoOperationInvocationAdmissionErrorKindV1, + kind: EchoOperationInvocationAdmissionErrorKind, detail: impl Into, -) -> EchoOperationInvocationAdmissionErrorV1 { - EchoOperationInvocationAdmissionErrorV1 { +) -> EchoOperationInvocationAdmissionError { + EchoOperationInvocationAdmissionError { kind, detail: detail.into(), } @@ -8215,8 +8336,8 @@ mod tests { InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, - EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, + EchoOperationInvocationAdmissionPolicy, + EchoOperationInvocation, Vec, ) { projected_create_fixture_with_key(max_output_bytes, "fixture-key", None) @@ -8230,8 +8351,8 @@ mod tests { InstalledEchoOperation, WorldlineState, EchoOperationEvaluationBasisV1, - EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, + EchoOperationInvocationAdmissionPolicy, + EchoOperationInvocation, Vec, ) { let operation_coordinate = "echo.fixture.ProjectedCreate.v1"; @@ -8379,7 +8500,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, @@ -8389,7 +8510,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), @@ -8563,7 +8684,7 @@ mod tests { 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"); @@ -8571,7 +8692,7 @@ mod tests { .expect_err("projected CAS must bind its expected digest"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -8608,7 +8729,7 @@ mod tests { ])) .expect("input encodes"), ); - invocation.kind = EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + invocation.kind = EchoOperationInvocationKind::AnchoredNodeAttachmentCompareAndSet { expected_value_digest: digest(212), }; admit_invocation_static_v1( @@ -8617,7 +8738,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( @@ -8628,21 +8749,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"); @@ -8656,13 +8777,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(); @@ -8679,7 +8800,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([ @@ -8688,13 +8809,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, ); @@ -8711,7 +8832,7 @@ mod tests { .expect_err("application input cannot retarget the graph consequence"); assert_eq!( error.kind(), - EchoOperationInvocationAdmissionErrorKindV1::ApplicationInputMismatch + EchoOperationInvocationAdmissionErrorKind::ApplicationInputMismatch ); } @@ -9217,7 +9338,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, @@ -9237,7 +9358,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), @@ -9307,7 +9428,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, @@ -9318,7 +9439,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), @@ -9379,17 +9500,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"); @@ -9549,7 +9669,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, @@ -9800,7 +9920,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( @@ -9828,8 +9948,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); @@ -9838,7 +9970,7 @@ mod tests { assert_eq!( operation_result_id( &installed, - &invocation, + invocation.node.expect("anchored fixture"), AnchoredNodeOperationModeV1::CompareAndSet { expected_value_digest: previous, }, @@ -9852,7 +9984,7 @@ 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 = EchoOperationProgram::anchored_node_attachment_compare_and_set( @@ -9907,7 +10039,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, @@ -9918,6 +10050,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", @@ -9942,9 +10075,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 { @@ -9958,7 +10103,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" ); } @@ -9973,7 +10118,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)), @@ -10019,7 +10165,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)), @@ -10123,9 +10269,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(); @@ -10142,13 +10288,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( @@ -10172,17 +10324,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"); }; @@ -10248,12 +10400,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()); } } @@ -10303,12 +10461,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 a7f27d1a..82fecf64 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 index 1c9400af..b5b31795 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -4,12 +4,11 @@ //! //! 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 crossings grants invocation rights or enables effect execution. +//! 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. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. -mod invocation; -pub use invocation::EffectInvocationV2; mod package; pub use package::{EffectPackageError, EffectProgram}; mod frame; diff --git a/crates/warp-core/src/edict_effects/invocation.rs b/crates/warp-core/src/edict_effects/invocation.rs deleted file mode 100644 index 3aa248fb..00000000 --- a/crates/warp-core/src/edict_effects/invocation.rs +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// © James Ross Ω FLYING•ROBOTS -//! Static V2 input/basis binding, without installed authority or freshness. -use super::package::{domain_hash, hash}; -use super::EffectPackageError; -use crate::edict_pure::{ - values::{bytes, exact_fields, field, number, require_text, text}, - EvaluationError, -}; -use crate::ExecutableOperationPackage; -use crate::{EchoOperationBudgetV1, EchoOperationEvaluationBasisV1}; -use echo_edict_canonical::{decode_canonical_cbor_v1, CanonicalValueV1 as Value}; -/// Prepared invocation bytes, not an admitted Action or verified grant. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectInvocationV2 { - canonical: Vec, - input: Vec, - basis: EchoOperationEvaluationBasisV1, - grant: [u8; 32], - identity: [u8; 32], - budget: EchoOperationBudgetV1, -} -impl EffectInvocationV2 { - /// Checks canonical invocation input against its pinned package contract. - /// - /// This checks static byte/basis consistency. Scheduler admission must still - /// verify the authority grant and installation, and evaluation must resolve - /// the retained parent against the actual runtime frontier. - /// # Errors - /// Refuses malformed or inconsistent input, budget, package and basis fields. - pub fn prepare( - bytes: &[u8], - package: &ExecutableOperationPackage, - ) -> Result { - let value = - decode_canonical_cbor_v1(bytes).map_err(|_| EffectPackageError::InvalidInvocation)?; - Self::parse(&value, bytes, package).map_err(|_| EffectPackageError::InvalidInvocation) - } - fn parse( - value: &Value, - canonical: &[u8], - package: &ExecutableOperationPackage, - ) -> Result { - exact_fields( - value, - &[ - "schema", - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "operation_coordinate", - "package_id", - "application_input_bytes", - ], - )?; - require_text(value, "schema", "echo.operation-invocation/v2")?; - let publication = package - .to_canonical_bytes() - .map_err(|_| EvaluationError::InvalidArtifact)?; - let effect = package - .program() - .as_effect_program() - .ok_or(EvaluationError::InvalidArtifact)?; - if hash(field(value, "package_id")?)? - != domain_hash(b"echo:operation-package:v1\0", &publication) - || text(field(value, "operation_coordinate")?)? != package.operation_coordinate() - { - return Err(EvaluationError::InvalidArtifact); - } - let input = bytes(field(value, "application_input_bytes")?)?; - if u64::try_from(input.len()).map_err(|_| EvaluationError::InvalidArtifact)? - > effect.max_input_bytes() - { - return Err(EvaluationError::InvalidArtifact); - } - decode_canonical_cbor_v1(input).map_err(|_| EvaluationError::InvalidArtifact)?; - let basis = - EchoOperationEvaluationBasisV1::from_value(field(value, "evaluation_basis")?.clone()) - .map_err(|_| EvaluationError::InvalidArtifact)?; - if basis.application_basis().schema_identity() - != domain_hash( - b"echo:operation-profile:v1\0", - b"echo.operation.basis.edict-effect/v1", - ) - || basis.application_basis().value_identity() - != domain_hash(b"echo:operation-edict-effect-basis:v1\0", input) - { - return Err(EvaluationError::InvalidArtifact); - } - let budget = field(value, "delegated_budget")?; - exact_fields(budget, &["steps", "read_bytes", "write_bytes"])?; - let budget = EchoOperationBudgetV1::new( - number(field(budget, "steps")?)?, - number(field(budget, "read_bytes")?)?, - number(field(budget, "write_bytes")?)?, - ); - let ceiling = package.budget_ceiling(); - if budget.steps() == 0 - || budget.steps() > ceiling.steps() - || budget.read_bytes() > ceiling.read_bytes() - || budget.write_bytes() > ceiling.write_bytes() - { - return Err(EvaluationError::InvalidArtifact); - } - Ok(Self { - canonical: canonical.to_vec(), - input: input.to_vec(), - basis, - grant: hash(field(value, "authority_grant_identity")?)?, - identity: domain_hash(b"echo:operation-invocation:v2\0", canonical), - budget, - }) - } - /// Requested resource allowance, checked only against the publication ceiling. - #[must_use] - pub const fn delegated_budget(&self) -> EchoOperationBudgetV1 { - self.budget - } - /// Exact canonical invocation bytes. - #[must_use] - pub fn canonical_bytes(&self) -> &[u8] { - &self.canonical - } - /// Exact application input whose digest is retained in the basis. - #[must_use] - pub fn application_input_bytes(&self) -> &[u8] { - &self.input - } - /// Claimed exact V1 parent basis; freshness has not been established. - #[must_use] - pub const fn evaluation_basis(&self) -> EchoOperationEvaluationBasisV1 { - self.basis - } - /// Claimed grant identity; no grant authority is conferred by decoding it. - #[must_use] - pub const fn authority_grant_identity(&self) -> [u8; 32] { - self.grant - } - /// Identity of exact invocation bytes under the distinct V2 domain. - #[must_use] - pub const fn identity(&self) -> [u8; 32] { - self.identity - } -} diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index 153406c0..95af067d 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -17,9 +17,6 @@ pub enum EffectPackageError { /// The publication has malformed or inconsistent fields or profiles. #[error("invalid effect package")] InvalidPackage, - /// The invocation has malformed or inconsistent package, input or basis fields. - #[error("invalid effect invocation")] - InvalidInvocation, } /// Prepared authored-effect program payload, without execution authority. #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index a0555216..0da35dad 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -259,7 +259,7 @@ 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, + echo_operation_target_profile_identity_v1, AdmittedEchoOperationInvocation, AdmittedExecutableOperationPackage, EchoOperationActionDecisionCoordinateV1, EchoOperationActionOutcomeV1, EchoOperationAdmissionError, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, @@ -268,10 +268,10 @@ pub use echo_operation::{ 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, EchoOperationProgram, EchoOperationProgramIdV1, EchoOperationReceiptV1, diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 704b7a8c..a308dbf7 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -64,7 +64,7 @@ use crate::{ SchedulerEchoOperationCandidateV1, }, provider_contract::admit_provider_contract_package_v1, - AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackage, + AdmittedEchoOperationInvocation, AdmittedExecutableOperationPackage, AdmittedProviderContractPackageV1, CausalAnchorAdmissionRequest, CausalAnchorClaim, CausalAnchorError, CausalAnchorId, CausalAnchorRootSupportPolicy, CausalAnchorSupportError, CausalFrontierRef, CausalReceiptHistoryObstruction, CausalReceiptHistoryPage, @@ -73,8 +73,8 @@ use crate::{ 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, @@ -158,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), @@ -866,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`]. @@ -887,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 { @@ -1069,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(); @@ -1091,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() @@ -1230,11 +1230,11 @@ impl TrustedRuntimeHost { /// 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( @@ -1272,7 +1272,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(), @@ -2007,7 +2007,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()); } @@ -7267,7 +7267,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, 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 88d08ac0..8f8f6f0b 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/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index ce8f5568..9613454e 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -4,7 +4,6 @@ #![cfg(feature = "trusted_runtime")] #![allow(clippy::expect_used, clippy::panic)] use echo_edict_canonical::CanonicalValueV1 as V; -use warp_core::edict_effects::EffectPackageError; use warp_core::ExecutableOperationPackage; use warp_core::TypeId; #[path = "common/edict_effect_publication.rs"] diff --git a/crates/warp-core/tests/edict_effect_package_tests/invocation.rs b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs index 99900cef..b4f3d13d 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/invocation.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/invocation.rs @@ -2,11 +2,12 @@ // © James Ross Ω FLYING•ROBOTS //! Invocation byte/basis binding without runtime authority admission. use super::*; -use warp_core::edict_effects::EffectInvocationV2; +use warp_core::EchoOperationInvocation; fn invocation(input: Vec) -> V { let package_bytes = encoded(&package(&program())); map([ - ("schema", text("echo.operation-invocation/v2")), + ("schema", text("echo.operation-invocation/v1")), + ("kind", text("edict-effect-program/v1")), ("authority_grant_identity", id(9)), ( "delegated_budget", @@ -43,8 +44,8 @@ fn invocation(input: Vec) -> V { ), ]) } -fn check(v: &V) -> Result { - EffectInvocationV2::prepare( +fn check(v: &V) -> Result { + EchoOperationInvocation::prepare( &encoded(v), &prepare(&package(&program())).expect("package"), ) @@ -65,13 +66,16 @@ fn basis_set(v: &mut V, k: &str, x: V) { 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 V2 invocation"); - assert_eq!(prepared.canonical_bytes(), bytes); + 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.identity(), - hash(b"echo:operation-invocation:v2\0", &bytes) + prepared.application_input_bytes(), + Some(encoded(&V::Integer(42)).as_slice()) ); - assert_eq!(prepared.application_input_bytes(), encoded(&V::Integer(42))); 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]); @@ -103,7 +107,7 @@ fn altered_input_cannot_keep_its_predecessors_basis_digest() { #[test] fn invocation_refuses_cross_version_cross_package_and_cross_operation_substitution() { for (k, v) in [ - ("schema", text("echo.operation-invocation/v1")), + ("schema", text("echo.operation-invocation/v2")), ("package_id", id(5)), ("operation_coordinate", text("app.other@1")), ("authority_grant_identity", V::Bytes(vec![9; 31])), @@ -112,7 +116,7 @@ fn invocation_refuses_cross_version_cross_package_and_cross_operation_substituti set(&mut value, k, v); assert!(check(&value).is_err()); } - for k in ["node", "replacement_bytes", "extra"] { + 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()); @@ -193,14 +197,15 @@ fn input_uses_the_selected_packages_smaller_ceiling_and_exact_outer_bytes() { ), ); assert_eq!( - EffectInvocationV2::prepare(&encoded(&value), &selected).is_ok(), + EchoOperationInvocation::prepare(&encoded(&value), &selected).is_ok(), accepts ); } let mut bytes = encoded(&invocation(encoded(&V::Integer(42)))); bytes.push(0); - assert!( - EffectInvocationV2::prepare(&bytes, &prepare(&package(&program())).expect("original")) - .is_err() - ); + assert!(EchoOperationInvocation::prepare( + &bytes, + &prepare(&package(&program())).expect("original") + ) + .is_err()); } diff --git a/crates/warp-core/tests/edict_projected_cas_tests.rs b/crates/warp-core/tests/edict_projected_cas_tests.rs index 4e0e071b..ec41de1f 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, - EchoOperationAdmissionPolicy, 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(), @@ -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, @@ -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 @@ -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) diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 66e0100a..441343c9 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -24,12 +24,12 @@ use warp_core::{ make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, EchoOperationActionOutcomeV1, EchoOperationAdmissionErrorKind, EchoOperationAdmissionPolicy, EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, - EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, - EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, - EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, - EchoOperationProgram, EchoOperationSemanticClosure, EchoOperationTerminalPostureV1, - EngineBuilder, ExecutableOperationPackage, GraphStore, HeadEligibility, InboxPolicy, - IngressTarget, InstalledEchoOperation, NodeKey, NodeRecord, PlaybackMode, RuntimeError, + 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, @@ -559,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), @@ -575,7 +575,7 @@ fn canonical_invocation( 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, @@ -678,7 +678,7 @@ fn admitted_data_only_program_commits_one_typed_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_compare_and_set( + let invocation = EchoOperationInvocation::anchored_node_attachment_compare_and_set( installed.package_id(), installed.operation_coordinate(), evaluation_basis, @@ -694,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 { @@ -864,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, @@ -876,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), @@ -945,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) @@ -1032,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) @@ -1278,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), @@ -1297,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), @@ -1312,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( @@ -1327,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( @@ -1346,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( @@ -1365,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, @@ -1388,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 @@ -1416,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 ); } @@ -1478,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) @@ -1500,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 @@ -1541,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) @@ -1606,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) @@ -1829,7 +1829,7 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { 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, @@ -1841,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), @@ -1868,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, @@ -1882,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), @@ -1990,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, @@ -2002,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 { @@ -2071,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, @@ -2083,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 { @@ -2139,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, @@ -2152,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 ); } @@ -2191,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, @@ -2203,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 { @@ -2241,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, @@ -2253,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 { @@ -2291,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, @@ -2302,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), @@ -2379,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, @@ -2390,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), @@ -2485,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, @@ -2497,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", @@ -2553,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, @@ -2565,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 ); } @@ -2604,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, @@ -2616,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 { @@ -2661,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, @@ -2674,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 { @@ -3370,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, @@ -3411,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, @@ -3749,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, @@ -3800,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) @@ -3827,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() @@ -3870,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, @@ -3945,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, @@ -4593,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, @@ -7187,3 +7186,211 @@ fn shared_catalog_preserves_action_execution_and_recovery_in_both_program_instal 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/trusted_runtime_host_loop_tests.rs b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs index 208a407c..b3127ea9 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 671b1dcc..808e21d5 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -34,10 +34,9 @@ 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 branch's current behavior, including its still-present -V1/V2 duplication; that duplication does not satisfy this policy. A completed -authored-effect bridge must replace it with one current format and update -the compiler, runtime, tests, and consumers together. +below describe the current consolidation. Any remaining parallel historical +implementations must be replaced while updating the compiler, runtime, tests, +and consumers together. ## Current Implementation @@ -724,29 +723,42 @@ physical evaluation arena. This is byte binding and contract preparation. Compiler bytes and upstream identities do not infer verifier approval or validate the compiled instruction -language. The invocation definitions described below still require consolidation under the one-current- -version policy. Preparation does not grant invocation or mutation authority. - -### V2 invocation input and basis preparation - -`EffectInvocationV2::prepare` checks an exact canonical V2 invocation against a -prepared current publication. Only its seven specified fields are accepted. The -package identity and operation coordinate must match the publication. The -application input must itself be canonical CBOR and fit that program's declared -input ceiling, including the input's CBOR framing bytes. The delegated budget -requires at least one step and cannot exceed any package budget axis. - -The invocation retains the existing V1 evaluation-basis map. Its application -basis schema is the effect-basis profile, and its application basis value is -BLAKE3 over `echo:operation-edict-effect-basis:v1\0`, the input byte length as a -little-endian u64, and the exact input bytes. An input change requires a new -basis digest and changes the versioned V2 invocation identity. The decoder -reuses V1's basis parser without changing its encoding or identity recipe. - -This is static preparation. It retains a claimed grant identity and parent; -it does not verify a grant, establish installation, resolve the current -frontier or admit an Action. Runtime-owned authority checks and evaluation at -the named parent remain required before effects can be accepted. +language. Static preparation does not grant invocation or mutation authority. + +### 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. Compiled instruction execution, the physical arena, +derived effects, Tick integration and effect decision recovery remain required. ### Runtime-owner package policy admission diff --git a/tests/docs/test_generated_rule_truth.sh b/tests/docs/test_generated_rule_truth.sh index 56edf0ce..113c75a5 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/run_edict_operation.rs b/xtask/src/run_edict_operation.rs index 23677cf0..89421033 100644 --- a/xtask/src/run_edict_operation.rs +++ b/xtask/src/run_edict_operation.rs @@ -25,7 +25,7 @@ use warp_core::{ make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, 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, @@ -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 e337ecfb..67868448 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}; @@ -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, ); From 894730152166e992c9a7cf5d811a049a09b180e7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 09:56:52 -0700 Subject: [PATCH 24/43] Bind effect programs to compiler artifacts --- CHANGELOG.md | 8 + crates/warp-core/src/echo_operation.rs | 12 + crates/warp-core/src/edict_effects.rs | 1 + .../src/edict_effects/compiled_program.rs | 236 +++++++++++++ crates/warp-core/src/edict_effects/package.rs | 22 +- .../tests/common/edict_effect_publication.rs | 202 ++++++++++- .../current_operation_publication_tests.rs | 1 - .../installation.rs | 4 +- .../tests/edict_effect_package_tests.rs | 314 +++++++++++++++++- docs/topics/GeneratedRules.md | 30 +- 10 files changed, 815 insertions(+), 15 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/compiled_program.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddddf21..9ef357ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ ### Changed +- 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 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 diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index e86c5122..5bc87b03 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -2336,6 +2336,18 @@ impl ExecutableOperationPackage { } #[cfg(feature = "trusted_runtime")] 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() { diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index b5b31795..3433f426 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,6 +9,7 @@ //! Private frames bind immutable runtime frontiers, typed read laws and shared //! logical meters. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. +mod compiled_program; mod package; pub use package::{EffectPackageError, EffectProgram}; mod frame; 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 00000000..2af28139 --- /dev/null +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Current compiler-artifact binding, before instruction semantics or execution. +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, +} +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", + ], + )?; + 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, + }) + } + 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 ids = std::collections::BTreeSet::new(); + for collection in ["steps", "requirements", "pureBindings"] { + if collection == "pureBindings" && field(target, collection).is_err() { + continue; + } + for instruction in array(field(target, collection)?)? { + let id = text_field(instruction, "id")?; + if id.is_empty() || !ids.insert(id) { + return Err(Error::InvalidArtifact); + } + } + } + for id in array(field(target, "executionOrder")?)? { + if !ids.remove(text(id)?) { + return Err(Error::InvalidArtifact); + } + } + if !ids.is_empty() { + 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); + } + Ok(()) + } +} diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index 95af067d..f62185f8 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -23,6 +23,7 @@ pub enum EffectPackageError { pub struct EffectProgram { value: Value, compiled: Vec, + compiled_contract: super::compiled_program::CompiledProgram, schema: EffectGraphSchema, law: EffectFootprintLaw, target_ir: [u8; 32], @@ -95,6 +96,10 @@ impl EffectProgram { Ok(Self { value: value.clone(), 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")?)?, @@ -105,6 +110,20 @@ impl EffectProgram { 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(crate) fn to_value(&self) -> Value { self.value.clone() } @@ -114,7 +133,8 @@ impl EffectProgram { pub(crate) const fn footprint_identity(&self) -> [u8; 32] { self.footprint } - /// Compiler bytes whose instruction language still requires validation. + /// Canonical Core/Target IR/exports bundle bound to the selected intent. + /// Instruction semantics and execution still require validation. #[must_use] pub fn compiled_program(&self) -> &[u8] { &self.compiled diff --git a/crates/warp-core/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index 8bff9ad1..f169a9b4 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -66,6 +66,192 @@ pub fn footprint(v: &V) -> V { 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("Nominal")), + ("contract", text("app.Result")), + ("representation", 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![])), + ("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 { + map([ + ("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))), + ]) +} +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")), @@ -78,8 +264,14 @@ pub fn program() -> V { "intrinsic_profile_identity", profile("echo.operation-intrinsics.edict-effect/v1"), ), - ("compiled_program", V::Bytes(vec![0xa0])), - ("target_ir_identity", id(4)), + ("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()))), @@ -99,11 +291,11 @@ pub fn package(program: &V) -> V { "semantic_closure", map([ ("application_schema_coordinate", text("app.Schema")), - ("application_schema_identity", id(9)), + ("application_schema_identity", exports_identity(program)), ("canonical_meaning_identity", id(3)), - ("core_identity", id(2)), + ("core_identity", core_identity(program)), ("edict_source_identity", id(1)), - ("target_ir_identity", id(4)), + ("target_ir_identity", target_identity(program)), ]), ), ("program", V::Bytes(encoded(program))), diff --git a/crates/warp-core/tests/current_operation_publication_tests.rs b/crates/warp-core/tests/current_operation_publication_tests.rs index 625c411a..22812512 100644 --- a/crates/warp-core/tests/current_operation_publication_tests.rs +++ b/crates/warp-core/tests/current_operation_publication_tests.rs @@ -26,7 +26,6 @@ fn current_package_codec_prepares_effects_without_a_parallel_package_definition( .program() .as_effect_program() .expect("effect program kind"); - assert_eq!(effect.compiled_program(), [0xa0]); assert!(effect .graph_schema() .allows_atom(TypeId([1; 32]), TypeId([2; 32]))); diff --git a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs index 65642618..fe982614 100644 --- a/crates/warp-core/tests/edict_effect_admission_tests/installation.rs +++ b/crates/warp-core/tests/edict_effect_admission_tests/installation.rs @@ -157,7 +157,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen .expect("install"); let before = owner.runtime_wal().expect("wal").frames().len(); let mut p = program(); - set(&mut p, "compiled_program", V::Bytes(vec![0xa1])); + set(&mut p, "max_input_bytes", V::Integer(65535)); let bytes = encoded(&package(&p)); let token = owner .admit_echo_operation_package( @@ -169,7 +169,7 @@ fn conflicting_coordinate_or_policy_cannot_replace_an_installed_meaning_or_appen ), bytes, ) - .expect("new pinned meaning"); + .expect("new pinned input contract"); assert!(owner .install_admitted_echo_operation_package(token) .is_err()); diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 9613454e..05907aa0 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! V2 byte binding and executable-law validation, before installation. +//! 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; @@ -42,7 +42,7 @@ fn current_package_binds_program_and_executes_its_schema_relations() { .as_effect_program() .expect("effect program") .compiled_program(), - [0xa0] + encoded(&compiler_program()) ); } #[test] @@ -215,8 +215,41 @@ fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_current_coo set(&mut p, key, V::Integer(0)); } set(&mut p, "output_type", text(&"é".repeat(512))); + let mut core = compiled_core(); + 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(&"x".repeat(1025))); + 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 @@ -288,3 +321,278 @@ fn a_new_pin_can_bind_changed_laws_but_cannot_reuse_the_old_contract_identity() } #[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_compiler_core_binds_without_a_bridge_specific_instruction_language() { + // These artifacts were captured during a public Edict JSONL build attempt, + // rather than manufactured by the bridge fixture builder. This witnesses + // canonical artifact binding, not a successful build, execution or verifier approval. + 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 bundle_bytes = encoded(&compiled_bundle(&core, &target, &exports, name)); + set(&mut p, "compiled_program", V::Bytes(bundle_bytes.clone())); + 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}")), + ); + let prepared = prepare(&v).expect("real compiler Core bound to its operation and type"); + assert_eq!( + prepared + .program() + .as_effect_program() + .expect("effect") + .compiled_program(), + bundle_bytes + ); +} + +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), + ); + package(&p) +} +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)); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 808e21d5..24ff1343 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -721,9 +721,33 @@ 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. -This is byte binding and contract preparation. Compiler bytes and upstream -identities do not infer verifier approval or validate the compiled instruction -language. Static preparation does not grant invocation or mutation authority. +The `compiled_program` payload uses one current +`echo.compiler-produced-effect-program/v1` artifact bundle with exactly +`schema`, `intent`, `core_artifact`, `target_ir_artifact`, and +`lawpack_exports_artifact`. The three artifacts are exact canonical CBOR bytes; +opaque bytes, an unbundled Core module, extra fields, and noncanonical nested +encodings refuse. There is no alternate historical decoder. + +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. + +These checks establish artifact binding and structural order, not full Core or +Target IR instruction semantics. In particular they do not authenticate the +source closure, validate the lowering relation, establish verifier approval, +interpret instructions, or prove that an application graph schema corresponds +to compiler exports. Instruction typing, effect lowering, resource execution +and the physical arena remain required. Static preparation grants no invocation +or mutation authority. The retained compiler-artifact test uses captured public +JSONL compiler artifacts; it does not claim a successful effect application +build or an accepted effect verifier report. ### Current invocation input, basis and owner grant admission From a6c4590039138ab636fd813aa9691f97f7009509 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 10:16:03 -0700 Subject: [PATCH 25/43] Validate ordered effect instructions and lexical scope --- CHANGELOG.md | 9 + crates/warp-core/src/edict_effects.rs | 1 + .../src/edict_effects/compiled_program.rs | 23 +- .../src/edict_effects/instruction_relation.rs | 261 +++++++++++++ crates/warp-core/src/edict_pure/functions.rs | 3 +- .../tests/common/edict_effect_publication.rs | 9 +- .../tests/edict_effect_package_tests.rs | 348 +++++++++++++++++- docs/topics/GeneratedRules.md | 29 +- 8 files changed, 656 insertions(+), 27 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/instruction_relation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef357ee..be00778f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ ### Changed +- 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, intrinsic/failure mapping validation 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 diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 3433f426..d61a792a 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -10,6 +10,7 @@ //! logical meters. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. mod compiled_program; +mod instruction_relation; mod package; pub use package::{EffectPackageError, EffectProgram}; mod frame; diff --git a/crates/warp-core/src/edict_effects/compiled_program.rs b/crates/warp-core/src/edict_effects/compiled_program.rs index 2af28139..ff14d230 100644 --- a/crates/warp-core/src/edict_effects/compiled_program.rs +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Current compiler-artifact binding, before instruction semantics or execution. +//! 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, @@ -174,26 +174,6 @@ impl CompiledProgram { (Err(_), Err(_)) => {} _ => return Err(Error::InvalidArtifact), } - let mut ids = std::collections::BTreeSet::new(); - for collection in ["steps", "requirements", "pureBindings"] { - if collection == "pureBindings" && field(target, collection).is_err() { - continue; - } - for instruction in array(field(target, collection)?)? { - let id = text_field(instruction, "id")?; - if id.is_empty() || !ids.insert(id) { - return Err(Error::InvalidArtifact); - } - } - } - for id in array(field(target, "executionOrder")?)? { - if !ids.remove(text(id)?) { - return Err(Error::InvalidArtifact); - } - } - if !ids.is_empty() { - return Err(Error::InvalidArtifact); - } let mut fields = vec![ "input", "output", @@ -231,6 +211,7 @@ impl CompiledProgram { if field(target, "result")? != field(body, "result")? { return Err(Error::InvalidArtifact); } + super::instruction_relation::validate(&self.module, intent, target)?; Ok(()) } } 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 00000000..e0dc4bf6 --- /dev/null +++ b/crates/warp-core/src/edict_effects/instruction_relation.rs @@ -0,0 +1,261 @@ +// 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::{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) -> 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, + 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)?; + } + 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.expression(field(node, "value")?, None)?; + 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)?; + } + } + "effect" => effect(node, target, &mut scope)?, + _ => return Err(Error::UnsupportedProgram), + } + } + scope.expression(field(body, "result")?, None)?; + if scope.used.len() != scope.inventory.len() { + return Err(Error::InvalidArtifact); + } + 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<'_>) -> 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.expression(field(core, "input")?, None)?; + 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, +} +impl<'a> Scope<'a> { + fn new(module: &'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 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, + }) + } + 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 predicate(&mut self, value: &Value) -> Result<(), Error> { + 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_pure/functions.rs b/crates/warp-core/src/edict_pure/functions.rs index 29278497..732f2a13 100644 --- a/crates/warp-core/src/edict_pure/functions.rs +++ b/crates/warp-core/src/edict_pure/functions.rs @@ -209,7 +209,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)?; diff --git a/crates/warp-core/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index f169a9b4..9bf942d4 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -105,7 +105,14 @@ pub fn compiled_core() -> V { ( "body", map([ - ("locals", V::Array(vec![])), + ( + "locals", + V::Array(vec![map([ + ("id", text("arg.0")), + ("type", text("U32")), + ("alphaName", text("$arg0")), + ])]), + ), ("nodes", V::Array(vec![])), ( "result", diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 05907aa0..b3fe41f5 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -216,6 +216,25 @@ fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_current_coo } 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("Nominal")), + ("contract", text(&"é".repeat(512))), + ("representation", text("U32")), + ]), + )); set(&mut core, "coordinate", text(&"x".repeat(1016))); let V::Map(ref mut fields) = core else { panic!("core map") @@ -455,7 +474,22 @@ fn bound_compiler_package(core: &V, target: &V, exports: &V, intent: &str) -> V "target_ir_identity", artifact_identity("edict.target-ir.artifact/v1", target), ); - package(&p) + 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 { @@ -596,3 +630,315 @@ fn compiled_artifact_claims_and_nested_canonical_bytes_cannot_be_substituted() { set(&mut p, "compiled_program", V::Bytes(encoded(&bundle))); refused(&package(&p)); } + +fn captured_compiler_artifacts() -> (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") + }; + (decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/core.hex")),decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/06-target-ir.hex")),decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex"))) +} +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) = captured_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) = captured_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) = captured_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", + )); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 24ff1343..6b1f8d23 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -739,9 +739,32 @@ 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. -These checks establish artifact binding and structural order, not full Core or -Target IR instruction semantics. In particular they do not authenticate the -source closure, validate the lowering relation, establish verifier approval, +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. + +These checks establish artifact binding, ordered node correspondence, expression +syntax and lexical scope, not full Core or Target IR instruction semantics. +They do not yet infer every expression's type, authorize helper calls, validate +intrinsic signatures or authenticate the mapping from source failure names to +lowered failure tags. They also do not authenticate the +source closure, validate the complete lowering relation, establish verifier approval, interpret instructions, or prove that an application graph schema corresponds to compiler exports. Instruction typing, effect lowering, resource execution and the physical arena remain required. Static preparation grants no invocation From 4523a512022c6c3894da9acb3ca2e06151cb49da Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 10:34:26 -0700 Subject: [PATCH 26/43] Bind effect lowering to pinned lawpack adapters --- CHANGELOG.md | 10 +- crates/warp-core/src/edict_effects.rs | 1 + .../src/edict_effects/compiled_program.rs | 13 + .../src/edict_effects/lowering_relation.rs | 234 ++++++++++++++++++ .../tests/common/edict_effect_publication.rs | 67 +++++ .../tests/edict_effect_package_tests.rs | 173 +++++++++++-- docs/topics/GeneratedRules.md | 48 +++- 7 files changed, 514 insertions(+), 32 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/lowering_relation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index be00778f..6e622f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,21 @@ ### Changed +- 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, intrinsic/failure mapping validation and execution remain open. + full typing, complete intrinsic signatures and execution remain open. - Authored-effect payloads now bind a single canonical compiler-artifact bundle diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index d61a792a..f0dd4f77 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -11,6 +11,7 @@ //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. mod compiled_program; mod instruction_relation; +mod lowering_relation; mod package; pub use package::{EffectPackageError, EffectProgram}; mod frame; diff --git a/crates/warp-core/src/edict_effects/compiled_program.rs b/crates/warp-core/src/edict_effects/compiled_program.rs index ff14d230..498e49f9 100644 --- a/crates/warp-core/src/edict_effects/compiled_program.rs +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -18,6 +18,8 @@ pub(super) struct CompiledProgram { target_identity: [u8; 32], exports_identity: [u8; 32], intent: String, + exports: Value, + lowering: Value, } impl CompiledProgram { pub(super) fn parse(bytes: &[u8]) -> Result { @@ -30,6 +32,7 @@ impl CompiledProgram { "core_artifact", "target_ir_artifact", "lawpack_exports_artifact", + "lowering_artifacts", ], )?; require_text( @@ -132,6 +135,8 @@ impl CompiledProgram { target_identity, exports_identity, intent, + exports, + lowering: field(&bundle, "lowering_artifacts")?.clone(), }) } pub(super) fn validate_binding( @@ -212,6 +217,14 @@ impl CompiledProgram { return Err(Error::InvalidArtifact); } super::instruction_relation::validate(&self.module, intent, target)?; + super::lowering_relation::validate( + &self.module, + intent, + &self.target, + target, + &self.exports, + &self.lowering, + )?; Ok(()) } } 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 00000000..505adb16 --- /dev/null +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -0,0 +1,234 @@ +// 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( + module: &Value, + intent: &Value, + target_artifact: &Value, + target: &Value, + exports: &Value, + artifacts: &Value, +) -> Result<(), Error> { + let steps = array(field(target, "steps")?)?; + if steps.is_empty() { + return if artifacts == &Value::Null { + Ok(()) + } else { + Err(Error::InvalidArtifact) + }; + } + exact_fields( + artifacts, + &["source_artifact", "lawpack_artifact", "adapter_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")?; + 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 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")?, + )?; + let profile = unique_profile(&adapter, text_field(intent, "requiredOperationProfile")?)?; + let authorized = array(field(profile, "semanticEffects")?)?; + let nodes = array(field(field(intent, "body")?, "nodes")?)?; + 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)?; + 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); + } + require_text( + field(core, "binding")?, + "type", + text_field(effect, "outputType")?, + )?; + 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")?, + )?; + } + } + Ok(()) +} +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/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index 9bf942d4..d7dd6d57 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -219,7 +219,14 @@ pub fn compiled_exports() -> V { ]) } 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))), @@ -227,6 +234,66 @@ pub fn compiled_bundle(core: &V, target: &V, exports: &V, intent: &str) -> V { ("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 { + 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 = captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/01-lawpack-adapter.hex" + )); + let exports = captured(include_str!( + "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex" + )); + 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") + }; + 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))), + ]) +} pub fn compiler_program() -> V { let core = compiled_core(); compiled_bundle( diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index b3fe41f5..1afca8e6 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -390,10 +390,10 @@ fn compiler_core_identity_operation_and_output_cannot_be_rebound_by_a_new_packag } #[test] -fn retained_public_compiler_core_binds_without_a_bridge_specific_instruction_language() { - // These artifacts were captured during a public Edict JSONL build attempt, - // rather than manufactured by the bridge fixture builder. This witnesses - // canonical artifact binding, not a successful build, execution or verifier approval. +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(), @@ -437,8 +437,15 @@ fn retained_public_compiler_core_binds_without_a_bridge_specific_instruction_lan ) .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 bundle_bytes = encoded(&compiled_bundle(&core, &target, &exports, name)); - set(&mut p, "compiled_program", V::Bytes(bundle_bytes.clone())); + 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"))), + ])); + let bundle_bytes = encoded(&bundle); + set(&mut p, "compiled_program", V::Bytes(bundle_bytes)); set( &mut p, "target_ir_identity", @@ -451,15 +458,7 @@ fn retained_public_compiler_core_binds_without_a_bridge_specific_instruction_lan "operation_coordinate", text(&format!("{coordinate}.{name}")), ); - let prepared = prepare(&v).expect("real compiler Core bound to its operation and type"); - assert_eq!( - prepared - .program() - .as_effect_program() - .expect("effect") - .compiled_program(), - bundle_bytes - ); + refused(&v); } fn bound_compiler_package(core: &V, target: &V, exports: &V, intent: &str) -> V { @@ -631,14 +630,52 @@ fn compiled_artifact_claims_and_nested_canonical_bytes_cannot_be_substituted() { refused(&package(&p)); } -fn captured_compiler_artifacts() -> (V, V, V) { +fn repinned_compiler_artifacts() -> (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") }; - (decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/core.hex")),decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/06-target-ir.hex")),decode(include_str!("../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex"))) + 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 artifacts = repinned_lowering_artifacts(); + let V::Bytes(bytes) = get(&artifacts, "lawpack_artifact") else { + panic!("lawpack") + }; + let lawpack = echo_edict_canonical::decode_canonical_cbor_v1(bytes).expect("lawpack"); + 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); @@ -659,7 +696,7 @@ fn first_step_mut(target: &mut V) -> &mut V { 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) = captured_compiler_artifacts(); + let (core, target, exports) = repinned_compiler_artifacts(); assert!(prepare(&bound_compiler_package( &core, &target, @@ -748,7 +785,7 @@ fn core_body_mut(core: &mut V) -> &mut V { } #[test] fn matching_core_and_target_still_cannot_forge_local_identity_or_scope() { - let (original_core, original_target, exports) = captured_compiler_artifacts(); + 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(); @@ -901,7 +938,7 @@ fn pure_bindings_preserve_values_and_become_available_only_after_their_definitio #[test] fn lowered_failure_arms_preserve_the_source_binders_and_values_without_duplicates() { - let (core, target, exports) = captured_compiler_artifacts(); + let (core, target, exports) = repinned_compiler_artifacts(); for mode in 0..3 { let mut target = target.clone(); let step = first_step_mut(&mut target); @@ -942,3 +979,99 @@ fn lowered_failure_arms_preserve_the_source_binders_and_values_without_duplicate )); } } + +#[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" + ); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 6b1f8d23..7fc7feeb 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -723,10 +723,15 @@ physical evaluation arena. The `compiled_program` payload uses one current `echo.compiler-produced-effect-program/v1` artifact bundle with exactly -`schema`, `intent`, `core_artifact`, `target_ir_artifact`, and -`lawpack_exports_artifact`. The three artifacts are exact canonical CBOR bytes; -opaque bytes, an unbundled Core module, extra fields, and noncanonical nested -encodings refuse. There is no alternate historical decoder. +`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`, and `adapter_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 @@ -759,18 +764,39 @@ 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. + +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. + These checks establish artifact binding, ordered node correspondence, expression syntax and lexical scope, not full Core or Target IR instruction semantics. -They do not yet infer every expression's type, authorize helper calls, validate -intrinsic signatures or authenticate the mapping from source failure names to -lowered failure tags. They also do not authenticate the -source closure, validate the complete lowering relation, establish verifier approval, +They do not yet infer every expression's type, authorize helper calls, or +validate complete intrinsic signatures. 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, effect input typing, failure authority +classes, 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. Instruction typing, effect lowering, resource execution and the physical arena remain required. Static preparation grants no invocation -or mutation authority. The retained compiler-artifact test uses captured public -JSONL compiler artifacts; it does not claim a successful effect application -build or an accepted effect verifier report. +or mutation authority. ### Current invocation input, basis and owner grant admission From 09ff5d28e746f77356f52a6d743cb98ca443243c Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 10:45:40 -0700 Subject: [PATCH 27/43] Check effect data types and declared failure authority --- CHANGELOG.md | 7 + .../src/edict_effects/compiled_program.rs | 10 +- .../src/edict_effects/instruction_relation.rs | 92 +++++++- .../src/edict_effects/lowering_relation.rs | 35 ++- crates/warp-core/src/edict_pure/functions.rs | 30 ++- .../tests/edict_effect_package_tests.rs | 216 ++++++++++++++++++ docs/topics/GeneratedRules.md | 44 +++- 7 files changed, 395 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e622f8b..b454a4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Changed +- 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. Full export/Core type authentication, nominal and + intrinsic semantics, and effect 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 diff --git a/crates/warp-core/src/edict_effects/compiled_program.rs b/crates/warp-core/src/edict_effects/compiled_program.rs index 498e49f9..352c3ecb 100644 --- a/crates/warp-core/src/edict_effects/compiled_program.rs +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -216,8 +216,7 @@ impl CompiledProgram { if field(target, "result")? != field(body, "result")? { return Err(Error::InvalidArtifact); } - super::instruction_relation::validate(&self.module, intent, target)?; - super::lowering_relation::validate( + let signatures = super::lowering_relation::validate( &self.module, intent, &self.target, @@ -225,6 +224,13 @@ impl CompiledProgram { &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/instruction_relation.rs b/crates/warp-core/src/edict_effects/instruction_relation.rs index e0dc4bf6..d0c6493f 100644 --- a/crates/warp-core/src/edict_effects/instruction_relation.rs +++ b/crates/warp-core/src/edict_effects/instruction_relation.rs @@ -3,7 +3,7 @@ //! Ordered Core/Target correspondence and exact lexical local identities. use crate::edict_pure::{ functions, - model::{MAX_DEPTH, MAX_PROGRAM_NODES}, + 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, @@ -11,7 +11,13 @@ use crate::edict_pure::{ 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) -> Result<(), Error> { +pub(super) fn validate( + module: &Value, + intent: &Value, + target: &Value, + exports: &Value, + signatures: &BTreeMap<&str, &Value>, +) -> Result<(), Error> { let mut fields = vec![ "operationProfile", "inputConstraints", @@ -33,12 +39,14 @@ pub(super) fn validate(module: &Value, intent: &Value, target: &Value) -> Result 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")?)?; @@ -74,7 +82,10 @@ pub(super) fn validate(module: &Value, intent: &Value, target: &Value) -> Result exact_fields(target, &["id", "binding", "value"])?; equal(node, target, "binding")?; equal(node, target, "value")?; - scope.expression(field(node, "value")?, None)?; + scope.typed_expression( + field(node, "value")?, + text_field(field(node, "binding")?, "type")?, + )?; scope.introduce(field(node, "binding")?)?; } "require" => { @@ -93,16 +104,30 @@ pub(super) fn validate(module: &Value, intent: &Value, target: &Value) -> Result } for (_, value) in map(field(reason, "payload")?)? { scope.expression(value, None)?; + scope.data_type(value)?; } } - "effect" => effect(node, target, &mut scope)?, + "effect" => effect( + node, + target, + &mut scope, + signatures + .get(text_field(target, "id")?) + .copied() + .ok_or(Error::InvalidArtifact)?, + )?, _ => return Err(Error::UnsupportedProgram), } } - scope.expression(field(body, "result")?, None)?; + 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> { @@ -111,7 +136,12 @@ fn equal(core: &Value, target: &Value, key: &str) -> Result<(), Error> { } Ok(()) } -fn effect(core: &Value, target: &Value, scope: &mut Scope<'_>) -> Result<(), Error> { +fn effect( + core: &Value, + target: &Value, + scope: &mut Scope<'_>, + signature: &Value, +) -> Result<(), Error> { exact_fields( core, &["kind", "binding", "effect", "input", "obstructionMap"], @@ -134,7 +164,7 @@ fn effect(core: &Value, target: &Value, scope: &mut Scope<'_>) -> Result<(), Err if text_field(target, "targetIntrinsic")?.is_empty() || text_field(core, "effect")?.is_empty() { return Err(Error::InvalidArtifact); } - scope.expression(field(core, "input")?, None)?; + 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"])?; @@ -170,14 +200,25 @@ struct Scope<'a> { 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, locals: &'a [Value], input_type: &str) -> Result { + 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)?; @@ -196,6 +237,10 @@ impl<'a> Scope<'a> { 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> { @@ -216,8 +261,37 @@ impl<'a> Scope<'a> { 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> { - self.parser.predicate(value, 0)?; + 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( diff --git a/crates/warp-core/src/edict_effects/lowering_relation.rs b/crates/warp-core/src/edict_effects/lowering_relation.rs index 505adb16..3a3bd1d6 100644 --- a/crates/warp-core/src/edict_effects/lowering_relation.rs +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -11,18 +11,18 @@ use echo_edict_canonical::{ }; use std::collections::{BTreeMap, BTreeSet}; -pub(super) fn validate( +pub(super) fn validate<'a>( module: &Value, intent: &Value, target_artifact: &Value, - target: &Value, - exports: &Value, + target: &'a Value, + exports: &'a Value, artifacts: &Value, -) -> Result<(), Error> { +) -> Result, Error> { let steps = array(field(target, "steps")?)?; if steps.is_empty() { return if artifacts == &Value::Null { - Ok(()) + Ok(BTreeMap::new()) } else { Err(Error::InvalidArtifact) }; @@ -89,6 +89,7 @@ pub(super) fn validate( 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))) @@ -123,6 +124,7 @@ pub(super) fn validate( "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")?; @@ -149,9 +151,30 @@ pub(super) fn validate( "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(()) + Ok(signatures) } fn pinned(reference: &Value, expected: [u8; 32]) -> Result<(), Error> { exact_fields(reference, &["id", "digest"])?; diff --git a/crates/warp-core/src/edict_pure/functions.rs b/crates/warp-core/src/edict_pure/functions.rs index 732f2a13..15e9fe69 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. @@ -221,7 +231,7 @@ pub(crate) 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 @@ -253,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>, @@ -478,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>, diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 1afca8e6..ca16c694 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1075,3 +1075,219 @@ fn fresh_bundle_pins_cannot_replace_or_omit_the_pinned_lowering_artifacts() { ); } } + +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", + )); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 7fc7feeb..f5eab49e 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -783,20 +783,40 @@ 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. -These checks establish artifact binding, ordered node correspondence, expression -syntax and lexical scope, not full Core or Target IR instruction semantics. -They do not yet infer every expression's type, authorize helper calls, or -validate complete intrinsic signatures. 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, effect input typing, failure authority -classes, cost/footprint/configuration obligations, and the complete lowering +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. + +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. +They do not authenticate Core type definitions against exported definitions or +preserve nominal identity through every inferred expression. Complete intrinsic +signatures and intrinsic-defined failure authority 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, 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. Instruction typing, effect lowering, resource execution -and the physical arena remain required. Static preparation grants no invocation -or mutation authority. +to compiler exports. Resource execution and the physical arena remain required. +Static preparation grants no invocation or mutation authority. ### Current invocation input, basis and owner grant admission From 4c767d25a66e4d75218e5451920687b23fe602c7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 10:56:24 -0700 Subject: [PATCH 28/43] Authenticate imported Core type declarations --- CHANGELOG.md | 9 +- crates/warp-core/src/edict_effects.rs | 1 + .../src/edict_effects/imported_types.rs | 170 ++++++++++++++++++ .../src/edict_effects/lowering_relation.rs | 1 + crates/warp-core/src/edict_pure/syntax.rs | 5 +- .../tests/edict_effect_package_tests.rs | 43 +++++ .../tests/edict_source_functions_tests.rs | 4 +- docs/topics/GeneratedRules.md | 24 ++- 8 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/imported_types.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b454a4a3..ba822bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,18 @@ ### Changed +- 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. Full export/Core type authentication, nominal and + failure authority class. Complete type/source authentication, nominal and intrinsic semantics, and effect execution remain unfinished. - Effect compiler preparation now binds the declared lawpack's exports and diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index f0dd4f77..5d79e96d 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -10,6 +10,7 @@ //! logical meters. Compiled effect instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. mod compiled_program; +mod imported_types; mod instruction_relation; mod lowering_relation; mod package; 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 00000000..3e627a6c --- /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/lowering_relation.rs b/crates/warp-core/src/edict_effects/lowering_relation.rs index 3a3bd1d6..e1241c51 100644 --- a/crates/warp-core/src/edict_effects/lowering_relation.rs +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -86,6 +86,7 @@ pub(super) fn validate<'a>( 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")?)?; diff --git a/crates/warp-core/src/edict_pure/syntax.rs b/crates/warp-core/src/edict_pure/syntax.rs index cc24469c..d794a9d2 100644 --- a/crates/warp-core/src/edict_pure/syntax.rs +++ b/crates/warp-core/src/edict_pure/syntax.rs @@ -50,7 +50,10 @@ 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), + kind @ ("U8" | "U16" | "U32" | "U64") => { + exact_fields(definition, &["kind"])?; + self.ty(kind, depth + 1) + } "Nominal" => self.ty(text_field(definition, "representation")?, depth + 1), "Bytes" => { let min = field(definition, "min").map_or(Ok(0), number)?; diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index ca16c694..01451c6e 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1291,3 +1291,46 @@ fn typed_contract_accepts_checked_source_helpers_and_refuses_wrong_arguments() { "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" + ); + } +} diff --git a/crates/warp-core/tests/edict_source_functions_tests.rs b/crates/warp-core/tests/edict_source_functions_tests.rs index d88c24d0..9703a42a 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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index f5eab49e..f2458f32 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -778,6 +778,25 @@ 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 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 @@ -805,8 +824,9 @@ 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. -They do not authenticate Core type definitions against exported definitions or -preserve nominal identity through every inferred expression. Complete intrinsic +Imported declarations in the selected lawpack namespace now bind to exports, +but this does not authenticate every source/local definition or preserve nominal +identity through every inferred expression. Complete intrinsic signatures and intrinsic-defined failure authority 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 From aaf888449e757b0d169d2b057c6a5d632a720677 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 11:09:49 -0700 Subject: [PATCH 29/43] Preserve nominal identities in shared inference --- CHANGELOG.md | 8 + crates/warp-core/src/edict_pure/evaluate.rs | 2 +- crates/warp-core/src/edict_pure/functions.rs | 15 ++ crates/warp-core/src/edict_pure/model.rs | 12 ++ crates/warp-core/src/edict_pure/syntax.rs | 12 +- .../tests/common/edict_effect_publication.rs | 12 +- .../tests/edict_effect_package_tests.rs | 178 +++++++++++++++++- docs/topics/GeneratedRules.md | 23 ++- 8 files changed, 239 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba822bf6..0298f5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ ### Changed +- 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 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 diff --git a/crates/warp-core/src/edict_pure/evaluate.rs b/crates/warp-core/src/edict_pure/evaluate.rs index 09ad459c..093e6160 100644 --- a/crates/warp-core/src/edict_pure/evaluate.rs +++ b/crates/warp-core/src/edict_pure/evaluate.rs @@ -466,7 +466,7 @@ pub(crate) fn validate( depth: usize, ) -> 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) => diff --git a/crates/warp-core/src/edict_pure/functions.rs b/crates/warp-core/src/edict_pure/functions.rs index 15e9fe69..60f0cc38 100644 --- a/crates/warp-core/src/edict_pure/functions.rs +++ b/crates/warp-core/src/edict_pure/functions.rs @@ -540,6 +540,20 @@ pub(crate) 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(()), @@ -711,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 a826b5f1..74ff3350 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 d794a9d2..fa4fe9fc 100644 --- a/crates/warp-core/src/edict_pure/syntax.rs +++ b/crates/warp-core/src/edict_pure/syntax.rs @@ -54,7 +54,17 @@ impl Parser<'_> { exact_fields(definition, &["kind"])?; self.ty(kind, depth + 1) } - "Nominal" => self.ty(text_field(definition, "representation")?, 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/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index d7dd6d57..01ddf592 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -71,17 +71,7 @@ pub fn compiled_core() -> V { ("apiVersion", text("edict.core/v1")), ("coordinate", text("app")), ("imports", V::Array(vec![])), - ( - "types", - map([( - "Result", - map([ - ("kind", text("Nominal")), - ("contract", text("app.Result")), - ("representation", text("U32")), - ]), - )]), - ), + ("types", map([("Result", map([("kind", text("U32"))]))])), ( "intents", map([( diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 01451c6e..8de7d01e 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -227,14 +227,7 @@ fn declaration_boundaries_accept_zero_io_and_allocation_and_preserve_current_coo else { panic!("types") }; - types.push(( - text(&"é".repeat(512)), - map([ - ("kind", text("Nominal")), - ("contract", text(&"é".repeat(512))), - ("representation", text("U32")), - ]), - )); + 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") @@ -1334,3 +1327,172 @@ fn pinned_exports_refuse_core_redefinitions_of_imported_type_contracts() { ); } } + +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", + )); +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index f2458f32..bef6e398 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -814,6 +814,25 @@ 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 @@ -825,8 +844,8 @@ 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 preserve nominal -identity through every inferred expression. Complete intrinsic +but this does not authenticate every source/local definition or establish a +complete typing proof across every pure main/projection path. Complete intrinsic signatures and intrinsic-defined failure authority 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 From de4fc4f85028769730806f5c51e584b813c94f95 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 11:37:45 -0700 Subject: [PATCH 30/43] Bind intrinsic catalogs to runtime read contracts --- CHANGELOG.md | 7 + crates/warp-core/src/edict_effects.rs | 2 + .../src/edict_effects/intrinsic_contract.rs | 88 ++++++++ .../src/edict_effects/lowering_relation.rs | 53 ++++- .../src/edict_effects/runtime_abi.rs | 64 ++++++ .../tests/common/edict_effect_publication.rs | 98 ++++++++- .../tests/edict_effect_package_tests.rs | 199 +++++++++++++++++- docs/topics/GeneratedRules.md | 48 ++++- 8 files changed, 550 insertions(+), 9 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/intrinsic_contract.rs create mode 100644 crates/warp-core/src/edict_effects/runtime_abi.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0298f5a2..5846b052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Changed +- 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. Other effect handlers, general signature unification, complete + profile obligations and compiled 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. diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 5d79e96d..6697d723 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -12,8 +12,10 @@ mod compiled_program; mod imported_types; mod instruction_relation; +mod intrinsic_contract; mod lowering_relation; mod package; +mod runtime_abi; pub use package::{EffectPackageError, EffectProgram}; mod frame; mod read_image; 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 00000000..38029a56 --- /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/lowering_relation.rs b/crates/warp-core/src/edict_effects/lowering_relation.rs index e1241c51..e251ebdd 100644 --- a/crates/warp-core/src/edict_effects/lowering_relation.rs +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -29,13 +29,50 @@ pub(super) fn validate<'a>( } exact_fields( artifacts, - &["source_artifact", "lawpack_artifact", "adapter_artifact"], + &[ + "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")?; @@ -68,6 +105,9 @@ pub(super) fn validate<'a>( 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); } @@ -108,6 +148,15 @@ pub(super) fn validate<'a>( 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", @@ -120,6 +169,8 @@ pub(super) fn validate<'a>( 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)?; require_text( field(core, "binding")?, "type", 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 00000000..8aa72de2 --- /dev/null +++ b/crates/warp-core/src/edict_effects/runtime_abi.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Runtime-owned ABI for implemented recording primitives; declarations cannot redefine it. +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> { + // Mutation intrinsics require their own actual handlers and contracts. A + // catalog declaration alone never registers executable code. + if name != "echo.dpo@1.node-atom-read" { + return Err(Error::UnsupportedProgram); + } + require_text(intrinsic, "effectKind", "read")?; + require_text(intrinsic, "writeClass", "read")?; + 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 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); + } + let output = parser.ty(text_field(effect, "outputType")?, 0)?; + 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/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index 01ddf592..2b94e86a 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -227,6 +227,12 @@ pub fn compiled_bundle(core: &V, target: &V, exports: &V, intent: &str) -> V { // 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) +} +pub fn lowering_fixture_inputs(catalog: Option, exports_override: Option) -> V { let captured = |value: &str| { echo_edict_canonical::decode_canonical_cbor_v1( &hex::decode(value.trim()).expect("captured bytes"), @@ -239,9 +245,82 @@ pub fn repinned_lowering_artifacts() -> V { let adapter = captured(include_str!( "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/01-lawpack-adapter.hex" )); - let exports = captured(include_str!( + 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( @@ -254,6 +333,18 @@ pub fn repinned_lowering_artifacts() -> V { 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, @@ -282,6 +373,11 @@ pub fn repinned_lowering_artifacts() -> V { ), ("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 { diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index 8de7d01e..ec93d1cc 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -436,6 +436,8 @@ fn retained_public_capture_refuses_its_inconsistent_lawpack_artifact_pins() { ("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)); @@ -624,6 +626,9 @@ fn compiled_artifact_claims_and_nested_canonical_bytes_cannot_be_substituted() { } 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"), @@ -639,11 +644,18 @@ fn repinned_compiler_artifacts() -> (V, V, V) { let exports = decode(include_str!( "../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/02-lawpack-exports.hex" )); - let artifacts = repinned_lowering_artifacts(); - let V::Bytes(bytes) = get(&artifacts, "lawpack_artifact") else { + 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")), ( @@ -1496,3 +1508,186 @@ fn checked_helpers_preserve_nominal_arguments_and_return_identity() { "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())); + 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())); + 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)); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index bef6e398..e4701018 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -727,7 +727,8 @@ The `compiled_program` payload uses one current `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`, and `adapter_artifact` byte strings; +`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 @@ -797,6 +798,42 @@ 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. Other intrinsic names refuse until their actual handlers and ABI +contracts exist. This does not enable compiled instruction execution. + +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 @@ -845,14 +882,15 @@ 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. Complete intrinsic -signatures and intrinsic-defined failure authority remain open. Adapter +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, cost/footprint/configuration obligations and the complete lowering -relation remain unchecked. These checks do not establish verifier approval, +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. From 0967d1f301cb3082b701f4e20c873da0fafadf16 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 11:49:36 -0700 Subject: [PATCH 31/43] Execute authored read programs in recorded frames --- CHANGELOG.md | 18 +- crates/warp-core/src/edict_effects.rs | 4 +- .../src/edict_effects/compiled_program.rs | 12 + .../warp-core/src/edict_effects/execution.rs | 287 +++++++++++ crates/warp-core/src/edict_effects/frame.rs | 50 ++ crates/warp-core/src/edict_effects/package.rs | 11 +- .../tests/edict_effect_package_tests.rs | 3 + .../edict_effect_package_tests/execution.rs | 478 ++++++++++++++++++ docs/topics/GeneratedRules.md | 54 +- 9 files changed, 900 insertions(+), 17 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/execution.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests/execution.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5846b052..403600a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,26 @@ ### Changed +- 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. Mutation instructions, 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. Other effect handlers, general signature unification, complete - profile obligations and compiled execution remain unfinished. + 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 effect execution remain + static typing, explicit construction operations and installed effect execution remain unfinished. - Effect compiler preparation now authenticates imported Core type declarations @@ -34,7 +41,7 @@ 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 effect execution remain unfinished. + 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 @@ -58,7 +65,7 @@ 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 execution remain unfinished. + semantics and authored-effect Action execution remain unfinished. - Invocations now use one `EchoOperationInvocation`, codec, owner policy and @@ -90,7 +97,8 @@ 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 and authored-effect execution remain unfinished. + effects. Invocation consolidation is complete; authored-effect Action execution + remains unfinished. - Recovery certificates now use one current operation-index recipe for empty, diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 6697d723..f1783f11 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -7,9 +7,11 @@ //! 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. Compiled effect instructions, a physical evaluation arena, +//! logical meters. Private ordered read/let/require evaluation uses those frames. +//! Mutation instructions, a physical evaluation arena, //! derived mutations and Action/receipt/WAL decision recovery remain unfinished. mod compiled_program; +mod execution; mod imported_types; mod instruction_relation; mod intrinsic_contract; diff --git a/crates/warp-core/src/edict_effects/compiled_program.rs b/crates/warp-core/src/edict_effects/compiled_program.rs index 352c3ecb..97044bff 100644 --- a/crates/warp-core/src/edict_effects/compiled_program.rs +++ b/crates/warp-core/src/edict_effects/compiled_program.rs @@ -139,6 +139,18 @@ impl CompiledProgram { 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, 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 00000000..99cf0d0d --- /dev/null +++ b/crates/warp-core/src/edict_effects/execution.rs @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Private ordered authored execution over the existing expression evaluator and recorded reads. +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), + Read { + binding: Binding, + failures: BTreeMap, + max_bytes: u64, + }, + Require { + condition: Predicate, + reason: Reason, + }, +} +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)?; + 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, EffectFrameError> { + 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, EffectFrameError> { + 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 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 { + meter.step(0)?; + match instruction { + 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)?; + self.output_bytes(&value, meter) + } + 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 index 80d0f0ed..19b97240 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -40,6 +40,15 @@ pub enum EffectFrameError { Read(EffectRangeReadError), /// A prior failure ended this frame; no further work or reads are allowed. Terminated, + /// 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 From for EffectFrameError { @@ -242,6 +251,47 @@ impl<'a> EffectEvaluationFrame<'a> { result } + /// Privately evaluates the prepared current authored program against recorded reads. + /// This grants no invocation or mutation authority. + /// # Errors + /// Any preparation, read, authored or resource refusal terminates the frame. + pub fn evaluate_effect( + &mut self, + program: &super::EffectProgram, + input: &[u8], + aperture: &[super::TypedNodeRead], + ) -> Result, EffectFrameError> { + if self.terminated { + return Err(EffectFrameError::Terminated); + } + self.terminated = true; + if self.evaluation_basis.is_none() { + return Err(EffectFrameError::UnboundParent); + } + 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 = 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); + self.terminated = result.is_err(); + result + } + /// Evaluates exact compiler-package bytes using the same cumulative meter. /// /// Package/input decoding is bounded host preparation. This is private diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index f62185f8..cd221333 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -124,6 +124,14 @@ impl EffectProgram { &self.output_type, ) } + 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() } @@ -134,7 +142,8 @@ impl EffectProgram { self.footprint } /// Canonical Core/Target IR/exports bundle bound to the selected intent. - /// Instruction semantics and execution still require validation. + /// Private read/let/require evaluation is available on a runtime-bound frame. + /// Mutation instructions and installed Action execution remain unfinished. #[must_use] pub fn compiled_program(&self) -> &[u8] { &self.compiled diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index ec93d1cc..c65af103 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1691,3 +1691,6 @@ fn outer_package_pins_cannot_replace_profile_or_catalog_bytes() { refused(&publication_for_lowering(&lowering)); } } +#[cfg(feature = "native_rule_bootstrap")] +#[path = "edict_effect_package_tests/execution.rs"] +mod execution; 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 00000000..ba4ee794 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -0,0 +1,478 @@ +// 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, +}; +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(), + })), + ); + let state = WorldlineState::from_root_store(store.clone(), key.local_id).expect("state"); + 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, + ) +} +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())), + ])) +} +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 +} +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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index e4701018..8042b8d0 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -825,7 +825,7 @@ runtime's logical storage ceiling. The four named atom-read fault tags and their or integrity classes are fixed, and each carries the input-address type. Mutually agreeing Core, lawpack and catalog declarations cannot change these requirements. Other intrinsic names refuse until their actual handlers and ABI -contracts exist. This does not enable compiled instruction execution. +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 @@ -895,6 +895,40 @@ 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. Unbound +frames, mismatched laws and invalid apertures refuse before 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. +Every refusal terminates the frame; later calls do no work. Successful calls +retain cumulative usage and observations. The new runtime witnesses use the +explicitly synthetic, coherently pinned read fixture, not a genuine public +compiler/verifier output. Physical arena guarantees, mutation intrinsics and +patch derivation, atomic scheduler settlement, receipts/WAL and original-decision +recovery remain required for the complete bridge. + ### Current invocation input, basis and owner grant admission `EchoOperationInvocation` is the sole current invocation representation and @@ -927,7 +961,7 @@ 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. Compiled instruction execution, the physical arena, +use their real anchors. Mutation instruction execution, the physical arena, derived effects, Tick integration and effect decision recovery remain required. ### Runtime-owner package policy admission @@ -960,8 +994,9 @@ 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 and compiled effect execution remain unfinished. Package -admission does not infer compiler/verifier approval from publication identities. +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 @@ -1028,9 +1063,8 @@ or recipe switch based on the presence of effect installations or Action outcome 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. Invocation -definitions still require consolidation under the one-current-version policy. -This is installation recovery, not recovery of -an authored-effect decision. Invocation grant admission, compiled execution, -physical allocation, derived effects and Action/Tick/receipt recovery remain -unfinished. +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. Mutation +instruction execution, physical allocation, derived effects and authored-effect +Action/Tick/receipt recovery remain unfinished. From 3d8a8b8f9116974842052f15693d6fd0380770c3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 12:02:42 -0700 Subject: [PATCH 32/43] Buffer compiled graph mutation intents --- CHANGELOG.md | 11 +- crates/warp-core/src/edict_effects.rs | 7 +- .../warp-core/src/edict_effects/execution.rs | 37 +- crates/warp-core/src/edict_effects/frame.rs | 20 +- .../src/edict_effects/lowering_relation.rs | 7 + .../warp-core/src/edict_effects/mutations.rs | 216 +++++++++++ crates/warp-core/src/edict_effects/package.rs | 4 +- .../src/edict_effects/runtime_abi.rs | 22 +- .../tests/common/edict_effect_publication.rs | 14 +- .../tests/edict_effect_package_tests.rs | 8 +- .../edict_effect_package_tests/execution.rs | 4 +- .../edict_effect_package_tests/mutations.rs | 367 ++++++++++++++++++ docs/topics/GeneratedRules.md | 46 ++- 13 files changed, 735 insertions(+), 28 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/mutations.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests/mutations.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 403600a4..98648911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,25 @@ ### Changed +- 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 components discard + partial emission. Emission does not modify the frontier or derive a patch; + mutation validation, actual write costs, physical allocation 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. Mutation instructions, installed Action execution, the physical arena + frame. Mutation validation, 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. Other effect handlers, general signature unification, complete + 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. diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index f1783f11..512a283f 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -8,14 +8,17 @@ //! 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. -//! Mutation instructions, a physical evaluation arena, -//! derived mutations and Action/receipt/WAL decision recovery remain unfinished. +//! Five mutation instructions buffer intents without modifying the frontier. +//! A physical arena, effect validation/patch derivation 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}; diff --git a/crates/warp-core/src/edict_effects/execution.rs b/crates/warp-core/src/edict_effects/execution.rs index 99cf0d0d..c61b7395 100644 --- a/crates/warp-core/src/edict_effects/execution.rs +++ b/crates/warp-core/src/edict_effects/execution.rs @@ -1,6 +1,7 @@ // 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}, @@ -26,6 +27,10 @@ struct Reason { } enum Instruction { Let(Binding), + Mutation { + kind: MutationKind, + binding: Binding, + }, Read { binding: Binding, failures: BTreeMap, @@ -36,6 +41,10 @@ enum Instruction { reason: Reason, }, } +pub(super) struct Evaluation { + pub(super) output: Vec, + pub(super) mutations: Vec, +} pub(super) struct Program { input: RuntimeType, output: RuntimeType, @@ -93,6 +102,12 @@ impl Program { "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); }; @@ -147,7 +162,7 @@ impl Program { access: &mut RecordedNodeAccess<'_, '_>, schema: &EffectGraphSchema, meter: &mut Meter, - ) -> Result, EffectFrameError> { + ) -> Result { meter.with_component(self.limits, |meter| { self.run_body(input, access, schema, meter) }) @@ -158,7 +173,7 @@ impl Program { access: &mut RecordedNodeAccess<'_, '_>, schema: &EffectGraphSchema, meter: &mut Meter, - ) -> Result, EffectFrameError> { + ) -> Result { validate(&input, &self.input, meter, 0).map_err(|e| { if e == Error::InvalidArtifact { Error::InvalidInput @@ -168,6 +183,7 @@ impl Program { })?; 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)?; @@ -183,6 +199,18 @@ impl Program { for instruction in &self.instructions { meter.step(0)?; match instruction { + Instruction::Mutation { kind, binding } => { + let value = expression(&binding.value, &locals, helpers, meter, 0)?; + let mutation = kind.emit(value, meter)?; + 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)?; @@ -241,7 +269,10 @@ impl Program { } let value = expression(&self.result, &locals, helpers, meter, 0)?; validate(&value, &self.output, meter, 0)?; - self.output_bytes(&value, meter) + Ok(Evaluation { + output: self.output_bytes(&value, meter)?, + mutations, + }) } fn output_bytes(&self, value: &Value, meter: &mut Meter) -> Result, EffectFrameError> { meter.charge(value, 0)?; diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 19b97240..5e1f7e98 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -79,6 +79,7 @@ pub struct EffectEvaluationFrame<'a> { meter: Meter, limits: EvaluationLimits, terminated: bool, + buffered_mutations: Vec, evaluation_basis: Option, } @@ -113,6 +114,7 @@ impl<'a> EffectEvaluationFrame<'a> { meter: Meter::new_atomic(limits), limits, terminated: false, + buffered_mutations: Vec::new(), evaluation_basis: None, }) } @@ -251,7 +253,8 @@ impl<'a> EffectEvaluationFrame<'a> { result } - /// Privately evaluates the prepared current authored program against recorded reads. + /// 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 /// Any preparation, read, authored or resource refusal terminates the frame. @@ -288,10 +291,25 @@ impl<'a> EffectEvaluationFrame<'a> { aperture, }; let result = executable.run(input, &mut access, program.graph_schema(), &mut self.meter); + let result = result.and_then(|result| { + self.buffered_mutations + .try_reserve(result.mutations.len()) + .map_err(|_| EffectFrameError::Evaluation(EvaluationError::HostAllocationFailed))?; + self.buffered_mutations.extend(result.mutations); + Ok(result.output) + }); self.terminated = result.is_err(); result } + /// Completed compiler-emitted mutation intents, in authored execution order. + /// These have not passed proposal validation and are not writes or a patch. + /// Failed components never append their partial intent buffer. + #[must_use] + pub fn buffered_mutations(&self) -> &[super::EffectMutation] { + &self.buffered_mutations + } + /// Evaluates exact compiler-package bytes using the same cumulative meter. /// /// Package/input decoding is bounded host preparation. This is private diff --git a/crates/warp-core/src/edict_effects/lowering_relation.rs b/crates/warp-core/src/edict_effects/lowering_relation.rs index e251ebdd..7ac03610 100644 --- a/crates/warp-core/src/edict_effects/lowering_relation.rs +++ b/crates/warp-core/src/edict_effects/lowering_relation.rs @@ -171,6 +171,13 @@ pub(super) fn validate<'a>( } 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", 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 00000000..74ce4cbf --- /dev/null +++ b/crates/warp-core/src/edict_effects/mutations.rs @@ -0,0 +1,216 @@ +// 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::{ + evaluate::Meter, + 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 MutationKind { + pub(super) fn emit(self, value: Value, meter: &mut Meter) -> 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); + } + } + // One deterministic EffectRow reservation; payload ownership is moved + // from its already charged expression value, never copied here. + meter.allocate(280)?; + 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/package.rs b/crates/warp-core/src/edict_effects/package.rs index cd221333..b2e6fcdf 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -142,8 +142,8 @@ impl EffectProgram { self.footprint } /// Canonical Core/Target IR/exports bundle bound to the selected intent. - /// Private read/let/require evaluation is available on a runtime-bound frame. - /// Mutation instructions and installed Action execution remain unfinished. + /// 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 diff --git a/crates/warp-core/src/edict_effects/runtime_abi.rs b/crates/warp-core/src/edict_effects/runtime_abi.rs index 8aa72de2..1441d90d 100644 --- a/crates/warp-core/src/edict_effects/runtime_abi.rs +++ b/crates/warp-core/src/edict_effects/runtime_abi.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // © James Ross Ω FLYING•ROBOTS -//! Runtime-owned ABI for implemented recording primitives; declarations cannot redefine it. +//! Runtime-owned recording/emitter ABIs; declarations cannot redefine them. use crate::edict_pure::{ model::{RuntimeType, MAX_PROGRAM_NODES}, syntax::Parser, @@ -15,13 +15,11 @@ pub(super) fn validate( effect: &Value, intrinsic: &Value, ) -> Result<(), Error> { - // Mutation intrinsics require their own actual handlers and contracts. A - // catalog declaration alone never registers executable code. - if name != "echo.dpo@1.node-atom-read" { + let mutation = super::mutations::MutationKind::from_intrinsic(name); + if name != "echo.dpo@1.node-atom-read" && mutation.is_none() { return Err(Error::UnsupportedProgram); } - require_text(intrinsic, "effectKind", "read")?; - require_text(intrinsic, "writeClass", "read")?; + let mut parser = Parser { types: field(module, "types")?, coordinate: text_field(module, "coordinate")?, @@ -29,6 +27,17 @@ pub(super) fn validate( }; 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); }; @@ -41,7 +50,6 @@ pub(super) fn validate( { return Err(Error::InvalidArtifact); } - let output = parser.ty(text_field(effect, "outputType")?, 0)?; if !matches!(output, RuntimeType::Bytes { min: 0, max } if max > 0 && max <= super::EFFECT_READ_STORAGE_CEILING_V1) { return Err(Error::UnsupportedProgram); diff --git a/crates/warp-core/tests/common/edict_effect_publication.rs b/crates/warp-core/tests/common/edict_effect_publication.rs index 2b94e86a..7fd19299 100644 --- a/crates/warp-core/tests/common/edict_effect_publication.rs +++ b/crates/warp-core/tests/common/edict_effect_publication.rs @@ -230,9 +230,13 @@ pub fn repinned_lowering_artifacts() -> V { lowering_fixture(None) } pub fn lowering_fixture(catalog: Option) -> V { - lowering_fixture_inputs(catalog, None) + lowering_fixture_inputs(catalog, None, None) } -pub fn lowering_fixture_inputs(catalog: Option, exports_override: Option) -> V { +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"), @@ -242,9 +246,11 @@ pub fn lowering_fixture_inputs(catalog: Option, exports_override: Option) let mut lawpack = captured(include_str!( "../../../echo-edict-provider-lowerer/tests/fixtures/node-atom-read/03-lawpack.hex" )); - let adapter = captured(include_str!( + 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" diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index c65af103..a9092993 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1622,7 +1622,7 @@ fn runtime_read_abi_rejects_a_self_consistent_sixteen_byte_node_address() { "definition", text("Record,typeId:Bytes,warpId:Bytes>"), ); - let lowering = lowering_fixture_inputs(None, Some(exports.clone())); + let lowering = lowering_fixture_inputs(None, Some(exports.clone()), None); let (mut core, mut target, _) = compiler_fixture(&lowering); set( value_field_mut( @@ -1659,7 +1659,7 @@ fn runtime_read_abi_rejects_catalog_and_lawpack_agreement_on_wrong_fault_authori .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())); + let lowering = lowering_fixture_inputs(None, Some(exports.clone()), None); let (core, target, _) = compiler_fixture(&lowering); refused(&publication_from_parts(&core, &target, &exports, &lowering)); } @@ -1694,3 +1694,7 @@ fn outer_package_pins_cannot_replace_profile_or_catalog_bytes() { #[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; diff --git a/crates/warp-core/tests/edict_effect_package_tests/execution.rs b/crates/warp-core/tests/edict_effect_package_tests/execution.rs index ba4ee794..e08ef996 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/execution.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -9,7 +9,7 @@ use warp_core::{ InboxPolicy, NodeId, NodeKey, NodeRecord, PlaybackMode, TrustedRuntimeHost, WarpId, WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, }; -fn host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { +pub(super) fn host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { let key = NodeKey { warp_id: WarpId([3; 32]), local_id: NodeId([4; 32]), @@ -88,7 +88,7 @@ fn publication() -> V { ); publication } -fn limits() -> EvaluationLimits { +pub(super) fn limits() -> EvaluationLimits { EvaluationLimits { max_package_bytes: 1_048_576, max_input_bytes: 65536, 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 00000000..3461bcf9 --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs @@ -0,0 +1,367 @@ +// 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"), + } +} +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(), 0); + 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.evaluate_effect(program, &input, &[]), + Err(EffectFrameError::Terminated) + ); + } +} diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 8042b8d0..dc31005d 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -824,8 +824,9 @@ 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. Other intrinsic names refuse until their actual handlers and ABI -contracts exist. Static binding does not itself execute instructions. +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 @@ -925,10 +926,49 @@ The final result is validated and encoded under the intersected output ceiling. Every refusal terminates the frame; later calls do no work. Successful calls retain cumulative usage and observations. The new runtime witnesses use the explicitly synthetic, coherently pinned read fixture, not a genuine public -compiler/verifier output. Physical arena guarantees, mutation intrinsics and +compiler/verifier output. Physical arena guarantees, mutation validation and patch derivation, 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` | + +The evaluator charges a deterministic 280-byte row reservation for each intent, +then moves the already charged expression payload into its typed intent. It +retains separate node and atom types and both sides of replacement comparisons. +Host allocation failure remains a host error. Only a successfully completed +component appends intents to `EffectEvaluationFrame::buffered_mutations`, in +authored execution order. Later output/resource/guard failure discards that +component's entire partial buffer and terminates the frame. Previously completed +private components remain inspectable; they are 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 neither +resolves graph slots nor charges write bytes, because Ensure resolution and +validation determine actual writes later. These intents are not a canonical +proposal, approved footprint, derived patch or successful Action. Reachability, +canonical ordering, duplicate targets, schema/identity/footprint rules, +preconditions, actual write costs and patch derivation remain required before +scheduler integration. Tests use explicitly synthetic pinned artifacts; genuine +compiler/verifier production and physical arena guarantees remain open. + ### Current invocation input, basis and owner grant admission `EchoOperationInvocation` is the sole current invocation representation and From bf2c2643444de40ee86c7a07cee51a0fa3197fb6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 12:14:46 -0700 Subject: [PATCH 33/43] Bind completed authored execution to its origin --- CHANGELOG.md | 10 +- crates/warp-core/src/echo_operation.rs | 18 +- crates/warp-core/src/edict_effects.rs | 2 + .../warp-core/src/edict_effects/completion.rs | 70 ++++++ crates/warp-core/src/edict_effects/frame.rs | 64 +++-- crates/warp-core/src/edict_effects/package.rs | 8 + .../tests/edict_effect_package_tests.rs | 4 + .../edict_effect_package_tests/completion.rs | 225 ++++++++++++++++++ .../edict_effect_package_tests/execution.rs | 4 +- .../edict_effect_package_tests/mutations.rs | 2 +- docs/topics/GeneratedRules.md | 48 +++- 11 files changed, 419 insertions(+), 36 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/completion.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests/completion.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 98648911..7e742f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,17 @@ ### Changed +- 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. + Installed authority, proposal validation and patch derivation remain open. + - 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 components discard + empty-record results and row reservations apply. Failed operations discard partial emission. Emission does not modify the frontier or derive a patch; mutation validation, actual write costs, physical allocation and durable scheduler execution remain unfinished. diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 5bc87b03..42ac324c 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -659,8 +659,7 @@ impl EchoOperationProgram { /// 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()?, ))) } @@ -3482,10 +3481,7 @@ impl EchoOperationInvocation { .application_input_bytes .as_deref() .ok_or_else(|| invalid_structure("effect input is absent"))?; - Ok(EchoOperationApplicationBasisV1::new( - profile_digest("echo.operation.basis.edict-effect/v1"), - domain_hash(b"echo:operation-edict-effect-basis:v1\0", input), - )) + Ok(effect_input_basis(input)) } /// Returns the canonical invocation identity. @@ -8018,6 +8014,16 @@ fn hash_port_set<'a>( } } +pub(crate) fn program_identity_bytes(bytes: &[u8]) -> Hash { + domain_hash(PROGRAM_ID_DOMAIN, bytes) +} +#[cfg(feature = "trusted_runtime")] +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); diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 512a283f..4f174443 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -22,7 +22,9 @@ pub use mutations::EffectMutation; mod package; mod runtime_abi; pub use package::{EffectPackageError, EffectProgram}; +mod completion; mod frame; +pub use completion::{CompletedEffectEvaluation, EffectCompletionError}; mod read_image; pub use read_image::{ EffectEdgePairRow, EffectReadImageError, EffectReadRow, EffectReadSetImage, EffectReadSetView, 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 00000000..59a0837a --- /dev/null +++ b/crates/warp-core/src/edict_effects/completion.rs @@ -0,0 +1,70 @@ +// 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. +/// Structural data getters grant no invocation or mutation authority. +#[derive(Debug)] +pub struct CompletedEffectEvaluation { + execution: AuthoredExecution, + reads: EffectReadSetImage, +} +impl CompletedEffectEvaluation { + pub(super) fn new(execution: AuthoredExecution, reads: EffectReadSetImage) -> Self { + Self { execution, reads } + } + /// 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/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 5e1f7e98..00359a3b 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -38,8 +38,10 @@ pub enum EffectFrameError { Evaluation(EvaluationError), /// Read refusal or host fault; host allocation faults must not become Actions. Read(EffectRangeReadError), - /// A prior failure ended this frame; no further work or reads are allowed. + /// Failure or completed whole authored execution ended this frame. Terminated, + /// Whole authored evaluation cannot adopt another private component's work. + UsedFrame, /// A resolved node/atom pair violates the exact prepared graph schema. GraphSchemaViolation, /// An authored terminal reason or mapped read failure, without an Action decision. @@ -79,7 +81,7 @@ pub struct EffectEvaluationFrame<'a> { meter: Meter, limits: EvaluationLimits, terminated: bool, - buffered_mutations: Vec, + completed: Option, evaluation_basis: Option, } @@ -114,7 +116,7 @@ impl<'a> EffectEvaluationFrame<'a> { meter: Meter::new_atomic(limits), limits, terminated: false, - buffered_mutations: Vec::new(), + completed: None, evaluation_basis: None, }) } @@ -257,13 +259,14 @@ impl<'a> EffectEvaluationFrame<'a> { /// Reads are recorded and mutations are buffered as unvalidated intents. /// This grants no invocation or mutation authority. /// # Errors - /// Any preparation, read, authored or resource refusal terminates the frame. + /// 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, EffectFrameError> { + ) -> Result<&[u8], EffectFrameError> { if self.terminated { return Err(EffectFrameError::Terminated); } @@ -271,6 +274,9 @@ impl<'a> EffectEvaluationFrame<'a> { 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()); } @@ -284,30 +290,54 @@ impl<'a> EffectEvaluationFrame<'a> { 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 result = result.and_then(|result| { - self.buffered_mutations - .try_reserve(result.mutations.len()) - .map_err(|_| EffectFrameError::Evaluation(EvaluationError::HostAllocationFailed))?; - self.buffered_mutations.extend(result.mutations); - Ok(result.output) + 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, }); - self.terminated = result.is_err(); - result + Ok(&completed.output) } - /// Completed compiler-emitted mutation intents, in authored execution order. + /// Intents from the completed whole authored operation, in execution order. /// These have not passed proposal validation and are not writes or a patch. - /// Failed components never append their partial intent buffer. + /// Failure never publishes a partial intent buffer. #[must_use] pub fn buffered_mutations(&self) -> &[super::EffectMutation] { - &self.buffered_mutations + 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 reads = self.into_read_set_image(max_read_bytes)?; + Ok(super::CompletedEffectEvaluation::new(execution, reads)) } /// Evaluates exact compiler-package bytes using the same cumulative meter. diff --git a/crates/warp-core/src/edict_effects/package.rs b/crates/warp-core/src/edict_effects/package.rs index b2e6fcdf..edc91687 100644 --- a/crates/warp-core/src/edict_effects/package.rs +++ b/crates/warp-core/src/edict_effects/package.rs @@ -22,6 +22,7 @@ pub enum EffectPackageError { #[derive(Debug, Clone, PartialEq, Eq)] pub struct EffectProgram { value: Value, + identity: crate::Hash, compiled: Vec, compiled_contract: super::compiled_program::CompiledProgram, schema: EffectGraphSchema, @@ -95,6 +96,10 @@ impl EffectProgram { } 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, @@ -124,6 +129,9 @@ impl EffectProgram { &self.output_type, ) } + pub(super) const fn identity(&self) -> crate::Hash { + self.identity + } pub(super) fn execution( &self, mut limits: crate::edict_pure::EvaluationLimits, diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index a9092993..e24502cb 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1698,3 +1698,7 @@ 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; 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 00000000..f579cb76 --- /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/execution.rs b/crates/warp-core/tests/edict_effect_package_tests/execution.rs index e08ef996..a35ee0f5 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/execution.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -53,7 +53,7 @@ pub(super) fn host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { key, ) } -fn input(node: NodeKey, expected: &[u8]) -> Vec { +pub(super) fn input(node: NodeKey, expected: &[u8]) -> Vec { encoded(&map([ ( "address", @@ -66,7 +66,7 @@ fn input(node: NodeKey, expected: &[u8]) -> Vec { ("expected", V::Bytes(expected.to_vec())), ])) } -fn publication() -> V { +pub(super) fn publication() -> V { let mut publication = publication_for_lowering(&repinned_lowering_artifacts()); let V::Bytes(raw) = get(&publication, "program") else { panic!("program") diff --git a/crates/warp-core/tests/edict_effect_package_tests/mutations.rs b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs index 3461bcf9..786e79bd 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/mutations.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs @@ -47,7 +47,7 @@ fn layout(name: &str) -> Vec<(&'static str, &'static str)> { _ => panic!("fixture kind"), } } -fn fixture(name: &str, profile: &str) -> (V, Vec) { +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 { diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index dc31005d..bdf48b31 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -900,8 +900,10 @@ Static preparation grants no invocation or mutation authority. `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. Unbound -frames, mismatched laws and invalid apertures refuse before graph observations. +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. @@ -923,8 +925,10 @@ 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. -Every refusal terminates the frame; later calls do no work. Successful calls -retain cumulative usage and observations. The new runtime witnesses use the +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. Physical arena guarantees, mutation validation and patch derivation, atomic scheduler settlement, receipts/WAL and original-decision @@ -953,11 +957,11 @@ logical storage ceiling. Declaration/catalog agreement cannot redefine this ABI. The evaluator charges a deterministic 280-byte row reservation for each intent, then moves the already charged expression payload into its typed intent. It retains separate node and atom types and both sides of replacement comparisons. -Host allocation failure remains a host error. Only a successfully completed -component appends intents to `EffectEvaluationFrame::buffered_mutations`, in -authored execution order. Later output/resource/guard failure discards that -component's entire partial buffer and terminates the frame. Previously completed -private components remain inspectable; they are not a validated proposal. +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 neither @@ -969,6 +973,32 @@ preconditions, actual write costs and patch derivation remain required before scheduler integration. 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. +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. + ### Current invocation input, basis and owner grant admission `EchoOperationInvocation` is the sole current invocation representation and From 512e82d8ca6826014bd4594615dfe84e8b8ee569 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 12:29:32 -0700 Subject: [PATCH 34/43] Enforce authored mutation emission budgets --- CHANGELOG.md | 9 +- crates/warp-core/src/edict_effects.rs | 2 + .../warp-core/src/edict_effects/execution.rs | 15 +- crates/warp-core/src/edict_effects/frame.rs | 29 ++- .../warp-core/src/edict_effects/mutations.rs | 22 +- .../edict_effect_package_tests/mutations.rs | 206 +++++++++++++++++- docs/topics/GeneratedRules.md | 18 +- 7 files changed, 281 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e742f37..2139207b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Changed +- 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 @@ -19,7 +26,7 @@ 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; - mutation validation, actual write costs, physical allocation and durable + mutation validation, charged validation reads, physical allocation and durable scheduler execution remain unfinished. - Runtime-bound private frames now evaluate prepared authored read/let/require diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index 4f174443..b1ec5a15 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -54,6 +54,8 @@ use crate::{ /// /// 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; diff --git a/crates/warp-core/src/edict_effects/execution.rs b/crates/warp-core/src/edict_effects/execution.rs index c61b7395..7540598e 100644 --- a/crates/warp-core/src/edict_effects/execution.rs +++ b/crates/warp-core/src/edict_effects/execution.rs @@ -197,11 +197,22 @@ impl Program { } } for instruction in &self.instructions { - meter.step(0)?; + // 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)?; - let mutation = kind.emit(value, meter)?; + 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)?; diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 00359a3b..81ab097d 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -42,6 +42,8 @@ pub enum EffectFrameError { 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. @@ -53,6 +55,25 @@ pub enum EffectFrameError { }, } +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) @@ -513,10 +534,14 @@ impl<'a> EffectEvaluationFrame<'a> { ) } - /// Global computation plus read steps, and the view's I/O consumption. + /// 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(), 0) + EchoOperationBudgetV1::new( + self.meter.usage().0, + self.reads.consumed().read_bytes(), + self.reads.consumed().write_bytes(), + ) } /// Global cumulative expression and read-value storage reservations. diff --git a/crates/warp-core/src/edict_effects/mutations.rs b/crates/warp-core/src/edict_effects/mutations.rs index 74ce4cbf..3560fc8f 100644 --- a/crates/warp-core/src/edict_effects/mutations.rs +++ b/crates/warp-core/src/edict_effects/mutations.rs @@ -57,10 +57,7 @@ impl MutationKind { } } -use crate::edict_pure::{ - evaluate::Meter, - values::{bytes, exact_fields, field}, -}; +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; @@ -118,8 +115,20 @@ pub enum EffectMutation { 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, meter: &mut Meter) -> Result { + pub(super) fn emit(self, value: Value) -> Result { exact_fields(&value, self.fields())?; for name in self.fields() { let value = bytes(field(&value, name)?)?; @@ -131,9 +140,6 @@ impl MutationKind { return Err(Error::InvalidArtifact); } } - // One deterministic EffectRow reservation; payload ownership is moved - // from its already charged expression value, never copied here. - meter.allocate(280)?; let Value::Map(fields) = value else { return Err(Error::InvalidArtifact); }; diff --git a/crates/warp-core/tests/edict_effect_package_tests/mutations.rs b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs index 786e79bd..9f54ef83 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/mutations.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/mutations.rs @@ -273,7 +273,10 @@ fn all_five_compiled_mutation_emitters_prepare_and_run_without_writing_the_front }; assert_eq!(frame.buffered_mutations(), &[expected]); assert!(frame.readings().is_empty()); - assert_eq!(frame.consumed().write_bytes(), 0); + assert_eq!( + frame.consumed().write_bytes(), + if name.ends_with("edge") { 128 } else { 71 } + ); assert_eq!(frame.evaluation_basis(), Some(basis)); } } @@ -359,9 +362,210 @@ fn failure_after_emission_does_not_publish_a_partial_intent_buffer() { ))) ); 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/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index bdf48b31..39b00b6b 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -954,8 +954,11 @@ logical storage ceiling. Declaration/catalog agreement cannot redefine this ABI. | `ensure-edge` | `warpId`, `edgeId`, `fromId`, `toId`, `edgeType` | | `replace-edge` | `warpId`, `edgeId`, `expectedToId`, `toId` | -The evaluator charges a deterministic 280-byte row reservation for each intent, -then moves the already charged expression payload into its typed intent. It +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 @@ -964,12 +967,15 @@ 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 neither -resolves graph slots nor charges write bytes, because Ensure resolution and -validation determine actual writes later. These intents are not a canonical +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, actual write costs and patch derivation remain required before +preconditions, validation-read charges and patch derivation remain required before scheduler integration. Tests use explicitly synthetic pinned artifacts; genuine compiler/verifier production and physical arena guarantees remain open. From 3fe20668266ae17ecef00617c055736a76146041 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 13:09:52 -0700 Subject: [PATCH 35/43] Derive private authored patches at exact runtime parents --- CHANGELOG.md | 18 +- crates/warp-core/src/echo_operation.rs | 3 + crates/warp-core/src/edict_effects.rs | 10 +- crates/warp-core/src/edict_effects/derive.rs | 250 +++++++ .../src/edict_effects/derive/checks.rs | 557 ++++++++++++++++ .../src/edict_effects/derive/context.rs | 351 ++++++++++ crates/warp-core/src/edict_effects/law.rs | 14 +- .../src/edict_effects/read_image/encode.rs | 12 +- .../src/edict_effects/read_image/rows.rs | 4 +- crates/warp-core/src/edict_effects/schema.rs | 4 + .../src/edict_effects/value_digest.rs | 12 + crates/warp-core/src/trusted_runtime_host.rs | 61 ++ .../tests/edict_effect_package_tests.rs | 4 + .../edict_effect_package_tests/derivation.rs | 618 ++++++++++++++++++ .../edict_effect_package_tests/execution.rs | 6 + docs/topics/GeneratedRules.md | 42 +- 16 files changed, 1930 insertions(+), 36 deletions(-) create mode 100644 crates/warp-core/src/edict_effects/derive.rs create mode 100644 crates/warp-core/src/edict_effects/derive/checks.rs create mode 100644 crates/warp-core/src/edict_effects/derive/context.rs create mode 100644 crates/warp-core/src/edict_effects/value_digest.rs create mode 100644 crates/warp-core/tests/edict_effect_package_tests/derivation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2139207b..b0e5ab02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ ### Changed +- 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, @@ -20,21 +27,22 @@ 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. - Installed authority, proposal validation and patch derivation remain open. + 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; - mutation validation, charged validation reads, physical allocation and durable - scheduler execution remain unfinished. + 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. Mutation validation, installed Action execution, the physical arena - and decision recovery remain unfinished. + 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 diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 42ac324c..4cf252d5 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -3858,6 +3858,9 @@ pub struct AdmittedEchoOperationInvocation { } 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 { diff --git a/crates/warp-core/src/edict_effects.rs b/crates/warp-core/src/edict_effects.rs index b1ec5a15..d9ef1393 100644 --- a/crates/warp-core/src/edict_effects.rs +++ b/crates/warp-core/src/edict_effects.rs @@ -9,8 +9,9 @@ //! 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. -//! A physical arena, effect validation/patch derivation and Action/receipt/WAL -//! decision recovery remain unfinished. +//! 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; @@ -23,6 +24,11 @@ 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; 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 00000000..2020d55a --- /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 { + /// The invocation was admitted by 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 00000000..de9a219b --- /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 00000000..3d5ae9e1 --- /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/law.rs b/crates/warp-core/src/edict_effects/law.rs index ab88956a..858cd96f 100644 --- a/crates/warp-core/src/edict_effects/law.rs +++ b/crates/warp-core/src/edict_effects/law.rs @@ -19,7 +19,7 @@ pub enum EffectLawError { /// Host-selected typed footprint permissions, not an admitted package capability. /// -/// Decoding validates all nine fields, including currently unused write fields. +/// 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 { @@ -29,9 +29,9 @@ pub struct EffectFootprintLaw { pub(super) ranges: BTreeSet, pub(super) max_reads: u64, pub(super) max_range_edges: u64, - _write_nodes: BTreeSet, - _write_edges: BTreeSet, - _max_writes: u64, + pub(super) write_nodes: BTreeSet, + pub(super) write_edges: BTreeSet, + pub(super) max_writes: u64, } impl EffectFootprintLaw { @@ -81,9 +81,9 @@ impl EffectFootprintLaw { 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: max_writes, + write_nodes: ids(value, "write_node_types")?, + write_edges: ids(value, "write_edge_types")?, + max_writes, }) } } diff --git a/crates/warp-core/src/edict_effects/read_image/encode.rs b/crates/warp-core/src/edict_effects/read_image/encode.rs index cbbfa06e..e21528f9 100644 --- a/crates/warp-core/src/edict_effects/read_image/encode.rs +++ b/crates/warp-core/src/edict_effects/read_image/encode.rs @@ -132,17 +132,7 @@ fn prepare<'a>( (SlotId::Attachment(_), SlotReading::Attachment(Some(AttachmentValue::Atom(atom)))) if atom.type_id == ty => { - 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( - &u64::try_from(atom.bytes.len()) - .map_err(|_| Error::InconsistentObservation)? - .to_le_bytes(), - ); - hash.update(&atom.bytes); - row.subject = hash.finalize().into(); + row.subject = super::super::value_digest::atom(atom); } ( SlotId::Attachment(key), diff --git a/crates/warp-core/src/edict_effects/read_image/rows.rs b/crates/warp-core/src/edict_effects/read_image/rows.rs index b8dd106e..173868e6 100644 --- a/crates/warp-core/src/edict_effects/read_image/rows.rs +++ b/crates/warp-core/src/edict_effects/read_image/rows.rs @@ -54,7 +54,7 @@ const _: () = assert!(std::mem::size_of::() == 192); const _: () = assert!(std::mem::size_of::() == 64); impl EffectSlotRow { - pub(super) fn from_slot(slot: SlotId) -> Result { + pub(in crate::edict_effects) fn from_slot(slot: SlotId) -> Result { let mut row = Self::zeroed(); match slot { SlotId::Node(key) => { @@ -92,7 +92,7 @@ impl EffectSlotRow { } Ok(row) } - pub(super) fn compare(&self, other: &Self) -> std::cmp::Ordering { + pub(in crate::edict_effects) fn compare(&self, other: &Self) -> std::cmp::Ordering { ( self.class, self.warp_id, diff --git a/crates/warp-core/src/edict_effects/schema.rs b/crates/warp-core/src/edict_effects/schema.rs index 925435f4..df01ac14 100644 --- a/crates/warp-core/src/edict_effects/schema.rs +++ b/crates/warp-core/src/edict_effects/schema.rs @@ -74,6 +74,10 @@ impl EffectGraphSchema { .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(); 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 00000000..5a77cd6a --- /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/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index a308dbf7..47c9da14 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -1225,6 +1225,67 @@ impl TrustedRuntimeHost { )?) } + /// 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) { + 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 diff --git a/crates/warp-core/tests/edict_effect_package_tests.rs b/crates/warp-core/tests/edict_effect_package_tests.rs index e24502cb..989d4da7 100644 --- a/crates/warp-core/tests/edict_effect_package_tests.rs +++ b/crates/warp-core/tests/edict_effect_package_tests.rs @@ -1702,3 +1702,7 @@ 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/derivation.rs b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs new file mode 100644 index 00000000..f725115b --- /dev/null +++ b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs @@ -0,0 +1,618 @@ +// 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, +) { + 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, &[]) + .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 + ); +} diff --git a/crates/warp-core/tests/edict_effect_package_tests/execution.rs b/crates/warp-core/tests/edict_effect_package_tests/execution.rs index a35ee0f5..d9d6be99 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/execution.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -28,6 +28,12 @@ pub(super) fn host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { 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.clone(), key.local_id).expect("state"); let head = WriterHeadKey { worldline_id: WorldlineId::from_bytes([5; 32]), diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 39b00b6b..715bd0b0 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -930,9 +930,9 @@ 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. Physical arena guarantees, mutation validation and -patch derivation, atomic scheduler settlement, receipts/WAL and original-decision -recovery remain required for the complete bridge. +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 @@ -975,8 +975,9 @@ 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, validation-read charges and patch derivation remain required before -scheduler integration. Tests use explicitly synthetic pinned artifacts; genuine +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 @@ -1005,6 +1006,28 @@ 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 its admission 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 @@ -1037,8 +1060,9 @@ 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. Mutation instruction execution, the physical arena, -derived effects, Tick integration and effect decision recovery remain required. +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 @@ -1141,6 +1165,6 @@ 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. Mutation -instruction execution, physical allocation, derived effects and authored-effect +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. From ef23584d0e12127e492b59cd90e8b0f227ecdcde Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 13:23:41 -0700 Subject: [PATCH 36/43] Bind completed authored evaluations to their runtime owner --- CHANGELOG.md | 6 + crates/warp-core/src/echo_operation.rs | 2 +- .../warp-core/src/edict_effects/completion.rs | 31 +- crates/warp-core/src/edict_effects/derive.rs | 2 +- crates/warp-core/src/edict_effects/frame.rs | 12 +- crates/warp-core/src/trusted_runtime_host.rs | 9 +- .../edict_effect_package_tests/derivation.rs | 381 +++++++++++++++++- .../edict_effect_package_tests/execution.rs | 9 +- docs/topics/GeneratedRules.md | 9 +- 9 files changed, 450 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e5ab02..46479481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ ### Changed +- 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, diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 4cf252d5..8c6dd2fb 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -145,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) } } diff --git a/crates/warp-core/src/edict_effects/completion.rs b/crates/warp-core/src/edict_effects/completion.rs index 59a0837a..b37017fb 100644 --- a/crates/warp-core/src/edict_effects/completion.rs +++ b/crates/warp-core/src/edict_effects/completion.rs @@ -27,15 +27,40 @@ pub enum EffectCompletionError { /// /// There is no public constructor. This binds completed private evaluation, /// not installed meaning, validated mutation preconditions, a patch or an Action. -/// Structural data getters grant no invocation or mutation authority. +/// 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) -> Self { - Self { execution, reads } + 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] diff --git a/crates/warp-core/src/edict_effects/derive.rs b/crates/warp-core/src/edict_effects/derive.rs index 2020d55a..4ed06d4c 100644 --- a/crates/warp-core/src/edict_effects/derive.rs +++ b/crates/warp-core/src/edict_effects/derive.rs @@ -6,7 +6,7 @@ use crate::{EchoOperationBudgetV1, Footprint, Hash, WarpTickPatchV1}; /// First failed validation rule, before publication or scheduler composition. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EffectDerivationKind { - /// The invocation was admitted by another runtime owner. + /// Admission or completed evaluation belongs to another runtime owner. AuthorityMismatch, /// The installed meaning is absent or has a different program kind. OperationUnavailable, diff --git a/crates/warp-core/src/edict_effects/frame.rs b/crates/warp-core/src/edict_effects/frame.rs index 81ab097d..a78c7f81 100644 --- a/crates/warp-core/src/edict_effects/frame.rs +++ b/crates/warp-core/src/edict_effects/frame.rs @@ -104,6 +104,7 @@ pub struct EffectEvaluationFrame<'a> { terminated: bool, completed: Option, evaluation_basis: Option, + evaluation_authority: Option, } impl<'a> EffectEvaluationFrame<'a> { @@ -139,6 +140,7 @@ impl<'a> EffectEvaluationFrame<'a> { terminated: false, completed: None, evaluation_basis: None, + evaluation_authority: None, }) } @@ -149,6 +151,7 @@ impl<'a> EffectEvaluationFrame<'a> { 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 @@ -159,6 +162,7 @@ impl<'a> EffectEvaluationFrame<'a> { } let mut frame = Self::new(reads, limits)?; frame.evaluation_basis = Some(basis); + frame.evaluation_authority = Some(authority); Ok(frame) } @@ -357,8 +361,14 @@ impl<'a> EffectEvaluationFrame<'a> { 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)) + Ok(super::CompletedEffectEvaluation::new( + execution, reads, authority, + )) } /// Evaluates exact compiler-package bytes using the same cumulative meter. diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 47c9da14..ec04ebdf 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -1221,7 +1221,10 @@ impl TrustedRuntimeHost { budget, ); Ok(crate::edict_effects::EffectEvaluationFrame::at_parent( - reads, limits, current, + reads, + limits, + current, + self.echo_operation_evaluation_authority.clone(), )?) } @@ -1236,7 +1239,9 @@ impl TrustedRuntimeHost { { 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) { + 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 diff --git a/crates/warp-core/tests/edict_effect_package_tests/derivation.rs b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs index f725115b..64f6414b 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/derivation.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/derivation.rs @@ -98,6 +98,19 @@ fn evaluate( 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"); @@ -179,7 +192,7 @@ fn evaluate( .with_footprint_law(program.footprint_law().clone()) .expect("law"); frame - .evaluate_effect(program, input, &[]) + .evaluate_effect(program, input, aperture) .expect("compiled mutation"); let completed = frame.into_effect_evaluation(4096).expect("origin"); (installed, admitted, completed) @@ -616,3 +629,369 @@ fn identical_ensure_still_requires_write_permission() { 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 index d9d6be99..18703585 100644 --- a/crates/warp-core/tests/edict_effect_package_tests/execution.rs +++ b/crates/warp-core/tests/edict_effect_package_tests/execution.rs @@ -34,7 +34,14 @@ pub(super) fn host_with_store( store: GraphStore, key: NodeKey, ) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { - let state = WorldlineState::from_root_store(store.clone(), key.local_id).expect("state"); + 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"), diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 715bd0b0..014277ab 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -996,6 +996,12 @@ 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. @@ -1010,7 +1016,8 @@ parent against their own authoritative evidence. The trusted runtime owner can consume `CompletedEffectEvaluation` through `derive_echo_operation_effects` with an independently admitted invocation. -The owner checks its admission authority, current installation, producing program +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. From afe900301f2866739ef58bb7364a27dd02600f9a Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 13:45:41 -0700 Subject: [PATCH 37/43] Declare bounded authored-effect provider configuration --- CHANGELOG.md | 5 + .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 21 +- .../package/v1/provider-manifest.echo.json | 24 +-- .../echo-wesley-gen/src/provider_artifacts.rs | 21 +- .../tests/provider_effect_schema.rs | 179 ++++++++++++++++++ .../echo-wesley-gen/tests/provider_package.rs | 2 +- .../tests/provider_package_corpus.rs | 4 +- docs/topics/GeneratedRules.md | 16 ++ .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 21 +- .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 21 +- .../package/v1/provider-manifest.echo.json | 24 +-- 17 files changed, 313 insertions(+), 37 deletions(-) create mode 100644 crates/echo-wesley-gen/tests/provider_effect_schema.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 46479481..2e5c9364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Changed +- 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 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 68b43831..000f9bb8 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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 bc4854f5..ae632a5d 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:c94c42b8205a0a3b82d85c88290a875c1158a96e579a70d52eb1607b1bbdc035","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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:ccbdbff0ebfd6f741cf5d5ac1381992335a39f6da14de8a22dddb3c1a80fd97d","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 7e434496..5f23f134 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 @@ -1274,7 +1274,8 @@ 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", @@ -1288,6 +1289,24 @@ 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, + 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 d54401f6..6a09116b 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:2083dd7057574b040fb9c043b17990e34012004fbed39bdeda253dbb0a522bcc" + "digest": "sha256:7c27394cce0df278f8ad85f20f0dbbaf28221e19f2e08a887eb6590548b69d28" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:a1a25c26e70c0d48d2b0e7e9643807077eab113f873a168468a460211d6ab4e2" + "digest": "sha256:e051a9da1dd79cc535cf2e3e5d84a0c7424e10aa9c8a55aa8b09c403ed6dcd5f" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:a298384754bc53136c592316aa9728cd95d3c0c4bf51c92cee7d96adbab2e2a6" + "digest": "sha256:f06c0ccc793db65a5177c3024ceef61f7760a2914864748be5bb940e578b264c" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7" + "digest": "sha256:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index 435f1556..ed4acb05 100644 --- a/crates/echo-wesley-gen/src/provider_artifacts.rs +++ b/crates/echo-wesley-gen/src/provider_artifacts.rs @@ -342,7 +342,8 @@ 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", @@ -356,6 +357,24 @@ 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, + 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/provider_effect_schema.rs b/crates/echo-wesley-gen/tests/provider_effect_schema.rs new file mode 100644 index 00000000..dcdb1343 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_effect_schema.rs @@ -0,0 +1,179 @@ +// 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)), + ( + "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"] { + 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()); +} diff --git a/crates/echo-wesley-gen/tests/provider_package.rs b/crates/echo-wesley-gen/tests/provider_package.rs index c42b848d..c30772d5 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 = - "2083dd7057574b040fb9c043b17990e34012004fbed39bdeda253dbb0a522bcc"; + "7c27394cce0df278f8ad85f20f0dbbaf28221e19f2e08a887eb6590548b69d28"; 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 13feaf52..69115aba 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:2083dd7057574b040fb9c043b17990e34012004fbed39bdeda253dbb0a522bcc"; + "sha256:7c27394cce0df278f8ad85f20f0dbbaf28221e19f2e08a887eb6590548b69d28"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "933201583b043c3b0bdb4be3d6ece5fb2d21ea0aecf5c447c1a9d20639bc39a8"; + "bf007a09b22b13216afb8dcae186f3ab941f04045dd3c83c6c7b8d5781311601"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 014277ab..e0a59c96 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -721,6 +721,22 @@ 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`, 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 68b43831..000f9bb8 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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 bc4854f5..ae632a5d 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:c94c42b8205a0a3b82d85c88290a875c1158a96e579a70d52eb1607b1bbdc035","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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:ccbdbff0ebfd6f741cf5d5ac1381992335a39f6da14de8a22dddb3c1a80fd97d","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 7e434496..5f23f134 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 @@ -1274,7 +1274,8 @@ 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", @@ -1288,6 +1289,24 @@ 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, + 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/generated/evidence/provenance.provider-generation.json b/schemas/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 68b43831..000f9bb8 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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 bc4854f5..ae632a5d 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:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5","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:c94c42b8205a0a3b82d85c88290a875c1158a96e579a70d52eb1607b1bbdc035","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:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915","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:ccbdbff0ebfd6f741cf5d5ac1381992335a39f6da14de8a22dddb3c1a80fd97d","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 7e434496..5f23f134 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 @@ -1274,7 +1274,8 @@ 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", @@ -1288,6 +1289,24 @@ 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, + 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 d54401f6..6a09116b 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:2083dd7057574b040fb9c043b17990e34012004fbed39bdeda253dbb0a522bcc" + "digest": "sha256:7c27394cce0df278f8ad85f20f0dbbaf28221e19f2e08a887eb6590548b69d28" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:a1a25c26e70c0d48d2b0e7e9643807077eab113f873a168468a460211d6ab4e2" + "digest": "sha256:e051a9da1dd79cc535cf2e3e5d84a0c7424e10aa9c8a55aa8b09c403ed6dcd5f" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:a298384754bc53136c592316aa9728cd95d3c0c4bf51c92cee7d96adbab2e2a6" + "digest": "sha256:f06c0ccc793db65a5177c3024ceef61f7760a2914864748be5bb940e578b264c" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:139ac50b29f4a6b524febd37e1735e9de3c905fe4b234779b705d68a19cd29e7" + "digest": "sha256:6755c6b6a52df1f9246a2cc76db38ff6a49d966826fb0389aeab26e9d41212b9" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:7f16189048b2dcd0a7a15f32ed1f3ee9d3ac0ba5f6ea03af9baa3cabcb8727e5" + "digest": "sha256:c2461f69dc9dcfa5f5d751c8e56530f92c7304f12f5d0d45d8a7369fe1c57915" } } }, From 71d70c63902be688025153d2e3dc9fa9ca0d6dc4 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 14:44:45 -0700 Subject: [PATCH 38/43] Publish reproducible combined effect-provider schema component --- .../lowerer.echo-dpo.component.wasm | Bin 485983 -> 485983 bytes .../package/v1/provider-manifest.echo.json | 6 +++--- .../echo-wesley-gen/tests/provider_package.rs | 2 +- .../tests/provider_package_corpus.rs | 4 ++-- schemas/edict-provider/README.md | 2 +- .../edict-provider/components/v1/README.md | 2 +- .../v1/lowerer.echo-dpo.component.wasm | Bin 485983 -> 485983 bytes .../lowerer.echo-dpo.component.wasm | Bin 485983 -> 485983 bytes .../package/v1/provider-manifest.echo.json | 6 +++--- xtask/src/provider_lowerer_component.rs | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) 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 424970f635a8345ed3fcc5a0efa39076a4bd6bcb..4568cf32ad11ef107a35bcc625c8b98c3f049183 100644 GIT binary patch delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INI$0#8q?lQnTNqfFSfnJHni(e=q^4M!nHwc1 z8=57X8km|TrJ;BStKW?nWUr{8CWD6m>49Q8>Cnyrzu%a=NDj?*lu@;?E{Ac NrKWAqyUq5o4*&raH3I+u delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INIvFIIq?)Isq#31|7+9F6rkNV2r5GC;nx!Ne zni&}zSehprq@)>Gn3-CnSf->Tn&=NDj?*lu@;?E{Ac NrKWAqyUq5o4*(4WG|>P6 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 68e599a4..c88e8730 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:88bf09ce01c62951d2d7c170f1eddb8450e346beb0115f1f276a71be14942608" + "digest": "sha256:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1" }, "artifacts": [ { @@ -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" } } }, diff --git a/crates/echo-wesley-gen/tests/provider_package.rs b/crates/echo-wesley-gen/tests/provider_package.rs index 9600d46b..4d2aef39 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 = - "88bf09ce01c62951d2d7c170f1eddb8450e346beb0115f1f276a71be14942608"; + "a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1"; 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 c6eeaf16..0955568e 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:88bf09ce01c62951d2d7c170f1eddb8450e346beb0115f1f276a71be14942608"; + "sha256:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "cb6593423bdcbceff83ee9da97e5d84bf02b2c5e909e07aa7850feb990a65596"; + "71fa156d1c4ecedb8e47bf3f648bfd24e9e1ad17a2871fc5c7cfad0fb99624b3"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index 958c9489..2be7538b 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 d17349a3..a4c5303f 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 424970f635a8345ed3fcc5a0efa39076a4bd6bcb..4568cf32ad11ef107a35bcc625c8b98c3f049183 100644 GIT binary patch delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INI$0#8q?lQnTNqfFSfnJHni(e=q^4M!nHwc1 z8=57X8km|TrJ;BStKW?nWUr{8CWD6m>49Q8>Cnyrzu%a=NDj?*lu@;?E{Ac NrKWAqyUq5o4*&raH3I+u delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INIvFIIq?)Isq#31|7+9F6rkNV2r5GC;nx!Ne zni&}zSehprq@)>Gn3-CnSf->Tn&=NDj?*lu@;?E{Ac NrKWAqyUq5o4*(4WG|>P6 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 424970f635a8345ed3fcc5a0efa39076a4bd6bcb..4568cf32ad11ef107a35bcc625c8b98c3f049183 100644 GIT binary patch delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INI$0#8q?lQnTNqfFSfnJHni(e=q^4M!nHwc1 z8=57X8km|TrJ;BStKW?nWUr{8CWD6m>49Q8>Cnyrzu%a=NDj?*lu@;?E{Ac NrKWAqyUq5o4*&raH3I+u delta 170 zcmcbAMfUy`*@hOz7N!>F7M2#)Eo@INIvFIIq?)Isq#31|7+9F6rkNV2r5GC;nx!Ne zni&}zSehprq@)>Gn3-CnSf->Tn&=NDj?*lu@;?E{Ac NrKWAqyUq5o4*(4WG|>P6 diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index 68e599a4..c88e8730 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:88bf09ce01c62951d2d7c170f1eddb8450e346beb0115f1f276a71be14942608" + "digest": "sha256:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1" }, "artifacts": [ { @@ -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" } } }, diff --git a/xtask/src/provider_lowerer_component.rs b/xtask/src/provider_lowerer_component.rs index 49cc91a9..8ac420df 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"; From 41037d48b66fea9e88af1de96a502d2b1654b77c Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 14:53:59 -0700 Subject: [PATCH 39/43] Bind explicit effect catalogs to selected target profiles --- CHANGELOG.md | 5 ++ .../src/executable_operation.rs | 5 ++ .../executable_operation/effect_resources.rs | 60 ++++++++++++++++ crates/echo-edict-provider-lowerer/src/lib.rs | 4 +- .../tests/bounded_read.rs | 70 +++++++++++++++++++ .../tests/fixtures/generated_echo_dpo.rs | 4 +- .../tests/lowerer_contract.rs | 2 +- .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 1 + .../package/v1/provider-manifest.echo.json | 24 +++---- .../echo-wesley-gen/src/provider_artifacts.rs | 1 + .../tests/provider_effect_schema.rs | 3 +- .../echo-wesley-gen/tests/provider_package.rs | 2 +- .../tests/provider_package_corpus.rs | 4 +- docs/topics/GeneratedRules.md | 9 +++ .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 1 + .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 1 + .../package/v1/provider-manifest.echo.json | 24 +++---- 23 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 crates/echo-edict-provider-lowerer/src/executable_operation/effect_resources.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ebf937..55a28758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Changed +- 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 diff --git a/crates/echo-edict-provider-lowerer/src/executable_operation.rs b/crates/echo-edict-provider-lowerer/src/executable_operation.rs index b081ff4e..5825a906 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 +//! 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 9bca5fc8..0e8d52db 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -823,7 +823,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 = - "8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"; + "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; /// 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 +860,7 @@ pub mod echo_dpo { const MUTATION_RULE_NAME: &str = concat!( "cmd/contract/", - "8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf", + "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430", "/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 29b2d7cd..7f5f782e 100644 --- a/crates/echo-edict-provider-lowerer/tests/bounded_read.rs +++ b/crates/echo-edict-provider-lowerer/tests/bounded_read.rs @@ -360,3 +360,73 @@ 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 catalog = include_bytes!( + "../../echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor" + ) + .to_vec(); + 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/fixtures/generated_echo_dpo.rs b/crates/echo-edict-provider-lowerer/tests/fixtures/generated_echo_dpo.rs index 832d8b49..d868eaeb 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 = - "8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"; + "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; /// 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/", - "8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf", + "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430", "/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 0d495947..078b3043 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 = - "8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"; + "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; 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/generated/evidence/provenance.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index e1185bd2..9563ddc5 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 106b6eac..6821b3db 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:67f8bd500a67070ebf98ba606321c6dd7780d75b28edff4b572f5f680a080da5","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 47ee1f28..e4fc5193 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 @@ -1300,6 +1300,7 @@ 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, 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 c88e8730..c2d5be6a 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:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1" + "digest": "sha256:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:3e5155345c9b1dfe1ad48b34982de8eb2862c472e409f6be64338f59e07a49a2" + "digest": "sha256:73604e41392e7588c7d47c8a2cc817d4945362ca8c477e858cb6cb32d8954bdb" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:2365be03e0a2421b7abb43a580f7e1cb75b95692350c921115e2cd3046ea095b" + "digest": "sha256:14569a66517ba4473b0f2f0e4b7da375e37fe53109324738cc053f651006e3a0" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf" + "digest": "sha256:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index 0d454419..4ef0a173 100644 --- a/crates/echo-wesley-gen/src/provider_artifacts.rs +++ b/crates/echo-wesley-gen/src/provider_artifacts.rs @@ -368,6 +368,7 @@ 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, diff --git a/crates/echo-wesley-gen/tests/provider_effect_schema.rs b/crates/echo-wesley-gen/tests/provider_effect_schema.rs index dcdb1343..d161bb0b 100644 --- a/crates/echo-wesley-gen/tests/provider_effect_schema.rs +++ b/crates/echo-wesley-gen/tests/provider_effect_schema.rs @@ -20,6 +20,7 @@ fn configuration() -> V { ), ("programKind", text("edict-effect-program/v1")), ("identityLaw", V::Integer(1)), + ("intrinsicCatalog", V::Bytes(vec![0xa0])), ( "graphSchema", V::Bytes( @@ -114,7 +115,7 @@ fn authored_effect_configuration_is_selected_with_explicit_resource_and_law_fiel set(&mut config, field, V::Integer(value)); assert!(accepts(&config).is_err(), "{field} outside current profile"); } - for field in ["graphSchema", "footprintLaw"] { + 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"); diff --git a/crates/echo-wesley-gen/tests/provider_package.rs b/crates/echo-wesley-gen/tests/provider_package.rs index 4d2aef39..4ce561a8 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 = - "a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1"; + "38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822"; 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 0955568e..ef2a9843 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:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1"; + "sha256:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "71fa156d1c4ecedb8e47bf3f648bfd24e9e1ad17a2871fc5c7cfad0fb99624b3"; + "40ba2920510f89fb9f0a4d464949d68ff839fe41bc54b241f78161da201849ef"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index af4f2880..5bc43bc0 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -138,6 +138,15 @@ 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. + ## Execution Corridors ### Bounded compiler-package computation 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 e1185bd2..9563ddc5 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 106b6eac..6821b3db 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:67f8bd500a67070ebf98ba606321c6dd7780d75b28edff4b572f5f680a080da5","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 47ee1f28..e4fc5193 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 @@ -1300,6 +1300,7 @@ 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, 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 e1185bd2..9563ddc5 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 106b6eac..6821b3db 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:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096","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:67f8bd500a67070ebf98ba606321c6dd7780d75b28edff4b572f5f680a080da5","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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 47ee1f28..e4fc5193 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 @@ -1300,6 +1300,7 @@ 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, diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index c88e8730..c2d5be6a 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:a764ec211867adb6f70192ec48983517c42e9e5efa242044c018d2536f9892b1" + "digest": "sha256:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:3e5155345c9b1dfe1ad48b34982de8eb2862c472e409f6be64338f59e07a49a2" + "digest": "sha256:73604e41392e7588c7d47c8a2cc817d4945362ca8c477e858cb6cb32d8954bdb" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:2365be03e0a2421b7abb43a580f7e1cb75b95692350c921115e2cd3046ea095b" + "digest": "sha256:14569a66517ba4473b0f2f0e4b7da375e37fe53109324738cc053f651006e3a0" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:8bdd69780848da563a0ed9672cc16c5056cd5172fbb8ccf4df208c040a70d8cf" + "digest": "sha256:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:b9ad2d3ce55fa27d9bc5ec0004db9ccd2537a9c529e88ce459d7813945d3a096" + "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" } } }, From cf21f82d737620d90cdf0fb40272b68f85616857 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 15:11:02 -0700 Subject: [PATCH 40/43] Serialize current effect proposals with derived artifact identities --- CHANGELOG.md | 5 + crates/echo-edict-provider-lowerer/Cargo.toml | 3 +- crates/echo-edict-provider-lowerer/README.md | 20 + .../src/effect_publication.rs | 389 ++++++++++++++++++ .../src/executable_operation.rs | 2 +- crates/echo-edict-provider-lowerer/src/lib.rs | 1 + .../tests/bounded_read.rs | 11 +- .../tests/current_effect_publication.rs | 245 +++++++++++ .../fixtures/current-effect-wire/README.md | 23 ++ .../fixtures/current-effect-wire/bundle.hex | 1 + .../current-effect-wire/configuration.hex | 1 + .../fixtures/current-effect-wire/source.hex | 1 + docs/topics/GeneratedRules.md | 8 + 13 files changed, 705 insertions(+), 5 deletions(-) create mode 100644 crates/echo-edict-provider-lowerer/src/effect_publication.rs create mode 100644 crates/echo-edict-provider-lowerer/tests/current_effect_publication.rs create mode 100644 crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/README.md create mode 100644 crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/bundle.hex create mode 100644 crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/configuration.hex create mode 100644 crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/source.hex diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a28758..f1fbd82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Changed +- 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; diff --git a/crates/echo-edict-provider-lowerer/Cargo.toml b/crates/echo-edict-provider-lowerer/Cargo.toml index 64e6e667..b3863ecf 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 f4bc6d02..87ddfb94 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 00000000..de913b93 --- /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 5825a906..8bacc360 100644 --- a/crates/echo-edict-provider-lowerer/src/executable_operation.rs +++ b/crates/echo-edict-provider-lowerer/src/executable_operation.rs @@ -2266,7 +2266,7 @@ fn hash_from_bound(bound: &BoundArtifact) -> 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/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index 0e8d52db..11c31d10 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 { diff --git a/crates/echo-edict-provider-lowerer/tests/bounded_read.rs b/crates/echo-edict-provider-lowerer/tests/bounded_read.rs index 7f5f782e..c3a535b6 100644 --- a/crates/echo-edict-provider-lowerer/tests/bounded_read.rs +++ b/crates/echo-edict-provider-lowerer/tests/bounded_read.rs @@ -405,10 +405,15 @@ fn effect_catalog_request(catalog: Option>) -> LoweringRequestV1 { #[test] fn effect_catalog_must_match_the_selected_profile_before_body_lowering() { use echo_edict_provider_lowerer::ProviderRefusalKind as K; - let catalog = include_bytes!( - "../../echo-edict-provider-verifier/resources/resource.target-intrinsics.cbor" + let fixture_config = decode( + &hex::decode(include_str!("fixtures/current-effect-wire/configuration.hex").trim()) + .expect("crate-local catalog fixture"), ) - .to_vec(); + .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); 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 00000000..da4f7d1c --- /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/fixtures/current-effect-wire/README.md b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/README.md new file mode 100644 index 00000000..172500a1 --- /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 00000000..ec195edc --- /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 00000000..6f4462dd --- /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 00000000..2f2c50fc --- /dev/null +++ b/crates/echo-edict-provider-lowerer/tests/fixtures/current-effect-wire/source.hex @@ -0,0 +1 @@ +581f73796e7468657469632073657269616c697a6174696f6e20636f6e74726f6c diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 5bc43bc0..acd51505 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -147,6 +147,14 @@ 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. + ## Execution Corridors ### Bounded compiler-package computation From 2e2bbe7055d670474738b70603bc8ef51ece80c2 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 15:13:35 -0700 Subject: [PATCH 41/43] Correct the effect wire fixture documentation license header --- .../tests/fixtures/current-effect-wire/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 172500a1..701a9610 100644 --- 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 @@ -1,4 +1,4 @@ - + # Current effect wire controls From 09239c7c30cda28d728ae9b183dd0f6c3ffc8de1 Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 15:37:09 -0700 Subject: [PATCH 42/43] Admit current effect envelopes through the provider output schema --- CHANGELOG.md | 5 + crates/echo-edict-provider-lowerer/src/lib.rs | 4 +- .../tests/fixtures/generated_echo_dpo.rs | 4 +- .../tests/lowerer_contract.rs | 2 +- .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 30 ++++- .../package/v1/provider-manifest.echo.json | 24 ++-- .../echo-wesley-gen/src/provider_artifacts.rs | 30 ++++- .../current-effect-publication/README.md | 14 +++ .../current-effect-publication/package.hex | 1 + .../tests/provider_effect_schema.rs | 108 ++++++++++++++++++ .../echo-wesley-gen/tests/provider_package.rs | 2 +- .../tests/provider_package_corpus.rs | 4 +- docs/topics/GeneratedRules.md | 8 ++ .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 30 ++++- .../provenance.provider-generation.json | 2 +- .../evidence/review.provider-generation.json | 2 +- .../schema.echo-provider-artifacts.cddl | 30 ++++- .../package/v1/provider-manifest.echo.json | 24 ++-- 22 files changed, 270 insertions(+), 62 deletions(-) create mode 100644 crates/echo-wesley-gen/tests/fixtures/current-effect-publication/README.md create mode 100644 crates/echo-wesley-gen/tests/fixtures/current-effect-publication/package.hex diff --git a/CHANGELOG.md b/CHANGELOG.md index f1fbd82f..4b03fb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Changed +- 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, diff --git a/crates/echo-edict-provider-lowerer/src/lib.rs b/crates/echo-edict-provider-lowerer/src/lib.rs index 11c31d10..2143bf0f 100644 --- a/crates/echo-edict-provider-lowerer/src/lib.rs +++ b/crates/echo-edict-provider-lowerer/src/lib.rs @@ -824,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 = - "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; + "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. @@ -861,7 +861,7 @@ pub mod echo_dpo { const MUTATION_RULE_NAME: &str = concat!( "cmd/contract/", - "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430", + "167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d", "/3389142194/a.b@1.t" ); const PROVIDER_OPERATIONS: [ProviderOperationV1<'static>; 1] = [ProviderOperationV1 { 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 d868eaeb..e1d192b4 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 = - "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; + "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/", - "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430", + "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 078b3043..78a8a2a9 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 = - "122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"; + "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/generated/evidence/provenance.provider-generation.json b/crates/echo-wesley-gen/assets/v1/edict-provider/package/v1/generated/evidence/provenance.provider-generation.json index 9563ddc5..1a6ab1ce 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 6821b3db..8db71963 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 e4fc5193..336718aa 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,15 +1274,23 @@ 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 = 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 c2d5be6a..b9c7118f 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:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822" + "digest": "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:73604e41392e7588c7d47c8a2cc817d4945362ca8c477e858cb6cb32d8954bdb" + "digest": "sha256:bdae385fd1cfa0463c45888387e7664dd627f9c5ece01df5f8f9a90a4c8da2d8" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:14569a66517ba4473b0f2f0e4b7da375e37fe53109324738cc053f651006e3a0" + "digest": "sha256:eb306bb4f8f3e0cfe07620eb46234c4a617744a2063d2f9a7854944cb934c4b2" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430" + "digest": "sha256:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs index 4ef0a173..0909d8e4 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,15 +342,23 @@ 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 = 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 00000000..08b03e96 --- /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 00000000..03fee0c9 --- /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 index d161bb0b..dbe400cb 100644 --- a/crates/echo-wesley-gen/tests/provider_effect_schema.rs +++ b/crates/echo-wesley-gen/tests/provider_effect_schema.rs @@ -178,3 +178,111 @@ fn authored_effect_configuration_is_selected_with_explicit_resource_and_law_fiel ); 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 4ce561a8..1dc43cd2 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 = - "38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822"; + "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 ef2a9843..2b0aa633 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:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822"; + "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c"; const EXPECTED_MANIFEST_RAW_SHA256: &str = - "40ba2920510f89fb9f0a4d464949d68ff839fe41bc54b241f78161da201849ef"; + "f5120976f8afc38cb1e01e631134d33fb3ad35df4c48c518cbeebb48409dfbe4"; const PACKAGE_PATHS: [&str; 25] = [ "components/lowerer.echo-dpo.component.wasm", diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index acd51505..17df53bd 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -155,6 +155,14 @@ 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 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 9563ddc5..1a6ab1ce 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 6821b3db..8db71963 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 e4fc5193..336718aa 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,15 +1274,23 @@ 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 = 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 9563ddc5..1a6ab1ce 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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 6821b3db..8db71963 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:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430"}],"generationInputDigest":"sha256:0c07274803d465ec212c313f74dd46396851f43019fa0f97da9571a167c84587","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4","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:770c844d424b827f0830364e5b93ff30dca4448177fc0157ab14713a06c2f9da","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 e4fc5193..336718aa 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,15 +1274,23 @@ 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 = diff --git a/schemas/edict-provider/package/v1/provider-manifest.echo.json b/schemas/edict-provider/package/v1/provider-manifest.echo.json index c2d5be6a..b9c7118f 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:38077c6e303ca122d881508da732d66450ab4aff48717e8948b98b4f31586822" + "digest": "sha256:25d2b8a67768b5522ad36b474131da5df4a7b60a9b4e6c269996633ac312400c" }, "artifacts": [ { @@ -21,7 +21,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -40,7 +40,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -59,7 +59,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -78,7 +78,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -102,7 +102,7 @@ "artifactKind": "generationProvenance", "resource": { "coordinate": "echo.edict-provider-generation-provenance@1", - "digest": "sha256:73604e41392e7588c7d47c8a2cc817d4945362ca8c477e858cb6cb32d8954bdb" + "digest": "sha256:bdae385fd1cfa0463c45888387e7664dd627f9c5ece01df5f8f9a90a4c8da2d8" }, "source": { "kind": "generated", @@ -112,7 +112,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -121,7 +121,7 @@ "artifactKind": "reviewArtifact", "resource": { "coordinate": "echo.edict-provider-generation-review@1", - "digest": "sha256:14569a66517ba4473b0f2f0e4b7da375e37fe53109324738cc053f651006e3a0" + "digest": "sha256:eb306bb4f8f3e0cfe07620eb46234c4a617744a2063d2f9a7854944cb934c4b2" }, "source": { "kind": "generated", @@ -131,7 +131,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -140,7 +140,7 @@ "artifactKind": "artifactSchema", "resource": { "coordinate": "echo.provider-artifacts.cddl@1", - "digest": "sha256:122547f6fdacd0e7c571348b47c058f4d0009e4640aa901df48bd376be73e430" + "digest": "sha256:167cda41b4dd0b7a4c5f1fd63e9539ea3111027c3a9ea263a6faad789a7dde1d" }, "source": { "kind": "generated", @@ -150,7 +150,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, @@ -169,7 +169,7 @@ }, "generator": { "coordinate": "echo-wesley-gen.provider-artifact-generator@1", - "digest": "sha256:d37f66e364e08950b29417bd34a4f0af566586e372612759c4f9d44437eb20c4" + "digest": "sha256:7f667a2aaf65bed92cfce9c7fec176b5679c11858698ccea8ed88ce4ec052343" } } }, From cd75aa04ce54f02b4da123bc4103f4561bd53f7a Mon Sep 17 00:00:00 2001 From: James Ross Date: Mon, 19 Oct 2026 16:00:05 -0700 Subject: [PATCH 43/43] Separate effect publication checks from trusted host authority --- CHANGELOG.md | 4 + crates/warp-core/src/echo_operation.rs | 39 +++------ crates/warp-core/src/edict_pure.rs | 4 + crates/warp-core/src/lib.rs | 13 +++ .../tests/effect_publication_validation.rs | 86 +++++++++++++++++++ .../current-effect-publication/README.md | 11 +++ .../current-effect-publication/package.hex | 1 + docs/topics/GeneratedRules.md | 9 ++ 8 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 crates/warp-core/tests/effect_publication_validation.rs create mode 100644 crates/warp-core/tests/fixtures/current-effect-publication/README.md create mode 100644 crates/warp-core/tests/fixtures/current-effect-publication/package.hex diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b03fb70..ebd597bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ### 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. diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 8c6dd2fb..4b0043fb 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -580,7 +580,6 @@ pub enum EchoOperationFootprintContractV1 { #[derive(Clone, Debug, PartialEq, Eq)] pub enum EchoOperationProgram { /// An authored program with executable schema, footprint law and resource declarations. - #[cfg(feature = "trusted_runtime")] EdictEffect(Arc), /// Compare and replace one anchored typed node attachment. @@ -605,7 +604,6 @@ pub enum EchoOperationProgram { impl EchoOperationProgram { /// Returns the authored-effect payload when this is that program kind. - #[cfg(feature = "trusted_runtime")] #[must_use] pub fn as_effect_program(&self) -> Option<&crate::edict_effects::EffectProgram> { match self { @@ -616,14 +614,12 @@ impl EchoOperationProgram { const fn interpreter_profile(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation-interpreter.edict-effect/v1", _ => INTERPRETER_PROFILE, } } const fn intrinsic_profile(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation-intrinsics.edict-effect/v1", _ => INTRINSIC_PROFILE, } @@ -676,14 +672,12 @@ impl EchoOperationProgram { Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => { profile_digest(CREATE_FOOTPRINT_CONTRACT) } - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(program) => program.footprint_identity(), } } const fn minimum_budget(&self) -> EchoOperationBudgetV1 { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => EchoOperationBudgetV1::new(1, 0, 0), Self::AnchoredNodeAttachmentCompareAndSet { .. } => { EchoOperationBudgetV1::new(4, 64, 32) @@ -700,7 +694,6 @@ impl EchoOperationProgram { const fn input_schema(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.input.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => INPUT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_INPUT_SCHEMA, @@ -709,7 +702,6 @@ impl EchoOperationProgram { const fn result_schema(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.result.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_SCHEMA, @@ -718,7 +710,6 @@ impl EchoOperationProgram { const fn obstruction_schema(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.obstruction.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_SCHEMA, @@ -727,7 +718,6 @@ impl EchoOperationProgram { const fn result_interpretation(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.result-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_INTERPRETATION, @@ -736,7 +726,6 @@ impl EchoOperationProgram { const fn obstruction_interpretation(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.obstruction-interpretation.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_INTERPRETATION, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_INTERPRETATION, @@ -745,7 +734,6 @@ impl EchoOperationProgram { const fn application_basis_schema(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation.basis.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => APPLICATION_BASIS_SCHEMA, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_APPLICATION_BASIS_SCHEMA, @@ -754,7 +742,6 @@ impl EchoOperationProgram { const fn target_profile(&self) -> &'static str { match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(_) => "echo.operation-target.edict-effect/v1", Self::AnchoredNodeAttachmentCompareAndSet { .. } => TARGET_PROFILE, Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_TARGET_PROFILE, @@ -786,8 +773,6 @@ impl EchoOperationProgram { fn validate_supported_profile(&self) -> Result<(), EchoOperationArtifactErrorV1> { match self { - #[cfg(feature = "trusted_runtime")] - Self::EdictEffect(_) => Ok(()), Self::AnchoredNodeAttachmentCompareAndSet { max_replacement_bytes, .. @@ -799,7 +784,8 @@ impl EchoOperationProgram { EchoOperationArtifactErrorKindV1::UnsupportedProgram, "program replacement bound must be nonzero", )), - Self::AnchoredNodeAttachmentCompareAndSet { .. } + Self::EdictEffect(_) + | Self::AnchoredNodeAttachmentCompareAndSet { .. } | Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => Ok(()), } } @@ -807,7 +793,6 @@ impl EchoOperationProgram { fn to_value(&self) -> CanonicalValueV1 { let (kind, required_node_type, required_attachment_type, max_replacement_bytes) = match self { - #[cfg(feature = "trusted_runtime")] Self::EdictEffect(program) => return program.to_value(), Self::AnchoredNodeAttachmentCompareAndSet { required_node_type, @@ -852,7 +837,6 @@ impl EchoOperationProgram { fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; - #[cfg(feature = "trusted_runtime")] 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))) @@ -1893,9 +1877,13 @@ pub struct ExecutableOperationPackage { 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. - #[cfg(feature = "trusted_runtime")] pub fn prepare( bytes: &[u8], expected: Hash, @@ -2323,17 +2311,12 @@ impl ExecutableOperationPackage { )); } self.semantic_closure.validate()?; - let requires_lawpack = match &self.program { - #[cfg(feature = "trusted_runtime")] - EchoOperationProgram::EdictEffect(_) => false, - _ => true, - }; + 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", )); } - #[cfg(feature = "trusted_runtime")] if let Some(effect) = self.program.as_effect_program() { effect .validate_compiled_binding( @@ -4095,6 +4078,11 @@ fn current_application_basis( 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 @@ -8020,7 +8008,6 @@ fn hash_port_set<'a>( pub(crate) fn program_identity_bytes(bytes: &[u8]) -> Hash { domain_hash(PROGRAM_ID_DOMAIN, bytes) } -#[cfg(feature = "trusted_runtime")] pub(crate) fn effect_input_basis(input: &[u8]) -> EchoOperationApplicationBasisV1 { EchoOperationApplicationBasisV1::new( profile_digest("echo.operation.basis.edict-effect/v1"), diff --git a/crates/warp-core/src/edict_pure.rs b/crates/warp-core/src/edict_pure.rs index cb48479c..882d07bc 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, diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 0da35dad..efcf1056 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -71,10 +71,23 @@ 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; 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 00000000..d33c89b0 --- /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/fixtures/current-effect-publication/README.md b/crates/warp-core/tests/fixtures/current-effect-publication/README.md new file mode 100644 index 00000000..1dd047bf --- /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 00000000..03fee0c9 --- /dev/null +++ b/crates/warp-core/tests/fixtures/current-effect-publication/package.hex @@ -0,0 +1 @@ +b266736368656d6178196563686f2e6f7065726174696f6e2d7061636b6167652f76316770726f6772616d5907c4ad646b696e647765646963742d6566666563742d70726f6772616d2f763166736368656d6178196563686f2e6f7065726174696f6e2d70726f6772616d2f76316b6f75747075745f747970656a6170702e526573756c746c67726170685f736368656d615876a26a656467655f7479706573a06a6e6f64655f7479706573a158200101010101010101010101010101010101010101010101010101010101010101a26a61646472657373696e67016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026c6964656e746974795f6c6177016d666f6f747072696e745f6c617758f6a9696d61785f7265616473016a61746f6d5f747970657381582002020202020202020202020202020202020202020202020202020202020202026a6d61785f777269746573016f6d61785f72616e67655f6564676573006f726561645f6e6f64655f747970657381582001010101010101010101010101010101010101010101010101010101010101017072616e67655f656467655f7479706573807077726974655f656467655f7479706573807077726974655f6e6f64655f7479706573815820010101010101010101010101010101010101010101010101010101010101010172706f7274616c5f6f776e65725f7479706573806f6d61785f696e7075745f62797465731a0001000070636f6d70696c65645f70726f6772616d5904c9a666696e74656e7468757064617465403166736368656d6178286563686f2e636f6d70696c65722d70726f64756365642d6566666563742d70726f6772616d2f76316d636f72655f6172746966616374590194a6657479706573a166526573756c74a1646b696e646355333267696d706f7274738067696e74656e7473a1687570646174654031a664626f6479a3656e6f64657380666c6f63616c7381a3626964656172672e3064747970656355333269616c7068614e616d6565246172673066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a6577696474686355333265696e70757463553332666f75747075746a6170702e526573756c7470696e707574436f6e73747261696e74738074636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a01000000781872657175697265644f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f76316a61706956657273696f6e6d65646963742e636f72652f76316a636f6f7264696e6174656361707078187265717569726564436f72654361706162696c697469657380726c6f776572696e675f617274696661637473f6727461726765745f69725f6172746966616374590200a6646b696e64776f7264657265645461726765744972417274696661637466646f6d61696e6f6563686f2e7370616e2d69722f763267696e74656e7473a1687570646174654031a86573746570738066726573756c74a2646b696e6465636f6e73746576616c7565a3646b696e6463696e746576616c7565182a657769647468635533326c7075726542696e64696e6773806c726571756972656d656e7473806e657865637574696f6e4f726465728070696e707574436f6e73747261696e747380706f7065726174696f6e50726f66696c65781a636f6e74696e75756d2e70726f66696c652e746f74616c2f763174636f72654576616c756174696f6e427564676574a3686d61785374657073016e6d61784f757470757442797465731a00010000716d6178416c6c6f636174656442797465731a010000006d74617267657450726f66696c65a26269646a6563686f2e64706f4031666469676573748266736861323536582006060606060606060606060606060606060606060606060606060606060606066f73656d616e746963436c6f73757265a2686c61777061636b73806a736f75726365436f7265a262696463617070666469676573748266736861323536582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d74736f75726365436f7265436f6f7264696e6174656361707078186c61777061636b5f6578706f7274735f6172746966616374589ca66574797065738067656666656374738069636f6e7374616e7473806c6f62737472756374696f6e7381a36a636f6f7264696e6174656b6170702e5265667573616c6d7061796c6f6164536368656d61736170702e5265667573616c2e5061796c6f61646e617574686f72697479436c61737366646f6d61696e6d7075726546756e6374696f6e7380716f7065726174696f6e50726f66696c6573a0706d61785f726573756c745f62797465731a00010000727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db0736d61785f616c6c6f63617465645f62797465731a01000000781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c6e6275646765745f6365696c696e67a36573746570730a6a726561645f62797465731910006b77726974655f62797465731910007073656d616e7469635f636c6f73757265a66d636f72655f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d727461726765745f69725f6964656e746974795820d9374cd2aaabfab6f1d5ca0b5298687a8b9c12a19362f982b8401eb0381f6db07565646963745f736f757263655f6964656e746974795820fdf14fbb05ba25e820cd4a23b594e6648429940f7aed5218077eef841288114c781a63616e6f6e6963616c5f6d65616e696e675f6964656e74697479582017e17862979412ae3477593727e6d5ec05ee50fe255fc02acdfc5777b9ac744d781b6170706c69636174696f6e5f736368656d615f6964656e74697479582017ff6f37fcc8b1f81e58cc3ef5c0d94a4c3f96fb955bfb9cae7ff0bac29cb249781d6170706c69636174696f6e5f736368656d615f636f6f7264696e6174656b6170702e4578706f727473746f7065726174696f6e5f636f6f7264696e6174656c6170702e757064617465403175696e7075745f736368656d615f6964656e746974795820d277705b5ab03fcb58590bad62fb8058e075c3b4c65efc21282bb531aa22b3ad766f62737472756374696f6e5f636f6f7264696e6174656b6170702e5265667573616c76726573756c745f736368656d615f6964656e746974795820de5c0c4f0c640354c40bb4f9d4886cd820353a548caea4953269eba15fe902bc777461726765745f70726f66696c655f6964656e746974795820eaca21c05664241efd1e1c5b5636c7132fe8854f6bcaddb2fd4f014e1bd4a479781a617574686f726974795f70726f66696c655f6964656e7469747958200808080808080808080808080808080808080808080808080808080808080808781a696e7472696e7369635f70726f66696c655f6964656e746974795820a0fc1b7a1e186598291754469fd6ea2220f676358284f7b84c7e2476901a3151781b666f6f747072696e745f636f6e74726163745f6964656e746974795820362b20fb18488ce180f6c1096b5189994354604fd1b1b126a3ea4ef839e5232b781b6f62737472756374696f6e5f736368656d615f6964656e746974795820b1f96fdf3ec65e84d500702951393a931b4fdd843ad02723ea3725f90a671418781c696e7465727072657465725f70726f66696c655f6964656e746974795820eaa6f8ca4f395d291fdb70af1bd47b804ab82aa17fdbc01b8aaeeb747f7d218c781e726573756c745f696e746572707265746174696f6e5f6964656e74697479582042734d5e73435acc0fdd5a66b97e5bfd94278e64fce2b4890ec609f054dc969e78206576616c756174696f6e5f62617369735f736368656d615f6964656e74697479582020b6239db569aa86ae1c27596b374df90caa5ea41e8e005fd4fe00bfe5c3b38178216170706c69636174696f6e5f62617369735f736368656d615f6964656e7469747958208b294c69120a2e749c56266691b0974e7e0323c198fc42a467fd3790790f791078236f62737472756374696f6e5f696e746572707265746174696f6e5f6964656e746974795820924462071f3992b34c629f4788cc666bd14a87a0b9d0c398feba929ff4dd25f9 diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index 17df53bd..32951afe 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -728,6 +728,15 @@ 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