diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d70c52..c7c4714b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,29 @@ compiler emission, a structurally separate target verifier, Jedit's rope lawpack, `ReplaceRange`, scheduler batch composition, or an independently implemented semantic oracle. +- The executable-operation corridor now has a separate + `AnchoredNodeAttachmentCreateIfAbsent` program (ADR 0024). The original + compare-and-set program remains update-only with its canonical program, + invocation, application-basis, target-profile, and result identities + unchanged. Creation has distinct schema, footprint, basis, result, and + target-profile identities; observes node and attachment occupancy + independently; succeeds only when both are absent; emits one atomic + `UpsertNode` plus `SetAttachment` consequence; charges the node type, + attachment type, and payload; and refuses every occupied target with + `PreconditionMismatch`. Filesystem-WAL recovery validates the exact + installed-program consequence, including operation and slot shape, + program-owned node and attachment types, the atom-only attachment algebra, + replacement bounds, and operations scoped to descended WARP instances. + Descended evaluation now validates the complete parent chain, retains every + portal attachment as both a footprint read and replay input, and charges each + portal-pointer read incrementally before dereferencing that portal, so + out-of-budget ancestry cannot affect evaluation; activation recovery + reconstructs each operation's exact parent state and rejects missing, + duplicate, substituted, or otherwise non-chain portal inputs, and + independently corroborates the creation receipt's total-absence proposition + against both target locations before replay. This closes only the single + anchored-node-plus-alpha-attachment creation gap. It does not establish + Graft-style multi-record mutation or a real Edict application crossing. - `TrustedRuntimeHost` can now admit a previously witnessed mutation for an installed Edict provider package with `admit_provider_contract_submission_v1(...)`. The shared installed-contract diff --git a/README.md b/README.md index 5eb36ada..4cad6add 100644 --- a/README.md +++ b/README.md @@ -254,10 +254,12 @@ canonical ExecutableOperationPackageV1 bytes ``` The first two paths are callback-shaped compatibility infrastructure. The -third proves Echo-owned execution of admitted data-only meaning, but no real -Edict compiler output or Jedit operation uses it yet. The next convergence -crossing must bind a real Jedit-owned Edict operation and lawpack to that -executable-operation package without reintroducing a native implementation. +third proves Echo-owned execution of admitted data-only meaning through +separate update-only compare-and-set and single-node create-if-absent program +profiles. No real Edict compiler output, Jedit operation, or Graft +multi-record mutation uses them yet. The next convergence crossing must bind a +real application-owned Edict operation and lawpack to the executable-operation +package without reintroducing a native implementation. ## Contracts And Boundaries diff --git a/crates/warp-core/src/echo_operation.rs b/crates/warp-core/src/echo_operation.rs index 830946ae..9d3d69bd 100644 --- a/crates/warp-core/src/echo_operation.rs +++ b/crates/warp-core/src/echo_operation.rs @@ -7,13 +7,18 @@ //! compatibility corridor. An executable operation package instead carries the //! complete data-only program interpreted here by Echo. //! -//! The first earned program profile is deliberately small: an invocation -//! anchors one typed node and compares the digest of its typed alpha attachment -//! before replacing that attachment. It has one unique match, a closed -//! attachment algebra, explicit resource bounds, and no callback, function -//! pointer, matcher, executor, or application-specific intrinsic. - -use std::{collections::BTreeMap, sync::Arc}; +//! The first two earned program profiles are deliberately small. One anchors a +//! typed node and compares the digest of its typed alpha attachment before +//! replacing that attachment. The other requires the node and attachment to be +//! entirely absent and creates both atomically (ADR 0024). Each has one unique +//! match, a closed attachment algebra, explicit resource bounds, and no +//! callback, function pointer, matcher, executor, or application-specific +//! intrinsic. + +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use blake3::Hasher; use bytes::Bytes; @@ -30,6 +35,7 @@ use crate::{ head::WriterHeadKey, ident::{EdgeKey, Hash, NodeKey, TypeId}, receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry}, + record::NodeRecord, snapshot::{compute_commit_hash_v2, Snapshot}, tick_patch::{SlotId, TickCommitStatus, TickPatchError, WarpOp, WarpTickPatchV1}, tx::TxId, @@ -51,6 +57,22 @@ 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 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"; +const CREATE_RESULT_SCHEMA: &str = "echo.operation.result.anchored-node-alpha-create-if-absent/v1"; +const CREATE_OBSTRUCTION_SCHEMA: &str = + "echo.operation.obstruction.anchored-node-alpha-create-if-absent/v1"; +const CREATE_RESULT_INTERPRETATION: &str = + "echo.operation.result-interpretation.anchored-node-alpha-create-if-absent/v1"; +const CREATE_OBSTRUCTION_INTERPRETATION: &str = + "echo.operation.obstruction-interpretation.anchored-node-alpha-create-if-absent/v1"; +const CREATE_APPLICATION_BASIS_SCHEMA: &str = + "echo.operation.basis.anchored-node-alpha-create-if-absent/v1"; +const CREATE_TARGET_PROFILE: &str = "echo.operation-target.anchored-node-alpha-create-if-absent/v1"; +const CREATE_ABSENCE_PRECONDITION: &str = "node-and-alpha-attachment-absent/v1"; const INTERPRETER_PROFILE: &str = "echo.operation-interpreter/v1"; const INTRINSIC_PROFILE: &str = "echo.operation-attachment-algebra/v1"; const PACKAGE_ID_DOMAIN: &[u8] = b"echo:operation-package:v1\0"; @@ -64,10 +86,16 @@ const INVOCATION_ADMISSION_ID_DOMAIN: &[u8] = b"echo:operation-invocation-admiss const PRIVATE_EVALUATION_ID_DOMAIN: &[u8] = b"echo:operation-private-evaluation:v1\0"; const PREPARATION_ID_DOMAIN: &[u8] = b"echo:operation-preparation:v1\0"; const RESULT_ID_DOMAIN: &[u8] = b"echo:operation-result:v1\0"; +const CREATE_RESULT_ID_DOMAIN: &[u8] = + b"echo:operation-result-anchored-node-alpha-create-if-absent:v1\0"; const OBSTRUCTION_ID_DOMAIN: &[u8] = b"echo:operation-obstruction:v1\0"; const TERMINAL_OUTCOME_ID_DOMAIN: &[u8] = b"echo:operation-terminal-outcome:v1\0"; const ATOM_VALUE_DOMAIN: &[u8] = b"echo:operation-atom-value:v1\0"; const APPLICATION_BASIS_VALUE_DOMAIN: &[u8] = b"echo:operation-anchored-node-alpha-basis:v1\0"; +const CREATE_APPLICATION_BASIS_DOMAIN: &[u8] = + b"echo:operation-anchored-node-alpha-create-if-absent-basis:v1\0"; +const CREATE_RESULT_ABSENCE_PROPOSITION_DOMAIN: &[u8] = + b"echo:operation-result-anchored-node-alpha-absence-proposition:v1\0"; const FOOTPRINT_DIGEST_DOMAIN: &[u8] = b"echo:operation-footprint:v1\0"; const RECEIPT_DIGEST_DOMAIN: &[u8] = b"echo:operation-receipt:v1\0"; const COMPOSITION_DIGEST_DOMAIN: &[u8] = b"echo:operation-singleton-composition:v1\0"; @@ -272,12 +300,84 @@ pub fn echo_operation_anchored_node_application_basis_v1( ) } +/// Closed occupancy proposition used by the create-if-absent application +/// basis. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EchoOperationAnchoredNodeOccupancyV1 { + /// Neither the node nor its alpha attachment exists. + Absent, + /// The node exists but its alpha attachment does not. + NodeOnly, + /// The alpha attachment exists without its owning node. + AttachmentOnly, + /// Both the node and its alpha attachment exist. + NodeAndAttachment, +} + +impl EchoOperationAnchoredNodeOccupancyV1 { + const fn from_presence(node_present: bool, attachment_present: bool) -> Self { + match (node_present, attachment_present) { + (false, false) => Self::Absent, + (true, false) => Self::NodeOnly, + (false, true) => Self::AttachmentOnly, + (true, true) => Self::NodeAndAttachment, + } + } + + const fn stable_code(self) -> u8 { + match self { + Self::Absent => 0, + Self::NodeOnly => 1, + Self::AttachmentOnly => 2, + Self::NodeAndAttachment => 3, + } + } +} + +/// Returns the create-if-absent profile's canonical application-basis +/// proposition for one exact occupancy state (ADR 0024). +/// +/// This profile observes the node and attachment locations independently, so +/// a bare node and an orphan attachment cannot corroborate as total absence. +#[must_use] +pub fn echo_operation_anchored_node_creation_application_basis_v1( + node: NodeKey, + occupancy: EchoOperationAnchoredNodeOccupancyV1, +) -> EchoOperationApplicationBasisV1 { + let mut hasher = Hasher::new(); + hasher.update(CREATE_APPLICATION_BASIS_DOMAIN); + hasher.update(node.warp_id.as_bytes()); + hasher.update(node.local_id.as_bytes()); + hasher.update(&[occupancy.stable_code()]); + EchoOperationApplicationBasisV1::new( + profile_digest(CREATE_APPLICATION_BASIS_SCHEMA), + hasher.finalize().into(), + ) +} + +/// Returns the create-if-absent profile's total-absence application basis. +#[must_use] +pub fn echo_operation_anchored_node_absent_application_basis_v1( + node: NodeKey, +) -> EchoOperationApplicationBasisV1 { + echo_operation_anchored_node_creation_application_basis_v1( + node, + EchoOperationAnchoredNodeOccupancyV1::Absent, + ) +} + /// Returns the exact target-profile identity implemented by the v1 evaluator. #[must_use] pub fn echo_operation_target_profile_identity_v1() -> Hash { profile_digest(TARGET_PROFILE) } +/// Returns the exact create-if-absent target-profile identity. +#[must_use] +pub fn echo_operation_create_if_absent_target_profile_identity_v1() -> Hash { + profile_digest(CREATE_TARGET_PROFILE) +} + /// A three-axis resource budget for one bounded operation evaluation. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct EchoOperationBudgetV1 { @@ -385,12 +485,15 @@ impl EchoOperationBudgetMeterV1 { pub enum EchoOperationFootprintContractV1 { /// Read one anchored node and alpha attachment; write that same attachment. AnchoredNodeAlphaExact, + /// Read an absent anchored node and alpha attachment; create both. + AnchoredNodeAlphaCreateIfAbsentExact, } impl EchoOperationFootprintContractV1 { const fn coordinate(self) -> &'static str { match self { Self::AnchoredNodeAlphaExact => FOOTPRINT_CONTRACT, + Self::AnchoredNodeAlphaCreateIfAbsentExact => CREATE_FOOTPRINT_CONTRACT, } } } @@ -407,6 +510,15 @@ pub enum EchoOperationProgramV1 { /// Maximum replacement byte count accepted by the program. max_replacement_bytes: u64, }, + /// Create one anchored typed node and alpha attachment only when both are absent. + AnchoredNodeAttachmentCreateIfAbsent { + /// Skeleton node type created by the program. + required_node_type: TypeId, + /// Alpha attachment atom type created by the program. + required_attachment_type: TypeId, + /// Maximum attachment byte count accepted by the program. + max_replacement_bytes: u64, + }, } impl EchoOperationProgramV1 { @@ -424,6 +536,20 @@ impl EchoOperationProgramV1 { } } + /// Creates the bounded create-if-absent operation program. + #[must_use] + pub const fn anchored_node_attachment_create_if_absent( + required_node_type: TypeId, + required_attachment_type: TypeId, + max_replacement_bytes: u64, + ) -> Self { + Self::AnchoredNodeAttachmentCreateIfAbsent { + required_node_type, + required_attachment_type, + max_replacement_bytes, + } + } + /// Returns the exact program identity. pub fn identity(&self) -> Result { Ok(EchoOperationProgramIdV1(domain_hash( @@ -443,6 +569,9 @@ impl EchoOperationProgramV1 { Self::AnchoredNodeAttachmentCompareAndSet { .. } => { EchoOperationFootprintContractV1::AnchoredNodeAlphaExact } + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + EchoOperationFootprintContractV1::AnchoredNodeAlphaCreateIfAbsentExact + } } } @@ -451,47 +580,138 @@ impl EchoOperationProgramV1 { Self::AnchoredNodeAttachmentCompareAndSet { .. } => { EchoOperationBudgetV1::new(4, 64, 32) } + // Steps meter deterministic evaluator stages, not emitted + // `WarpOp`s: node probe, attachment probe, and one atomic + // create consequence. The consequence emits two operations but + // remains one semantic rewrite step. + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + EchoOperationBudgetV1::new(3, 64, 64) + } } } + const fn input_schema(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => INPUT_SCHEMA, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_INPUT_SCHEMA, + } + } + + const fn result_schema(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_SCHEMA, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_SCHEMA, + } + } + + const fn obstruction_schema(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_SCHEMA, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_SCHEMA, + } + } + + const fn result_interpretation(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => RESULT_INTERPRETATION, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_RESULT_INTERPRETATION, + } + } + + const fn obstruction_interpretation(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => OBSTRUCTION_INTERPRETATION, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_OBSTRUCTION_INTERPRETATION, + } + } + + const fn application_basis_schema(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => APPLICATION_BASIS_SCHEMA, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_APPLICATION_BASIS_SCHEMA, + } + } + + const fn target_profile(&self) -> &'static str { + match self { + Self::AnchoredNodeAttachmentCompareAndSet { .. } => TARGET_PROFILE, + Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => CREATE_TARGET_PROFILE, + } + } + + const fn accepts_invocation_kind(&self, kind: EchoOperationInvocationKindV1) -> bool { + matches!( + (self, kind), + ( + Self::AnchoredNodeAttachmentCompareAndSet { .. }, + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { .. } + ) | ( + Self::AnchoredNodeAttachmentCreateIfAbsent { .. }, + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent + ) + ) + } + fn validate_supported_profile(&self) -> Result<(), EchoOperationArtifactErrorV1> { match self { Self::AnchoredNodeAttachmentCompareAndSet { max_replacement_bytes, .. + } + | Self::AnchoredNodeAttachmentCreateIfAbsent { + max_replacement_bytes, + .. } if *max_replacement_bytes == 0 => Err(artifact_error( EchoOperationArtifactErrorKindV1::UnsupportedProgram, "program replacement bound must be nonzero", )), - Self::AnchoredNodeAttachmentCompareAndSet { .. } => Ok(()), + Self::AnchoredNodeAttachmentCompareAndSet { .. } + | Self::AnchoredNodeAttachmentCreateIfAbsent { .. } => Ok(()), } } fn to_value(&self) -> CanonicalValueV1 { - match self { + let (kind, required_node_type, required_attachment_type, max_replacement_bytes) = match self + { Self::AnchoredNodeAttachmentCompareAndSet { required_node_type, required_attachment_type, max_replacement_bytes, - } => map_value([ - ( - "interpreter_profile_identity", - hash_value(profile_digest(INTERPRETER_PROFILE)), - ), - ( - "intrinsic_profile_identity", - hash_value(profile_digest(INTRINSIC_PROFILE)), - ), - ("kind", text_value(PROGRAM_KIND)), - ("max_replacement_bytes", uint_value(*max_replacement_bytes)), - ( - "required_attachment_type", - hash_value(required_attachment_type.0), - ), - ("required_node_type", hash_value(required_node_type.0)), - ("schema", text_value(PROGRAM_SCHEMA)), - ]), - } + } => ( + PROGRAM_KIND, + required_node_type, + required_attachment_type, + max_replacement_bytes, + ), + Self::AnchoredNodeAttachmentCreateIfAbsent { + required_node_type, + required_attachment_type, + max_replacement_bytes, + } => ( + CREATE_PROGRAM_KIND, + required_node_type, + required_attachment_type, + max_replacement_bytes, + ), + }; + map_value([ + ( + "interpreter_profile_identity", + hash_value(profile_digest(INTERPRETER_PROFILE)), + ), + ( + "intrinsic_profile_identity", + hash_value(profile_digest(INTRINSIC_PROFILE)), + ), + ("kind", text_value(kind)), + ("max_replacement_bytes", uint_value(*max_replacement_bytes)), + ( + "required_attachment_type", + hash_value(required_attachment_type.0), + ), + ("required_node_type", hash_value(required_node_type.0)), + ("schema", text_value(PROGRAM_SCHEMA)), + ]) } fn from_canonical_bytes(bytes: &[u8]) -> Result { @@ -509,7 +729,7 @@ impl EchoOperationProgramV1 { ], )?; require_text(&mut fields, "schema", PROGRAM_SCHEMA)?; - require_text(&mut fields, "kind", PROGRAM_KIND)?; + let kind = take_text(&mut fields, "kind")?; let interpreter_profile_identity = take_hash(&mut fields, "interpreter_profile_identity")?; let intrinsic_profile_identity = take_hash(&mut fields, "intrinsic_profile_identity")?; if interpreter_profile_identity != profile_digest(INTERPRETER_PROFILE) @@ -523,11 +743,24 @@ impl EchoOperationProgramV1 { let required_node_type = TypeId(take_hash(&mut fields, "required_node_type")?); let required_attachment_type = TypeId(take_hash(&mut fields, "required_attachment_type")?); let max_replacement_bytes = take_u64(&mut fields, "max_replacement_bytes")?; - let program = Self::anchored_node_attachment_compare_and_set( - required_node_type, - required_attachment_type, - max_replacement_bytes, - ); + let program = match kind.as_str() { + PROGRAM_KIND => Self::anchored_node_attachment_compare_and_set( + required_node_type, + required_attachment_type, + max_replacement_bytes, + ), + CREATE_PROGRAM_KIND => Self::anchored_node_attachment_create_if_absent( + required_node_type, + required_attachment_type, + max_replacement_bytes, + ), + _ => { + return Err(artifact_error( + EchoOperationArtifactErrorKindV1::UnsupportedProgram, + "operation program names an unsupported kind", + )); + } + }; program.validate_supported_profile()?; Ok(program) } @@ -682,6 +915,13 @@ impl ExecutableOperationPackageV1 { budget_ceiling: EchoOperationBudgetV1, program: EchoOperationProgramV1, ) -> Self { + let input_schema_identity = profile_digest(program.input_schema()); + let result_schema_identity = profile_digest(program.result_schema()); + let obstruction_schema_identity = profile_digest(program.obstruction_schema()); + let result_interpretation_identity = profile_digest(program.result_interpretation()); + let obstruction_interpretation_identity = + profile_digest(program.obstruction_interpretation()); + let application_basis_schema_identity = profile_digest(program.application_basis_schema()); Self { operation_coordinate: operation_coordinate.into(), semantic_closure, @@ -689,12 +929,12 @@ impl ExecutableOperationPackageV1 { interpreter_profile_identity: profile_digest(INTERPRETER_PROFILE), intrinsic_profile_identity: profile_digest(INTRINSIC_PROFILE), authority_profile_identity, - input_schema_identity: profile_digest(INPUT_SCHEMA), - result_schema_identity: profile_digest(RESULT_SCHEMA), - obstruction_schema_identity: profile_digest(OBSTRUCTION_SCHEMA), - result_interpretation_identity: profile_digest(RESULT_INTERPRETATION), - obstruction_interpretation_identity: profile_digest(OBSTRUCTION_INTERPRETATION), - application_basis_schema_identity: profile_digest(APPLICATION_BASIS_SCHEMA), + input_schema_identity, + result_schema_identity, + obstruction_schema_identity, + result_interpretation_identity, + obstruction_interpretation_identity, + application_basis_schema_identity, evaluation_basis_schema_identity: profile_digest(BASIS_SCHEMA), footprint_contract_identity: profile_digest(program.footprint_contract().coordinate()), budget_ceiling, @@ -922,32 +1162,32 @@ impl ExecutableOperationPackageV1 { ( "input schema", self.input_schema_identity, - profile_digest(INPUT_SCHEMA), + profile_digest(self.program.input_schema()), ), ( "result schema", self.result_schema_identity, - profile_digest(RESULT_SCHEMA), + profile_digest(self.program.result_schema()), ), ( "obstruction schema", self.obstruction_schema_identity, - profile_digest(OBSTRUCTION_SCHEMA), + profile_digest(self.program.obstruction_schema()), ), ( "result interpretation", self.result_interpretation_identity, - profile_digest(RESULT_INTERPRETATION), + profile_digest(self.program.result_interpretation()), ), ( "obstruction interpretation", self.obstruction_interpretation_identity, - profile_digest(OBSTRUCTION_INTERPRETATION), + profile_digest(self.program.obstruction_interpretation()), ), ( "application basis schema", self.application_basis_schema_identity, - profile_digest(APPLICATION_BASIS_SCHEMA), + profile_digest(self.program.application_basis_schema()), ), ( "evaluation basis schema", @@ -969,7 +1209,7 @@ impl ExecutableOperationPackageV1 { format!("unsupported {label} identity"), )); } - if self.target_profile_identity != profile_digest(TARGET_PROFILE) { + if self.target_profile_identity != profile_digest(self.program.target_profile()) { return Err(artifact_error( EchoOperationArtifactErrorKindV1::UnsupportedTargetProfile, "unsupported Echo target profile identity", @@ -1220,6 +1460,10 @@ impl InstalledEchoOperationV1 { self.program_id } + pub(crate) const fn program(&self) -> &EchoOperationProgramV1 { + &self.program + } + /// Returns the semantic identity bound by the admitted package. #[must_use] pub const fn semantic_identity(&self) -> Hash { @@ -1645,6 +1889,18 @@ impl EchoOperationEvaluationBasisV1 { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EchoOperationInvocationKindV1 { + AnchoredNodeAttachmentCompareAndSet { expected_value_digest: Hash }, + AnchoredNodeAttachmentCreateIfAbsent, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum AnchoredNodeOperationModeV1 { + CompareAndSet { expected_value_digest: Hash }, + CreateIfAbsent, +} + /// Canonical basis-bearing invocation emitted by a generated client/helper. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EchoOperationInvocationV1 { @@ -1654,12 +1910,12 @@ pub struct EchoOperationInvocationV1 { authority_grant_identity: Hash, delegated_budget: EchoOperationBudgetV1, node: NodeKey, - expected_value_digest: Hash, + kind: EchoOperationInvocationKindV1, replacement_bytes: Vec, } impl EchoOperationInvocationV1 { - /// Creates an invocation for the first supported operation program. + /// Creates an invocation for the update-only compare-and-set program. #[allow(clippy::too_many_arguments)] #[must_use] pub fn anchored_node_attachment_compare_and_set( @@ -1679,7 +1935,33 @@ impl EchoOperationInvocationV1 { authority_grant_identity, delegated_budget, node, - expected_value_digest, + kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + expected_value_digest, + }, + replacement_bytes, + } + } + + /// Creates an invocation for the create-if-absent program. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn anchored_node_attachment_create_if_absent( + package_id: EchoOperationPackageIdV1, + operation_coordinate: impl Into, + evaluation_basis: EchoOperationEvaluationBasisV1, + authority_grant_identity: Hash, + delegated_budget: EchoOperationBudgetV1, + node: NodeKey, + replacement_bytes: Vec, + ) -> Self { + Self { + package_id, + operation_coordinate: operation_coordinate.into(), + evaluation_basis, + authority_grant_identity, + delegated_budget, + node, + kind: EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, replacement_bytes, } } @@ -1706,51 +1988,128 @@ impl EchoOperationInvocationV1 { "invocation delegated step budget must be nonzero", )); } - let value = map_value([ - ( - "authority_grant_identity", - hash_value(self.authority_grant_identity), - ), - ("delegated_budget", self.delegated_budget.to_value()), - ("evaluation_basis", self.evaluation_basis.to_value()), - ( - "expected_value_digest", - hash_value(self.expected_value_digest), - ), - ("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(INVOCATION_SCHEMA)), - ("warp_id", hash_value(self.node.warp_id.0)), - ]); + 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 mut fields = Vec::from(common(INVOCATION_SCHEMA)); + fields.push(("expected_value_digest", hash_value(expected_value_digest))); + CanonicalValueV1::Map( + fields + .into_iter() + .map(|(key, value)| (text_value(key), value)) + .collect(), + ) + } + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent => { + let mut fields = Vec::from(common(CREATE_INVOCATION_SCHEMA)); + fields.push(( + "absence_precondition", + text_value(CREATE_ABSENCE_PRECONDITION), + )); + CanonicalValueV1::Map( + fields + .into_iter() + .map(|(key, value)| (text_value(key), value)) + .collect(), + ) + } + }; encode_canonical_cbor_v1(&value).map_err(canonical_error) } fn from_canonical_bytes(bytes: &[u8]) -> Result { let value = decode_canonical_cbor_v1(bytes).map_err(canonical_error)?; - let mut fields = exact_text_map( - value, - &[ - "authority_grant_identity", - "delegated_budget", - "evaluation_basis", - "expected_value_digest", - "node_id", - "operation_coordinate", - "package_id", - "replacement_bytes", - "schema", - "warp_id", - ], + let schema = match &value { + CanonicalValueV1::Map(entries) => entries + .iter() + .find_map(|(key, value)| { + (key == &CanonicalValueV1::Text("schema".to_owned())).then_some(value) + }) + .and_then(|value| match value { + CanonicalValueV1::Text(value) => Some(value.as_str()), + _ => None, + }) + .ok_or_else(|| invalid_structure("invocation schema must be text"))?, + _ => return Err(invalid_structure("artifact root must be a map")), + }; + let (expected_fields, create_if_absent) = 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, + ), + CREATE_INVOCATION_SCHEMA => ( + &[ + "absence_precondition", + "authority_grant_identity", + "delegated_budget", + "evaluation_basis", + "node_id", + "operation_coordinate", + "package_id", + "replacement_bytes", + "schema", + "warp_id", + ][..], + true, + ), + _ => return Err(invalid_structure("unsupported invocation schema")), + }; + let mut fields = exact_text_map(value, expected_fields)?; + require_text( + &mut fields, + "schema", + if create_if_absent { + CREATE_INVOCATION_SCHEMA + } else { + INVOCATION_SCHEMA + }, )?; - require_text(&mut fields, "schema", INVOCATION_SCHEMA)?; + let kind = if create_if_absent { + require_text( + &mut fields, + "absence_precondition", + CREATE_ABSENCE_PRECONDITION, + )?; + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent + } else { + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + expected_value_digest: take_hash(&mut fields, "expected_value_digest")?, + } + }; let invocation = Self { package_id: EchoOperationPackageIdV1(take_hash(&mut fields, "package_id")?), operation_coordinate: take_text(&mut fields, "operation_coordinate")?, @@ -1767,7 +2126,7 @@ impl EchoOperationInvocationV1 { warp_id: crate::WarpId(take_hash(&mut fields, "warp_id")?), local_id: crate::NodeId(take_hash(&mut fields, "node_id")?), }, - expected_value_digest: take_hash(&mut fields, "expected_value_digest")?, + kind, replacement_bytes: take_bytes(&mut fields, "replacement_bytes")?, }; if invocation.operation_coordinate.is_empty() || !invocation.delegated_budget.is_nonzero() { @@ -1827,6 +2186,8 @@ pub enum EchoOperationInvocationAdmissionErrorKindV1 { OperationUnavailable, /// The invocation's public operation coordinate disagrees with the package. OperationCoordinateMismatch, + /// The invocation schema does not match the installed program profile. + OperationProfileMismatch, /// The runtime-owned authority profile disagrees with the package. AuthorityProfileMismatch, /// The invocation's authority grant was not admitted by runtime policy. @@ -1903,6 +2264,12 @@ pub(crate) fn admit_invocation_v1( "invocation operation coordinate differs from installed package", )); } + if !installed.program.accepts_invocation_kind(invocation.kind) { + return Err(invocation_admission_error( + EchoOperationInvocationAdmissionErrorKindV1::OperationProfileMismatch, + "invocation schema differs from the installed program profile", + )); + } if policy.authority_profile_identity != installed.authority_profile_identity { return Err(invocation_admission_error( EchoOperationInvocationAdmissionErrorKindV1::AuthorityProfileMismatch, @@ -2030,6 +2397,19 @@ fn current_application_basis( &atom.bytes, )) } + EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + let store = state + .store(&invocation.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(), + ); + Ok(echo_operation_anchored_node_creation_application_basis_v1( + invocation.node, + occupancy, + )) + } } } @@ -2092,7 +2472,9 @@ pub enum EchoOperationObstructionKindV1 { AttachmentNotAtom, /// The alpha atom has a different declared type. AttachmentTypeMismatch, - /// The current atom digest differs from the invocation precondition. + /// The current atom digest differs from the invocation precondition, or + /// the invocation expected the node and attachment to be entirely + /// absent (create-from-absence) but one or both already exist. PreconditionMismatch, /// Actual resource access exceeded the declared footprint contract. FootprintViolation, @@ -2302,38 +2684,124 @@ pub(crate) fn prepare_operation_v1( return obstruction(EchoOperationObstructionKindV1::BasisChanged); } - match installed.program { - EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { - required_node_type, - required_attachment_type, - max_replacement_bytes, - } => { - let Ok(replacement_len) = u64::try_from(admitted.invocation.replacement_bytes.len()) - else { - return obstruction(EchoOperationObstructionKindV1::ReplacementTooLarge); - }; - if replacement_len > max_replacement_bytes { - return obstruction(EchoOperationObstructionKindV1::ReplacementTooLarge); + let (required_node_type, required_attachment_type, max_replacement_bytes, mode) = + match (&installed.program, admitted.invocation.kind) { + ( + EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { + required_node_type, + required_attachment_type, + max_replacement_bytes, + }, + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCompareAndSet { + expected_value_digest, + }, + ) => ( + *required_node_type, + *required_attachment_type, + *max_replacement_bytes, + AnchoredNodeOperationModeV1::CompareAndSet { + expected_value_digest, + }, + ), + ( + EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + required_node_type, + required_attachment_type, + max_replacement_bytes, + }, + EchoOperationInvocationKindV1::AnchoredNodeAttachmentCreateIfAbsent, + ) => ( + *required_node_type, + *required_attachment_type, + *max_replacement_bytes, + AnchoredNodeOperationModeV1::CreateIfAbsent, + ), + _ => return obstruction(EchoOperationObstructionKindV1::OperationUnavailable), + }; + let Ok(replacement_len) = u64::try_from(admitted.invocation.replacement_bytes.len()) else { + return obstruction(EchoOperationObstructionKindV1::ReplacementTooLarge); + }; + if replacement_len > max_replacement_bytes { + return obstruction(EchoOperationObstructionKindV1::ReplacementTooLarge); + } + let node = admitted.invocation.node; + let mut actual_footprint = Footprint::default(); + let mut budget_meter = EchoOperationBudgetMeterV1::new(admitted.invocation.delegated_budget); + let descent_stack = + match operation_descent_stack_with_portal_reads(state, node.warp_id, |portal| { + if !budget_meter.charge(1, 32, 0) { + return false; + } + actual_footprint.a_read.insert(portal); + true + }) { + Ok(descent_stack) => descent_stack, + Err(kind) => return obstruction(kind), + }; + let Some(store) = state.store(&node.warp_id) else { + return obstruction(EchoOperationObstructionKindV1::NodeMissing); + }; + let declared_footprint = match mode { + AnchoredNodeOperationModeV1::CompareAndSet { .. } => { + anchored_node_compare_and_set_footprint(node, &descent_stack) + } + AnchoredNodeOperationModeV1::CreateIfAbsent => { + anchored_node_create_if_absent_footprint(node, &descent_stack) + } + }; + if !budget_meter.charge(1, 32, 0) { + return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); + } + record_node_read(&mut actual_footprint, node); + let slot = AttachmentKey::node_alpha(node); + let (out_slots, write_ops) = match mode { + AnchoredNodeOperationModeV1::CreateIfAbsent => { + if store.node(&node.local_id).is_some() { + return obstruction(EchoOperationObstructionKindV1::PreconditionMismatch); } - let node = admitted.invocation.node; - let declared_footprint = anchored_node_footprint(node); - let mut actual_footprint = Footprint::default(); - let mut budget_meter = - EchoOperationBudgetMeterV1::new(admitted.invocation.delegated_budget); - let Some(store) = state.store(&node.warp_id) else { - return obstruction(EchoOperationObstructionKindV1::NodeMissing); - }; if !budget_meter.charge(1, 32, 0) { return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); } - record_node_read(&mut actual_footprint, node); + actual_footprint.a_read.insert(slot); + if store.node_attachment(&node.local_id).is_some() { + return obstruction(EchoOperationObstructionKindV1::PreconditionMismatch); + } + let Some(write_bytes) = 64_u64.checked_add(replacement_len) else { + return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); + }; + if !budget_meter.charge(1, 0, write_bytes) { + return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); + } + actual_footprint.n_write.insert(node); + actual_footprint.a_write.insert(slot); + ( + vec![SlotId::Node(node), SlotId::Attachment(slot)], + vec![ + WarpOp::UpsertNode { + node, + record: NodeRecord { + ty: required_node_type, + }, + }, + WarpOp::SetAttachment { + key: slot, + value: Some(AttachmentValue::Atom(AtomPayload::new( + required_attachment_type, + Bytes::from(admitted.invocation.replacement_bytes.clone()), + ))), + }, + ], + ) + } + AnchoredNodeOperationModeV1::CompareAndSet { + expected_value_digest, + } => { let Some(record) = store.node(&node.local_id) else { return obstruction(EchoOperationObstructionKindV1::NodeMissing); }; if record.ty != required_node_type { return obstruction(EchoOperationObstructionKindV1::NodeTypeMismatch); } - let slot = AttachmentKey::node_alpha(node); if !budget_meter.charge(1, 32, 0) { return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); } @@ -2354,7 +2822,7 @@ pub(crate) fn prepare_operation_v1( return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); } if echo_operation_atom_value_digest_v1(atom.type_id, &atom.bytes) - != admitted.invocation.expected_value_digest + != expected_value_digest { return obstruction(EchoOperationObstructionKindV1::PreconditionMismatch); } @@ -2364,16 +2832,8 @@ pub(crate) fn prepare_operation_v1( if !budget_meter.charge(1, 0, write_bytes) { return obstruction(EchoOperationObstructionKindV1::BudgetExceeded); } - let consumed_budget = budget_meter.consumed(); actual_footprint.a_write.insert(slot); - if actual_footprint != declared_footprint { - return obstruction(EchoOperationObstructionKindV1::FootprintViolation); - } - let patch = WarpTickPatchV1::new( - policy_id, - installed.installed_operation_id.as_hash(), - TickCommitStatus::Committed, - vec![SlotId::Node(node), SlotId::Attachment(slot)], + ( vec![SlotId::Attachment(slot)], vec![WarpOp::SetAttachment { key: slot, @@ -2382,57 +2842,69 @@ pub(crate) fn prepare_operation_v1( Bytes::from(admitted.invocation.replacement_bytes.clone()), ))), }], - ); - let declared_footprint_digest = footprint_digest(&declared_footprint); - let actual_footprint_digest = footprint_digest(&actual_footprint); - let replacement_value_digest = echo_operation_atom_value_digest_v1( - required_attachment_type, - &admitted.invocation.replacement_bytes, - ); - let result_id = operation_result_id( - installed, - &admitted.invocation, - admitted.invocation.expected_value_digest, - replacement_value_digest, - patch.digest(), - ); - let private_evaluation_id = private_evaluation_id_from_parts( - installed.installed_operation_id, - installed.program_id, - admitted.admission_id, - admitted.invocation_id, - current_basis.identity(), - declared_footprint_digest, - actual_footprint_digest, - consumed_budget, - patch.digest(), - result_id, - ); - let preparation_id = preparation_id(private_evaluation_id, patch.digest(), result_id); - EchoOperationPreparationV1::Prepared(Box::new(PreparedEchoOperationV1 { - installed: installed.clone(), - invocation: admitted.invocation, - invocation_id, - canonical_invocation_bytes: admitted.canonical_invocation_bytes, - invocation_admission_policy_id: admitted.admission_policy_id, - invocation_admission_maximum_budget: admitted - .admission_policy - .maximum_delegated_budget, - invocation_admission_id: admitted.admission_id, - evaluation_basis: current_basis, - declared_footprint, - actual_footprint, - declared_footprint_digest, - actual_footprint_digest, - consumed_budget, - patch, - result_id, - private_evaluation_id, - preparation_id, - evaluation_authority: admitted.evaluation_authority, - })) + ) } - } + }; + let consumed_budget = budget_meter.consumed(); + if actual_footprint != declared_footprint { + return obstruction(EchoOperationObstructionKindV1::FootprintViolation); + } + let mut in_slots = vec![SlotId::Node(node), SlotId::Attachment(slot)]; + in_slots.extend(descent_stack.iter().copied().map(SlotId::Attachment)); + let patch = WarpTickPatchV1::new( + policy_id, + installed.installed_operation_id.as_hash(), + TickCommitStatus::Committed, + in_slots, + out_slots, + write_ops, + ); + let declared_footprint_digest = footprint_digest(&declared_footprint); + let actual_footprint_digest = footprint_digest(&actual_footprint); + let replacement_value_digest = echo_operation_atom_value_digest_v1( + required_attachment_type, + &admitted.invocation.replacement_bytes, + ); + let result_id = operation_result_id( + installed, + &admitted.invocation, + mode, + replacement_value_digest, + patch.digest(), + ); + let private_evaluation_id = private_evaluation_id_from_parts( + installed.installed_operation_id, + installed.program_id, + admitted.admission_id, + admitted.invocation_id, + current_basis.identity(), + declared_footprint_digest, + actual_footprint_digest, + consumed_budget, + patch.digest(), + result_id, + ); + let preparation_id = preparation_id(private_evaluation_id, patch.digest(), result_id); + EchoOperationPreparationV1::Prepared(Box::new(PreparedEchoOperationV1 { + installed: installed.clone(), + invocation: admitted.invocation, + invocation_id, + canonical_invocation_bytes: admitted.canonical_invocation_bytes, + invocation_admission_policy_id: admitted.admission_policy_id, + invocation_admission_maximum_budget: admitted.admission_policy.maximum_delegated_budget, + invocation_admission_id: admitted.admission_id, + evaluation_basis: current_basis, + declared_footprint, + actual_footprint, + declared_footprint_digest, + actual_footprint_digest, + consumed_budget, + patch, + result_id, + private_evaluation_id, + preparation_id, + evaluation_authority: admitted.evaluation_authority, + })) } /// Terminal posture bound by an executable-operation receipt. @@ -3670,17 +4142,33 @@ fn invocation_admission_id( fn operation_result_id( installed: &InstalledEchoOperationV1, invocation: &EchoOperationInvocationV1, - previous_value_digest: Hash, + mode: AnchoredNodeOperationModeV1, replacement_value_digest: Hash, patch_digest: Hash, ) -> EchoOperationResultIdV1 { let mut hasher = Hasher::new(); - 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(&previous_value_digest); + match mode { + AnchoredNodeOperationModeV1::CompareAndSet { + expected_value_digest, + } => { + // This is the exact pre-ADR-0024 update identity layout. Do not + // add a variant tag or otherwise widen these hash inputs. + 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(&expected_value_digest); + } + AnchoredNodeOperationModeV1::CreateIfAbsent => { + hasher.update(CREATE_RESULT_ID_DOMAIN); + 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(CREATE_RESULT_ABSENCE_PROPOSITION_DOMAIN); + } + } hasher.update(&replacement_value_digest); hasher.update(&patch_digest); EchoOperationResultIdV1(hasher.finalize().into()) @@ -3744,15 +4232,93 @@ fn obstruction_kind_code(kind: EchoOperationObstructionKindV1) -> u8 { } } -fn anchored_node_footprint(node: NodeKey) -> Footprint { +fn anchored_node_compare_and_set_footprint( + node: NodeKey, + descent_stack: &[AttachmentKey], +) -> Footprint { let mut footprint = Footprint::default(); record_node_read(&mut footprint, node); + for portal in descent_stack { + footprint.a_read.insert(*portal); + } let attachment = AttachmentKey::node_alpha(node); footprint.a_read.insert(attachment); footprint.a_write.insert(attachment); footprint } +fn anchored_node_create_if_absent_footprint( + node: NodeKey, + descent_stack: &[AttachmentKey], +) -> Footprint { + let mut footprint = anchored_node_compare_and_set_footprint(node, descent_stack); + footprint.n_write.insert(node); + footprint +} + +pub(crate) fn operation_descent_stack( + state: &WorldlineState, + target_warp: crate::WarpId, +) -> Option> { + operation_descent_stack_with_portal_reads(state, target_warp, |_| true).ok() +} + +fn operation_descent_stack_with_portal_reads( + state: &WorldlineState, + target_warp: crate::WarpId, + mut read_portal: impl FnMut(AttachmentKey) -> bool, +) -> Result, EchoOperationObstructionKindV1> { + let mut current_warp = target_warp; + let mut visited = BTreeSet::new(); + let mut reversed = Vec::new(); + + loop { + if !visited.insert(current_warp) { + return Err(EchoOperationObstructionKindV1::FootprintViolation); + } + let instance = state + .warp_state() + .instance(¤t_warp) + .ok_or(EchoOperationObstructionKindV1::FootprintViolation)?; + let Some(parent) = instance.parent else { + if current_warp != state.root().warp_id { + return Err(EchoOperationObstructionKindV1::FootprintViolation); + } + reversed.reverse(); + return Ok(reversed); + }; + if !read_portal(parent) { + return Err(EchoOperationObstructionKindV1::BudgetExceeded); + } + let parent_warp = match parent.owner { + crate::AttachmentOwner::Node(node) => { + if state + .store(&node.warp_id) + .ok_or(EchoOperationObstructionKindV1::FootprintViolation)? + .node_attachment(&node.local_id) + != Some(&AttachmentValue::Descend(current_warp)) + { + return Err(EchoOperationObstructionKindV1::FootprintViolation); + } + node.warp_id + } + crate::AttachmentOwner::Edge(edge) => { + if state + .store(&edge.warp_id) + .ok_or(EchoOperationObstructionKindV1::FootprintViolation)? + .edge_attachment(&edge.local_id) + != Some(&AttachmentValue::Descend(current_warp)) + { + return Err(EchoOperationObstructionKindV1::FootprintViolation); + } + edge.warp_id + } + }; + reversed.push(parent); + current_warp = parent_warp; + } +} + fn record_node_read(footprint: &mut Footprint, node: NodeKey) { footprint.n_read.insert(node); footprint.factor_mask |= 1_u64 << (node.local_id.0[0] & 63); @@ -4196,6 +4762,275 @@ mod tests { } } + #[test] + fn descended_creation_reads_and_retains_its_portal_chain() { + let root_warp = crate::make_warp_id("operation-descended-root"); + let root_node = crate::make_node_id("operation-descended-root-node"); + let root = NodeKey { + warp_id: root_warp, + local_id: root_node, + }; + let root_portal = AttachmentKey::node_alpha(root); + let upper_warp = crate::make_warp_id("operation-descended-upper"); + let upper_root = crate::make_node_id("operation-descended-upper-root"); + let upper_root_key = NodeKey { + warp_id: upper_warp, + local_id: upper_root, + }; + let upper_portal = AttachmentKey::node_alpha(upper_root_key); + let middle_warp = crate::make_warp_id("operation-descended-middle"); + let middle_root = crate::make_node_id("operation-descended-middle-root"); + let middle_root_key = NodeKey { + warp_id: middle_warp, + local_id: middle_root, + }; + let middle_portal = AttachmentKey::node_alpha(middle_root_key); + let child_warp = crate::make_warp_id("operation-descended-child"); + let child_root = crate::make_node_id("operation-descended-child-root"); + let target = NodeKey { + warp_id: child_warp, + local_id: crate::make_node_id("operation-descended-target"), + }; + + let mut root_store = crate::GraphStore::new(root_warp); + root_store.insert_node( + root_node, + NodeRecord { + ty: crate::make_type_id("operation-descended-root-type"), + }, + ); + root_store.set_node_attachment(root_node, Some(AttachmentValue::Descend(upper_warp))); + let mut upper_store = crate::GraphStore::new(upper_warp); + upper_store.insert_node( + upper_root, + NodeRecord { + ty: crate::make_type_id("operation-descended-upper-root-type"), + }, + ); + upper_store.set_node_attachment(upper_root, Some(AttachmentValue::Descend(middle_warp))); + let mut middle_store = crate::GraphStore::new(middle_warp); + middle_store.insert_node( + middle_root, + NodeRecord { + ty: crate::make_type_id("operation-descended-middle-root-type"), + }, + ); + middle_store.set_node_attachment(middle_root, Some(AttachmentValue::Descend(child_warp))); + let mut child_store = crate::GraphStore::new(child_warp); + child_store.insert_node( + child_root, + NodeRecord { + ty: crate::make_type_id("operation-descended-child-root-type"), + }, + ); + let mut warp_state = crate::WarpState::new(); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: root_warp, + root_node, + parent: None, + }, + root_store, + ); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: upper_warp, + root_node: upper_root, + parent: Some(root_portal), + }, + upper_store, + ); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: middle_warp, + root_node: middle_root, + parent: Some(upper_portal), + }, + middle_store, + ); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: child_warp, + root_node: child_root, + parent: Some(middle_portal), + }, + child_store, + ); + let state = WorldlineState::new(warp_state, root).expect("the descended fixture is lawful"); + + let operation_coordinate = "echo.fixture.DescendedCreateIfAbsent.v1"; + let authority_profile = digest(40); + let package = ExecutableOperationPackageV1::new( + operation_coordinate, + EchoOperationSemanticClosureV1::new( + digest(41), + digest(42), + digest(43), + digest(44), + "echo.fixture.DescendedSchema.v1", + digest(45), + "echo.fixture.DescendedLawpack.v1", + digest(46), + ), + 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( + crate::make_type_id("operation-descended-created-node"), + crate::make_type_id("operation-descended-created-atom"), + 1_024, + ), + ); + let package_bytes = package.to_canonical_bytes().expect("package encodes"); + let package_id = echo_operation_package_id_v1(&package_bytes); + let installed = installed_from_admitted( + admit_package_v1( + &EchoOperationAdmissionPolicyV1::exact( + package_id, + operation_coordinate, + authority_profile, + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ), + package_bytes, + ) + .expect("package admits"), + ) + .expect("package installs"); + + let writer_head = WriterHeadKey { + worldline_id: crate::WorldlineId::from_bytes(digest(47)), + head_id: crate::HeadId::from_bytes(digest(48)), + }; + let evaluation_basis = EchoOperationEvaluationBasisV1::new( + writer_head, + WorldlineTick::ZERO, + None, + state.state_root(), + digest(49), + echo_operation_anchored_node_absent_application_basis_v1(target), + ); + let authority_grant = digest(50); + let invocation = EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + installed.package_id, + operation_coordinate, + evaluation_basis, + authority_grant, + EchoOperationBudgetV1::new(6, 160, 71), + target, + b"created".to_vec(), + ); + let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new( + authority_profile, + authority_grant, + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ); + let evaluation_authority = EchoOperationEvaluationAuthorityV1::new(); + let admitted = admit_invocation_v1( + Some(&installed), + invocation_policy, + &invocation_bytes, + evaluation_basis, + &state, + evaluation_authority.clone(), + ) + .expect("the descended invocation admits"); + let EchoOperationPreparationV1::Prepared(prepared) = prepare_operation_v1( + Some(&installed), + admitted, + evaluation_basis, + &state, + crate::POLICY_ID_NO_POLICY_V0, + &evaluation_authority, + ) else { + panic!("the descended invocation prepares"); + }; + + for portal in [root_portal, upper_portal, middle_portal] { + assert!( + prepared + .actual_footprint() + .a_read + .iter() + .any(|key| key == &portal), + "a descended creation must read every portal that makes its target reachable" + ); + assert!( + prepared + .patch() + .in_slots() + .contains(&SlotId::Attachment(portal)), + "the replayable patch must retain every portal-chain dependency" + ); + } + assert_eq!( + prepared.consumed_budget(), + EchoOperationBudgetV1::new(6, 160, 71), + "each portal pointer read must be charged as a bounded evaluator step" + ); + + let mut budget_limited_state = state.clone(); + budget_limited_state + .warp_state + .store_mut(&root_warp) + .expect("the fixture retains its root store") + .set_node_attachment( + root_node, + Some(AttachmentValue::Atom(AtomPayload::new( + crate::make_type_id("operation-descended-out-of-budget-corruption"), + Bytes::from_static(b"must-not-be-read"), + ))), + ); + let budget_limited_basis = EchoOperationEvaluationBasisV1::new( + writer_head, + WorldlineTick::ZERO, + None, + budget_limited_state.state_root(), + digest(51), + echo_operation_anchored_node_absent_application_basis_v1(target), + ); + let budget_limited_invocation = + EchoOperationInvocationV1::anchored_node_attachment_create_if_absent( + installed.package_id, + operation_coordinate, + budget_limited_basis, + authority_grant, + EchoOperationBudgetV1::new(3, 64, 71), + target, + b"created".to_vec(), + ); + let admitted = admit_invocation_v1( + Some(&installed), + EchoOperationInvocationAdmissionPolicyV1::new( + authority_profile, + authority_grant, + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ), + &budget_limited_invocation + .to_canonical_bytes() + .expect("budget-limited invocation encodes"), + budget_limited_basis, + &budget_limited_state, + evaluation_authority.clone(), + ) + .expect("the budget-limited invocation admits"); + let EchoOperationPreparationV1::Obstructed(obstruction) = prepare_operation_v1( + Some(&installed), + admitted, + budget_limited_basis, + &budget_limited_state, + crate::POLICY_ID_NO_POLICY_V0, + &evaluation_authority, + ) else { + panic!("the portal traversal must stop at its delegated read allowance"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::BudgetExceeded, + "out-of-budget portal state must not influence the obstruction" + ); + } + #[test] fn public_artifact_encoders_refuse_values_their_decoders_refuse() { let invalid_program = EchoOperationProgramV1::anchored_node_attachment_compare_and_set( @@ -4563,4 +5398,172 @@ mod tests { EchoOperationArtifactErrorKindV1::InvalidStructure ); } + + #[test] + fn operation_result_id_preserves_the_legacy_hash_for_updates() { + // The compare-and-set program keeps the exact pre-ADR-0024 hash + // 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( + installed.package_id(), + installed.operation_coordinate(), + EchoOperationEvaluationBasisV1::new( + WriterHeadKey { + worldline_id: crate::WorldlineId::from_bytes(digest(21)), + head_id: crate::HeadId::from_bytes(digest(22)), + }, + WorldlineTick::ZERO, + None, + digest(23), + digest(24), + EchoOperationApplicationBasisV1::new(digest(25), digest(26)), + ), + digest(27), + EchoOperationBudgetV1::new(8, 512, 512), + node, + digest(28), + Vec::new(), + ); + let replacement_value_digest = digest(29); + let patch_digest = digest(30); + let previous = digest(28); + + let mut expected = Hasher::new(); + 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(&previous); + expected.update(&replacement_value_digest); + expected.update(&patch_digest); + let expected_some = EchoOperationResultIdV1(expected.finalize().into()); + + assert_eq!( + operation_result_id( + &installed, + &invocation, + AnchoredNodeOperationModeV1::CompareAndSet { + expected_value_digest: previous, + }, + replacement_value_digest, + patch_digest, + ), + expected_some, + "compare-and-set must hash identically to the legacy untagged digest layout" + ); + } + + #[test] + 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( + required_node_type, + required_attachment_type, + 1_024, + ); + let expected_program = encode_canonical_cbor_v1(&map_value([ + ( + "interpreter_profile_identity", + hash_value(profile_digest("echo.operation-interpreter/v1")), + ), + ( + "intrinsic_profile_identity", + hash_value(profile_digest("echo.operation-attachment-algebra/v1")), + ), + ( + "kind", + text_value("anchored-node-attachment-compare-and-set/v1"), + ), + ("max_replacement_bytes", uint_value(1_024)), + ( + "required_attachment_type", + hash_value(required_attachment_type.0), + ), + ("required_node_type", hash_value(required_node_type.0)), + ("schema", text_value("echo.operation-program/v1")), + ])) + .expect("the legacy program fixture encodes"); + assert_eq!( + program.to_canonical_bytes().expect("program encodes"), + expected_program, + "the legacy update program bytes are a compatibility fixture" + ); + assert_eq!( + program.identity().expect("program has an identity"), + EchoOperationProgramIdV1(domain_hash( + b"echo:operation-program:v1\0", + &expected_program, + )) + ); + + let node = node_key_from_bytes([33; 64]); + let basis = EchoOperationEvaluationBasisV1::new( + WriterHeadKey { + worldline_id: crate::WorldlineId::from_bytes(digest(34)), + head_id: crate::HeadId::from_bytes(digest(35)), + }, + WorldlineTick::from_raw(7), + Some(GlobalTick::from_raw(11)), + digest(36), + digest(37), + EchoOperationApplicationBasisV1::new(digest(38), digest(39)), + ); + let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set( + EchoOperationPackageIdV1(digest(40)), + "echo.fixture.LegacyCompareAndSet.v1", + basis, + digest(41), + EchoOperationBudgetV1::new(4, 70, 37), + node, + digest(42), + b"after".to_vec(), + ); + let expected_invocation = encode_canonical_cbor_v1(&map_value([ + ("authority_grant_identity", hash_value(digest(41))), + ( + "delegated_budget", + EchoOperationBudgetV1::new(4, 70, 37).to_value(), + ), + ("evaluation_basis", basis.to_value()), + ("expected_value_digest", hash_value(digest(42))), + ("node_id", hash_value(node.local_id.0)), + ( + "operation_coordinate", + text_value("echo.fixture.LegacyCompareAndSet.v1"), + ), + ("package_id", hash_value(digest(40))), + ( + "replacement_bytes", + CanonicalValueV1::Bytes(b"after".to_vec()), + ), + ("schema", text_value("echo.operation-invocation/v1")), + ("warp_id", hash_value(node.warp_id.0)), + ])) + .expect("the legacy invocation fixture encodes"); + assert_eq!( + invocation.to_canonical_bytes().expect("invocation encodes"), + expected_invocation, + "the legacy update invocation bytes are a compatibility fixture" + ); + + let mut widened_value = + decode_canonical_cbor_v1(&expected_invocation).expect("fixture decodes"); + let CanonicalValueV1::Map(fields) = &mut widened_value else { + panic!("fixture is a map"); + }; + let expected_digest = fields + .iter_mut() + .find(|(key, _)| key == &text_value("expected_value_digest")) + .expect("fixture carries the update precondition"); + expected_digest.1 = CanonicalValueV1::Null; + let widened_bytes = + encode_canonical_cbor_v1(&widened_value).expect("mutated fixture re-encodes"); + assert!( + EchoOperationInvocationV1::from_canonical_bytes(&widened_bytes).is_err(), + "null must never gain creation meaning under the legacy invocation schema" + ); + } } diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 2a5b5cd9..02a0ab7b 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -234,11 +234,15 @@ pub use dynamic_binding::{ StructuredBindingRuntime, StructuredRuntimeBindings, }; pub use echo_operation::{ - echo_operation_anchored_node_application_basis_v1, echo_operation_atom_value_digest_v1, - echo_operation_package_id_v1, echo_operation_target_profile_identity_v1, - AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackageV1, - EchoOperationAdmissionErrorKindV1, EchoOperationAdmissionErrorV1, - EchoOperationAdmissionPolicyV1, EchoOperationApplicationBasisV1, + echo_operation_anchored_node_absent_application_basis_v1, + echo_operation_anchored_node_application_basis_v1, + echo_operation_anchored_node_creation_application_basis_v1, + echo_operation_atom_value_digest_v1, + echo_operation_create_if_absent_target_profile_identity_v1, echo_operation_package_id_v1, + echo_operation_target_profile_identity_v1, AdmittedEchoOperationInvocationV1, + AdmittedExecutableOperationPackageV1, EchoOperationAdmissionErrorKindV1, + EchoOperationAdmissionErrorV1, EchoOperationAdmissionPolicyV1, + EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, EchoOperationArtifactErrorKindV1, EchoOperationArtifactErrorV1, EchoOperationBudgetV1, EchoOperationCommitErrorV1, EchoOperationEvaluationBasisIdV1, EchoOperationEvaluationBasisV1, EchoOperationExecutionEvidenceV1, EchoOperationFootprintContractV1, diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index 5f7f6796..87d16c4e 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -44,9 +44,9 @@ use crate::{ decode_invocation_route_v1, genesis_commit_id, install_recovered_v1, installed_from_admitted, not_committed_basis_changed, not_committed_evaluation_authority_mismatch, not_committed_installation_unavailable, - prepare_operation_v1, recover_committed_execution_receipt_v1, recover_installation_v1, - retain_committed_execution_v1, retain_installation_v1, validate_receipt_installation_v1, - EchoOperationEvaluationAuthorityV1, + operation_descent_stack, prepare_operation_v1, recover_committed_execution_receipt_v1, + recover_installation_v1, retain_committed_execution_v1, retain_installation_v1, + validate_receipt_installation_v1, EchoOperationEvaluationAuthorityV1, }, provider_contract::admit_provider_contract_package_v1, AdmittedEchoOperationInvocationV1, AdmittedExecutableOperationPackageV1, @@ -903,7 +903,14 @@ impl TrustedRuntimeHost { (material, entry) }; + let installed = self + .engine + .installed_echo_operation_package_v1(prepared.package_id()); if let Some(runtime_wal) = self.runtime_wal.as_mut() { + let installed = + installed.ok_or(TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "committed operation installation is unavailable for WAL validation", + })?; let state_delta = WalRuntimeStateDeltaRecord::from_provenance_entry( material.tick_receipt.digest(), None, @@ -917,6 +924,7 @@ impl TrustedRuntimeHost { retained_execution, &state_delta, state_delta_digest, + installed, ) { if !runtime_wal.recover_filesystem_executable_operation_tick_after_error( material.evidence.receipt().digest(), @@ -1671,6 +1679,14 @@ fn ensure_runtime_authority_is_durable( }); } + let mut recovered_provenance = provenance.clone(); + restore_provenance_entries(&mut recovered_provenance, &recovery.provenance_entries).map_err( + |_| TrustedRuntimeWalError::RuntimeAuthorityNotDurable { + gap: RuntimeWalActivationGap::Provenance, + }, + )?; + validate_recovered_echo_operation_parent_states(runtime, &recovered_provenance, recovery)?; + let recovered_submissions = recovery .witnessed_submissions .records() @@ -2376,6 +2392,7 @@ impl TrustedRuntimeWal { retained_execution_bytes: Vec, state_delta: &WalRuntimeStateDeltaRecord, state_delta_digest: Hash, + installed: &InstalledEchoOperationV1, ) -> Result { let retained_receipt = recover_committed_execution_receipt_v1(&retained_execution_bytes)?; if &retained_receipt != receipt || state_delta.digest()? != state_delta_digest { @@ -2383,7 +2400,8 @@ impl TrustedRuntimeWal { detail: "live operation material disagrees with retained bytes", }); } - validate_operation_receipt_state_delta(receipt, state_delta)?; + validate_receipt_installation_v1(receipt, installed)?; + validate_operation_receipt_state_delta(receipt, state_delta, installed)?; let next_receipt_frontier = executable_operation_receipt_frontier_digest( self.executable_operation_receipt_frontier_digest, receipt.digest(), @@ -3137,7 +3155,7 @@ fn recover_echo_operation_material( }, )?; validate_receipt_installation_v1(&receipt, installed)?; - validate_operation_receipt_state_delta(&receipt, &state_delta)?; + validate_operation_receipt_state_delta(&receipt, &state_delta, installed)?; validate_operation_receipt_parent_material( receipt.evaluation_basis(), state_delta.provenance_entry(), @@ -3163,34 +3181,166 @@ fn recover_echo_operation_material( }) } -fn operation_patch_scope_v1(patch: &crate::WorldlineTickPatchV1) -> Option { - let [crate::WarpOp::SetAttachment { key, .. }] = patch.ops.as_slice() else { - return None; - }; - let crate::AttachmentOwner::Node(node) = key.owner else { +/// Recognizes only the canonical patch consequence selected by the exact +/// installed program. Update and create-if-absent are separate executable +/// programs; recovery must not infer meaning from an arbitrary patch that +/// merely resembles one program's operation and slot silhouette. +/// +/// `patch.warp_id` is the parent worldline root, while a scoped operation may +/// target a node in a descended WARP instance. The exact `NodeKey` carried by +/// the operation and slots therefore owns operation scope; root validation is +/// performed independently by the surrounding provenance checks. +fn operation_patch_scope_v1( + patch: &crate::WorldlineTickPatchV1, + program: &crate::EchoOperationProgramV1, +) -> Option { + match program { + crate::EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { + required_attachment_type, + max_replacement_bytes, + .. + } => match patch.ops.as_slice() { + [crate::WarpOp::SetAttachment { + key, + value: Some(crate::AttachmentValue::Atom(atom)), + }] => { + let crate::AttachmentOwner::Node(node) = key.owner else { + return None; + }; + let attachment_slot = crate::AttachmentKey::node_alpha(node); + if *key != attachment_slot + || atom.type_id != *required_attachment_type + || u64::try_from(atom.bytes.len()) + .map_or(true, |len| len > *max_replacement_bytes) + || !operation_patch_inputs_match_v1(patch, node, attachment_slot) + || patch.out_slots.as_slice() != [crate::SlotId::Attachment(attachment_slot)] + { + return None; + } + Some(node) + } + _ => None, + }, + crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + required_node_type, + required_attachment_type, + max_replacement_bytes, + } => match patch.ops.as_slice() { + [crate::WarpOp::UpsertNode { node, record }, crate::WarpOp::SetAttachment { + key, + value: Some(crate::AttachmentValue::Atom(atom)), + }] => { + let node = *node; + let crate::AttachmentOwner::Node(attachment_node) = key.owner else { + return None; + }; + let attachment_slot = crate::AttachmentKey::node_alpha(node); + if attachment_node != node + || *key != attachment_slot + || record.ty != *required_node_type + || atom.type_id != *required_attachment_type + || u64::try_from(atom.bytes.len()) + .map_or(true, |len| len > *max_replacement_bytes) + || !operation_patch_inputs_match_v1(patch, node, attachment_slot) + || patch.out_slots.as_slice() + != [ + crate::SlotId::Node(node), + crate::SlotId::Attachment(attachment_slot), + ] + { + return None; + } + Some(node) + } + _ => None, + }, + } +} + +fn operation_patch_inputs_match_v1( + patch: &crate::WorldlineTickPatchV1, + node: crate::NodeKey, + attachment_slot: crate::AttachmentKey, +) -> bool { + let mut has_node = false; + let mut has_attachment = false; + let mut seen_portals = std::collections::BTreeSet::new(); + let mut has_root_portal = false; + + for slot in &patch.in_slots { + match *slot { + crate::SlotId::Node(candidate) if candidate == node && !has_node => { + has_node = true; + } + crate::SlotId::Attachment(candidate) + if candidate == attachment_slot && !has_attachment => + { + has_attachment = true; + } + crate::SlotId::Attachment(portal) + if operation_attachment_owner_warp_v1(portal) != node.warp_id => + { + if !seen_portals.insert(portal) { + return false; + } + has_root_portal |= operation_attachment_owner_warp_v1(portal) == patch.warp_id; + } + _ => return false, + } + } + + has_node + && has_attachment + && if node.warp_id == patch.warp_id { + seen_portals.is_empty() + } else { + !seen_portals.is_empty() && has_root_portal + } +} + +fn operation_patch_scope_in_parent_state_v1( + patch: &crate::WorldlineTickPatchV1, + program: &crate::EchoOperationProgramV1, + parent_state: &crate::WorldlineState, +) -> Option { + let node = operation_patch_scope_v1(patch, program)?; + if patch.warp_id != parent_state.root().warp_id { return None; - }; + } + let descent_stack = operation_descent_stack(parent_state, node.warp_id)?; + if matches!( + program, + crate::EchoOperationProgramV1::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() { + return None; + } + } let attachment_slot = crate::AttachmentKey::node_alpha(node); - if *key != attachment_slot - || patch.warp_id != node.warp_id - || patch.in_slots.as_slice() - != [ - crate::SlotId::Node(node), - crate::SlotId::Attachment(attachment_slot), - ] - || patch.out_slots.as_slice() != [crate::SlotId::Attachment(attachment_slot)] - { - return None; + let expected_inputs = std::iter::once(crate::SlotId::Node(node)) + .chain(std::iter::once(crate::SlotId::Attachment(attachment_slot))) + .chain(descent_stack.into_iter().map(crate::SlotId::Attachment)) + .collect::>(); + let actual_inputs = patch.in_slots.iter().copied().collect::>(); + (patch.in_slots.len() == expected_inputs.len() && actual_inputs == expected_inputs) + .then_some(node) +} + +fn operation_attachment_owner_warp_v1(key: crate::AttachmentKey) -> crate::WarpId { + match key.owner { + crate::AttachmentOwner::Node(node) => node.warp_id, + crate::AttachmentOwner::Edge(edge) => edge.warp_id, } - Some(node) } fn operation_tick_binds_patch_v1( tick_receipt: &crate::TickReceipt, patch: &crate::WorldlineTickPatchV1, expected_rule_id: Hash, + program: &crate::EchoOperationProgramV1, ) -> bool { - let Some(expected_scope) = operation_patch_scope_v1(patch) else { + let Some(expected_scope) = operation_patch_scope_v1(patch, program) else { return false; }; matches!( @@ -3205,9 +3355,114 @@ fn operation_tick_binds_patch_v1( ) } +fn operation_tick_scope_in_parent_state_v1( + tick_receipt: &crate::TickReceipt, + patch: &crate::WorldlineTickPatchV1, + expected_rule_id: Hash, + program: &crate::EchoOperationProgramV1, + parent_state: &crate::WorldlineState, +) -> Option { + let expected_scope = operation_patch_scope_in_parent_state_v1(patch, program, parent_state)?; + matches!( + tick_receipt.entries(), + [tick_entry] + if patch.rule_pack_id() == expected_rule_id + && tick_entry.rule_id == expected_rule_id + && tick_entry.scope == expected_scope + && tick_entry.scope_hash == crate::scope_hash(&expected_rule_id, &expected_scope) + && tick_entry.disposition == crate::TickReceiptDisposition::Applied + && tick_receipt.blocked_by(0).is_empty() + ) + .then_some(expected_scope) +} + +fn operation_application_basis_matches_scope_v1( + program: &crate::EchoOperationProgramV1, + node: crate::NodeKey, + application_basis: EchoOperationApplicationBasisV1, +) -> bool { + match program { + crate::EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet { .. } => true, + crate::EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { .. } => { + application_basis + == crate::echo_operation_anchored_node_absent_application_basis_v1(node) + } + } +} + +fn validate_recovered_echo_operation_parent_states( + runtime: &WorldlineRuntime, + recovered_provenance: &ProvenanceService, + recovery: &TrustedRuntimeWalRecovery, +) -> Result<(), TrustedRuntimeWalError> { + let installations = recovery + .installed_echo_operations + .iter() + .map(|installed| (installed.package_id(), installed)) + .collect::>(); + let entries = recovery + .provenance_entries + .iter() + .map(|entry| ((entry.worldline_id, entry.worldline_tick), entry)) + .collect::>(); + + for receipt in &recovery.echo_operation_receipts { + let basis = receipt.evaluation_basis(); + let worldline_id = basis.writer_head().worldline_id; + let frontier = runtime.worldlines().get(&worldline_id).ok_or( + TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "operation receipt names an unavailable recovery worldline", + }, + )?; + let parent_state = recovered_provenance + .replay_worldline_state_at(worldline_id, frontier.state(), basis.worldline_tick()) + .map_err(|_| TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "operation receipt parent state cannot be reconstructed", + })?; + let entry = entries.get(&(worldline_id, basis.worldline_tick())).ok_or( + TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "operation receipt has no recovered state transition", + }, + )?; + let installed = installations.get(&receipt.package_id()).ok_or( + TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "operation receipt has no recovered installation", + }, + )?; + let exact_parent_state_binding = entry + .patch + .as_ref() + .zip(entry.tick_receipt.as_ref()) + .is_some_and(|(patch, tick_receipt)| { + let exact_scope = operation_tick_scope_in_parent_state_v1( + tick_receipt, + patch, + receipt.installed_operation_id().as_hash(), + installed.program(), + &parent_state, + ); + let application_basis_matches = exact_scope.is_some_and(|node| { + operation_application_basis_matches_scope_v1( + installed.program(), + node, + basis.application_basis(), + ) + }); + parent_state.state_root() == basis.state_root() && application_basis_matches + }); + if !exact_parent_state_binding { + return Err(TrustedRuntimeWalError::EchoOperationExecutionMismatch { + detail: "operation receipt patch disagrees with its reconstructed parent state", + }); + } + } + Ok(()) +} + fn validate_operation_receipt_state_delta( receipt: &EchoOperationReceiptV1, state_delta: &WalRuntimeStateDeltaRecord, + installed: &InstalledEchoOperationV1, ) -> Result<(), TrustedRuntimeWalError> { let entry = state_delta.provenance_entry(); let basis = receipt.evaluation_basis(); @@ -3221,7 +3476,12 @@ fn validate_operation_receipt_state_delta( .as_ref() .zip(entry.tick_receipt.as_ref()) .is_some_and(|(patch, tick_receipt)| { - operation_tick_binds_patch_v1(tick_receipt, patch, expected_rule_id) + operation_tick_binds_patch_v1( + tick_receipt, + patch, + expected_rule_id, + installed.program(), + ) }); let worldline_tick_after = entry .worldline_tick @@ -3526,7 +3786,6 @@ fn operation_tick_records_from_transaction( let state_delta = WalRuntimeStateDeltaRecord::from_payload_bytes(&state_delta_frame.payload.canonical_bytes)?; let state_delta_digest = state_delta.digest()?; - validate_operation_receipt_state_delta(&receipt, &state_delta)?; Ok((receipt, state_delta, state_delta_digest)) } @@ -4171,6 +4430,7 @@ mod tests { CausalTickReceiptRef, GlobalTick, IngressSubmissionGeneration, IngressTarget, WorldlineId, WorldlineTick, WriterHeadKey, }; + use bytes::Bytes; fn test_head_key() -> WriterHeadKey { WriterHeadKey { @@ -4179,6 +4439,402 @@ mod tests { } } + fn creation_scope_patch(node: crate::NodeKey) -> crate::WorldlineTickPatchV1 { + let attachment = crate::AttachmentKey::node_alpha(node); + let parent_warp = crate::make_warp_id("operation-wal-parent-root"); + let portal = crate::AttachmentKey::node_alpha(crate::NodeKey { + warp_id: parent_warp, + local_id: crate::make_node_id("operation-wal-parent-portal"), + }); + let middle_warp = crate::make_warp_id("operation-wal-middle"); + let middle_portal = crate::AttachmentKey::node_alpha(crate::NodeKey { + warp_id: middle_warp, + local_id: crate::make_node_id("operation-wal-middle-portal"), + }); + crate::WorldlineTickPatchV1 { + header: crate::WorldlineTickHeaderV1 { + commit_global_tick: GlobalTick::from_raw(1), + policy_id: 7, + rule_pack_id: [3; 32], + plan_digest: [4; 32], + decision_digest: [5; 32], + rewrites_digest: [6; 32], + }, + // The parent worldline root is intentionally different from the + // descendant node's WARP id. + warp_id: parent_warp, + ops: vec![ + crate::WarpOp::UpsertNode { + node, + record: crate::NodeRecord { + ty: crate::make_type_id("operation-wal-created-node"), + }, + }, + crate::WarpOp::SetAttachment { + key: attachment, + value: Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + crate::make_type_id("operation-wal-created-attachment"), + Bytes::from_static(b"created"), + ))), + }, + ], + in_slots: vec![ + crate::SlotId::Node(node), + crate::SlotId::Attachment(attachment), + crate::SlotId::Attachment(portal), + crate::SlotId::Attachment(middle_portal), + ], + out_slots: vec![ + crate::SlotId::Node(node), + crate::SlotId::Attachment(attachment), + ], + patch_digest: [7; 32], + } + } + + fn creation_scope_parent_state(node: crate::NodeKey) -> crate::WorldlineState { + let parent_warp = crate::make_warp_id("operation-wal-parent-root"); + let parent_node = crate::make_node_id("operation-wal-parent-portal"); + let portal = crate::AttachmentKey::node_alpha(crate::NodeKey { + warp_id: parent_warp, + local_id: parent_node, + }); + let middle_warp = crate::make_warp_id("operation-wal-middle"); + let middle_node = crate::make_node_id("operation-wal-middle-portal"); + let middle_portal = crate::AttachmentKey::node_alpha(crate::NodeKey { + warp_id: middle_warp, + local_id: middle_node, + }); + let child_root = crate::make_node_id("operation-wal-descendant-root"); + + let mut parent_store = crate::GraphStore::new(parent_warp); + parent_store.insert_node( + parent_node, + crate::NodeRecord { + ty: crate::make_type_id("operation-wal-parent-node"), + }, + ); + parent_store.set_node_attachment( + parent_node, + Some(crate::AttachmentValue::Descend(middle_warp)), + ); + let mut middle_store = crate::GraphStore::new(middle_warp); + middle_store.insert_node( + middle_node, + crate::NodeRecord { + ty: crate::make_type_id("operation-wal-middle-node"), + }, + ); + middle_store.set_node_attachment( + middle_node, + Some(crate::AttachmentValue::Descend(node.warp_id)), + ); + let mut child_store = crate::GraphStore::new(node.warp_id); + child_store.insert_node( + child_root, + crate::NodeRecord { + ty: crate::make_type_id("operation-wal-descendant-root-node"), + }, + ); + + let mut warp_state = crate::WarpState::new(); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: parent_warp, + root_node: parent_node, + parent: None, + }, + parent_store, + ); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: middle_warp, + root_node: middle_node, + parent: Some(portal), + }, + middle_store, + ); + warp_state.upsert_instance( + crate::WarpInstance { + warp_id: node.warp_id, + root_node: child_root, + parent: Some(middle_portal), + }, + child_store, + ); + crate::WorldlineState::new( + warp_state, + crate::NodeKey { + warp_id: parent_warp, + local_id: parent_node, + }, + ) + .expect("the recovery parent-state fixture is lawful") + } + + #[test] + fn creation_wal_scope_accepts_descendants_and_rejects_mutated_shapes() { + let node = crate::NodeKey { + warp_id: crate::make_warp_id("operation-wal-descendant"), + 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::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!( + operation_patch_scope_v1(&patch, &installed_program), + Some(node), + "the parent worldline root must not erase descendant operation scope" + ); + assert_eq!( + operation_patch_scope_in_parent_state_v1(&patch, &installed_program, &parent_state), + Some(node), + "activation recovery must corroborate the exact retained portal chain" + ); + assert!(operation_application_basis_matches_scope_v1( + &installed_program, + node, + crate::echo_operation_anchored_node_absent_application_basis_v1(node), + )); + assert!( + !operation_application_basis_matches_scope_v1( + &installed_program, + node, + EchoOperationApplicationBasisV1::new([0x91; 32], [0x92; 32]), + ), + "creation recovery must bind the receipt to the canonical absence proposition" + ); + + let mut node_occupied_parent = parent_state.clone(); + node_occupied_parent + .warp_state + .store_mut(&node.warp_id) + .expect("the fixture retains its descendant store") + .insert_node( + node.local_id, + crate::NodeRecord { + ty: crate::make_type_id("operation-wal-existing-node"), + }, + ); + assert_eq!( + operation_patch_scope_in_parent_state_v1( + &patch, + &installed_program, + &node_occupied_parent + ), + None, + "creation recovery must reject an occupied node even when its attachment is absent" + ); + + let mut attachment_occupied_parent = parent_state.clone(); + attachment_occupied_parent + .warp_state + .store_mut(&node.warp_id) + .expect("the fixture retains its descendant store") + .set_node_attachment( + node.local_id, + Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + crate::make_type_id("operation-wal-existing-attachment"), + Bytes::from_static(b"occupied"), + ))), + ); + assert_eq!( + operation_patch_scope_in_parent_state_v1( + &patch, + &installed_program, + &attachment_occupied_parent + ), + None, + "creation recovery must reject an orphan attachment even when its node is absent" + ); + + let mut fully_occupied_parent = node_occupied_parent; + fully_occupied_parent + .warp_state + .store_mut(&node.warp_id) + .expect("the fixture retains its descendant store") + .set_node_attachment( + node.local_id, + Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + crate::make_type_id("operation-wal-existing-attachment"), + Bytes::from_static(b"occupied"), + ))), + ); + assert_eq!( + operation_patch_scope_in_parent_state_v1( + &patch, + &installed_program, + &fully_occupied_parent + ), + None, + "creation recovery must reject a fully occupied target" + ); + + assert_eq!( + operation_patch_scope_v1(&patch, &update_program), + None, + "the update profile must reject the creation program's two-op patch" + ); + + let mut missing_portal_read = patch.clone(); + missing_portal_read.in_slots.retain(|slot| { + matches!( + slot, + crate::SlotId::Node(candidate) if candidate == &node + ) || matches!( + slot, + crate::SlotId::Attachment(candidate) + if candidate == &crate::AttachmentKey::node_alpha(node) + ) + }); + assert_eq!( + operation_patch_scope_v1(&missing_portal_read, &installed_program), + None, + "a descendant operation must retain the root portal dependency" + ); + + let mut duplicate_portal_read = patch.clone(); + duplicate_portal_read + .in_slots + .push(duplicate_portal_read.in_slots[2]); + assert_eq!( + operation_patch_scope_v1(&duplicate_portal_read, &installed_program), + None, + "a descendant operation must reject duplicate portal dependencies" + ); + + let mut substituted_root_portal = patch.clone(); + substituted_root_portal.in_slots[2] = + crate::SlotId::Attachment(crate::AttachmentKey::node_alpha(crate::NodeKey { + warp_id: substituted_root_portal.warp_id, + local_id: crate::make_node_id("operation-wal-unrelated-root-portal"), + })); + assert_eq!( + operation_patch_scope_in_parent_state_v1( + &substituted_root_portal, + &installed_program, + &parent_state + ), + None, + "recovery must reject an unrelated root-owned portal substituted for the exact descent chain" + ); + + let mut missing_middle_portal = patch.clone(); + missing_middle_portal.in_slots.remove(3); + assert_eq!( + operation_patch_scope_v1(&missing_middle_portal, &installed_program), + Some(node), + "the byte-shape layer alone cannot infer a portal's Descend target" + ); + assert_eq!( + operation_patch_scope_in_parent_state_v1( + &missing_middle_portal, + &installed_program, + &parent_state + ), + None, + "activation recovery must reject a patch that omits an intermediate portal" + ); + + let mut reversed = patch.clone(); + reversed.ops.reverse(); + assert_eq!( + operation_patch_scope_v1(&reversed, &installed_program), + None + ); + + let mut missing_node_write = patch.clone(); + missing_node_write.ops.remove(0); + assert_eq!( + operation_patch_scope_v1(&missing_node_write, &installed_program), + None + ); + + let mut attachment_only_output = patch.clone(); + attachment_only_output.out_slots.remove(0); + assert_eq!( + operation_patch_scope_v1(&attachment_only_output, &installed_program), + None + ); + + let mut mismatched_attachment = patch; + let other_node = crate::NodeKey { + warp_id: node.warp_id, + local_id: crate::make_node_id("operation-wal-other-node"), + }; + let crate::WarpOp::SetAttachment { key, .. } = &mut mismatched_attachment.ops[1] else { + panic!("fixture has the canonical attachment operation"); + }; + *key = crate::AttachmentKey::node_alpha(other_node); + assert_eq!( + operation_patch_scope_v1(&mismatched_attachment, &installed_program), + None + ); + + let mut wrong_node_type = creation_scope_patch(node); + let crate::WarpOp::UpsertNode { record, .. } = &mut wrong_node_type.ops[0] else { + panic!("fixture has the canonical node operation"); + }; + record.ty = crate::make_type_id("operation-wal-wrong-node-type"); + assert_eq!( + operation_patch_scope_v1(&wrong_node_type, &installed_program), + None, + "recovery must reject a node type the installed program cannot emit" + ); + + let mut wrong_attachment_type = creation_scope_patch(node); + let crate::WarpOp::SetAttachment { value, .. } = &mut wrong_attachment_type.ops[1] else { + panic!("fixture has the canonical attachment operation"); + }; + *value = Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + crate::make_type_id("operation-wal-wrong-attachment-type"), + Bytes::from_static(b"created"), + ))); + assert_eq!( + operation_patch_scope_v1(&wrong_attachment_type, &installed_program), + None, + "recovery must reject an attachment type the installed program cannot emit" + ); + + let mut descended_attachment = creation_scope_patch(node); + let crate::WarpOp::SetAttachment { value, .. } = &mut descended_attachment.ops[1] else { + panic!("fixture has the canonical attachment operation"); + }; + *value = Some(crate::AttachmentValue::Descend(crate::make_warp_id( + "operation-wal-hidden-descendant", + ))); + assert_eq!( + operation_patch_scope_v1(&descended_attachment, &installed_program), + None, + "recovery must reject attachment algebras the installed program cannot emit" + ); + + let mut oversized_attachment = creation_scope_patch(node); + let crate::WarpOp::SetAttachment { value, .. } = &mut oversized_attachment.ops[1] else { + panic!("fixture has the canonical attachment operation"); + }; + *value = Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + crate::make_type_id("operation-wal-created-attachment"), + Bytes::from_static(b"too-long"), + ))); + assert_eq!( + operation_patch_scope_v1(&oversized_attachment, &installed_program), + None, + "recovery must enforce the installed program's replacement bound" + ); + } + #[test] fn operation_recovery_rejects_uncorroborated_frontier_root() { let expected_frontiers = [AffectedFrontier { @@ -4225,6 +4881,12 @@ 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( + crate::make_type_id("operation-recovery-node"), + attachment_type, + 7, + ); let runtime_patch = crate::WarpTickPatchV1::new( 7, rule_id, @@ -4236,7 +4898,10 @@ mod tests { vec![crate::SlotId::Attachment(attachment_slot)], vec![crate::WarpOp::SetAttachment { key: attachment_slot, - value: None, + value: Some(crate::AttachmentValue::Atom(crate::AtomPayload::new( + attachment_type, + Bytes::from_static(b"updated"), + ))), }], ); let patch = crate::WorldlineTickPatchV1 { @@ -4267,7 +4932,9 @@ mod tests { ) }; let valid = receipt(node, crate::scope_hash(&rule_id, &node)); - assert!(operation_tick_binds_patch_v1(&valid, &patch, rule_id)); + assert!(operation_tick_binds_patch_v1( + &valid, &patch, rule_id, &program, + )); let wrong_scope = crate::NodeKey { warp_id: node.warp_id, @@ -4278,14 +4945,16 @@ mod tests { assert!(!operation_tick_binds_patch_v1( &self_consistent_wrong_scope, &patch, - rule_id + rule_id, + &program, )); let forged_scope_hash = receipt(node, [25; 32]); assert!(!operation_tick_binds_patch_v1( &forged_scope_hash, &patch, - rule_id + rule_id, + &program, )); } diff --git a/crates/warp-core/tests/executable_operation_pipeline_tests.rs b/crates/warp-core/tests/executable_operation_pipeline_tests.rs index 5f20793f..42d8de85 100644 --- a/crates/warp-core/tests/executable_operation_pipeline_tests.rs +++ b/crates/warp-core/tests/executable_operation_pipeline_tests.rs @@ -19,7 +19,8 @@ use warp_core::causal_wal::{ use warp_core::{ make_head_id, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentValue, EchoOperationAdmissionErrorKindV1, EchoOperationAdmissionPolicyV1, - EchoOperationApplicationBasisV1, EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, + EchoOperationAnchoredNodeOccupancyV1, EchoOperationApplicationBasisV1, + EchoOperationArtifactErrorKindV1, EchoOperationBudgetV1, EchoOperationInvocationAdmissionErrorKindV1, EchoOperationInvocationAdmissionPolicyV1, EchoOperationInvocationV1, EchoOperationObstructionKindV1, EchoOperationPreparationV1, EchoOperationProgramV1, EchoOperationSemanticClosureV1, EchoOperationTerminalPostureV1, @@ -30,6 +31,7 @@ use warp_core::{ }; const OPERATION_COORDINATE: &str = "echo.fixture.SetAnchoredAtom.v1"; +const CREATE_OPERATION_COORDINATE: &str = "echo.fixture.CreateAnchoredAtomIfAbsent.v1"; static TEMP_DIR_COUNTER: AtomicUsize = AtomicUsize::new(0); struct TempWalDir(PathBuf); @@ -155,6 +157,169 @@ fn fixture_host() -> (TrustedRuntimeHost, WriterHeadKey, NodeKey) { (host, head_key, node) } +/// Like [`fixture_host`], but the warp-scoped store also contains a second, +/// bare node -- present, but with no alpha attachment set -- at +/// `second_node_local_id`, typed `second_node_type`. Used to exercise +/// create-from-absence's ADR 0024 "existing node, absent attachment" and +/// "existing node, wrong type" refusal paths, which the standard fixture's +/// single fully-populated node cannot reach. +fn fixture_host_with_bare_node( + second_node_local_id: warp_core::NodeId, + second_node_type: warp_core::TypeId, + second_attachment: Option<(warp_core::TypeId, &'static [u8])>, +) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey, NodeKey) { + let warp_id = make_warp_id("operation-fixture"); + let node_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 node = NodeKey { + warp_id, + local_id: node_id, + }; + let second_node = NodeKey { + warp_id, + local_id: second_node_local_id, + }; + let mut store = GraphStore::new(warp_id); + store.insert_node(node_id, NodeRecord { ty: node_type }); + store.set_node_attachment( + node_id, + Some(AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"before"), + ))), + ); + store.insert_node( + second_node_local_id, + NodeRecord { + ty: second_node_type, + }, + ); + if let Some((second_attachment_type, second_attachment_bytes)) = second_attachment { + store.set_node_attachment( + second_node_local_id, + Some(AttachmentValue::Atom(AtomPayload::new( + second_attachment_type, + Bytes::from_static(second_attachment_bytes), + ))), + ); + } + let state = WorldlineState::from_root_store(store, node_id) + .expect("the fixture state has one lawful root"); + let worldline_id = WorldlineId::from_bytes(digest("operation-fixture-worldline")); + let head_key = WriterHeadKey { + worldline_id, + head_id: make_head_id("operation-fixture-writer"), + }; + let mut runtime = WorldlineRuntime::new(); + runtime + .register_worldline(worldline_id, state) + .expect("the fixture worldline registers"); + runtime + .register_writer_head(WriterHead::with_routing( + head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )) + .expect("the fixture writer registers"); + + let mut engine_store = GraphStore::default(); + let engine_root = make_node_id("root"); + engine_store.insert_node( + engine_root, + NodeRecord { + ty: make_type_id("world"), + }, + ); + let engine = EngineBuilder::new(engine_store, engine_root) + .scheduler(SchedulerKind::Radix) + .workers(1) + .build(); + let host = TrustedRuntimeHost::new(runtime, engine) + .expect("the trusted Echo runtime host initializes"); + (host, head_key, node, second_node) +} + +/// Like [`fixture_host`], but the warp-scoped store also contains an +/// *orphaned* alpha attachment -- present at `orphan_local_id` with no +/// corresponding `NodeRecord` -- reachable because +/// `GraphStore::set_node_attachment` does not require the owning node to +/// exist. Used to exercise create-from-absence's PR #686 review finding #2 +/// (CodeRabbit + Codex): a node that is absent must not be treated as fully +/// absent when its attachment slot is independently occupied. +fn fixture_host_with_orphan_attachment( + orphan_local_id: warp_core::NodeId, + orphan_attachment_type: warp_core::TypeId, + orphan_bytes: &[u8], +) -> (TrustedRuntimeHost, WriterHeadKey, NodeKey, NodeKey) { + let warp_id = make_warp_id("operation-fixture"); + let node_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 node = NodeKey { + warp_id, + local_id: node_id, + }; + let orphan_node = NodeKey { + warp_id, + local_id: orphan_local_id, + }; + let mut store = GraphStore::new(warp_id); + store.insert_node(node_id, NodeRecord { ty: node_type }); + store.set_node_attachment( + node_id, + Some(AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"before"), + ))), + ); + store.set_node_attachment( + orphan_local_id, + Some(AttachmentValue::Atom(AtomPayload::new( + orphan_attachment_type, + Bytes::copy_from_slice(orphan_bytes), + ))), + ); + let state = WorldlineState::from_root_store(store, node_id) + .expect("the fixture state has one lawful root"); + let worldline_id = WorldlineId::from_bytes(digest("operation-fixture-worldline")); + let head_key = WriterHeadKey { + worldline_id, + head_id: make_head_id("operation-fixture-writer"), + }; + let mut runtime = WorldlineRuntime::new(); + runtime + .register_worldline(worldline_id, state) + .expect("the fixture worldline registers"); + runtime + .register_writer_head(WriterHead::with_routing( + head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )) + .expect("the fixture writer registers"); + + let mut engine_store = GraphStore::default(); + let engine_root = make_node_id("root"); + engine_store.insert_node( + engine_root, + NodeRecord { + ty: make_type_id("world"), + }, + ); + let engine = EngineBuilder::new(engine_store, engine_root) + .scheduler(SchedulerKind::Radix) + .workers(1) + .build(); + let host = TrustedRuntimeHost::new(runtime, engine) + .expect("the trusted Echo runtime host initializes"); + (host, head_key, node, orphan_node) +} + fn semantic_closure() -> EchoOperationSemanticClosureV1 { EchoOperationSemanticClosureV1::new( digest("fixture-edict-source"), @@ -194,6 +359,24 @@ fn operation_package_at( ) } +fn creation_operation_package( + node_type: warp_core::TypeId, + attachment_type: warp_core::TypeId, +) -> ExecutableOperationPackageV1 { + ExecutableOperationPackageV1::new( + CREATE_OPERATION_COORDINATE, + 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( + node_type, + attachment_type, + 1_024, + ), + ) +} + fn install_fixture_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperationV1 { install_fixture_operation_at(host, OPERATION_COORDINATE) } @@ -225,6 +408,29 @@ fn install_fixture_operation_at( .expect("the admitted fixture package installs") } +fn install_fixture_creation_operation(host: &mut TrustedRuntimeHost) -> InstalledEchoOperationV1 { + let package_bytes = creation_operation_package( + make_type_id("operation-fixture-node"), + make_type_id("operation-fixture-atom"), + ) + .to_canonical_bytes() + .expect("fixture creation package encodes"); + let package_id = warp_core::echo_operation_package_id_v1(&package_bytes); + let admitted = host + .admit_echo_operation_package_v1( + &EchoOperationAdmissionPolicyV1::exact( + package_id, + CREATE_OPERATION_COORDINATE, + digest("fixture-authority-profile"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ), + package_bytes, + ) + .expect("the exact fixture creation package is admitted"); + host.install_admitted_echo_operation_package_v1(admitted) + .expect("the admitted fixture creation package installs") +} + fn application_basis() -> EchoOperationApplicationBasisV1 { warp_core::echo_operation_anchored_node_application_basis_v1( NodeKey { @@ -333,7 +539,7 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { installed.operation_coordinate(), evaluation_basis, digest("fixture-authority-grant"), - EchoOperationBudgetV1::new(16, 4_096, 4_096), + EchoOperationBudgetV1::new(4, 70, 37), node, warp_core::echo_operation_atom_value_digest_v1(attachment_type, b"before"), b"after".to_vec(), @@ -424,7 +630,7 @@ fn admitted_data_only_program_commits_one_typed_attachment_patch() { ); assert_eq!( execution.receipt().delegated_budget(), - EchoOperationBudgetV1::new(16, 4_096, 4_096) + EchoOperationBudgetV1::new(4, 70, 37) ); assert_eq!( execution.receipt().consumed_budget(), @@ -1613,3 +1819,725 @@ fn filesystem_wal_recovers_installed_meaning_consequence_and_typed_receipt() { receipt_digests ); } + +/// ADR 0024: the create-if-absent program, invoked against a node and +/// attachment that genuinely do not exist yet, commits one atomic patch that +/// creates both using exactly the program's declared +/// `required_node_type`/`required_attachment_type`. +#[test] +fn create_from_absence_commits_one_new_node_and_attachment_patch() { + let (mut host, head_key, _existing_node) = fixture_host(); + let node_type = make_type_id("operation-fixture-node"); + let attachment_type = make_type_id("operation-fixture-atom"); + let installed = install_fixture_creation_operation(&mut host); + + let new_node = NodeKey { + warp_id: make_warp_id("operation-fixture"), + local_id: make_node_id("operation-fixture-created"), + }; + let application_basis = + warp_core::echo_operation_anchored_node_absent_application_basis_v1(new_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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(3, 64, 71), + new_node, + b"created".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .expect("Echo independently admits the create-from-absence invocation"); + let preparation = host.prepare_echo_operation_v1(admitted_invocation); + let EchoOperationPreparationV1::Prepared(prepared) = preparation else { + panic!("a lawful create-from-absence invocation must prepare"); + }; + assert_eq!(prepared.declared_footprint(), prepared.actual_footprint()); + + let execution = host + .commit_prepared_echo_operation_v1(prepared) + .expect("Echo commits the create-from-absence patch"); + assert_eq!( + execution.receipt().terminal_posture(), + EchoOperationTerminalPostureV1::Committed + ); + assert_eq!( + execution.receipt().consumed_budget(), + EchoOperationBudgetV1::new(3, 64, 71) + ); + + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the committed worldline remains registered") + .state(); + let store = state + .store(&new_node.warp_id) + .expect("the warp-scoped store exists"); + assert_eq!( + store.node(&new_node.local_id), + Some(&NodeRecord { ty: node_type }) + ); + assert_eq!( + store.node_attachment(&new_node.local_id), + Some(&AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"created"), + ))) + ); +} + +/// ADR 0024: create-from-absence is a precondition like any other -- it +/// refuses, rather than silently updating, when the node already exists. +#[test] +fn create_from_absence_refuses_when_the_node_already_exists() { + let (mut host, head_key, node) = fixture_host(); + let attachment_type = make_type_id("operation-fixture-atom"); + let installed = install_fixture_creation_operation(&mut host); + + // The evaluation basis honestly reflects the real, present current value + // ("before") so that admission's independent basis corroboration passes. + // The creation program's total-absence precondition is the thing under + // test: it must still refuse against real existing state, + // exercising `prepare_operation_v1`'s defense-in-depth check rather than + // the coarser admission-time basis check. + let application_basis = warp_core::echo_operation_anchored_node_creation_application_basis_v1( + node, + EchoOperationAnchoredNodeOccupancyV1::NodeAndAttachment, + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + node, + b"clobbered".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .expect("Echo independently admits the invocation"); + let preparation = host.prepare_echo_operation_v1(admitted_invocation); + let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { + panic!("create-from-absence against an existing node must not prepare a patch"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::PreconditionMismatch + ); + + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the untouched worldline remains registered") + .state(); + assert_eq!( + state + .store(&node.warp_id) + .and_then(|store| store.node_attachment(&node.local_id)), + Some(&AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"before"), + ))), + "an obstructed create-from-absence must leave the existing attachment untouched" + ); +} + +/// ADR 0024: the legacy update profile still rejects a genuinely absent node +/// during admission; the separate creation profile must not widen update +/// admission. +#[test] +fn update_precondition_still_refuses_when_the_node_is_absent() { + let (mut host, head_key, _existing_node) = fixture_host(); + let attachment_type = make_type_id("operation-fixture-atom"); + let installed = install_fixture_operation(&mut host); + + let absent_node = NodeKey { + warp_id: make_warp_id("operation-fixture"), + local_id: make_node_id("operation-fixture-never-created"), + }; + let application_basis = warp_core::echo_operation_anchored_node_application_basis_v1( + absent_node, + attachment_type, + b"before", + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + absent_node, + warp_core::echo_operation_atom_value_digest_v1(attachment_type, b"before"), + b"after".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the update invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .expect_err("the update profile must reject a missing node at admission"); + assert_eq!( + error.kind(), + EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + ); +} + +/// Regression for PR #686 review finding #4 (Codex, P2): create-from-absence +/// refuses with `PreconditionMismatch`, not `NodeTypeMismatch`, when a node +/// exists at the claimed-absent coordinate with a different `NodeRecord.ty` +/// than the installed package declares. ADR 0024 states every occupied +/// create target refuses as a precondition failure, regardless of what about +/// it differs -- `NodeTypeMismatch` is reserved for the update path, where a +/// caller who correctly expected the node to exist got its type wrong. The +/// node has no attachment set, so admission corroborates the honest +/// node-only occupancy proposition; `prepare_operation_v1`'s semantic check +/// is what classifies that occupancy as `PreconditionMismatch`. +#[test] +fn create_from_absence_refuses_when_the_node_exists_with_the_wrong_type() { + let wrong_type_node_id = make_node_id("operation-fixture-wrong-type"); + let wrong_node_type = make_type_id("operation-fixture-wrong-node-type"); + let (mut host, head_key, _existing_node, bare_node) = + fixture_host_with_bare_node(wrong_type_node_id, wrong_node_type, None); + let installed = install_fixture_creation_operation(&mut host); + + let application_basis = warp_core::echo_operation_anchored_node_creation_application_basis_v1( + bare_node, + EchoOperationAnchoredNodeOccupancyV1::NodeOnly, + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + bare_node, + b"created".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .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 { + panic!("create-from-absence against a wrong-typed existing node must not prepare a patch"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::PreconditionMismatch + ); +} + +/// ADR 0024: create-from-absence refuses with `PreconditionMismatch`, not +/// silent success, when a node exists at the claimed-absent coordinate with +/// the correct type but no alpha attachment set yet. Creation is atomic over +/// both slots or it refuses -- there is no path that attaches onto a +/// pre-existing bare node. +#[test] +fn create_from_absence_refuses_when_the_node_exists_without_its_attachment() { + let bare_node_id = make_node_id("operation-fixture-bare"); + let node_type = make_type_id("operation-fixture-node"); + let (mut host, head_key, _existing_node, bare_node) = + fixture_host_with_bare_node(bare_node_id, node_type, None); + let installed = install_fixture_creation_operation(&mut host); + + let application_basis = warp_core::echo_operation_anchored_node_creation_application_basis_v1( + bare_node, + EchoOperationAnchoredNodeOccupancyV1::NodeOnly, + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + bare_node, + b"created".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .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 { + panic!("create-from-absence against a bare existing node must not prepare a patch"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::PreconditionMismatch + ); +} + +/// ADR 0024: the existing basis-changed TOCTOU protection covers a prepared +/// create-from-absence patch exactly as it already covers a prepared update, +/// via the same generic exact-basis commit check -- not a create-specific +/// carve-out. +#[test] +fn create_from_absence_cannot_commit_after_its_parent_basis_changes() { + let (mut host, head_key, _existing_node) = fixture_host(); + let installed = install_fixture_creation_operation(&mut host); + let attachment_type = make_type_id("operation-fixture-atom"); + let new_node = NodeKey { + warp_id: make_warp_id("operation-fixture"), + local_id: make_node_id("operation-fixture-race-created"), + }; + let application_basis = + warp_core::echo_operation_anchored_node_absent_application_basis_v1(new_node); + + let prepare = |host: &TrustedRuntimeHost, replacement: &[u8]| { + 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( + installed.package_id(), + installed.operation_coordinate(), + basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + new_node, + replacement.to_vec(), + ); + let bytes = invocation.to_canonical_bytes().expect("invocation encodes"); + let admitted = host + .admit_echo_operation_invocation_v1( + &EchoOperationInvocationAdmissionPolicyV1::new( + digest("fixture-authority-profile"), + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ), + &bytes, + ) + .expect("invocation is admitted"); + match host.prepare_echo_operation_v1(admitted) { + EchoOperationPreparationV1::Prepared(prepared) => prepared, + EchoOperationPreparationV1::Obstructed(obstruction) => { + panic!("lawful create-from-absence invocation obstructed: {obstruction:?}") + } + } + }; + + let stale_preparation = prepare(&host, b"stale-created"); + let winning_preparation = prepare(&host, b"winning-created"); + host.commit_prepared_echo_operation_v1(winning_preparation) + .expect("one exact-basis create-from-absence operation commits"); + let evidence = host + .commit_prepared_echo_operation_v1(stale_preparation) + .expect("basis refusal is typed evidence, not a host fault"); + assert_eq!( + evidence.receipt().terminal_posture(), + EchoOperationTerminalPostureV1::NotCommittedBasisChanged + ); + assert_eq!(evidence.receipt().committed_patch_digest(), None); + assert_eq!(evidence.receipt().committed_result_id(), None); + assert_eq!(evidence.receipt().composition_digest(), None); + + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the worldline remains registered") + .state(); + assert_eq!(state.current_tick().as_u64(), 1); + assert_eq!( + state + .store(&new_node.warp_id) + .and_then(|store| store.node_attachment(&new_node.local_id)), + Some(&AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"winning-created"), + ))) + ); +} + +/// Regression for PR #686 review finding #1 (Codex, P0): a durably committed +/// create-from-absence patch (WarpOp::UpsertNode + WarpOp::SetAttachment, +/// two-element out_slots) must be recoverable by a fresh host, exactly like +/// an update-shaped patch already is. +#[test] +fn filesystem_wal_recovers_a_create_from_absence_commit() { + let wal_dir = TempWalDir::new(); + let node_type = make_type_id("operation-fixture-node"); + let attachment_type = make_type_id("operation-fixture-atom"); + let new_node = NodeKey { + warp_id: make_warp_id("operation-fixture"), + local_id: make_node_id("operation-fixture-wal-created"), + }; + let application_basis = + warp_core::echo_operation_anchored_node_absent_application_basis_v1(new_node); + let committed_result_id; + let committed_receipt_digest; + let installed_package_id; + + { + let (mut host, head_key, _existing_node) = fixture_host(); + host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect("the fresh filesystem WAL opens"); + let installed = install_fixture_creation_operation(&mut host); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + new_node, + b"wal-created".to_vec(), + ); + let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes"); + let admitted = host + .admit_echo_operation_invocation_v1( + &EchoOperationInvocationAdmissionPolicyV1::new( + digest("fixture-authority-profile"), + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + ), + &invocation_bytes, + ) + .expect("the create-from-absence invocation is independently admitted"); + let EchoOperationPreparationV1::Prepared(prepared) = + host.prepare_echo_operation_v1(admitted) + else { + panic!("the lawful create-from-absence invocation must prepare"); + }; + let execution = host + .commit_prepared_echo_operation_v1(prepared) + .expect("the durable create-from-absence operation commits"); + committed_result_id = execution.receipt().committed_result_id(); + committed_receipt_digest = execution.receipt().digest(); + installed_package_id = installed.package_id(); + } + + let (mut recovered, head_key, _existing_node) = fixture_host(); + recovered + .enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path())) + .expect( + "a fresh host recovers a create-from-absence commit without replaying application code", + ); + let state = recovered + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the recovered worldline exists") + .state(); + assert_eq!( + state + .store(&new_node.warp_id) + .and_then(|store| store.node(&new_node.local_id)), + Some(&NodeRecord { ty: node_type }) + ); + assert_eq!( + state + .store(&new_node.warp_id) + .and_then(|store| store.node_attachment(&new_node.local_id)), + Some(&AttachmentValue::Atom(AtomPayload::new( + attachment_type, + Bytes::from_static(b"wal-created"), + ))) + ); + let recovery = recovered + .runtime_wal() + .expect("the recovered WAL remains enabled") + .recover_read_only() + .expect("fresh-host read-only recovery remains stable"); + assert_eq!( + recovery + .installed_echo_operations + .iter() + .map(InstalledEchoOperationV1::package_id) + .collect::>(), + vec![installed_package_id], + "the exact creation package installation must recover" + ); + assert_eq!(recovery.echo_operation_receipts.len(), 1); + let recovered_receipt = &recovery.echo_operation_receipts[0]; + assert_eq!(recovered_receipt.digest(), committed_receipt_digest); + assert_eq!(recovered_receipt.committed_result_id(), committed_result_id); + assert_eq!( + recovered_receipt.terminal_posture(), + EchoOperationTerminalPostureV1::Committed + ); +} + +/// Regression for PR #686 review finding #2 (CodeRabbit, P0): create-from- +/// absence must not silently overwrite an orphaned attachment (present with +/// no owning `NodeRecord` -- reachable because `GraphStore::set_node_attachment` +/// never requires the node to exist). Exercised with a basis that honestly +/// reflects the orphan's real present value, so admission passes and the +/// evaluation-time check in `prepare_operation_v1` is isolated as the thing +/// under test. +#[test] +fn create_from_absence_refuses_when_the_attachment_exists_without_its_node() { + let orphan_local_id = make_node_id("operation-fixture-orphan-attachment"); + let orphan_attachment_type = make_type_id("operation-fixture-atom"); + let (mut host, head_key, _existing_node, orphan_node) = + fixture_host_with_orphan_attachment(orphan_local_id, orphan_attachment_type, b"orphaned"); + let installed = install_fixture_creation_operation(&mut host); + + let application_basis = warp_core::echo_operation_anchored_node_creation_application_basis_v1( + orphan_node, + EchoOperationAnchoredNodeOccupancyV1::AttachmentOnly, + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + orphan_node, + b"clobbered".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .expect( + "Echo admits the invocation -- the orphaned attachment honestly \ + corroborates as present, matching this invocation's claimed basis", + ); + let preparation = host.prepare_echo_operation_v1(admitted_invocation); + let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else { + panic!("create-from-absence against an orphaned attachment must not prepare a patch"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::PreconditionMismatch + ); + + let state = host + .runtime() + .worldlines() + .get(&head_key.worldline_id) + .expect("the untouched worldline remains registered") + .state(); + assert_eq!( + state + .store(&orphan_node.warp_id) + .and_then(|store| store.node_attachment(&orphan_node.local_id)), + Some(&AttachmentValue::Atom(AtomPayload::new( + orphan_attachment_type, + Bytes::from_static(b"orphaned"), + ))), + "an obstructed create-from-absence must leave the orphaned attachment untouched" + ); +} + +/// Regression for PR #686 review finding #2 (Codex, P2): admission's +/// independent application-basis corroboration must not treat a node with an +/// orphaned attachment as absent. A create-from-absence invocation that +/// (incorrectly) claims absence against a real orphan must be refused at +/// admission with `BasisMismatch`, not let through to evaluation. +#[test] +fn create_from_absence_invocation_refuses_at_admission_against_an_orphaned_attachment() { + let orphan_local_id = make_node_id("operation-fixture-orphan-admission"); + let orphan_attachment_type = make_type_id("operation-fixture-atom"); + let (mut host, head_key, _existing_node, orphan_node) = + fixture_host_with_orphan_attachment(orphan_local_id, orphan_attachment_type, b"orphaned"); + let installed = install_fixture_creation_operation(&mut host); + + let claimed_absent_basis = + warp_core::echo_operation_anchored_node_absent_application_basis_v1(orphan_node); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + orphan_node, + b"clobbered".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .expect_err("a dishonest absence claim against a real orphaned attachment must refuse"); + assert_eq!( + error.kind(), + EchoOperationInvocationAdmissionErrorKindV1::BasisMismatch + ); +} + +/// Regression for PR #686 review finding #3 (Codex, P2): the create-from- +/// absence write charge must account for the `UpsertNode` op's 32-byte +/// `NodeRecord.ty` in addition to the attachment atom's 32-byte type id and +/// its replacement bytes -- not `32 + replacement_len` alone, which +/// undercounts by exactly the node record's width and lets a caller commit +/// more bytes than it delegated. +#[test] +fn create_from_absence_charges_the_node_record_against_the_write_budget() { + let (mut host, head_key, _existing_node) = fixture_host(); + let installed = install_fixture_creation_operation(&mut host); + let new_node = NodeKey { + warp_id: make_warp_id("operation-fixture"), + local_id: make_node_id("operation-fixture-budget-created"), + }; + let application_basis = + warp_core::echo_operation_anchored_node_absent_application_basis_v1(new_node); + let evaluation_basis = host + .echo_operation_evaluation_basis_v1(head_key, application_basis) + .expect("Echo resolves the exact current parent basis"); + let replacement = b"created".to_vec(); + // 32 (attachment type) + replacement.len() = 39: enough under the buggy + // charge, which never counts the UpsertNode's own 32-byte NodeRecord.ty. + // 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + insufficient_for_node_record, + new_node, + replacement, + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the create-from-absence invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .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 { + panic!( + "a write budget that covers only the attachment, not the node record too, \ + must not prepare a patch" + ); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::BudgetExceeded + ); +} + +/// Companion to the PR #686 review finding #4 fix: `NodeTypeMismatch` is not +/// dead code after `expects_creation` is checked first in the `Some(record)` +/// arm -- it remains the correct obstruction for the update path, where a +/// caller who correctly expected the exact prior digest named the wrong type. +#[test] +fn update_precondition_refuses_when_the_node_has_the_wrong_type() { + let wrong_type_node_id = make_node_id("operation-fixture-update-wrong-type"); + let wrong_node_type = make_type_id("operation-fixture-wrong-node-type"); + let attachment_type = make_type_id("operation-fixture-atom"); + let (mut host, head_key, _existing_node, wrong_type_node) = fixture_host_with_bare_node( + wrong_type_node_id, + wrong_node_type, + Some((attachment_type, b"before")), + ); + let installed = install_fixture_operation(&mut host); + + let application_basis = warp_core::echo_operation_anchored_node_application_basis_v1( + wrong_type_node, + attachment_type, + b"before", + ); + 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( + installed.package_id(), + installed.operation_coordinate(), + evaluation_basis, + digest("fixture-authority-grant"), + EchoOperationBudgetV1::new(16, 4_096, 4_096), + wrong_type_node, + warp_core::echo_operation_atom_value_digest_v1(attachment_type, b"before"), + b"after".to_vec(), + ); + let invocation_bytes = invocation + .to_canonical_bytes() + .expect("the update invocation is canonical"); + let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::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) + .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 { + panic!("an update against a wrong-typed existing node must not prepare a patch"); + }; + assert_eq!( + obstruction.kind(), + EchoOperationObstructionKindV1::NodeTypeMismatch + ); +} diff --git a/docs/adr/0024-anchored-node-creation-from-absence.md b/docs/adr/0024-anchored-node-creation-from-absence.md new file mode 100644 index 00000000..4070e3ff --- /dev/null +++ b/docs/adr/0024-anchored-node-creation-from-absence.md @@ -0,0 +1,330 @@ + + + +# ADR 0024: Anchored-Node Creation as a Separate Executable Program + +- **Status:** Accepted +- **Date:** 2026-07-23 +- **Amends:** ADR 0023 +- **Supersedes:** this ADR's original optional-precondition design + +## Context + +ADR 0023 established a hook-free executable-operation corridor distinct from +provider-v1. Its first earned program, +`EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet`, updates one +typed node's alpha attachment under an exact prior-value digest. + +The first version of this ADR widened that program's invocation precondition +from `Hash` to `Option`: + +- `Some(digest)` meant update; +- `None` meant create the node and attachment from total absence. + +That implementation found real missing runtime capabilities. It proved that +creation needs an `UpsertNode` plus `SetAttachment` consequence, node and +attachment occupancy must be checked independently, the node type must be +charged to the write budget, and the two-operation patch must survive WAL +validation and fresh-host recovery. + +The widening itself was nevertheless the wrong semantic boundary. Update and +creation differ in every executable dimension: + +| Dimension | Compare-and-set update | Create if absent | +| --------------------- | ------------------------------- | ------------------------------- | +| Precondition | Existing exact attachment value | Node and attachment both absent | +| Patch | `SetAttachment` | `UpsertNode` + `SetAttachment` | +| Output slots | Attachment | Node + attachment | +| Footprint | Attachment write | Node + attachment writes | +| Minimum write bytes | Attachment type | Node type + attachment type | +| Recovery shape | One operation | Two ordered operations | +| Occupancy obstruction | Digest mismatch | Any occupied target | +| Result identity input | Prior value digest | Absence proposition | + +Keeping one program kind while widening its decoder would let already-produced +program bytes and profile identities acquire creation authority merely because +a newer Echo runtime interpreted `Null` differently. An executable content +identity must bind one closed law. Runtime revision is not an admissible hidden +semantic input. + +## Decision + +### Compare-and-set remains exactly update-only + +`EchoOperationProgramV1::AnchoredNodeAttachmentCompareAndSet` retains its +original canonical program bytes and profile coordinates. Its invocation again +requires: + +```rust +expected_value_digest: Hash +``` + +The legacy invocation schema accepts a 32-byte digest and rejects `Null`. +Update admission still requires the node and canonical atom attachment to +exist. Its application-basis proposition, footprint, minimum budget, patch +shape, result schema, result identity calculation, and target profile remain +unchanged. + +This compatibility boundary is executable, not rhetorical: + +- a unit witness reconstructs the legacy canonical program and invocation + bytes independently and requires exact equality; +- the legacy result-ID witness reconstructs the pre-ADR-0024 hash input and + requires exact equality; +- a mutated legacy invocation carrying `Null` is refused. + +### Creation is a second earned program + +Creation uses: + +```rust +EchoOperationProgramV1::AnchoredNodeAttachmentCreateIfAbsent { + required_node_type, + required_attachment_type, + max_replacement_bytes, +} +``` + +It has its own: + +- program-kind coordinate; +- canonical invocation schema with the explicit closed precondition + `node-and-alpha-attachment-absent/v1`; +- input, result, and obstruction schemas; +- result and obstruction interpretations; +- application-basis schema and proposition domain; +- exact footprint contract; +- target-profile identity; +- result-ID domain; +- canonical two-operation recovery shape. + +The interpreter and low-level attachment-algebra profile remain shared. This +is one Echo evaluator with two closed semantic programs, not two runtime +engines and not duplicated application logic. + +Package self-validation derives the required profile identities from the +installed program variant. An update invocation cannot be admitted against a +creation package, and a creation invocation cannot be admitted against an +update package. + +### Creation observes the full occupancy matrix + +Creation's application-basis proposition records node and attachment presence +independently as a closed four-state value: + +| Node | Attachment | Occupancy proposition | Evaluation | +| ------- | ---------- | --------------------- | ---------------------- | +| Absent | Absent | `Absent` | Create both | +| Present | Absent | `NodeOnly` | `PreconditionMismatch` | +| Absent | Present | `AttachmentOnly` | `PreconditionMismatch` | +| Present | Present | `NodeAndAttachment` | `PreconditionMismatch` | + +This preserves the distinction between coarse basis corroboration and the +program precondition. A caller can name the honest current occupancy +proposition; Echo independently corroborates it during admission; the creation +program then refuses every occupied state uniformly during private evaluation. +A dishonest claim of total absence against a bare node or orphan attachment is +rejected at admission with `BasisMismatch`. + +The evaluator checks node and attachment storage independently. It does not +infer attachment absence from node absence, because `GraphStore` can represent +an orphan attachment. It checks occupancy before validating an occupant's type: +the type of an occupied target is irrelevant to the caller's false vacancy +claim. + +### Creation is one atomic semantic rewrite + +A successful creation emits exactly: + +```text +UpsertNode(required_node_type) +SetAttachment(Atom(required_attachment_type, replacement_bytes)) +``` + +with: + +```text +in_slots = [node, alpha attachment] +out_slots = [node, alpha attachment] +``` + +The declared and actual footprint reads both locations and writes both +locations. There is no partial-create path that attaches onto an existing bare +node. + +The step counter measures deterministic evaluator stages, not the number of +emitted `WarpOp`s. Creation consumes three stages: + +1. probe the node; +2. probe the alpha attachment; +3. emit one atomic creation consequence. + +The consequence contains two ordered `WarpOp`s but is one semantic rewrite +step. Creation's minimum and exact successful budget for payload length `n` is: + +```text +steps = 3 +read_bytes = 64 +write_bytes = 64 + n +``` + +The 64 write bytes are the 32-byte `NodeRecord.ty` plus the 32-byte attachment +atom type. Compare-and-set remains four stages and charges `32 + n` write bytes. + +### Result identity binds absence without perturbing updates + +Compare-and-set retains the original result-ID domain, result schema, and raw +prior-digest hash input. + +Creation uses a separate result-ID domain and result schema. Its identity input +binds an explicit domain-separated absence proposition rather than an optional +digest or an untagged empty value. Consequently, adding creation does not add a +tag, marker, or alternate interpretation to any update result identity. + +### WAL recovery validates the installed semantic shape + +Recovery selects the canonical consequence from the exact installed program, +which package and receipt validation bind to the retained commit: + +- compare-and-set accepts only one `SetAttachment` operation and an + attachment-only output list; +- create-if-absent accepts only ordered `UpsertNode`, `SetAttachment` + operations and node-plus-attachment outputs. + +Both profiles additionally require the program-owned attachment type, an atom +value, and a payload no larger than `max_replacement_bytes`. Creation also +requires the program-owned `NodeRecord.ty`. Missing, reversed, partial, +cross-node, cross-profile, wrong-typed, non-atom, or oversized consequences are +refused. + +`WorldlineTickPatchV1.warp_id` names the parent worldline's root WARP. Operation +scope comes from the exact `NodeKey` carried by the operations and slots, which +may name a descended WARP instance. Recovery validates the parent root and the +operation scope independently; it does not require those WARP IDs to be equal. +For a descended target, private evaluation walks the validated instance-parent +chain to the root, records every portal attachment as an actual and declared +footprint read, charges one bounded pointer read per portal, and retains the +complete chain in the patch input slots. Recovery requires descendant +consequences to carry attachment inputs reaching the parent root rather than +accepting the target node and attachment alone. + +## Scope + +This decision closes exactly the: + +> single anchored-node-plus-alpha-attachment creation gap + +It does not close creation for arbitrary "new facts." It does not provide +multi-node, multi-record, edge, relation, deletion, or general DPO rewrite +semantics. + +In particular, Graft's `recordGitWarpImportBatch` operation declares creation +of both `GitWarpImportBatch` and `StructuralBasis`. This ADR does not establish +that one Echo node plus one opaque attachment truthfully represents those two +logical records, and it does not authorize packing them together merely to fit +this primitive. A pinned Graft crossing must determine whether the operation +requires the smallest bounded atomic multi-record program. + +This ADR also does not create a real Edict crossing. The current Echo evidence +still uses Echo-side package fixtures. Edict does not yet emit the exact +`ExecutableOperationPackageV1` form, a real Graft lawpack does not yet traverse +the corridor, and a structurally separate target verifier does not yet +corroborate the package. + +## Verification Evidence Grade + +The evidence grade remains **deterministic self-validation**, not independent +semantic conformance. + +The branch's executable-operation pipeline grew from 13 to 24 tests. The +creation matrix and durability witnesses cover: + +- success from node-absent plus attachment-absent; +- refusal for node-only occupancy; +- refusal for attachment-only occupancy; +- refusal for node-plus-attachment occupancy; +- uniform `PreconditionMismatch` for a wrong-typed occupied node; +- dishonest absence refusal during admission; +- exact `64 + payload` creation write budget and one-byte-under refusal; +- exact-basis TOCTOU refusal; +- filesystem-WAL commit and fresh-host recovery of the installed package, + node, attachment, result identity, and typed receipt; +- legacy update missing-state and wrong-node-type behavior. + +Focused unit witnesses additionally cover: + +- exact legacy update program and invocation canonical bytes; +- exact legacy update result identity; +- rejection of `Null` under the legacy invocation schema; +- installed-program-selected WAL consequences; +- rejection of reversed, partial, wrong-output, cross-node, cross-profile, + wrong-typed, non-atom, and oversized creation patches; +- descendant-node scope independent of the parent worldline root; +- complete descendant portal-chain footprint, budget, and patch-input binding. + +The complete `warp-core` library suite passes with 690 tests, and the focused +pipeline passes all 24 tests. Independent implementation or differential +oracle evidence remains future work. + +## Rejected Alternatives + +- **Keep `Option` in compare-and-set.** Rejected because `None` is a + strong negative application condition, not "no precondition," and because it + changes the meaning of existing program and profile identities. +- **Introduce one `/v2` program with `Exact(Hash) | Absent`.** This would be + semantically honest, but update and creation still have different patch, + footprint, budget, output, recovery, and result contracts. Two explicit + program profiles make those differences structural. +- **Infer the program from patch shape during recovery.** Rejected because + recovery must validate the consequence authorized by the installed semantic + profile, not let arbitrary retained operations choose their interpreter. +- **Attach onto a pre-existing bare node.** Rejected because it is a third + semantic program with a different precondition and footprint, and no earned + caller currently requires it. +- **Represent absence as empty bytes.** Rejected because a present empty atom + is not absence. +- **Generalize directly to arbitrary multi-record DPO rewrites.** Rejected + until the pinned Graft RED establishes the smallest missing bounded program. + +## Consequences + +- Existing compare-and-set program, invocation, and result identities remain + stable. +- Creation packages and invocations necessarily receive new identities because + they bind a different executable law. +- Runtime package validation now depends on the selected program profile rather + than one global set of schema and target constants. +- Creation receipts truthfully report three semantic evaluator steps and + `64 + payload` write bytes. +- Fresh-host recovery accepts the exact two-operation creation consequence and + rejects malformed or cross-profile shapes. +- The next convergence task is a pinned external-consumer RED using exact + Graft schema, manifest, invocation, and real Edict output—without an + Echo-side handwritten package builder. + +## Non-Goals + +Unchanged from ADR 0023: no application-specific intrinsic, native callback, +general-purpose VM, arbitrary graph interpreter, external effect execution, +Continuum transport, scheduler batch composition, or production application +cutover is introduced here. + +This decision additionally excludes multi-record creation, multi-node +creation, edges, deletion, partial creation, a Graft-specific mapping, Edict +package emission, and replacement of Graft's current transport. + +## Evidence Anchors + +- `crates/warp-core/src/echo_operation.rs`: + `EchoOperationProgramV1`, `EchoOperationInvocationV1`, + `current_application_basis`, `prepare_operation_v1`, + `operation_result_id`. +- `crates/warp-core/src/trusted_runtime_host.rs`: + `operation_patch_scope_v1`, `operation_tick_binds_patch_v1`. +- `crates/warp-core/tests/executable_operation_pipeline_tests.rs`: the 24-test + external-consumer pipeline, including the creation occupancy, budget, TOCTOU, + and filesystem-WAL witnesses. +- `docs/adr/0023-admitted-executable-operation-packages.md`: the executable + operation category and program substitution boundary. +- `CHANGELOG.md`: current shipped-surface limits, including the absent real + Edict and application crossing. diff --git a/docs/adr/README.md b/docs/adr/README.md index 97504cb1..170b0ecf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -45,6 +45,7 @@ track work, progress, priority, or release readiness. | [0021](0021-public-optic-observation-boundary.md) | Accepted | Public WARP optic over internal observation | | [0022](0022-application-requested-causal-anchor-admission.md) | Accepted | Application-requested, Echo-owned anchor admission | | [0023](0023-admitted-executable-operation-packages.md) | Accepted | Admitted executable operation packages | +| [0024](0024-anchored-node-creation-from-absence.md) | Accepted | Anchored-node creation as a separate executable program | ADR 0006 predates this index contract and did not declare a status. Its superseded tombstone preserves that fact without silently ratifying the old diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index 4f1793bc..9824987d 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -148,21 +148,26 @@ Echo runtime authority. The preceding paragraph describes the implemented provider-v1 compatibility corridor. [ADR 0023](../adr/0023-admitted-executable-operation-packages.md) accepts a distinct executable-operation category for new application -mutations. Echo now implements its first generic runtime slice: package and -invocation admission, installation of a data-only anchored-attachment program, -private bounded evaluation, exact-basis singleton commit, typed evidence, WAL, -and callback-free fresh-host recovery of committed consequences. Noncommitted -evidence is returned to the caller but does not enter the operation-tick WAL. -The slice exposes no application -matcher, executor, or footprint callback. It is not yet emitted by Edict, does -not implement Jedit `ReplaceRange`, and has no structurally separate target -verifier or general scheduler batch composition. Provider v1 remains stable -while consumers migrate; it is not renamed or silently reinterpreted as the -executable-operation corridor. The program digest supplies executable meaning -only: it cannot independently confer an operation coordinate, invocability, or -authority, and Echo cannot install or invoke it naked. The admitted operation -package binds the public contract and semantic closure to the exact program, -after which Echo independently admits each invocation. +mutations. Echo now implements two bounded anchored-attachment programs in that +runtime slice: the original update-only compare-and-set and a separately +identified single-node-plus-alpha-attachment create-if-absent profile. Package +and invocation admission, private bounded evaluation, exact-basis singleton +commit, typed evidence, WAL, and callback-free fresh-host recovery validate the +selected program's exact footprint, budget, result, and patch shape. +Operations targeting a descended WARP retain and meter every portal attachment +in the root-to-target reachability chain as a causal input. +Noncommitted evidence is returned to the caller but does not enter the +operation-tick WAL. The slice exposes no application matcher, executor, or +footprint callback. It is not yet emitted by Edict, does not implement Jedit +`ReplaceRange` or Graft's multi-record mutation, and has no structurally +separate target verifier or general scheduler batch composition. Provider v1 +remains stable while consumers migrate; it is not renamed or silently +reinterpreted as the executable-operation corridor. The program digest +supplies executable meaning only: it cannot independently confer an operation +coordinate, invocability, or authority, and Echo cannot install or invoke it +naked. The admitted operation package binds the public contract and semantic +closure to the exact program, after which Echo independently admits each +invocation. ## External Edict Provider Artifacts diff --git a/docs/topics/GeneratedRules.md b/docs/topics/GeneratedRules.md index c6fb832c..67175756 100644 --- a/docs/topics/GeneratedRules.md +++ b/docs/topics/GeneratedRules.md @@ -147,9 +147,14 @@ exact canonical ExecutableOperationPackageV1 bytes -> committed outcomes only: execution-kernel WAL retention and callback-free recovery ``` -The first program is an anchored typed-node alpha-attachment compare-and-set. -It is a generic vertical witness, not Jedit `ReplaceRange`, a rope intrinsic, or -a general-purpose virtual machine. The package binds Edict source, canonical +The first two programs are an anchored typed-node alpha-attachment +compare-and-set and a separate create-if-absent profile. Compare-and-set keeps +its original update-only bytes and identities. Creation has distinct +invocation, schema, footprint, application-basis, result, recovery, and target +profiles; it creates exactly one node and its alpha attachment only when both +locations are absent. These are generic vertical witnesses, not Jedit +`ReplaceRange`, a Graft multi-record mutation, a rope intrinsic, or a +general-purpose virtual machine. The package binds Edict source, canonical meaning, Core, Target IR, application-schema, and lawpack identities as opaque substitution evidence. Echo does not yet obtain those identities from a real Edict compiler crossing or validate their semantic relation through a @@ -175,11 +180,13 @@ The program bytes bind their interpreter and intrinsic profiles directly. The parent patch and singleton tick entry use the admitted installed-operation identity as their rule-pack/rule identity, so two packages that reuse identical program bytes do not collapse into one causal operation identity. -The first slice composes one preparation and does not claim general scheduler -batch integration or independent implementation evidence. Its canonical -decoding, identity recomputation, budget and footprint checks, exact-basis -comparison, deterministic repeatability, and WAL recovery are deterministic -self-validation. +The current slice composes one preparation and does not claim general scheduler +batch integration, Graft-style multi-record mutation, or independent +implementation evidence. Its canonical decoding, identity recomputation, +budget and footprint checks, exact-basis comparison, deterministic +repeatability, and WAL recovery are deterministic self-validation. +For descended targets, the retained footprint and patch inputs include every +portal attachment in the validated root-to-target reachability chain. The slice currently reuses `TrustedRuntimeHost`, whose module is exposed only under the joint `native_rule_bootstrap` and `trusted_runtime` feature gate. That diff --git a/tests/docs/test_adr_namespace.sh b/tests/docs/test_adr_namespace.sh index 3de45c8e..45417e27 100755 --- a/tests/docs/test_adr_namespace.sh +++ b/tests/docs/test_adr_namespace.sh @@ -34,9 +34,10 @@ readonly current_adrs=( "0021-public-optic-observation-boundary.md" "0022-application-requested-causal-anchor-admission.md" "0023-admitted-executable-operation-packages.md" + "0024-anchored-node-creation-from-absence.md" ) -readonly current_adr_last=23 +readonly current_adr_last=24 readonly superseded_legacy_adrs=( "ADR-0003-Materialization-Bus.md"