From 1eacd3876d1529088575360fd534010317daaf23 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 13 Jun 2026 12:52:10 -0700 Subject: [PATCH 1/6] Fix: require posture on strand creation --- CHANGELOG.md | 5 + crates/warp-core/src/coordinator.rs | 370 +++++++++++++++++- crates/warp-core/src/neighborhood.rs | 30 ++ crates/warp-core/src/observation.rs | 27 ++ crates/warp-core/src/revelation.rs | 64 +++ crates/warp-core/src/settlement.rs | 28 ++ crates/warp-core/src/strand.rs | 11 + .../warp-core/tests/strand_contract_tests.rs | 54 ++- 8 files changed, 583 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d654b21a..1b0422b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### Added +- `warp-core` strand creation now carries explicit `RetentionPosture` through + `ForkStrandRequest`, `ForkStrandReceipt`, and `Strand`. Session-default and + debugger fork constructors choose posture policy explicitly, debugger forks + never silently become `Shared`, and `StrandRegistry` rejects incoherent + retained posture such as `Shared` without an admission scope. - `warp-core` import admission receipts now bind local source-shared import admission to an explicit imported artifact identity. A receipt minted for one imported artifact cannot admit another import into a local shared admission diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 8c5450c0..97db1cbc 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -25,6 +25,10 @@ use crate::provenance_store::{ ProvenanceService, ProvenanceStore, ReplayError, }; use crate::receipt::{TickReceiptDisposition, TickReceiptRejection}; +use crate::revelation::{ + AuthorityDomainRef, CausalPosture, OriginId, PostureDerivation, PostureObstruction, + RetentionPosture, SessionContext, +}; use crate::strand::{ForkBasisRef, Strand, StrandError, StrandId, StrandRegistry, SupportPin}; use crate::worldline::{ApplyError, WorldlineId}; use crate::worldline_registry::WorldlineRegistry; @@ -866,6 +870,61 @@ pub struct ForkStrandRequest { pub child_worldline_id: WorldlineId, /// Writer heads to register for the child worldline. pub writer_heads: Vec, + /// Explicit posture, authority, admission scope, and retention contract. + pub retention_posture: RetentionPosture, +} + +impl ForkStrandRequest { + /// Builds a fork request that inherits the session's default posture. + /// + /// # Errors + /// + /// Returns a posture obstruction if the session default cannot produce a + /// valid retained-work posture. + pub fn from_session_default( + strand_id: StrandId, + source_lane_id: WorldlineId, + fork_tick: WorldlineTick, + child_worldline_id: WorldlineId, + writer_heads: Vec, + session: &SessionContext, + ) -> Result { + Ok(Self { + strand_id, + source_lane_id, + fork_tick, + child_worldline_id, + writer_heads, + retention_posture: session.retention_posture(PostureDerivation::SessionDefault)?, + }) + } + + /// Builds a debugger fork request under the session authority. + /// + /// Debugger-created strands are never silently admitted into shared + /// history, even when the session default posture is `Shared`. + /// + /// # Errors + /// + /// Returns a posture obstruction if the session authority context cannot + /// produce a valid debugger posture. + pub fn debugger_default( + strand_id: StrandId, + source_lane_id: WorldlineId, + fork_tick: WorldlineTick, + child_worldline_id: WorldlineId, + writer_heads: Vec, + session: &SessionContext, + ) -> Result { + Ok(Self { + strand_id, + source_lane_id, + fork_tick, + child_worldline_id, + writer_heads, + retention_posture: session.debugger_retention_posture()?, + }) + } } /// Receipt returned after a strand fork succeeds. @@ -879,6 +938,8 @@ pub struct ForkStrandReceipt { pub child_worldline_id: WorldlineId, /// Writer heads authorized for the child worldline. pub writer_heads: Vec, + /// Retention posture registered on the created strand. + pub retention_posture: RetentionPosture, } // ============================================================================= @@ -1540,6 +1601,7 @@ impl WorldlineRuntime { .iter() .map(|head| *head.key()) .collect::>(); + let retention_posture = request.retention_posture; self.register_worldline(request.child_worldline_id, child_state)?; for head in request.writer_heads { @@ -1551,6 +1613,7 @@ impl WorldlineRuntime { child_worldline_id: request.child_worldline_id, writer_heads: writer_heads.clone(), support_pins: Vec::new(), + retention_posture, })?; Ok(ForkStrandReceipt { @@ -1558,6 +1621,7 @@ impl WorldlineRuntime { fork_basis_ref, child_worldline_id: request.child_worldline_id, writer_heads, + retention_posture, }) })(); @@ -2760,6 +2824,10 @@ fn hash_strand_error(hasher: &mut blake3::Hasher, err: &StrandError) { hasher.update(b"self-support-pin"); hasher.update(strand_id.as_bytes()); } + StrandError::Posture(obstruction) => { + hasher.update(b"posture"); + hash_posture_obstruction(hasher, obstruction); + } StrandError::DuplicateSupportTarget { owner, target } => { hasher.update(b"duplicate-support-target"); hasher.update(owner.as_bytes()); @@ -2782,6 +2850,115 @@ fn hash_strand_error(hasher: &mut blake3::Hasher, err: &StrandError) { } } +fn hash_causal_posture(hasher: &mut blake3::Hasher, posture: CausalPosture) { + hasher.update(&[posture.canonical_tag()]); +} + +fn hash_posture_derivation(hasher: &mut blake3::Hasher, derivation: PostureDerivation) { + let tag = match derivation { + PostureDerivation::ExplicitIntent => b"explicit-intent".as_slice(), + PostureDerivation::SessionDefault => b"session-default", + PostureDerivation::DebuggerDefault => b"debugger-default", + PostureDerivation::CounterfactualDefault => b"counterfactual-default", + PostureDerivation::LegacyDurableAssumedShared => b"legacy-durable-assumed-shared", + PostureDerivation::LegacyEphemeralAssumedScratch => b"legacy-ephemeral-assumed-scratch", + PostureDerivation::ImportedManifest => b"imported-manifest", + }; + hasher.update(tag); +} + +fn hash_origin_id(hasher: &mut blake3::Hasher, origin_id: &OriginId) { + hasher.update(origin_id.as_bytes()); +} + +fn hash_authority_domain_ref(hasher: &mut blake3::Hasher, authority: &AuthorityDomainRef) { + hash_origin_id(hasher, &authority.origin_id); + hasher.update(authority.domain_id.as_bytes()); +} + +fn hash_posture_obstruction(hasher: &mut blake3::Hasher, obstruction: &PostureObstruction) { + match obstruction { + PostureObstruction::NarrowingRefused { from, requested } => { + hasher.update(b"narrowing-refused"); + hash_causal_posture(hasher, *from); + hash_causal_posture(hasher, *requested); + } + PostureObstruction::AlreadyAtPosture { posture } => { + hasher.update(b"already-at-posture"); + hash_causal_posture(hasher, *posture); + } + PostureObstruction::ExceedsLeastRevealedMember { + shell, + least_revealed_member, + } => { + hasher.update(b"exceeds-least-revealed-member"); + hash_causal_posture(hasher, *shell); + hash_causal_posture(hasher, *least_revealed_member); + } + PostureObstruction::EmptyWitness => { + hasher.update(b"empty-witness"); + } + PostureObstruction::MissingAdmissionScope { posture } => { + hasher.update(b"missing-admission-scope"); + hash_causal_posture(hasher, *posture); + } + PostureObstruction::UnexpectedAdmissionScope { posture } => { + hasher.update(b"unexpected-admission-scope"); + hash_causal_posture(hasher, *posture); + } + PostureObstruction::InvalidMaterializationTransition { from, to } => { + hasher.update(b"invalid-materialization-transition"); + hash_causal_posture(hasher, *from); + hash_causal_posture(hasher, *to); + } + PostureObstruction::PromotionRequiresSharedTarget { to } => { + hasher.update(b"promotion-requires-shared-target"); + hash_causal_posture(hasher, *to); + } + PostureObstruction::SharedAdmissionRequiresIntent => { + hasher.update(b"shared-admission-requires-intent"); + } + PostureObstruction::AuthorityProofMismatch { authorized_by } => { + hasher.update(b"authority-proof-mismatch"); + hash_authority_domain_ref(hasher, authorized_by); + } + PostureObstruction::AuthorityOriginMismatch { + origin_id, + authority_origin, + } => { + hasher.update(b"authority-origin-mismatch"); + hash_origin_id(hasher, origin_id); + hash_origin_id(hasher, authority_origin); + } + PostureObstruction::AuthorityBindingOriginMismatch { + origin_id, + binding_origin, + } => { + hasher.update(b"authority-binding-origin-mismatch"); + hash_origin_id(hasher, origin_id); + hash_origin_id(hasher, binding_origin); + } + PostureObstruction::AuthorityBindingDomainMismatch { author_domain } => { + hasher.update(b"authority-binding-domain-mismatch"); + hash_authority_domain_ref(hasher, author_domain); + } + PostureObstruction::PostureDerivationMismatch { + posture, + derivation, + } => { + hasher.update(b"posture-derivation-mismatch"); + hash_causal_posture(hasher, *posture); + hash_posture_derivation(hasher, *derivation); + } + PostureObstruction::LegacyAuthorityCannotAuthorizeNewAdmission => { + hasher.update(b"legacy-authority-cannot-authorize-new-admission"); + } + PostureObstruction::WitnessIsNotAuthorityCapability => { + hasher.update(b"witness-is-not-authority-capability"); + } + } +} + fn scheduler_error_cause_digest(err: &RuntimeError) -> Hash { let mut hasher = blake3::Hasher::new(); hasher.update(b"echo.scheduler-fault-cause.error"); @@ -3221,7 +3398,12 @@ mod tests { use super::*; use crate::head::{make_head_id, WriterHead}; use crate::head_inbox::{make_intent_kind, InboxPolicy}; - use crate::playback::PlaybackMode; + use crate::playback::{PlaybackMode, SessionId}; + use crate::revelation::{ + ActorId, AdmissionScopeId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, + CausalPosture, OriginId, PostureDerivation, RetentionContractId, RetentionPosture, + SealStrength, SessionContext, + }; use crate::rule::{ConflictPolicy, PatternGraph, RewriteRule}; use crate::strand::make_strand_id; use crate::worldline::WorldlineId; @@ -3245,6 +3427,36 @@ mod tests { [n; 32] } + fn test_session_context( + n: u8, + default_posture: CausalPosture, + default_admission_scope: Option, + ) -> SessionContext { + let origin_id = OriginId::from_bytes([0x40u8.wrapping_add(n); 32]); + let author_domain = AuthorityDomainRef::new( + origin_id, + AuthorityDomainId::from_bytes([0x50u8.wrapping_add(n); 32]), + ); + SessionContext::new( + SessionId([0x60u8.wrapping_add(n); 32]), + origin_id, + ActorId::from_bytes([0x70u8.wrapping_add(n); 32]), + author_domain, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + default_posture, + default_admission_scope, + RetentionContractId::from_bytes([0x80u8.wrapping_add(n); 32]), + ) + .unwrap() + } + + fn test_retention_posture(n: u8) -> RetentionPosture { + test_session_context(n, CausalPosture::AuthorOnly, None) + .retention_posture(PostureDerivation::SessionDefault) + .unwrap() + } + fn empty_engine() -> Engine { let mut store = GraphStore::default(); let root = make_node_id("root"); @@ -3570,6 +3782,7 @@ mod tests { None, true, )], + retention_posture: test_retention_posture(10), }, ) .unwrap(); @@ -3597,6 +3810,159 @@ mod tests { assert_eq!(provenance.len(child_worldline_id).unwrap(), 1); } + #[test] + fn session_default_posture_inherits_into_created_work() { + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + let source_lane_id = wl(1); + let child_worldline_id = wl(2); + let strand_id = make_strand_id("fork-session-posture"); + let admission_scope = AdmissionScopeId::from_bytes([0x91; 32]); + let session = test_session_context(1, CausalPosture::Shared, Some(admission_scope)); + + runtime + .register_worldline(source_lane_id, WorldlineState::empty()) + .unwrap(); + register_head( + &mut runtime, + source_lane_id, + "source-default", + None, + true, + InboxPolicy::AcceptAll, + ); + + let mut provenance = mirrored_provenance(&runtime); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-source-commit", + ); + + let child_head_key = WriterHeadKey { + worldline_id: child_worldline_id, + head_id: make_head_id("child-default"), + }; + let request = ForkStrandRequest::from_session_default( + strand_id, + source_lane_id, + wt(0), + child_worldline_id, + vec![WriterHead::with_routing( + child_head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )], + &session, + ) + .unwrap(); + assert_eq!( + request.retention_posture.causal_posture, + CausalPosture::Shared + ); + assert_eq!( + request.retention_posture.posture_derivation, + PostureDerivation::SessionDefault + ); + assert_eq!( + request.retention_posture.admission_scope, + Some(admission_scope) + ); + + let receipt = runtime.fork_strand(&mut provenance, request).unwrap(); + let strand = runtime.strands().get(&strand_id).unwrap(); + assert_eq!(receipt.retention_posture, strand.retention_posture); + assert_eq!( + strand.retention_posture.posture_derivation, + PostureDerivation::SessionDefault + ); + assert_eq!( + strand.retention_posture.admission_scope, + Some(admission_scope) + ); + } + + #[test] + fn debugger_fork_defaults_to_non_shared_posture() { + let mut runtime = WorldlineRuntime::new(); + let mut engine = empty_engine(); + let source_lane_id = wl(1); + let child_worldline_id = wl(2); + let strand_id = make_strand_id("fork-debugger-posture"); + let session = test_session_context( + 2, + CausalPosture::Shared, + Some(AdmissionScopeId::from_bytes([0x92; 32])), + ); + + runtime + .register_worldline(source_lane_id, WorldlineState::empty()) + .unwrap(); + register_head( + &mut runtime, + source_lane_id, + "source-default", + None, + true, + InboxPolicy::AcceptAll, + ); + + let mut provenance = mirrored_provenance(&runtime); + commit_one_tick( + &mut runtime, + &mut provenance, + &mut engine, + source_lane_id, + "fork-source-commit", + ); + + let child_head_key = WriterHeadKey { + worldline_id: child_worldline_id, + head_id: make_head_id("child-default"), + }; + let request = ForkStrandRequest::debugger_default( + strand_id, + source_lane_id, + wt(0), + child_worldline_id, + vec![WriterHead::with_routing( + child_head_key, + PlaybackMode::Play, + InboxPolicy::AcceptAll, + None, + true, + )], + &session, + ) + .unwrap(); + assert_ne!( + request.retention_posture.causal_posture, + CausalPosture::Shared + ); + assert_eq!( + request.retention_posture.causal_posture, + CausalPosture::AuthorOnly + ); + assert_eq!( + request.retention_posture.posture_derivation, + PostureDerivation::DebuggerDefault + ); + assert_eq!(request.retention_posture.admission_scope, None); + + let receipt = runtime.fork_strand(&mut provenance, request).unwrap(); + let strand = runtime.strands().get(&strand_id).unwrap(); + assert_eq!(receipt.retention_posture, strand.retention_posture); + assert_eq!( + strand.retention_posture.posture_derivation, + PostureDerivation::DebuggerDefault + ); + assert_eq!(strand.retention_posture.admission_scope, None); + } + #[test] fn fork_strand_from_non_tip_tick_materializes_historical_basis() { let mut runtime = WorldlineRuntime::new(); @@ -3659,6 +4025,7 @@ mod tests { None, true, )], + retention_posture: test_retention_posture(11), }, ) .unwrap(); @@ -3721,6 +4088,7 @@ mod tests { Some(InboxAddress("wrong-worldline".to_owned())), false, )], + retention_posture: test_retention_posture(12), }, ) .unwrap_err(); diff --git a/crates/warp-core/src/neighborhood.rs b/crates/warp-core/src/neighborhood.rs index a06315d5..478d840c 100644 --- a/crates/warp-core/src/neighborhood.rs +++ b/crates/warp-core/src/neighborhood.rs @@ -612,6 +612,11 @@ mod tests { use crate::provenance_store::{ProvenanceEntry, ProvenanceRef}; use crate::receipt::TickReceipt; use crate::record::NodeRecord; + use crate::revelation::{ + ActorId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, CausalAuthority, + CausalPosture, OriginId, PostureDerivation, RetentionContractId, RetentionPosture, + SealStrength, + }; use crate::snapshot::Snapshot; use crate::strand::{make_strand_id, ForkBasisRef, Strand}; use crate::tick_patch::{TickCommitStatus, WarpTickPatchV1}; @@ -632,6 +637,27 @@ mod tests { GlobalTick::from_raw(raw) } + fn test_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x31; 32]); + let authority = + AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x32; 32])); + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x33; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .unwrap(), + RetentionContractId::from_bytes([0x34; 32]), + None, + ) + .unwrap() + } + fn committed_state( worldline_id: WorldlineId, global_tick: GlobalTick, @@ -860,6 +886,7 @@ mod tests { child_worldline_id: support_worldline, writer_heads: vec![support_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); @@ -881,6 +908,7 @@ mod tests { child_worldline_id: primary_worldline, writer_heads: vec![primary_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); runtime @@ -1047,6 +1075,7 @@ mod tests { child_worldline_id: support_worldline, writer_heads: vec![support_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); let primary_strand_id = make_strand_id("primary"); @@ -1067,6 +1096,7 @@ mod tests { child_worldline_id: primary_worldline, writer_heads: vec![primary_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); runtime diff --git a/crates/warp-core/src/observation.rs b/crates/warp-core/src/observation.rs index b48d3d41..69162957 100644 --- a/crates/warp-core/src/observation.rs +++ b/crates/warp-core/src/observation.rs @@ -2575,6 +2575,11 @@ mod tests { use crate::provenance_store::replay_artifacts_for_entry; use crate::receipt::TickReceipt; use crate::record::{EdgeRecord, NodeRecord}; + use crate::revelation::{ + ActorId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, CausalAuthority, + CausalPosture, OriginId, PostureDerivation, RetentionContractId, RetentionPosture, + SealStrength, + }; use crate::snapshot::compute_commit_hash_v2; use crate::strand::{make_strand_id, ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, TickCommitStatus, WarpOp, WarpTickPatchV1}; @@ -2622,6 +2627,27 @@ mod tests { GlobalTick::from_raw(raw) } + fn test_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x41; 32]); + let authority = + AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x42; 32])); + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x43; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .unwrap(), + RetentionContractId::from_bytes([0x44; 32]), + None, + ) + .unwrap() + } + fn optic_request( worldline_id: WorldlineId, shape: OpticApertureShape, @@ -2996,6 +3022,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); diff --git a/crates/warp-core/src/revelation.rs b/crates/warp-core/src/revelation.rs index ce3d9eb5..a29818a9 100644 --- a/crates/warp-core/src/revelation.rs +++ b/crates/warp-core/src/revelation.rs @@ -303,6 +303,18 @@ impl RetentionPosture { admission_scope, }) } + + /// Re-validates a retained posture bundle after direct field mutation. + /// + /// # Errors + /// + /// Returns an obstruction when the posture/scope pair, derivation, or + /// authority context is incoherent. + pub fn validate(&self) -> Result<(), PostureObstruction> { + validate_admission_scope(self.causal_posture, self.admission_scope)?; + validate_posture_derivation(self.causal_posture, self.posture_derivation)?; + self.authority.validate() + } } /// Session context posture and authority defaults. @@ -361,6 +373,58 @@ impl SessionContext { retention_contract, }) } + + /// Builds retained work posture from this session's explicit default. + /// + /// # Errors + /// + /// Returns a posture obstruction if this session's authority/default + /// posture no longer validates. + pub fn retention_posture( + &self, + posture_derivation: PostureDerivation, + ) -> Result { + RetentionPosture::new( + self.default_posture, + posture_derivation, + CausalAuthority::new( + self.origin_id, + self.actor_id, + self.author_domain, + self.authority_binding, + self.seal_strength, + )?, + self.retention_contract, + self.default_admission_scope, + ) + } + + /// Builds debugger-created strand posture for this session. + /// + /// Debugger work is real causal work, but it is never silently admitted + /// into shared history. The named constructor is the policy boundary that + /// chooses `AuthorOnly` even when the surrounding session default is + /// `Shared`. + /// + /// # Errors + /// + /// Returns a posture obstruction if this session's authority context does + /// not validate. + pub fn debugger_retention_posture(&self) -> Result { + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::DebuggerDefault, + CausalAuthority::new( + self.origin_id, + self.actor_id, + self.author_domain, + self.authority_binding, + self.seal_strength, + )?, + self.retention_contract, + None, + ) + } } /// Obstruction raised when a posture act is unlawful. diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 2dc715f6..d034f1ad 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -1542,6 +1542,10 @@ mod tests { use crate::ident::{make_edge_id, make_node_id, make_type_id}; use crate::playback::PlaybackMode; use crate::record::{EdgeRecord, NodeRecord}; + use crate::revelation::{ + ActorId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, CausalAuthority, + OriginId, PostureDerivation, RetentionContractId, RetentionPosture, SealStrength, + }; use crate::strand::{ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, WarpOp}; use crate::{GraphStore, WorldlineState}; @@ -1558,6 +1562,27 @@ mod tests { GlobalTick::from_raw(raw) } + fn test_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x51; 32]); + let authority = + AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x52; 32])); + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x53; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .unwrap(), + RetentionContractId::from_bytes([0x54; 32]), + None, + ) + .unwrap() + } + fn register_head( runtime: &mut WorldlineRuntime, worldline_id: WorldlineId, @@ -1852,6 +1877,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }; runtime.register_strand(strand).unwrap(); @@ -1909,6 +1935,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); } @@ -1960,6 +1987,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }) .unwrap(); ( diff --git a/crates/warp-core/src/strand.rs b/crates/warp-core/src/strand.rs index 98ee92ed..f8e11938 100644 --- a/crates/warp-core/src/strand.rs +++ b/crates/warp-core/src/strand.rs @@ -33,6 +33,7 @@ use thiserror::Error; use crate::clock::WorldlineTick; use crate::ident::Hash; use crate::provenance_store::{ProvenanceRef, ProvenanceService, ProvenanceStore}; +use crate::revelation::{PostureObstruction, RetentionPosture}; use crate::tick_patch::SlotId; use crate::worldline::WorldlineId; @@ -143,6 +144,8 @@ pub struct Strand { pub writer_heads: Vec, /// Read-only support pins for braid geometry. pub support_pins: Vec, + /// Explicit retention, authority, and admission posture for the strand. + pub retention_posture: RetentionPosture, } impl Strand { @@ -467,6 +470,10 @@ pub enum StrandError { #[error("strand must not support-pin itself: {0:?}")] SelfSupportPin(StrandId), + /// The strand carried an incoherent retention posture. + #[error("strand posture obstruction: {0:?}")] + Posture(PostureObstruction), + /// A support pin duplicated an already pinned support target. #[error("duplicate support pin target: owner {owner:?}, target {target:?}")] DuplicateSupportTarget { @@ -596,6 +603,10 @@ impl StrandRegistry { if self.strands.contains_key(&strand.strand_id) { return Err(StrandError::AlreadyExists(strand.strand_id)); } + strand + .retention_posture + .validate() + .map_err(StrandError::Posture)?; // INV-S7: distinct worldlines. if strand.child_worldline_id == strand.fork_basis_ref.source_lane_id { return Err(StrandError::InvariantViolation( diff --git a/crates/warp-core/tests/strand_contract_tests.rs b/crates/warp-core/tests/strand_contract_tests.rs index 7c30b150..ee4b9adf 100644 --- a/crates/warp-core/tests/strand_contract_tests.rs +++ b/crates/warp-core/tests/strand_contract_tests.rs @@ -12,11 +12,13 @@ use warp_core::strand::{ StrandRevalidationState, SupportPin, }; use warp_core::{ - make_head_id, make_node_id, make_type_id, make_warp_id, GlobalTick, GraphStore, HashTriplet, - HeadEligibility, LocalProvenanceStore, NodeRecord, PlaybackHeadRegistry, PlaybackMode, - ProvenanceEntry, ProvenanceRef, ProvenanceService, ProvenanceStore, RunnableWriterSet, SlotId, - WarpId, WorldlineId, WorldlineState, WorldlineTick, WorldlineTickHeaderV1, - WorldlineTickPatchV1, WriterHead, WriterHeadKey, + make_head_id, make_node_id, make_type_id, make_warp_id, ActorId, AuthorityBinding, + AuthorityDomainId, AuthorityDomainRef, CausalAuthority, CausalPosture, GlobalTick, GraphStore, + HashTriplet, HeadEligibility, LocalProvenanceStore, NodeRecord, OriginId, PlaybackHeadRegistry, + PlaybackMode, PostureDerivation, PostureObstruction, ProvenanceEntry, ProvenanceRef, + ProvenanceService, ProvenanceStore, RetentionContractId, RetentionPosture, RunnableWriterSet, + SealStrength, SlotId, WarpId, WorldlineId, WorldlineState, WorldlineTick, + WorldlineTickHeaderV1, WorldlineTickPatchV1, WriterHead, WriterHeadKey, }; // ── Helpers ───────────────────────────────────────────────────────────────── @@ -29,6 +31,26 @@ fn wt(n: u64) -> WorldlineTick { WorldlineTick::from_raw(n) } +fn test_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x21; 32]); + let authority = AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x22; 32])); + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x23; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .expect("test authority"), + RetentionContractId::from_bytes([0x24; 32]), + None, + ) + .expect("test retention posture") +} + fn test_initial_state() -> WorldlineState { let warp_id = make_warp_id("strand-test-warp"); let root = make_node_id("strand-test-root"); @@ -93,6 +115,7 @@ fn make_test_strand( child_worldline_id: child_worldline, writer_heads: vec![head_key], support_pins: Vec::new(), + retention_posture: test_retention_posture(), } } @@ -405,6 +428,7 @@ fn live_basis_report_allows_parent_advance_outside_owned_footprint() { head_id: make_head_id("live-basis-disjoint-head"), }], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }; let report = strand @@ -476,6 +500,7 @@ fn live_basis_report_requires_revalidation_when_parent_invades_owned_footprint() head_id: make_head_id("live-basis-overlap-head"), }], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }; let report = strand @@ -582,6 +607,7 @@ fn registry_insert_rejects_inv_s8_wrong_head_worldline() { head_id: make_head_id("wrong-wl-head"), }], support_pins: Vec::new(), + retention_posture: test_retention_posture(), }; let err = registry.insert(strand).expect_err("INV-S8 should reject"); assert!( @@ -590,6 +616,23 @@ fn registry_insert_rejects_inv_s8_wrong_head_worldline() { ); } +#[test] +fn registry_insert_rejects_shared_posture_without_admission_scope() { + let mut registry = StrandRegistry::new(); + let mut strand = make_test_strand("shared-without-scope", wl(1), wl(2), wt(5)); + strand.retention_posture.causal_posture = CausalPosture::Shared; + strand.retention_posture.admission_scope = None; + let err = registry + .insert(strand) + .expect_err("Shared strand without admission scope should reject"); + assert_eq!( + err, + StrandError::Posture(PostureObstruction::MissingAdmissionScope { + posture: CausalPosture::Shared, + }) + ); +} + #[test] fn registry_insert_accepts_valid_nonempty_support_pins() { let mut registry = StrandRegistry::new(); @@ -695,6 +738,7 @@ fn registry_insert_rejects_duplicate_support_target() { state_hash: [2; 32], }, ], + retention_posture: test_retention_posture(), }; let err = registry .insert(owner) From 03a8f01c7c0d712113074def555cad63e64ad2e5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 13 Jun 2026 17:50:15 -0700 Subject: [PATCH 2/6] Fix: reject non-shared strand settlement --- CHANGELOG.md | 4 ++ crates/warp-core/src/settlement.rs | 81 +++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0422b6..3012dfd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -550,6 +550,10 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Changed +- `warp-core` settlement planning now rejects non-`Shared` strands before + producing import candidates. Author-only/debugger strand suffixes can remain + real causal work, but they cannot enter base shared history without an + explicit shared admission posture. - Local determinism tooling now fails closed around `scripts/check-warp-core-serialization-boundaries.sh`. The serialization boundary guard is mandatory, runs through `bash` rather than executable mode, diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index d034f1ad..1bf16b1a 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -552,6 +552,14 @@ pub enum SettlementError { /// The strand fork coordinate cannot advance to a suffix start tick. #[error("fork tick overflow for strand {0:?}")] ForkTickOverflow(StrandId), + /// Settlement may only admit shared strands into base history. + #[error("strand {strand_id:?} with posture {posture:?} is not shared-admitted for settlement")] + NonSharedStrand { + /// Strand that attempted settlement. + strand_id: StrandId, + /// Effective posture carried by the strand. + posture: CausalPosture, + }, /// Runtime frontier state and provenance history disagree for a worldline. #[error("runtime/provenance drift for worldline {worldline_id:?}: frontier {frontier_tick}, provenance {provenance_len}")] RuntimeProvenanceDrift { @@ -686,6 +694,12 @@ impl SettlementService { policy: &SettlementPolicy, ) -> Result { let strand = strand(runtime.strands(), strand_id)?; + if strand.retention_posture.causal_posture != CausalPosture::Shared { + return Err(SettlementError::NonSharedStrand { + strand_id, + posture: strand.retention_posture.causal_posture, + }); + } let delta = Self::compare(runtime, provenance, strand_id)?; let target_worldline = strand.fork_basis_ref.source_lane_id; let target_frontier_tick = @@ -1543,8 +1557,9 @@ mod tests { use crate::playback::PlaybackMode; use crate::record::{EdgeRecord, NodeRecord}; use crate::revelation::{ - ActorId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, CausalAuthority, - OriginId, PostureDerivation, RetentionContractId, RetentionPosture, SealStrength, + ActorId, AdmissionScopeId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, + CausalAuthority, OriginId, PostureDerivation, RetentionContractId, RetentionPosture, + SealStrength, }; use crate::strand::{ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, WarpOp}; @@ -1562,12 +1577,14 @@ mod tests { GlobalTick::from_raw(raw) } - fn test_retention_posture() -> RetentionPosture { + fn test_retention_posture(posture: CausalPosture) -> RetentionPosture { let origin_id = OriginId::from_bytes([0x51; 32]); let authority = AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x52; 32])); + let admission_scope = + (posture == CausalPosture::Shared).then_some(AdmissionScopeId::from_bytes([0x55; 32])); RetentionPosture::new( - CausalPosture::AuthorOnly, + posture, PostureDerivation::ExplicitIntent, CausalAuthority::new( origin_id, @@ -1578,11 +1595,19 @@ mod tests { ) .unwrap(), RetentionContractId::from_bytes([0x54; 32]), - None, + admission_scope, ) .unwrap() } + fn shared_retention_posture() -> RetentionPosture { + test_retention_posture(CausalPosture::Shared) + } + + fn author_only_retention_posture() -> RetentionPosture { + test_retention_posture(CausalPosture::AuthorOnly) + } + fn register_head( runtime: &mut WorldlineRuntime, worldline_id: WorldlineId, @@ -1817,6 +1842,19 @@ mod tests { StrandId, WorldlineId, WorldlineId, + ) { + setup_runtime_with_strand_posture(parent_drift, shared_retention_posture()) + } + + fn setup_runtime_with_strand_posture( + parent_drift: ParentDrift, + retention_posture: RetentionPosture, + ) -> ( + WorldlineRuntime, + ProvenanceService, + StrandId, + WorldlineId, + WorldlineId, ) { let base_worldline = wl(1); let child_worldline = wl(2); @@ -1877,7 +1915,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), - retention_posture: test_retention_posture(), + retention_posture, }; runtime.register_strand(strand).unwrap(); @@ -1935,7 +1973,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), - retention_posture: test_retention_posture(), + retention_posture, }) .unwrap(); } @@ -1987,7 +2025,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), - retention_posture: test_retention_posture(), + retention_posture, }) .unwrap(); ( @@ -2051,6 +2089,33 @@ mod tests { .is_some()); } + #[test] + fn settlement_rejects_author_only_strand_without_shared_admission() { + let (runtime, provenance, strand_id, _, _) = + setup_runtime_with_strand_posture(ParentDrift::None, author_only_retention_posture()); + + let err = SettlementService::plan(&runtime, &provenance, strand_id) + .expect_err("AuthorOnly strand suffixes must not settle into shared base history"); + assert!(matches!( + err, + SettlementError::NonSharedStrand { + strand_id: rejected, + posture: CausalPosture::AuthorOnly, + } if rejected == strand_id + )); + let mut runtime_for_settle = runtime.clone(); + let mut provenance_for_settle = provenance.clone(); + assert!( + SettlementService::settle( + &mut runtime_for_settle, + &mut provenance_for_settle, + strand_id + ) + .is_err(), + "settle must not bypass the planning gate" + ); + } + #[test] fn settlement_imports_child_suffix_when_parent_advanced_disjoint() { let (mut runtime, mut provenance, strand_id, base_worldline, child_worldline) = From 2164cc4c7b1835f8ec9efb0180392f4bdc6bb5f5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 13 Jun 2026 17:54:44 -0700 Subject: [PATCH 3/6] Fix: bind session posture derivation --- CHANGELOG.md | 7 ++++--- crates/warp-core/src/coordinator.rs | 4 ++-- crates/warp-core/src/revelation.rs | 31 ++++++++++++++++++++++++----- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3012dfd0..8e956b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ - `warp-core` strand creation now carries explicit `RetentionPosture` through `ForkStrandRequest`, `ForkStrandReceipt`, and `Strand`. Session-default and - debugger fork constructors choose posture policy explicitly, debugger forks - never silently become `Shared`, and `StrandRegistry` rejects incoherent - retained posture such as `Shared` without an admission scope. + debugger fork constructors choose posture policy explicitly, session-default + work always records `PostureDerivation::SessionDefault`, debugger forks never + silently become `Shared`, and `StrandRegistry` rejects incoherent retained + posture such as `Shared` without an admission scope. - `warp-core` import admission receipts now bind local source-shared import admission to an explicit imported artifact identity. A receipt minted for one imported artifact cannot admit another import into a local shared admission diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 97db1cbc..01a020f1 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -895,7 +895,7 @@ impl ForkStrandRequest { fork_tick, child_worldline_id, writer_heads, - retention_posture: session.retention_posture(PostureDerivation::SessionDefault)?, + retention_posture: session.retention_posture()?, }) } @@ -3453,7 +3453,7 @@ mod tests { fn test_retention_posture(n: u8) -> RetentionPosture { test_session_context(n, CausalPosture::AuthorOnly, None) - .retention_posture(PostureDerivation::SessionDefault) + .retention_posture() .unwrap() } diff --git a/crates/warp-core/src/revelation.rs b/crates/warp-core/src/revelation.rs index a29818a9..9942b487 100644 --- a/crates/warp-core/src/revelation.rs +++ b/crates/warp-core/src/revelation.rs @@ -380,13 +380,10 @@ impl SessionContext { /// /// Returns a posture obstruction if this session's authority/default /// posture no longer validates. - pub fn retention_posture( - &self, - posture_derivation: PostureDerivation, - ) -> Result { + pub fn retention_posture(&self) -> Result { RetentionPosture::new( self.default_posture, - posture_derivation, + PostureDerivation::SessionDefault, CausalAuthority::new( self.origin_id, self.actor_id, @@ -1820,6 +1817,30 @@ mod tests { ); } + #[test] + fn session_default_posture_derivation_is_not_caller_selectable() { + let session = SessionContext::new( + SessionId([0x51; 32]), + OriginId::from_bytes([0xA1; 32]), + ActorId::from_bytes([0xA2; 32]), + fixture_authority_ref(), + AuthorityBinding::LocalUnbound { + origin: OriginId::from_bytes([0xA1; 32]), + }, + SealStrength::Advisory, + CausalPosture::Shared, + Some(AdmissionScopeId::from_bytes([0x55; 32])), + RetentionContractId::from_bytes([0xC0; 32]), + ) + .unwrap(); + + let posture = session.retention_posture().unwrap(); + assert_eq!( + posture.posture_derivation, + PostureDerivation::SessionDefault + ); + } + fn fixture_authority_ref() -> AuthorityDomainRef { AuthorityDomainRef::new( OriginId::from_bytes([0xA1; 32]), From dea53815fee0ad0eac94fc4c3971c33340a76f00 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 13 Jun 2026 17:58:49 -0700 Subject: [PATCH 4/6] Fix: preserve settlement shell posture --- CHANGELOG.md | 3 +++ crates/warp-core/src/settlement.rs | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e956b69..0943bb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -555,6 +555,9 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract producing import candidates. Author-only/debugger strand suffixes can remain real causal work, but they cannot enter base shared history without an explicit shared admission posture. +- `warp-core` settlement plural artifacts and retained braid shells now carry + the source strand posture instead of hard-coding author-only posture for + shared settlement records. - Local determinism tooling now fails closed around `scripts/check-warp-core-serialization-boundaries.sh`. The serialization boundary guard is mandatory, runs through `bash` rather than executable mode, diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 1bf16b1a..9397556a 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -700,6 +700,7 @@ impl SettlementService { posture: strand.retention_posture.causal_posture, }); } + let strand_posture = strand.retention_posture.causal_posture; let delta = Self::compare(runtime, provenance, strand_id)?; let target_worldline = strand.fork_basis_ref.source_lane_id; let target_frontier_tick = @@ -826,6 +827,7 @@ impl SettlementService { &source_entry, entry_overlap_slots, policy, + strand_posture, ))); continue; } else { @@ -888,9 +890,13 @@ impl SettlementService { braid_shell: None, }); } - let (fork_basis_ref, support_pins) = { + let (fork_basis_ref, support_pins, strand_posture) = { let settled = strand(runtime.strands(), strand_id)?; - (settled.fork_basis_ref, settled.support_pins.clone()) + ( + settled.fork_basis_ref, + settled.support_pins.clone(), + settled.retention_posture.causal_posture, + ) }; let runtime_before = runtime.clone(); @@ -942,6 +948,7 @@ impl SettlementService { &plan, fork_basis_ref, &support_pins, + strand_posture, policy, &appended_imports, )?; @@ -1301,6 +1308,7 @@ fn build_braid_shell( plan: &SettlementPlan, fork_basis_ref: crate::strand::ForkBasisRef, support_pins: &[crate::strand::SupportPin], + strand_posture: CausalPosture, policy: &SettlementPolicy, appended_imports: &[ProvenanceRef], ) -> Result { @@ -1373,7 +1381,7 @@ fn build_braid_shell( claim_digest: claim_hasher.finalize().into(), verdict, verdict_digest: verdict_hasher.finalize().into(), - posture: CausalPosture::AuthorOnly, + posture: strand_posture, }; BraidShell::assemble( @@ -1382,7 +1390,7 @@ fn build_braid_shell( vec![member], policy.policy_id, outcome, - CausalPosture::AuthorOnly, + strand_posture, ) } @@ -1443,6 +1451,7 @@ fn plural_draft( source_entry: &ProvenanceEntry, mut overlapping_slots: Vec, policy: &SettlementPolicy, + posture: CausalPosture, ) -> PluralAlternativeDraft { canonicalize_slots(&mut overlapping_slots); PluralAlternativeDraft { @@ -1460,7 +1469,7 @@ fn plural_draft( .collect(), overlapping_slots, policy_id: policy.policy_id, - posture: CausalPosture::AuthorOnly, + posture, } } @@ -2387,7 +2396,7 @@ mod tests { return; }; assert_eq!(draft.policy_id, plural_policy().policy_id); - assert_eq!(draft.posture, CausalPosture::AuthorOnly); + assert_eq!(draft.posture, CausalPosture::Shared); assert!(!draft.overlapping_slots.is_empty()); assert_eq!(draft.source_ref.worldline_id, child_worldline); @@ -2417,7 +2426,7 @@ mod tests { assert!(matches!( retained.event_kind, ProvenanceEventKind::PluralArtifact { - posture: CausalPosture::AuthorOnly, + posture: CausalPosture::Shared, .. } )); @@ -2482,7 +2491,7 @@ mod tests { let shell = provenance.braid_shell(&shell_digest).unwrap(); assert_eq!(shell.policy_id, plural_policy().policy_id); - assert_eq!(shell.posture, crate::revelation::CausalPosture::AuthorOnly); + assert_eq!(shell.posture, crate::revelation::CausalPosture::Shared); assert_eq!(shell.worldline_id, base_worldline); assert!(shell.has_member_strand(&strand_id)); assert_eq!(shell.members.len(), 1); @@ -2798,7 +2807,7 @@ mod tests { let query = crate::braid_shell::BraidShellQuery { member_strand: Some(strand_id), outcome: Some(AdmissionOutcomeKind::Plural), - posture: Some(crate::revelation::CausalPosture::AuthorOnly), + posture: Some(crate::revelation::CausalPosture::Shared), ..crate::braid_shell::BraidShellQuery::default() }; assert_eq!(provenance.query_braid_shells(query).count(), 1); From 334648cd924f708d0dcbc6c30fccbb6ba223d7f4 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sat, 13 Jun 2026 19:18:16 -0700 Subject: [PATCH 5/6] Fix: update warp-wasm posture test fixtures --- crates/warp-wasm/src/lib.rs | 2 ++ crates/warp-wasm/src/warp_kernel.rs | 40 ++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs index 17cf8c9c..42bca014 100644 --- a/crates/warp-wasm/src/lib.rs +++ b/crates/warp-wasm/src/lib.rs @@ -1263,6 +1263,8 @@ mod init_tests { worldline_tick: WorldlineTick(1), commit_hash: vec![5; 32], }], + appended_plurals: Vec::new(), + braid_shell_digest: None, }) } diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index df99fc71..ae5507c6 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -1187,13 +1187,16 @@ mod tests { }; use warp_core::{ compute_commit_hash_v2, make_edge_id, make_head_id, make_node_id, make_strand_id, - make_type_id, make_warp_id, materialization::make_channel_id, AdmissionLawId, CoordinateAt, - EchoCoordinate, EdgeRecord, ForkBasisRef, GlobalTick, GraphStore, HashTriplet, InboxPolicy, - IntentFamilyId, NodeId, NodeKey, NodeRecord, OpticActorId, OpticCapabilityId, OpticCause, - OpticReadBudget, PlaybackMode, ProvenanceEntry, ProvenanceService, ProvenanceStore, SlotId, - Strand, StrandId, TickCommitStatus, WarpOp, WarpTickPatchV1, WorldlineHeadOptic, - WorldlineRuntime, WorldlineState, WorldlineTick, WorldlineTickHeaderV1, - WorldlineTickPatchV1, WriterHead, WriterHeadKey, + make_type_id, make_warp_id, materialization::make_channel_id, ActorId, AdmissionLawId, + AdmissionScopeId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, CausalAuthority, + CausalPosture, CoordinateAt, EchoCoordinate, EdgeRecord, ForkBasisRef, GlobalTick, + GraphStore, HashTriplet, InboxPolicy, IntentFamilyId, NodeId, NodeKey, NodeRecord, + OpticActorId, OpticCapabilityId, OpticCause, OpticReadBudget, OriginId, PlaybackMode, + PostureDerivation, ProvenanceEntry, ProvenanceService, ProvenanceStore, + RetentionContractId, RetentionPosture, SealStrength, SlotId, Strand, StrandId, + TickCommitStatus, WarpOp, WarpTickPatchV1, WorldlineHeadOptic, WorldlineRuntime, + WorldlineState, WorldlineTick, WorldlineTickHeaderV1, WorldlineTickPatchV1, WriterHead, + WriterHeadKey, }; fn start_until_idle(kernel: &mut WarpKernel, cycle_limit: Option) -> DispatchResponse { @@ -1208,6 +1211,27 @@ mod tests { AbiObservationRequest::builtin_one_shot(coordinate, frame, projection).unwrap() } + fn shared_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x51; 32]); + let authority = + AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x52; 32])); + RetentionPosture::new( + CausalPosture::Shared, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x53; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .unwrap(), + RetentionContractId::from_bytes([0x54; 32]), + Some(AdmissionScopeId::from_bytes([0x55; 32])), + ) + .unwrap() + } + fn start_until_idle_result( kernel: &mut WarpKernel, cycle_limit: Option, @@ -1477,6 +1501,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: shared_retention_posture(), }) .unwrap(); @@ -1538,6 +1563,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: shared_retention_posture(), }) .unwrap(); ( From 0c7ca74ef254677db4314e6254c661e7f2018243 Mon Sep 17 00:00:00 2001 From: James Ross Date: Sun, 14 Jun 2026 00:40:00 -0700 Subject: [PATCH 6/6] Fix: map non-shared settlement ABI errors --- CHANGELOG.md | 3 ++ crates/warp-wasm/src/warp_kernel.rs | 67 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0943bb81..5218361c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -630,6 +630,9 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Fixed +- `warp-wasm` settlement publication now maps non-`Shared` strand admission + rejection to the stable `INVALID_STRAND` ABI error code instead of + collapsing the lawful posture denial into `ENGINE_ERROR`. - `echo-file-aperture` now normalizes `HostFileSnapshot` material at the aperture boundary so caller-forged snapshot metadata or fingerprints cannot bind a basis, observation receipt, or materialization verification to bytes diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs index ae5507c6..82149e9e 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -423,6 +423,12 @@ impl WarpKernel { code: error_codes::INVALID_STRAND, message: format!("invalid strand: {strand_id:?}"), }, + SettlementError::NonSharedStrand { strand_id, posture } => AbiError { + code: error_codes::INVALID_STRAND, + message: format!( + "strand {strand_id:?} with posture {posture:?} is not shared-admitted for settlement" + ), + }, _ => AbiError { code: error_codes::ENGINE_ERROR, message: err.to_string(), @@ -1232,6 +1238,27 @@ mod tests { .unwrap() } + fn author_only_retention_posture() -> RetentionPosture { + let origin_id = OriginId::from_bytes([0x61; 32]); + let authority = + AuthorityDomainRef::new(origin_id, AuthorityDomainId::from_bytes([0x62; 32])); + RetentionPosture::new( + CausalPosture::AuthorOnly, + PostureDerivation::ExplicitIntent, + CausalAuthority::new( + origin_id, + ActorId::from_bytes([0x63; 32]), + authority, + AuthorityBinding::LocalUnbound { origin: origin_id }, + SealStrength::Advisory, + ) + .unwrap(), + RetentionContractId::from_bytes([0x64; 32]), + None, + ) + .unwrap() + } + fn start_until_idle_result( kernel: &mut WarpKernel, cycle_limit: Option, @@ -1433,6 +1460,19 @@ mod tests { StrandId, WorldlineId, WorldlineId, + ) { + setup_runtime_with_strand_posture(parent_drift, shared_retention_posture()) + } + + fn setup_runtime_with_strand_posture( + parent_drift: ParentDrift, + retention_posture: RetentionPosture, + ) -> ( + WorldlineRuntime, + ProvenanceService, + StrandId, + WorldlineId, + WorldlineId, ) { let base_worldline = wl(1); let child_worldline = wl(2); @@ -1501,7 +1541,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), - retention_posture: shared_retention_posture(), + retention_posture: retention_posture.clone(), }) .unwrap(); @@ -1563,7 +1603,7 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), - retention_posture: shared_retention_posture(), + retention_posture, }) .unwrap(); ( @@ -2319,6 +2359,29 @@ mod tests { assert!(result.appended_conflicts.is_empty()); } + #[test] + fn settlement_publication_rejects_author_only_strand_as_invalid_strand() { + let mut kernel = WarpKernel::new().unwrap(); + let (runtime, provenance, strand_id, base_worldline, _) = + setup_runtime_with_strand_posture(ParentDrift::None, author_only_retention_posture()); + kernel.runtime = runtime; + kernel.provenance = provenance; + kernel.default_worldline = base_worldline; + + let request = AbiSettlementRequest { + strand_id: echo_wasm_abi::kernel_port::StrandId::from_bytes(*strand_id.as_bytes()), + }; + let plan_err = kernel.plan_settlement(request.clone()).unwrap_err(); + assert_eq!(plan_err.code, error_codes::INVALID_STRAND); + assert!(plan_err.message.contains("AuthorOnly")); + assert!(plan_err.message.contains("not shared-admitted")); + + let settle_err = kernel.settle_strand(request).unwrap_err(); + assert_eq!(settle_err.code, error_codes::INVALID_STRAND); + assert!(settle_err.message.contains("AuthorOnly")); + assert!(settle_err.message.contains("not shared-admitted")); + } + #[test] fn settlement_publication_imports_when_parent_advanced_disjoint() { let mut kernel = WarpKernel::new().unwrap();