diff --git a/CHANGELOG.md b/CHANGELOG.md index d654b21a..2ff49bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ ### Added +- `warp-core` casting a dynamically postured strand to statically shared now returns a semantically precise `PostureObstruction::PostureMismatch` instead of `PostureObstruction::NarrowingRefused`. +- `warp-core` renamed `ProofEnvelope::verify` to `validate_shape` and updated error variants to `ProofShapeValidationFailed` to accurately reflect shape/input checks rather than full cryptographic proof verification. +- `warp-core` strand creation now carries explicit `RetentionPosture` through + `ForkStrandRequest`, `ForkStrandReceipt`, and `Strand`. Session-default and + 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 @@ -545,6 +553,22 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Changed +- `warp-core` renamed the generated contract package host API from + `install_contract_package(...)` to `register_contract_package(...)` so the + trusted-runtime boundary reads as explicit runtime-owned registration instead + of process-global installation. +- `warp-core` sealed braid member lookup now requires authority-bound sealed + query material, redacts non-public blinding material from debug output, and + keeps hidden-member commitments stable across parent frontier movement. +- `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. Settlement compare remains local + revelation/inspection only: it can inspect a locally held strand suffix + without promoting, planning, admitting, or settling it. +- `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, @@ -617,6 +641,38 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Fixed +- `warp-core` evolving braid logs now reject unchecked incremental mutations: + `Braid::apply` returns typed lifecycle errors, rejects duplicate member + weaving and mixed revealed/sealed membership, refuses empty-frontier + settlement finalization, detects member sequence overflow with checked + arithmetic, rejects empty collapse witnesses, and exposes folded state through + read-only accessors instead of public mutable fields. Duplicate checks now use + a deterministic member index instead of scanning the append-ordered frontier. +- `warp-core` braid-shell digests now bind optional proof-shaped envelopes: + proof-bearing shells have distinct content identity from proof-less shells, + mutating proof bytes after assembly is caught by shell validation, and + `BRAID_SHELL_VERSION` is now `2` for the proof-digest marker shape. + Shape-only proof envelope admission is limited to replay-trace evidence; + cryptographic proof kinds require a verifier backend before admission. +- `warp-core` sealed braid members now require caller-supplied blinding material, + preserve hidden shared source disclosure in settlement shells, mix a + settlement-local `MemberBlindingSalt` into hidden settlement member + commitments, reject mixed revealed/sealed shell member sets, and treat sealed + member authority as part of duplicate-member identity. +- `warp-core` retained braid shell queries now distinguish revealed member + lookup from sealed member lookup: `has_revealed_member_strand` and + `BraidShellQuery::revealed_member_strand` only match revealed references, + while `BraidShellMemberQuery` carries blinding material for sealed matches. +- `warp-core` crate-root braid exports now include `BraidError`, `BraidStatus`, + and `BraidMemberRef` so external consumers can handle public braid results. +- `warp-core` shared-strand settlement handles now re-enter the live registry + path before planning or settling, and crate-internal settlement helpers reject + stale handles that no longer match registered strand state. `CausalPostureState` + is sealed to Echo's marker types so external crates cannot add typestate + implementations outside the runtime posture gate. +- `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/backlog/cool-ideas/CI-003-append-only-braid-membership.md b/backlog/cool-ideas/CI-003-append-only-braid-membership.md new file mode 100644 index 00000000..c4899497 --- /dev/null +++ b/backlog/cool-ideas/CI-003-append-only-braid-membership.md @@ -0,0 +1,57 @@ + + + +# CI-003 — Append-Only Braid Membership + +Legend: [WARP — Causal History] + +## Idea + +Model braids as append-only witnessed relationships over strand intervals, +not as binary pairings and not as permanent strand merges. + +A braid can begin with multiple members and later weave additional strands +into the relation without pretending those strands were present from the +beginning: + +```text +t0: braid B includes s0 and s1 +t1: braid B weaves in s2 +``` + +The source of truth should be a braid event log: + +```text +BraidCreated { members: [s0, s1], ... } +BraidMemberWovenIn { member: s2, ... } +``` + +Materialized braid views can report current membership, but historical views +must preserve membership as of the requested coordinate. + +## Why + +1. **Doctrine:** Braided does not mean settled. Related does not mean admitted. +2. **Causality:** Weaving in `s2` at `t1` must not rewrite `t0` membership. +3. **Scale:** Real review/conflict/proposal workflows can involve more than + two strands. +4. **Posture:** A braid may reveal a shared projection or relationship summary + while sealed member source chains remain AuthorOnly. + +## Acceptance Sketch + +- Create braid `B` with `s0` and `s1` at `t0`. +- Weave `s2` into `B` at `t1`. +- Current braid view after `t1` includes `s0`, `s1`, and `s2`. +- Historical braid view before `t1` excludes `s2`. +- Braid membership changes are append-only events, not mutable list rewrites. +- A shared braid projection can reveal the relationship without revealing a + sealed member source chain. +- Settlement can admit a braid projection without collapsing member strands. +- Weaving in an AuthorOnly member requires authority or records a sealed + member reference. + +## Effort + +Medium-Large — requires braid event types, interval/member views, revelation +policy around sealed members, and settlement/projection integration. diff --git a/crates/warp-core/src/braid.rs b/crates/warp-core/src/braid.rs new file mode 100644 index 00000000..6cb8e8ba --- /dev/null +++ b/crates/warp-core/src/braid.rs @@ -0,0 +1,694 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Evolving coordination log ("Braid") representation. + +use std::collections::BTreeSet; +use thiserror::Error; + +use crate::braid_shell::BraidMemberRef; +use crate::ident::Hash; +use crate::revelation::{AuthorityDomainRef, WitnessDigest}; + +/// Concrete status of a coordination braid lifecycle. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BraidStatus { + /// Active coordination state, accepting member weaving. + Active, + /// Settlement has been finalized. + Finalized, + /// Braid has been collapsed from a plural state to a single derived state. + Collapsed, +} + +/// Error kinds returned during coordination braid lifecycle updates or folds. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum BraidError { + /// The event log was empty. + #[error("empty event stream")] + EmptyLog, + /// The event log did not begin with BraidCreated. + #[error("first event must be BraidCreated")] + MissingCreated, + /// The BraidCreated event appeared more than once. + #[error("BraidCreated event can only appear once at the start of the log")] + DuplicateCreated, + /// The member sequence number was out of order. + #[error("incoherent member sequence: expected {expected}, got {actual}")] + IncoherentSequence { + /// Expected sequence number. + expected: u64, + /// Actual sequence number. + actual: u64, + }, + /// An invalid transition was attempted for the current braid status. + #[error("cannot transition braid state: cannot {action} in status {status:?}")] + InvalidTransition { + /// Attempted action or event kind. + action: String, + /// Current braid status. + status: BraidStatus, + }, + /// The member sequence number cannot advance without overflowing. + #[error("member sequence number overflow at {sequence_num}")] + SequenceOverflow { + /// Sequence number that could not be advanced. + sequence_num: u64, + }, + /// A member reference was woven more than once. + #[error("duplicate braid member reference {member_ref:?}")] + DuplicateMember { + /// Member reference that appeared more than once. + member_ref: BraidMemberRef, + }, + /// Braid membership cannot mix revealed and sealed references. + #[error("braid members must use a single revealed/sealed reference posture")] + MixedMemberReferencePosture, + /// Settlement cannot finalize an empty member frontier. + #[error("cannot finalize settlement without woven braid members")] + EmptySettlementFrontier, + /// Collapse events must carry a non-empty witness digest. + #[error("braid collapse witness must be non-empty")] + EmptyCollapseWitness, +} + +/// Lifecycle events that define the evolution of a coordination braid. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BraidEvent { + /// Initial creation of the coordination braid. + BraidCreated { + /// Unique content-addressed identifier. + braid_id: Hash, + /// Authority domain under which this braid was initiated. + creator_domain: AuthorityDomainRef, + }, + /// A member strand was woven into the braid's speculative frontier. + MemberWoven { + /// Reference to the strand, which may be revealed or sealed. + member_ref: BraidMemberRef, + /// Monotonically increasing sequence number. + sequence_num: u64, + }, + /// A settlement was finalized, binding the current braid state. + SettlementFinalized { + /// Content digest of the final braid shell. + settlement_digest: Hash, + }, + /// A previously plural braid was collapsed into a single derived resolution. + BraidCollapsed { + /// Witness digest proving the collapse transition. + collapse_witness: Hash, + /// Content digest of the new derived braid shell. + outcome_digest: Hash, + }, +} + +/// Evolving state of a coordination braid reconstructed from its event log. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Braid { + /// Unique identifier of the braid. + id: Hash, + /// Ordered event stream. + events: Vec, + /// Ordered list of woven member references. + members: Vec, + /// Deterministic membership index for duplicate checks. + member_index: BTreeSet, + /// Expected sequence number of the next member to be woven. + next_sequence_num: u64, + /// Digest of the latest finalized settlement, if any. + latest_settlement: Option, + /// Current lifecycle status of the braid. + status: BraidStatus, +} + +impl Braid { + /// Creates a new coordination braid state with a creation event. + #[must_use] + pub fn new(braid_id: Hash, creator_domain: AuthorityDomainRef) -> Self { + let initial_event = BraidEvent::BraidCreated { + braid_id, + creator_domain, + }; + Self { + id: braid_id, + events: vec![initial_event], + members: Vec::new(), + member_index: BTreeSet::new(), + next_sequence_num: 0, + latest_settlement: None, + status: BraidStatus::Active, + } + } + + /// Appends an event to the log and updates the folded state. + /// + /// # Errors + /// + /// Returns a [`BraidError`] if the event would violate lifecycle, + /// membership, sequence, or witness invariants. + pub fn apply(&mut self, event: BraidEvent) -> Result<(), BraidError> { + match &event { + BraidEvent::BraidCreated { braid_id, .. } => { + if !self.events.is_empty() { + return Err(BraidError::DuplicateCreated); + } + self.id = *braid_id; + self.status = BraidStatus::Active; + } + BraidEvent::MemberWoven { + member_ref, + sequence_num, + } => { + if self.status != BraidStatus::Active { + return Err(BraidError::InvalidTransition { + action: "weave member".to_string(), + status: self.status, + }); + } + if *sequence_num != self.next_sequence_num { + return Err(BraidError::IncoherentSequence { + expected: self.next_sequence_num, + actual: *sequence_num, + }); + } + if self.member_index.contains(member_ref) { + return Err(BraidError::DuplicateMember { + member_ref: *member_ref, + }); + } + if let Some(first) = self.members.first() { + let first_is_sealed = member_ref_is_sealed(first); + if first_is_sealed != member_ref_is_sealed(member_ref) { + return Err(BraidError::MixedMemberReferencePosture); + } + } + let next_sequence_num = + sequence_num + .checked_add(1) + .ok_or(BraidError::SequenceOverflow { + sequence_num: *sequence_num, + })?; + self.members.push(*member_ref); + self.member_index.insert(*member_ref); + self.next_sequence_num = next_sequence_num; + } + BraidEvent::SettlementFinalized { settlement_digest } => { + if self.status != BraidStatus::Active { + return Err(BraidError::InvalidTransition { + action: "finalize settlement".to_string(), + status: self.status, + }); + } + if self.members.is_empty() { + return Err(BraidError::EmptySettlementFrontier); + } + self.latest_settlement = Some(*settlement_digest); + self.status = BraidStatus::Finalized; + } + BraidEvent::BraidCollapsed { + collapse_witness, + outcome_digest, + } => { + if self.status != BraidStatus::Finalized { + return Err(BraidError::InvalidTransition { + action: "collapse braid".to_string(), + status: self.status, + }); + } + WitnessDigest::new(*collapse_witness) + .map_err(|_| BraidError::EmptyCollapseWitness)?; + self.latest_settlement = Some(*outcome_digest); + self.status = BraidStatus::Collapsed; + } + } + self.events.push(event); + Ok(()) + } + + /// Reconstructs the braid state by folding over a stream of events. + /// + /// # Errors + /// + /// Returns a [`BraidError`] if the event log is empty, if the log does + /// not begin with `BraidCreated`, or if any sequence numbering is incoherent. + pub fn fold(events: impl IntoIterator) -> Result { + let mut iter = events.into_iter(); + let first = iter.next().ok_or(BraidError::EmptyLog)?; + + let mut braid = match &first { + BraidEvent::BraidCreated { + braid_id, + creator_domain, + } => Self::new(*braid_id, *creator_domain), + _ => return Err(BraidError::MissingCreated), + }; + + for event in iter { + braid.apply(event)?; + } + Ok(braid) + } + + /// Returns the braid identifier. + #[must_use] + pub fn braid_id(&self) -> Hash { + self.id + } + + /// Returns the ordered event log. + #[must_use] + pub fn events(&self) -> &[BraidEvent] { + &self.events + } + + /// Returns the next expected member sequence number. + #[must_use] + pub fn next_sequence_num(&self) -> u64 { + self.next_sequence_num + } + + /// Returns the latest finalized or collapsed settlement digest. + #[must_use] + pub fn latest_settlement(&self) -> Option { + self.latest_settlement + } + + /// Returns the current braid lifecycle status. + #[must_use] + pub fn status(&self) -> BraidStatus { + self.status + } + + /// Returns the current coordination frontier (active woven members). + #[must_use] + pub fn frontier(&self) -> &[BraidMemberRef] { + &self.members + } +} + +const fn member_ref_is_sealed(member_ref: &BraidMemberRef) -> bool { + matches!(member_ref, BraidMemberRef::Sealed { .. }) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::strand::make_strand_id; + + fn authority_ref() -> AuthorityDomainRef { + AuthorityDomainRef { + origin_id: crate::revelation::OriginId::from_bytes([0x10; 32]), + domain_id: crate::revelation::AuthorityDomainId::from_bytes([0x20; 32]), + } + } + + #[test] + fn test_braid_lifecycle_and_folding() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + + let mut braid = Braid::new(braid_id, auth); + assert_eq!(braid.braid_id(), braid_id); + assert_eq!(braid.next_sequence_num(), 0); + assert_eq!(braid.status(), BraidStatus::Active); + assert!(braid.frontier().is_empty()); + + let m1 = BraidMemberRef::Revealed(make_strand_id("strand-1")); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 0, + }) + .unwrap(); + assert_eq!(braid.next_sequence_num(), 1); + assert_eq!(braid.frontier(), &[m1]); + assert_eq!(braid.status(), BraidStatus::Active); + + let m2 = BraidMemberRef::Revealed(make_strand_id("strand-2")); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m2, + sequence_num: 1, + }) + .unwrap(); + assert_eq!(braid.next_sequence_num(), 2); + assert_eq!(braid.frontier(), &[m1, m2]); + + let settlement = [0x5E; 32]; + braid + .apply(BraidEvent::SettlementFinalized { + settlement_digest: settlement, + }) + .unwrap(); + assert_eq!(braid.latest_settlement(), Some(settlement)); + assert_eq!(braid.status(), BraidStatus::Finalized); + + let collapse_witness = [0x33; 32]; + let collapse_outcome = [0x88; 32]; + braid + .apply(BraidEvent::BraidCollapsed { + collapse_witness, + outcome_digest: collapse_outcome, + }) + .unwrap(); + assert_eq!(braid.latest_settlement(), Some(collapse_outcome)); + assert_eq!(braid.status(), BraidStatus::Collapsed); + } + + #[test] + fn test_braid_fold_validation() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let m1 = BraidMemberRef::Revealed(make_strand_id("strand-1")); + let m2 = BraidMemberRef::Revealed(make_strand_id("strand-2")); + let settlement = [0x5E; 32]; + let collapse_witness = [0x33; 32]; + let collapse_outcome = [0x88; 32]; + + // Valid sequence + let events = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 0, + }, + BraidEvent::MemberWoven { + member_ref: m2, + sequence_num: 1, + }, + BraidEvent::SettlementFinalized { + settlement_digest: settlement, + }, + BraidEvent::BraidCollapsed { + collapse_witness, + outcome_digest: collapse_outcome, + }, + ]; + let braid = Braid::fold(events).unwrap(); + assert_eq!(braid.braid_id(), braid_id); + assert_eq!(braid.frontier(), &[m1, m2]); + assert_eq!(braid.status(), BraidStatus::Collapsed); + + // Invalid: missing initial BraidCreated + let bad_events_no_created = vec![BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 0, + }]; + assert_eq!( + Braid::fold(bad_events_no_created), + Err(BraidError::MissingCreated) + ); + + // Invalid: duplicate BraidCreated + let bad_events_dup_created = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + ]; + assert_eq!( + Braid::fold(bad_events_dup_created), + Err(BraidError::DuplicateCreated) + ); + + // Invalid: out-of-order sequence + let bad_events_out_of_order = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 1, // Expected 0 + }, + ]; + assert_eq!( + Braid::fold(bad_events_out_of_order), + Err(BraidError::IncoherentSequence { + expected: 0, + actual: 1 + }) + ); + + // Invalid: MemberWoven after finalized + let bad_events_weave_after_finalized = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 0, + }, + BraidEvent::SettlementFinalized { + settlement_digest: settlement, + }, + BraidEvent::MemberWoven { + member_ref: m2, + sequence_num: 1, + }, + ]; + assert_eq!( + Braid::fold(bad_events_weave_after_finalized), + Err(BraidError::InvalidTransition { + action: "weave member".to_string(), + status: BraidStatus::Finalized + }) + ); + + // Invalid: BraidCollapsed before finalized + let bad_events_collapse_before_finalized = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::BraidCollapsed { + collapse_witness, + outcome_digest: collapse_outcome, + }, + ]; + assert_eq!( + Braid::fold(bad_events_collapse_before_finalized), + Err(BraidError::InvalidTransition { + action: "collapse braid".to_string(), + status: BraidStatus::Active + }) + ); + } + + #[test] + fn test_braid_apply_rejects_invalid_incremental_events() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let m1 = BraidMemberRef::Revealed(make_strand_id("strand-1")); + let m2 = BraidMemberRef::Revealed(make_strand_id("strand-2")); + let mut braid = Braid::new(braid_id, auth); + + assert_eq!( + braid.apply(BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }), + Err(BraidError::DuplicateCreated) + ); + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 1, + }), + Err(BraidError::IncoherentSequence { + expected: 0, + actual: 1, + }) + ); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 0, + }) + .unwrap(); + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 1, + }), + Err(BraidError::DuplicateMember { member_ref: m1 }) + ); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m2, + sequence_num: 1, + }) + .unwrap(); + braid + .apply(BraidEvent::SettlementFinalized { + settlement_digest: [0x5E; 32], + }) + .unwrap(); + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: BraidMemberRef::Revealed(make_strand_id("strand-3")), + sequence_num: 2, + }), + Err(BraidError::InvalidTransition { + action: "weave member".to_string(), + status: BraidStatus::Finalized, + }) + ); + } + + #[test] + fn test_braid_tracks_members_in_deterministic_index() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let m1 = BraidMemberRef::Revealed(make_strand_id("strand-1")); + let m2 = BraidMemberRef::Revealed(make_strand_id("strand-2")); + let mut braid = Braid::new(braid_id, auth); + + assert!(braid.member_index.is_empty()); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m2, + sequence_num: 0, + }) + .unwrap(); + braid + .apply(BraidEvent::MemberWoven { + member_ref: m1, + sequence_num: 1, + }) + .unwrap(); + + assert_eq!(braid.frontier(), &[m2, m1]); + assert!(braid.member_index.contains(&m1)); + assert!(braid.member_index.contains(&m2)); + assert_eq!(braid.member_index.len(), 2); + } + + #[test] + fn test_braid_rejects_mixed_member_reference_posture() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let revealed = BraidMemberRef::Revealed(make_strand_id("strand-1")); + let sealed = BraidMemberRef::Sealed { + blinded_commitment: [0x44; 32], + authority: auth, + }; + let mut braid = Braid::new(braid_id, auth); + + braid + .apply(BraidEvent::MemberWoven { + member_ref: revealed, + sequence_num: 0, + }) + .unwrap(); + + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: sealed, + sequence_num: 1, + }), + Err(BraidError::MixedMemberReferencePosture) + ); + assert_eq!(braid.frontier(), &[revealed]); + } + + #[test] + fn test_braid_rejects_empty_settlement_finalization() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let mut braid = Braid::new(braid_id, auth); + + assert_eq!( + braid.apply(BraidEvent::SettlementFinalized { + settlement_digest: [0x5E; 32], + }), + Err(BraidError::EmptySettlementFrontier) + ); + assert_eq!(braid.status(), BraidStatus::Active); + assert_eq!(braid.latest_settlement(), None); + } + + #[test] + fn test_braid_fold_rejects_duplicate_member_and_empty_collapse_witness() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let member = BraidMemberRef::Revealed(make_strand_id("strand-1")); + + let duplicate_member_events = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::MemberWoven { + member_ref: member, + sequence_num: 0, + }, + BraidEvent::MemberWoven { + member_ref: member, + sequence_num: 1, + }, + ]; + assert_eq!( + Braid::fold(duplicate_member_events), + Err(BraidError::DuplicateMember { member_ref: member }) + ); + + let empty_collapse_witness_events = vec![ + BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }, + BraidEvent::MemberWoven { + member_ref: member, + sequence_num: 0, + }, + BraidEvent::SettlementFinalized { + settlement_digest: [0x5E; 32], + }, + BraidEvent::BraidCollapsed { + collapse_witness: [0; 32], + outcome_digest: [0x88; 32], + }, + ]; + assert_eq!( + Braid::fold(empty_collapse_witness_events), + Err(BraidError::EmptyCollapseWitness) + ); + } + + #[test] + fn test_braid_apply_rejects_sequence_overflow() { + let braid_id = [0xAA; 32]; + let auth = authority_ref(); + let mut braid = Braid { + id: braid_id, + events: vec![BraidEvent::BraidCreated { + braid_id, + creator_domain: auth, + }], + members: Vec::new(), + member_index: BTreeSet::new(), + next_sequence_num: u64::MAX, + latest_settlement: None, + status: BraidStatus::Active, + }; + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: BraidMemberRef::Revealed(make_strand_id("max-seq")), + sequence_num: u64::MAX, + }), + Err(BraidError::SequenceOverflow { + sequence_num: u64::MAX, + }) + ); + } +} diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index cad65f4b..95aae89e 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -23,7 +23,7 @@ use crate::admission::AdmissionOutcomeKind; use crate::ident::Hash; use crate::provenance_store::ProvenanceRef; use crate::revelation::{ - shell_posture_obstruction, CausalPosture, PostureObstruction, WitnessDigest, + shell_posture_obstruction, AuthorityDomainRef, CausalPosture, PostureObstruction, WitnessDigest, }; use crate::strand::StrandId; use crate::worldline::WorldlineId; @@ -32,9 +32,13 @@ const SHELL_DOMAIN: &[u8] = b"echo.shell.braid.v1\0"; const MEMBER_DOMAIN: &[u8] = b"echo.braid.member.v1\0"; const WITNESS_DOMAIN: &[u8] = b"echo.braid.witness.v1\0"; const COORDINATE_DOMAIN: &[u8] = b"echo.braid.coordinate.v1\0"; +const SEALED_MEMBER_DOMAIN: &[u8] = b"echo.braid.member.sealed.v1\0"; /// Current braid shell body version. -pub const BRAID_SHELL_VERSION: u32 = 1; +/// +/// Version 2 binds the optional proof-envelope digest marker into shell +/// identity. Version 1 proofless shells used the pre-proof digest body. +pub const BRAID_SHELL_VERSION: u32 = 2; /// Compact settlement verdict for one braid member. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -61,11 +65,96 @@ impl MemberVerdict { } } +/// Reference to a braid member, supporting both revealed and sealed references. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BraidMemberRef { + /// Publicly revealed strand identity. + Revealed(StrandId), + /// Sealed member reference. + Sealed { + /// Domain-separated commitment digest of the member's identity. + blinded_commitment: Hash, + /// Causal authority domain controlling the private history. + authority: AuthorityDomainRef, + }, +} + +impl BraidMemberRef { + /// Computes the commitment for a sealed reference using caller-supplied + /// non-public blinding material. + #[must_use] + pub fn seal( + strand_id: StrandId, + child_worldline_id: WorldlineId, + blinding_secret: Hash, + ) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(SEALED_MEMBER_DOMAIN); + hasher.update(&blinding_secret); + hasher.update(child_worldline_id.as_bytes()); + hasher.update(strand_id.as_bytes()); + hasher.finalize().into() + } + + /// Returns whether this sealed member reference matches the given strand + /// ID, child worldline ID, authority, and blinding material. + #[must_use] + pub fn matches_strand( + &self, + strand_id: &StrandId, + child_worldline_id: &WorldlineId, + authority: &AuthorityDomainRef, + blinding_secret: &Hash, + ) -> bool { + match self { + Self::Revealed(_) => false, + Self::Sealed { + blinded_commitment, + authority: member_authority, + } => { + let expected = Self::seal(*strand_id, *child_worldline_id, *blinding_secret); + member_authority == authority && *blinded_commitment == expected + } + } + } + + const fn is_sealed(self) -> bool { + matches!(self, Self::Sealed { .. }) + } + + /// Stable wire tag for canonical serialization. + #[must_use] + pub fn canonical_tag(self) -> u8 { + match self { + Self::Revealed(_) => 0x01, + Self::Sealed { .. } => 0x02, + } + } + + /// Hash this member reference into the given hasher. + pub fn hash_into(self, hasher: &mut Hasher) { + hasher.update(&[self.canonical_tag()]); + match self { + Self::Revealed(strand_id) => { + hasher.update(strand_id.as_bytes()); + } + Self::Sealed { + blinded_commitment, + authority, + } => { + hasher.update(&blinded_commitment); + hasher.update(authority.origin_id.as_bytes()); + hasher.update(authority.domain_id.as_bytes()); + } + } + } +} + /// One member entry in a braid shell: compact replay facts, never history. #[derive(Clone, Debug, PartialEq, Eq)] pub struct BraidShellMember { - /// Strand whose claims this member summarizes. - pub strand_ref: StrandId, + /// Reference to the member strand, which may be revealed or sealed. + pub member_ref: BraidMemberRef, /// Digest over the member's support-pin set. pub support_pin_digest: Hash, /// Digest over the member's fork basis facts. @@ -90,7 +179,7 @@ impl BraidShellMember { pub fn member_digest(&self) -> Hash { let mut hasher = Hasher::new(); hasher.update(MEMBER_DOMAIN); - hasher.update(self.strand_ref.as_bytes()); + self.member_ref.hash_into(&mut hasher); hasher.update(&self.support_pin_digest); hasher.update(&self.basis_digest); hasher.update(&self.frontier_digest); @@ -245,6 +334,12 @@ pub enum BraidShellError { /// Witness digest recomputed from the body. recomputed: Hash, }, + /// The proof shape validation failed. + #[error("proof shape validation failed: {reason}")] + ProofShapeValidationFailed { + /// Reason for shape validation failure. + reason: String, + }, /// Member entries are not in canonical order. #[error("braid shell members are not in canonical order")] NonCanonicalMemberOrder, @@ -293,11 +388,14 @@ pub enum BraidShellError { alternative_id: Hash, }, /// One strand may appear at most once among shell members. - #[error("duplicate member strand {strand_id:?}")] + #[error("duplicate member strand {member_ref:?}")] DuplicateMemberStrand { - /// Strand that appeared more than once. - strand_id: StrandId, + /// Member reference that appeared more than once. + member_ref: BraidMemberRef, }, + /// Revealed and sealed member references may not be mixed in one shell. + #[error("braid shell mixes revealed and sealed member references")] + MixedMemberReferencePosture, /// A retained plural artifact id may never migrate to a different shell. #[error("plural artifact {plural_id:?} already bound to shell {existing_shell:?}")] PluralArtifactAlreadyBound { @@ -358,6 +456,8 @@ pub struct BraidShell { pub witness_digest: Hash, /// Revelation posture of the shell itself. pub posture: CausalPosture, + /// Optional proof-shaped evidence envelope bound into shell identity. + pub proof: Option, /// Canonical content digest of the full shell body. pub digest: Hash, } @@ -380,12 +480,42 @@ impl BraidShell { /// disagrees with member verdicts /// ([`BraidShellError::OutcomeMemberMismatch`]). pub fn assemble( + worldline_id: WorldlineId, + basis: ProvenanceRef, + members: Vec, + policy_id: Hash, + outcome: BraidShellOutcome, + posture: CausalPosture, + ) -> Result { + Self::assemble_with_proof( + worldline_id, + basis, + members, + policy_id, + outcome, + posture, + None, + ) + } + + /// Assembles a shell with a proof-shaped envelope: validates member order, + /// checks posture floor and coherence, validates the proof envelope shape + /// (if present) against the derived witness, and seals the shell digest. + /// Proof cryptographic validity is not verified; only envelope shape and + /// public-input binding are validated. + /// + /// # Errors + /// + /// Returns [`BraidShellError`] if any structure constraints are violated or if + /// the proof envelope validation fails. + pub fn assemble_with_proof( worldline_id: WorldlineId, basis: ProvenanceRef, mut members: Vec, policy_id: Hash, mut outcome: BraidShellOutcome, posture: CausalPosture, + proof: Option, ) -> Result { if members.is_empty() { return Err(BraidShellError::EmptyMembers); @@ -430,6 +560,14 @@ impl BraidShell { &outcome, posture, ); + + if let Some(ref p) = proof { + if let Err(err) = p.validate_shape(witness_digest) { + return Err(BraidShellError::ProofShapeValidationFailed { reason: err }); + } + } + let proof_digest = proof.as_ref().map(crate::proof::ProofEnvelope::digest); + let digest = compute_shell_digest( BRAID_SHELL_VERSION, worldline_id, @@ -439,6 +577,7 @@ impl BraidShell { &outcome, witness_digest, posture, + proof_digest, ); Ok(Self { version: BRAID_SHELL_VERSION, @@ -451,6 +590,7 @@ impl BraidShell { witness_digest, posture, digest, + proof, }) } @@ -527,6 +667,13 @@ impl BraidShell { recomputed: witness, }); } + if let Some(ref p) = self.proof { + if let Err(err) = p.validate_shape(self.witness_digest) { + return Err(BraidShellError::ProofShapeValidationFailed { reason: err }); + } + } + let proof_digest = self.proof.as_ref().map(crate::proof::ProofEnvelope::digest); + let digest = compute_shell_digest( self.version, self.worldline_id, @@ -536,6 +683,7 @@ impl BraidShell { &self.outcome, self.witness_digest, self.posture, + proof_digest, ); if digest != self.digest { return Err(BraidShellError::DigestMismatch { @@ -552,12 +700,33 @@ impl BraidShell { self.outcome.kind() } - /// Returns whether the shell summarizes the given member strand. + /// Returns whether the shell summarizes the given revealed member strand. #[must_use] - pub fn has_member_strand(&self, strand_id: &StrandId) -> bool { - self.members - .iter() - .any(|member| member.strand_ref == *strand_id) + pub fn has_revealed_member_strand(&self, strand_id: &StrandId) -> bool { + self.members.iter().any(|member| match member.member_ref { + BraidMemberRef::Revealed(id) => id == *strand_id, + BraidMemberRef::Sealed { .. } => false, + }) + } + + /// Returns whether the shell summarizes the given member strand, using + /// non-public blinding material for sealed references. + #[must_use] + pub fn has_member_strand_secure( + &self, + strand_id: &StrandId, + child_worldline_id: &WorldlineId, + authority: &AuthorityDomainRef, + blinding_secret: &Hash, + ) -> bool { + self.members.iter().any(|member| { + member.member_ref.matches_strand( + strand_id, + child_worldline_id, + authority, + blinding_secret, + ) + }) } } @@ -579,13 +748,20 @@ fn check_outcome_law(outcome: &BraidShellOutcome) -> Result<(), BraidShellError> /// One strand may appear at most once among shell members. fn check_unique_member_strands(members: &[BraidShellMember]) -> Result<(), BraidShellError> { - for (index, member) in members.iter().enumerate() { - if members[..index] + if let Some(first) = members.first() { + let first_is_sealed = first.member_ref.is_sealed(); + if members .iter() - .any(|earlier| earlier.strand_ref == member.strand_ref) + .any(|member| member.member_ref.is_sealed() != first_is_sealed) { + return Err(BraidShellError::MixedMemberReferencePosture); + } + } + let mut seen = std::collections::BTreeSet::new(); + for member in members { + if !seen.insert(member.member_ref) { return Err(BraidShellError::DuplicateMemberStrand { - strand_id: member.strand_ref, + member_ref: member.member_ref, }); } } @@ -694,6 +870,7 @@ fn compute_shell_digest( outcome: &BraidShellOutcome, witness_digest: Hash, posture: CausalPosture, + proof_digest: Option, ) -> Hash { let mut hasher = Hasher::new(); hasher.update(SHELL_DOMAIN); @@ -708,6 +885,15 @@ fn compute_shell_digest( posture, ); hasher.update(&witness_digest); + match proof_digest { + Some(digest) => { + hasher.update(&[0x01]); + hasher.update(&digest); + } + None => { + hasher.update(&[0x00]); + } + } hasher.finalize().into() } @@ -733,7 +919,7 @@ pub struct BraidShellReplay { /// Outcome arm reproduced from the shell. pub outcome_kind: AdmissionOutcomeKind, /// Member verdicts in canonical member order. - pub member_verdicts: Vec<(StrandId, MemberVerdict)>, + pub member_verdicts: Vec<(BraidMemberRef, MemberVerdict)>, /// Settlement policy identity the act ran under. pub policy_id: Hash, /// Witness digest binding the act. @@ -798,7 +984,7 @@ pub fn replay_braid_shell( member_verdicts: shell .members .iter() - .map(|member| (member.strand_ref, member.verdict)) + .map(|member| (member.member_ref, member.verdict)) .collect(), policy_id: shell.policy_id, witness_digest: shell.witness_digest, @@ -1027,6 +1213,30 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor } } +/// Secure member lookup material for sealed braid member references. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct BraidShellMemberQuery { + /// Strand identity being matched. + pub strand_id: StrandId, + /// Child worldline identity bound into sealed member commitments. + pub child_worldline_id: WorldlineId, + /// Causal authority domain bound into the sealed member reference. + pub authority: AuthorityDomainRef, + /// Non-public blinding material used to derive sealed member commitments. + pub blinding_secret: Hash, +} + +impl core::fmt::Debug for BraidShellMemberQuery { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("BraidShellMemberQuery") + .field("strand_id", &self.strand_id) + .field("child_worldline_id", &self.child_worldline_id) + .field("authority", &self.authority) + .field("blinding_secret", &"") + .finish() + } +} + /// Scan-backed query over retained braid shells. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct BraidShellQuery { @@ -1034,8 +1244,10 @@ pub struct BraidShellQuery { pub coordinate: Option, /// Match shells judged against this comparison basis. pub basis: Option, - /// Match shells summarizing this member strand. - pub member_strand: Option, + /// Match shells summarizing this revealed member strand. + pub revealed_member_strand: Option, + /// Match shells summarizing this member using sealed-reference material. + pub secure_member: Option, /// Match shells with this outcome arm. pub outcome: Option, /// Match shells with this revelation posture. @@ -1051,9 +1263,17 @@ impl BraidShell { .is_none_or(|coordinate| self.coordinate == coordinate) && query.basis.is_none_or(|basis| self.basis == basis) && query - .member_strand + .revealed_member_strand .as_ref() - .is_none_or(|strand| self.has_member_strand(strand)) + .is_none_or(|strand| self.has_revealed_member_strand(strand)) + && query.secure_member.is_none_or(|member| { + self.has_member_strand_secure( + &member.strand_id, + &member.child_worldline_id, + &member.authority, + &member.blinding_secret, + ) + }) && query .outcome .is_none_or(|outcome| self.outcome_kind() == outcome) @@ -1083,7 +1303,7 @@ mod tests { fn member(label: &str, verdict: MemberVerdict) -> BraidShellMember { BraidShellMember { - strand_ref: make_strand_id(label), + member_ref: BraidMemberRef::Revealed(make_strand_id(label)), support_pin_digest: [0x21; 32], basis_digest: [0x22; 32], frontier_digest: [0x23; 32], @@ -1095,6 +1315,35 @@ mod tests { } } + fn authority(origin: u8, domain: u8) -> AuthorityDomainRef { + AuthorityDomainRef::new( + crate::revelation::OriginId::from_bytes([origin; 32]), + crate::revelation::AuthorityDomainId::from_bytes([domain; 32]), + ) + } + + fn sealed_member( + commitment: Hash, + authority: AuthorityDomainRef, + verdict: MemberVerdict, + claim_byte: u8, + ) -> BraidShellMember { + BraidShellMember { + member_ref: BraidMemberRef::Sealed { + blinded_commitment: commitment, + authority, + }, + support_pin_digest: [0x21; 32], + basis_digest: [0x22; 32], + frontier_digest: [0x23; 32], + footprint_digest: [0x24; 32], + claim_digest: [claim_byte; 32], + verdict, + verdict_digest: [0x26; 32], + posture: CausalPosture::AuthorOnly, + } + } + fn plural_shell(members: Vec) -> BraidShell { BraidShell::assemble( wl(1), @@ -1146,10 +1395,10 @@ mod tests { member("member-b", MemberVerdict::Derived), ]); let digest = shell.digest; - let expected_verdicts: Vec<(StrandId, MemberVerdict)> = shell + let expected_verdicts: Vec<(BraidMemberRef, MemberVerdict)> = shell .members .iter() - .map(|member| (member.strand_ref, member.verdict)) + .map(|member| (member.member_ref, member.verdict)) .collect(); let records = Records::with([shell]); @@ -1274,11 +1523,65 @@ mod tests { assert_eq!( result, Err(BraidShellError::DuplicateMemberStrand { - strand_id: make_strand_id("member-a"), + member_ref: BraidMemberRef::Revealed(make_strand_id("member-a")), }) ); } + #[test] + fn sealed_members_with_same_commitment_under_different_authorities_are_distinct() { + let commitment = [0x44; 32]; + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![ + sealed_member( + commitment, + authority(0x10, 0x20), + MemberVerdict::Plural, + 0x25, + ), + sealed_member( + commitment, + authority(0x11, 0x20), + MemberVerdict::Plural, + 0x26, + ), + ], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + ); + + assert!(result.is_ok()); + } + + #[test] + fn mixed_revealed_and_sealed_members_are_refused() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![ + member("member-a", MemberVerdict::Plural), + sealed_member( + [0x44; 32], + authority(0x10, 0x20), + MemberVerdict::Plural, + 0x26, + ), + ], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + ); + + assert_eq!(result, Err(BraidShellError::MixedMemberReferencePosture)); + } + #[test] fn obstructed_collapse_names_its_plural_parent() { let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); @@ -1518,7 +1821,8 @@ mod tests { assert!(shell.matches(&BraidShellQuery { coordinate: Some(shell.coordinate), basis: Some(basis_ref()), - member_strand: Some(make_strand_id("member-a")), + revealed_member_strand: Some(make_strand_id("member-a")), + secure_member: None, outcome: Some(AdmissionOutcomeKind::Plural), posture: Some(CausalPosture::AuthorOnly), })); @@ -1527,8 +1831,268 @@ mod tests { ..BraidShellQuery::default() })); assert!(!shell.matches(&BraidShellQuery { - member_strand: Some(make_strand_id("nobody")), + revealed_member_strand: Some(make_strand_id("nobody")), + ..BraidShellQuery::default() + })); + } + + #[test] + fn sealed_member_query_requires_secure_material() { + let strand_id = make_strand_id("sealed-member"); + let child_worldline_id = wl(9); + let member_authority = authority(0xA1, 0xB1); + let blinding_secret = [0xA5; 32]; + let member_ref = BraidMemberRef::seal(strand_id, child_worldline_id, blinding_secret); + let shell = plural_shell(vec![sealed_member( + member_ref, + member_authority, + MemberVerdict::Plural, + 0x27, + )]); + + assert!(!shell.has_revealed_member_strand(&strand_id)); + assert!(shell.has_member_strand_secure( + &strand_id, + &child_worldline_id, + &member_authority, + &blinding_secret + )); + assert!(!shell.has_member_strand_secure( + &strand_id, + &child_worldline_id, + &authority(0xA1, 0xB2), + &blinding_secret + )); + assert!(!shell.matches(&BraidShellQuery { + revealed_member_strand: Some(strand_id), ..BraidShellQuery::default() })); + assert!(shell.matches(&BraidShellQuery { + secure_member: Some(BraidShellMemberQuery { + strand_id, + child_worldline_id, + authority: member_authority, + blinding_secret, + }), + ..BraidShellQuery::default() + })); + let debug = format!( + "{:?}", + BraidShellMemberQuery { + strand_id, + child_worldline_id, + authority: member_authority, + blinding_secret, + } + ); + assert!(debug.contains("blinding_secret: \"\"")); + assert!(!debug.contains("blinding_secret: [")); + } + + #[test] + fn assemble_with_proof_validates_envelope() { + use crate::proof::{ProofEnvelope, ProofKind}; + + let members = vec![member("member-a", MemberVerdict::Plural)]; + + // Build it without proof first to retrieve the expected witness digest. + let temp_shell = BraidShell::assemble( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + ) + .unwrap(); + assert_eq!(temp_shell.version, BRAID_SHELL_VERSION); + assert_eq!(BRAID_SHELL_VERSION, 2); + let expected_witness = temp_shell.witness_digest; + + // Valid replay-trace evidence: matches the witness_digest and has non-empty bytes. + let valid_proof = ProofEnvelope { + kind: ProofKind::ReplayTrace, + proof_bytes: vec![1, 2, 3], + public_inputs_hash: expected_witness, + }; + + let shell_with_valid_proof = BraidShell::assemble_with_proof( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + Some(valid_proof), + ) + .unwrap(); + assert_eq!(shell_with_valid_proof.version, BRAID_SHELL_VERSION); + shell_with_valid_proof.validate().unwrap(); + assert_ne!( + temp_shell.digest, shell_with_valid_proof.digest, + "proof-bearing shells must have a distinct content identity" + ); + + let mut proof_tampered = shell_with_valid_proof; + assert!(proof_tampered.proof.is_some()); + if let Some(proof) = proof_tampered.proof.as_mut() { + proof.proof_bytes.push(4); + } + assert!(matches!( + proof_tampered.validate(), + Err(BraidShellError::DigestMismatch { .. }) + )); + + // Invalid proof: mismatched public inputs hash + let invalid_proof_mismatch = ProofEnvelope { + kind: ProofKind::ReplayTrace, + proof_bytes: vec![1, 2, 3], + public_inputs_hash: [0x99; 32], + }; + let result_mismatch = BraidShell::assemble_with_proof( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + Some(invalid_proof_mismatch), + ); + assert!(matches!( + result_mismatch, + Err(BraidShellError::ProofShapeValidationFailed { .. }) + )); + + // Invalid proof: empty proof bytes + let invalid_proof_empty = ProofEnvelope { + kind: ProofKind::ReplayTrace, + proof_bytes: Vec::new(), + public_inputs_hash: expected_witness, + }; + let result_empty = BraidShell::assemble_with_proof( + wl(1), + basis_ref(), + members, + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + Some(invalid_proof_empty), + ); + assert!(matches!( + result_empty, + Err(BraidShellError::ProofShapeValidationFailed { .. }) + )); + } + + #[test] + fn cryptographic_proof_kinds_require_verifier_backend() { + use crate::proof::{ProofEnvelope, ProofKind}; + + let members = vec![member("member-a", MemberVerdict::Plural)]; + let temp_shell = BraidShell::assemble( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + ) + .unwrap(); + + for kind in [ProofKind::ZkSnark, ProofKind::VectorOpening] { + let result = BraidShell::assemble_with_proof( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + CausalPosture::AuthorOnly, + Some(ProofEnvelope { + kind, + proof_bytes: vec![1, 2, 3], + public_inputs_hash: temp_shell.witness_digest, + }), + ); + assert!(matches!( + result, + Err(BraidShellError::ProofShapeValidationFailed { .. }) + )); + } + } + + #[test] + fn test_secure_sealed_member_matching() { + let strand_id = make_strand_id("secure-member"); + let child_worldline = WorldlineId::from_bytes([0x88; 32]); + let member_authority = authority(0x10, 0x20); + let blinding_secret = [0x44; 32]; + + let blinded_commitment = BraidMemberRef::seal(strand_id, child_worldline, blinding_secret); + let sealed_ref = BraidMemberRef::Sealed { + blinded_commitment, + authority: member_authority, + }; + + // Verification matches correctly. + assert!(sealed_ref.matches_strand( + &strand_id, + &child_worldline, + &member_authority, + &blinding_secret + )); + + // Revealed references are not accepted by the sealed secure path. + assert!(!BraidMemberRef::Revealed(strand_id).matches_strand( + &strand_id, + &child_worldline, + &member_authority, + &blinding_secret + )); + + // Mismatched strand_id fails. + let wrong_strand_id = make_strand_id("wrong-member"); + assert!(!sealed_ref.matches_strand( + &wrong_strand_id, + &child_worldline, + &member_authority, + &blinding_secret + )); + + // Mismatched child_worldline fails. + let wrong_child_worldline = WorldlineId::from_bytes([0x99; 32]); + assert!(!sealed_ref.matches_strand( + &strand_id, + &wrong_child_worldline, + &member_authority, + &blinding_secret + )); + + // Mismatched authority fails. + assert!(!sealed_ref.matches_strand( + &strand_id, + &child_worldline, + &authority(0x10, 0x21), + &blinding_secret + )); + + // Mismatched blinding secret fails. + assert!(!sealed_ref.matches_strand( + &strand_id, + &child_worldline, + &member_authority, + &[0x45; 32] + )); } } diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index 8c5450c0..4796666b 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, SourceDisclosurePolicy, +}; 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()?, + }) + } + + /// 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,8 @@ impl WorldlineRuntime { child_worldline_id: request.child_worldline_id, writer_heads: writer_heads.clone(), support_pins: Vec::new(), + retention_posture, + _marker: std::marker::PhantomData, })?; Ok(ForkStrandReceipt { @@ -1558,6 +1622,7 @@ impl WorldlineRuntime { fork_basis_ref, child_worldline_id: request.child_worldline_id, writer_heads, + retention_posture, }) })(); @@ -2760,6 +2825,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 +2851,142 @@ 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_source_disclosure_policy( + hasher: &mut blake3::Hasher, + source_disclosure: SourceDisclosurePolicy, +) { + let tag = match source_disclosure { + SourceDisclosurePolicy::RevealNone => b"reveal-none".as_slice(), + SourceDisclosurePolicy::RevealStub => b"reveal-stub", + SourceDisclosurePolicy::RevealRedacted => b"reveal-redacted", + SourceDisclosurePolicy::RevealFull => b"reveal-full", + SourceDisclosurePolicy::RevealByAuthorityOnly => b"reveal-by-authority-only", + }; + 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::UnexpectedSourceDisclosure { + posture, + source_disclosure, + } => { + hasher.update(b"unexpected-source-disclosure"); + hash_causal_posture(hasher, *posture); + hash_source_disclosure_policy(hasher, *source_disclosure); + } + 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"); + } + PostureObstruction::PostureMismatch { actual, expected } => { + hasher.update(b"posture-mismatch"); + hash_causal_posture(hasher, *actual); + hash_causal_posture(hasher, *expected); + } + } +} + fn scheduler_error_cause_digest(err: &RuntimeError) -> Hash { let mut hasher = blake3::Hasher::new(); hasher.update(b"echo.scheduler-fault-cause.error"); @@ -3216,12 +3421,17 @@ impl SchedulerCoordinator { } #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] 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 +3455,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() + .unwrap() + } + fn empty_engine() -> Engine { let mut store = GraphStore::default(); let root = make_node_id("root"); @@ -3570,6 +3810,7 @@ mod tests { None, true, )], + retention_posture: test_retention_posture(10), }, ) .unwrap(); @@ -3597,6 +3838,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 +4053,7 @@ mod tests { None, true, )], + retention_posture: test_retention_posture(11), }, ) .unwrap(); @@ -3721,6 +4116,7 @@ mod tests { Some(InboxAddress("wrong-worldline".to_owned())), false, )], + retention_posture: test_retention_posture(12), }, ) .unwrap_err(); @@ -4676,7 +5072,7 @@ mod tests { }; let contract_correlation = ReceiptCorrelationRecord { contract: Some(contract.clone()), - ..correlation.clone() + ..correlation }; let applied_with_contract = IntentOutcome::from_observation(IntentOutcomeObservation::Decided { diff --git a/crates/warp-core/src/engine_impl.rs b/crates/warp-core/src/engine_impl.rs index 6d125cb0..71f3d412 100644 --- a/crates/warp-core/src/engine_impl.rs +++ b/crates/warp-core/src/engine_impl.rs @@ -1188,7 +1188,7 @@ impl Engine { .map(|record| record.evidence_identity(query_id, crate::ContractOperationKind::Query)) } - /// Installs a generated contract package through the package registry + /// Registers a generated contract package through the package registry /// boundary. /// /// This runtime-owner bootstrap surface verifies the generated registry, @@ -1201,10 +1201,10 @@ impl Engine { /// /// Returns [`InstalledContractPackageError`] if registry verification fails, /// any handler/observer names an unsupported operation, or the package would - /// conflict with an already-installed package, rule, mutation op, or query op. + /// conflict with an already-registered package, rule, mutation op, or query op. #[cfg(feature = "native_rule_bootstrap")] #[doc(hidden)] - pub fn install_contract_package<'a>( + pub fn register_contract_package<'a>( &mut self, package: InstalledContractPackage<'a>, ) -> Result> { diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 45e92c27..841d3d39 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -41,6 +41,7 @@ pub mod wsc; mod admission; mod attachment; +mod braid; mod braid_shell; mod causal_facts; pub mod causal_wal; @@ -138,6 +139,7 @@ mod optic_artifact; pub mod parallel; mod payload; mod playback; +pub mod proof; mod provenance_store; mod receipt; mod record; @@ -253,12 +255,17 @@ pub use retained_evidence::{ }; // --- Session types --- pub use playback::{SessionId, ViewSession}; +// --- Proof types --- +pub use proof::{ObserverHonestyClaim, ProofEnvelope, ProofKind}; +// --- Braid Log types --- +pub use braid::{Braid, BraidError, BraidEvent, BraidStatus}; // --- Retained boundary shell family (θ_tick, θ_braid) --- pub use braid_shell::{ - collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidShell, BraidShellError, - BraidShellMember, BraidShellOutcome, BraidShellQuery, BraidShellRecords, BraidShellReplay, - CollapsePolicy, CollapseResult, MemberVerdict, RetainedBoundaryKind, RetainedBoundaryRecord, - BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, + collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidMemberRef, BraidShell, + BraidShellError, BraidShellMember, BraidShellMemberQuery, BraidShellOutcome, BraidShellQuery, + BraidShellRecords, BraidShellReplay, CollapsePolicy, CollapseResult, MemberVerdict, + RetainedBoundaryKind, RetainedBoundaryRecord, BRAID_SHELL_VERSION, + COLLAPSE_WITHOUT_POLICY_REASON, }; pub use neighborhood::{ NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, @@ -317,14 +324,14 @@ pub use revelation::RevelationPosture; pub use revelation::{ import_posture_disposition, least_revealed, revelation_operation_effect, shell_posture_obstruction, ActorId, AdmissionId, AdmissionScopeId, AdmissionSourceDisclosure, - AuthorityBinding, AuthorityCapabilityDigest, AuthorityDomainId, AuthorityDomainRef, - AuthorityResolutionProof, CapabilityProof, CausalAuthority, CausalPosture, - ImportAdmissionReceipt, ImportPostureDisposition, ImportQuarantineNamespace, + AuthorOnly, AuthorityBinding, AuthorityCapabilityDigest, AuthorityDomainId, AuthorityDomainRef, + AuthorityResolutionProof, CapabilityProof, CausalAuthority, CausalPosture, CausalPostureState, + DynamicPosture, ImportAdmissionReceipt, ImportPostureDisposition, ImportQuarantineNamespace, ImportedArtifactId, IntentId, KeyId, KeyProofId, MaterializationBasis, MaterializationReceipt, OperationPostureEffect, OriginId, PostureDerivation, PostureObstruction, ProjectionPolicy, ProjectionSpecId, PromotionBasis, PromotionIntent, RetentionContractId, RetentionPosture, - RevelationOperation, SealStrength, SessionContext, SharedAdmission, SourceDisclosurePolicy, - WitnessDigest, + RevelationOperation, Scratch, SealStrength, SessionContext, Shared, SharedAdmission, + SourceDisclosurePolicy, WitnessDigest, }; #[cfg(feature = "native_rule_bootstrap")] pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; @@ -333,9 +340,9 @@ pub use sandbox::DeterminismError; pub use sandbox::{build_engine, run_pair_determinism, EchoConfig}; pub use scheduler::SchedulerKind; pub use settlement::{ - ConflictArtifactDraft, ConflictReason, ImportCandidate, PluralAlternativeDraft, - PluralSettlementPolicy, SettlementDecision, SettlementDelta, SettlementError, SettlementPlan, - SettlementPolicy, SettlementResult, SettlementService, + ConflictArtifactDraft, ConflictReason, ImportCandidate, MemberBlindingSalt, + PluralAlternativeDraft, PluralSettlementPolicy, SettlementDecision, SettlementDelta, + SettlementError, SettlementPlan, SettlementPolicy, SettlementResult, SettlementService, }; pub use snapshot::{ compute_commit_hash_v2, compute_emissions_digest, compute_op_emission_index_digest, diff --git a/crates/warp-core/src/neighborhood.rs b/crates/warp-core/src/neighborhood.rs index a06315d5..45bcfcdc 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,8 @@ mod tests { child_worldline_id: support_worldline, writer_heads: vec![support_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }) .unwrap(); @@ -881,6 +909,8 @@ mod tests { child_worldline_id: primary_worldline, writer_heads: vec![primary_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }) .unwrap(); runtime @@ -1047,6 +1077,8 @@ mod tests { child_worldline_id: support_worldline, writer_heads: vec![support_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }) .unwrap(); let primary_strand_id = make_strand_id("primary"); @@ -1067,6 +1099,8 @@ mod tests { child_worldline_id: primary_worldline, writer_heads: vec![primary_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }) .unwrap(); runtime diff --git a/crates/warp-core/src/observation.rs b/crates/warp-core/src/observation.rs index b48d3d41..de08d230 100644 --- a/crates/warp-core/src/observation.rs +++ b/crates/warp-core/src/observation.rs @@ -2558,7 +2558,12 @@ fn current_cycle_tick(runtime: &WorldlineRuntime) -> Option { } #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::unnecessary_wraps +)] mod tests { use super::*; use crate::coordinator::WorldlineRuntime; @@ -2575,6 +2580,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 +2632,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 +3027,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }) .unwrap(); diff --git a/crates/warp-core/src/proof.rs b/crates/warp-core/src/proof.rs new file mode 100644 index 00000000..e9dfb1a7 --- /dev/null +++ b/crates/warp-core/src/proof.rs @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Proof envelopes and honesty assertions. + +use blake3::Hasher; + +use crate::braid_shell::BraidCoordinate; +use crate::ident::Hash; +use crate::revelation::AuthorityDomainRef; + +const PROOF_ENVELOPE_DOMAIN: &[u8] = b"echo.proof.envelope.v1\0"; + +/// The kind of proof-shaped evidence enclosed. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ProofKind { + /// Zero-Knowledge Succinct Non-Interactive Argument of Knowledge. + /// + /// Reserved until a verifier backend is wired. + ZkSnark, + /// Plain execution replay trace evidence. + ReplayTrace, + /// Verkle/Merkle vector commitment opening. + /// + /// Reserved until a verifier backend is wired. + VectorOpening, +} + +impl ProofKind { + /// Stable wire tag for canonical hashing. + #[must_use] + pub fn canonical_tag(self) -> u8 { + match self { + Self::ZkSnark => 0x01, + Self::ReplayTrace => 0x02, + Self::VectorOpening => 0x03, + } + } + + const fn accepts_shape_only(self) -> bool { + matches!(self, Self::ReplayTrace) + } +} + +/// A proof-shaped envelope whose current validation admits replay-trace +/// evidence by checking structure and public-input binding. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProofEnvelope { + /// The style/kind of proof-shaped evidence. + pub kind: ProofKind, + /// Raw serialized proof/evidence bytes. These bytes are not cryptographically + /// verified by [`Self::validate_shape`]. + pub proof_bytes: Vec, + /// Salted commitment digest binding public inputs. + pub public_inputs_hash: Hash, +} + +impl ProofEnvelope { + /// Validates the envelope shape and public inputs hash. + /// + /// Does not perform cryptographic proof verification; only validates + /// envelope structure and public-input hash binding. + /// + /// # Errors + /// + /// Returns a validation error string if proof bytes are empty or public inputs mismatch. + pub fn validate_shape(&self, expected_public_inputs_hash: Hash) -> Result<(), String> { + if !self.kind.accepts_shape_only() { + return Err(format!( + "{:?} proof envelopes require a verifier backend before admission", + self.kind + )); + } + if self.proof_bytes.is_empty() { + return Err("Proof payload is empty".to_string()); + } + if self.public_inputs_hash != expected_public_inputs_hash { + return Err(format!( + "Public inputs mismatch: expected {:?}, got {:?}", + expected_public_inputs_hash, self.public_inputs_hash + )); + } + Ok(()) + } + + /// Returns the canonical digest of the envelope material. + #[must_use] + pub fn digest(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(PROOF_ENVELOPE_DOMAIN); + hasher.update(&[self.kind.canonical_tag()]); + hasher.update(&(self.proof_bytes.len() as u64).to_le_bytes()); + hasher.update(&self.proof_bytes); + hasher.update(&self.public_inputs_hash); + hasher.finalize().into() + } +} + +/// An assertion of honesty regarding a braid's causal execution path. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ObserverHonestyClaim { + /// Braid coordinate whose history is claimed to be correct. + pub coordinate: BraidCoordinate, + /// Target shell digest certifying the settlement. + pub shell_digest: Hash, + /// Domain identifying the observer. + pub observer_domain: AuthorityDomainRef, +} diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index c1b0c610..c9b9a68f 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -3630,7 +3630,9 @@ mod tests { use crate::braid_shell::{BraidShellMember, BraidShellOutcome, MemberVerdict}; use crate::revelation::CausalPosture; let member = BraidShellMember { - strand_ref: crate::strand::make_strand_id("m"), + member_ref: crate::braid_shell::BraidMemberRef::Revealed( + crate::strand::make_strand_id("m"), + ), support_pin_digest: [1; 32], basis_digest: [2; 32], frontier_digest: [3; 32], diff --git a/crates/warp-core/src/revelation.rs b/crates/warp-core/src/revelation.rs index ce3d9eb5..110c7a2f 100644 --- a/crates/warp-core/src/revelation.rs +++ b/crates/warp-core/src/revelation.rs @@ -73,6 +73,61 @@ pub enum CausalPosture { #[deprecated(note = "Use CausalPosture")] pub type RevelationPosture = CausalPosture; +mod posture_state_seal { + pub trait Sealed {} +} + +/// Trait representing compile-time typestate for causal posture. +/// +/// This trait is sealed to Echo's marker types. Runtime posture validation +/// remains the authority for settlement admission. +pub trait CausalPostureState: + Clone + std::fmt::Debug + PartialEq + Eq + posture_state_seal::Sealed +{ + /// Returns the runtime CausalPosture value for this typestate, or None if dynamic. + fn causal_posture() -> Option; +} + +/// Marker struct representing the Shared causal posture. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Shared; +impl posture_state_seal::Sealed for Shared {} +impl CausalPostureState for Shared { + fn causal_posture() -> Option { + Some(CausalPosture::Shared) + } +} + +/// Marker struct representing the AuthorOnly causal posture. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AuthorOnly; +impl posture_state_seal::Sealed for AuthorOnly {} +impl CausalPostureState for AuthorOnly { + fn causal_posture() -> Option { + Some(CausalPosture::AuthorOnly) + } +} + +/// Marker struct representing the Scratch causal posture. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Scratch; +impl posture_state_seal::Sealed for Scratch {} +impl CausalPostureState for Scratch { + fn causal_posture() -> Option { + Some(CausalPosture::Scratch) + } +} + +/// Representation of causal posture whose type is dynamic/erased at compile-time. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DynamicPosture; +impl posture_state_seal::Sealed for DynamicPosture {} +impl CausalPostureState for DynamicPosture { + fn causal_posture() -> Option { + None + } +} + impl CausalPosture { /// Stable wire tag for canonical serialization and digest domains. #[must_use] @@ -276,6 +331,8 @@ pub struct RetentionPosture { pub retention_contract: RetentionContractId, /// Shared-admission scope, present only for `Shared`. pub admission_scope: Option, + /// Source identity disclosure policy for retained shared projections. + pub source_disclosure: SourceDisclosurePolicy, } impl RetentionPosture { @@ -301,8 +358,37 @@ impl RetentionPosture { authority, retention_contract, admission_scope, + source_disclosure: default_source_disclosure(causal_posture), }) } + + /// Returns this posture with an explicit source-disclosure policy. + /// + /// # Errors + /// + /// Returns an obstruction when a non-shared posture tries to carry a shared + /// source-disclosure policy. + pub fn with_source_disclosure( + mut self, + source_disclosure: SourceDisclosurePolicy, + ) -> Result { + validate_source_disclosure(self.causal_posture, source_disclosure)?; + self.source_disclosure = source_disclosure; + Ok(self) + } + + /// 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)?; + validate_source_disclosure(self.causal_posture, self.source_disclosure)?; + self.authority.validate() + } } /// Session context posture and authority defaults. @@ -361,6 +447,55 @@ 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) -> Result { + RetentionPosture::new( + self.default_posture, + PostureDerivation::SessionDefault, + 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. @@ -397,6 +532,13 @@ pub enum PostureObstruction { /// Posture that unlawfully carried a scope. posture: CausalPosture, }, + /// Non-shared posture must not carry a shared source-disclosure policy. + UnexpectedSourceDisclosure { + /// Posture that unlawfully carried a source-disclosure policy. + posture: CausalPosture, + /// Source-disclosure policy that only belongs to shared projections. + source_disclosure: SourceDisclosurePolicy, + }, /// Durable materialization has exactly one posture pair: /// `Scratch -> AuthorOnly`. InvalidMaterializationTransition { @@ -447,6 +589,13 @@ pub enum PostureObstruction { LegacyAuthorityCannotAuthorizeNewAdmission, /// Generic causal witness digests are not authority-capability proofs. WitnessIsNotAuthorityCapability, + /// Expected a specific posture (e.g. Shared), but found a different one. + PostureMismatch { + /// The actual posture of the artifact. + actual: CausalPosture, + /// The expected posture. + expected: CausalPosture, + }, } /// Witness digest with a quality bar: zero and empty-input digests refused. @@ -1145,6 +1294,32 @@ fn validate_admission_scope( } } +const fn default_source_disclosure(posture: CausalPosture) -> SourceDisclosurePolicy { + match posture { + CausalPosture::Shared => SourceDisclosurePolicy::RevealFull, + CausalPosture::Scratch | CausalPosture::AuthorOnly => SourceDisclosurePolicy::RevealNone, + } +} + +fn validate_source_disclosure( + posture: CausalPosture, + source_disclosure: SourceDisclosurePolicy, +) -> Result<(), PostureObstruction> { + match (posture, source_disclosure) { + (CausalPosture::Shared, _) + | ( + CausalPosture::Scratch | CausalPosture::AuthorOnly, + SourceDisclosurePolicy::RevealNone, + ) => Ok(()), + (CausalPosture::Scratch | CausalPosture::AuthorOnly, _) => { + Err(PostureObstruction::UnexpectedSourceDisclosure { + posture, + source_disclosure, + }) + } + } +} + fn validate_posture_derivation( posture: CausalPosture, derivation: PostureDerivation, @@ -1756,6 +1931,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]), diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 2dc715f6..8d24f20e 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -18,7 +18,7 @@ use crate::provenance_store::{ ProvenanceEventKind, ProvenanceRef, ProvenanceService, ProvenanceStore, }; use crate::record::{EdgeRecord, NodeRecord}; -use crate::revelation::CausalPosture; +use crate::revelation::{CausalPosture, RetentionPosture, SourceDisclosurePolicy}; use crate::snapshot::{compute_commit_hash_v2, compute_state_root_for_warp_state}; use crate::strand::{ StrandBasisReport, StrandError, StrandId, StrandOverlapRevalidation, StrandRegistry, @@ -33,6 +33,9 @@ use crate::WorldlineState; const CONFLICT_ARTIFACT_DOMAIN: &[u8] = b"echo:settlement-conflict-artifact:v1\0"; const PLURAL_ARTIFACT_DOMAIN: &[u8] = b"echo:settlement-plural-artifact:v1\0"; const REFUSE_PLURAL_POLICY_DOMAIN: &[u8] = b"echo:settlement-policy:refuse-plural:v1\0"; +const DEFAULT_MEMBER_BLINDING_SALT_DOMAIN: &[u8] = + b"echo:settlement-member-blinding-salt:default:v1\0"; +const SETTLEMENT_MEMBER_BLINDING_DOMAIN: &[u8] = b"echo.settlement.member.blinding.v1\0"; /// Deterministic reasons a source settlement step could not be imported. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -88,6 +91,24 @@ pub enum PluralSettlementPolicy { AllowOverFootprintOverlap, } +/// Non-public settlement-local salt for sealed braid member commitments. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct MemberBlindingSalt(Hash); + +impl MemberBlindingSalt { + /// Builds a member blinding salt from caller-supplied entropy. + #[must_use] + pub const fn from_bytes(bytes: Hash) -> Self { + Self(bytes) + } +} + +impl core::fmt::Debug for MemberBlindingSalt { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("MemberBlindingSalt()") + } +} + /// Named law governing one settlement act. /// /// The plural-settlement policy decides whether plurality may *exist*; it is @@ -99,6 +120,8 @@ pub struct SettlementPolicy { pub policy_id: Hash, /// Plural-settlement law selected for this act. pub plural: PluralSettlementPolicy, + /// Non-public salt mixed into sealed braid member commitments. + pub member_blinding_salt: MemberBlindingSalt, } impl SettlementPolicy { @@ -108,21 +131,38 @@ impl SettlementPolicy { Self { policy_id, plural: PluralSettlementPolicy::AllowOverFootprintOverlap, + member_blinding_salt: default_member_blinding_salt(policy_id), } } + + /// Sets the non-public salt used for sealed braid member commitments. + #[must_use] + pub const fn with_member_blinding_salt(mut self, salt: MemberBlindingSalt) -> Self { + self.member_blinding_salt = salt; + self + } } impl Default for SettlementPolicy { fn default() -> Self { let mut hasher = Hasher::new(); hasher.update(REFUSE_PLURAL_POLICY_DOMAIN); + let policy_id = hasher.finalize().into(); Self { - policy_id: hasher.finalize().into(), + policy_id, plural: PluralSettlementPolicy::Refused, + member_blinding_salt: default_member_blinding_salt(policy_id), } } } +fn default_member_blinding_salt(policy_id: Hash) -> MemberBlindingSalt { + let mut hasher = Hasher::new(); + hasher.update(DEFAULT_MEMBER_BLINDING_SALT_DOMAIN); + hasher.update(&policy_id); + MemberBlindingSalt(hasher.finalize().into()) +} + /// Compare surface for one strand suffix relative to its recorded base. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SettlementDelta { @@ -552,6 +592,20 @@ 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, + }, + /// A statically shared strand handle did not match the live registry entry. + #[error("stale strand handle does not match live registry entry: {strand_id:?}")] + StaleStrandHandle { + /// Strand whose handle no longer matches the registry. + strand_id: StrandId, + }, /// 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 { @@ -626,12 +680,27 @@ struct RecordedEntryDraft { impl SettlementService { /// Compares the strand suffix against its recorded base coordinate. + /// + /// Compare is local inspection/revelation only: it identifies the suffix + /// window and basis evidence without promoting, planning, admitting, or + /// settling the strand. Shared-admission gates live on planning and + /// settlement execution. pub fn compare( runtime: &WorldlineRuntime, provenance: &ProvenanceService, strand_id: StrandId, ) -> Result { - let strand = strand(runtime.strands(), strand_id)?; + let d_strand = strand(runtime.strands(), strand_id)?; + Self::compare_internal(runtime, provenance, d_strand) + } + + /// Compares the strand suffix against its recorded base coordinate, generic over posture. + pub fn compare_internal( + runtime: &WorldlineRuntime, + provenance: &ProvenanceService, + strand: &crate::strand::Strand

, + ) -> Result { + let strand_id = strand.strand_id; ensure_frontier_matches_provenance( runtime, provenance, @@ -679,14 +748,32 @@ impl SettlementService { } /// Produces a deterministic settlement plan under an explicit named policy. + /// + /// # Errors + /// + /// Returns a [`SettlementError`] if the plan cannot be generated. pub fn plan_with_policy( runtime: &WorldlineRuntime, provenance: &ProvenanceService, strand_id: StrandId, policy: &SettlementPolicy, ) -> Result { - let strand = strand(runtime.strands(), strand_id)?; - let delta = Self::compare(runtime, provenance, strand_id)?; + let strand = shared_strand(runtime.strands(), strand_id)?; + Self::plan_with_policy_internal(runtime, provenance, &strand, policy) + } + + /// Produces a deterministic settlement plan under an explicit named policy, statically gated on Shared posture. + pub(crate) fn plan_with_policy_internal( + runtime: &WorldlineRuntime, + provenance: &ProvenanceService, + strand: &crate::strand::Strand, + policy: &SettlementPolicy, + ) -> Result { + let strand_id = strand.strand_id; + ensure_shared_runtime_posture(strand)?; + ensure_live_registered_shared_strand(runtime, strand)?; + let strand_posture = strand.retention_posture.causal_posture; + let delta = Self::compare_internal(runtime, provenance, strand)?; let target_worldline = strand.fork_basis_ref.source_lane_id; let target_frontier_tick = ensure_frontier_matches_provenance(runtime, provenance, target_worldline)?; @@ -812,6 +899,7 @@ impl SettlementService { &source_entry, entry_overlap_slots, policy, + strand_posture, ))); continue; } else { @@ -858,13 +946,29 @@ impl SettlementService { } /// Executes the deterministic settlement plan under an explicit named policy. + /// + /// # Errors + /// + /// Returns a [`SettlementError`] if settlement fails. pub fn settle_with_policy( runtime: &mut WorldlineRuntime, provenance: &mut ProvenanceService, strand_id: StrandId, policy: &SettlementPolicy, ) -> Result { - let plan = Self::plan_with_policy(runtime, provenance, strand_id, policy)?; + let strand = shared_strand(runtime.strands(), strand_id)?; + Self::settle_with_policy_internal(runtime, provenance, &strand, policy) + } + + /// Executes the deterministic settlement plan under an explicit named policy, statically gated on Shared posture. + pub(crate) fn settle_with_policy_internal( + runtime: &mut WorldlineRuntime, + provenance: &mut ProvenanceService, + strand: &crate::strand::Strand, + policy: &SettlementPolicy, + ) -> Result { + ensure_shared_runtime_posture(strand)?; + let plan = Self::plan_with_policy_internal(runtime, provenance, strand, policy)?; if plan.decisions.is_empty() { return Ok(SettlementResult { plan, @@ -874,10 +978,9 @@ impl SettlementService { braid_shell: None, }); } - let (fork_basis_ref, support_pins) = { - let settled = strand(runtime.strands(), strand_id)?; - (settled.fork_basis_ref, settled.support_pins.clone()) - }; + let fork_basis_ref = strand.fork_basis_ref; + let support_pins = strand.support_pins.clone(); + let retention_posture = strand.retention_posture; let runtime_before = runtime.clone(); let provenance_before = provenance.checkpoint_for([plan.target_worldline])?; @@ -928,6 +1031,7 @@ impl SettlementService { &plan, fork_basis_ref, &support_pins, + &retention_posture, policy, &appended_imports, )?; @@ -953,12 +1057,53 @@ impl SettlementService { fn strand( registry: &StrandRegistry, strand_id: StrandId, -) -> Result<&crate::strand::Strand, SettlementError> { +) -> Result<&crate::strand::Strand, SettlementError> { registry .get(&strand_id) .ok_or(SettlementError::StrandNotFound(strand_id)) } +fn shared_strand( + registry: &StrandRegistry, + strand_id: StrandId, +) -> Result, SettlementError> { + let d_strand = strand(registry, strand_id)?; + d_strand + .clone() + .try_into_shared() + .map_err(|_| SettlementError::NonSharedStrand { + strand_id, + posture: d_strand.retention_posture.causal_posture, + }) +} + +fn ensure_shared_runtime_posture( + strand: &crate::strand::Strand, +) -> Result<(), SettlementError> { + if strand.retention_posture.causal_posture == CausalPosture::Shared { + Ok(()) + } else { + Err(SettlementError::NonSharedStrand { + strand_id: strand.strand_id, + posture: strand.retention_posture.causal_posture, + }) + } +} + +fn ensure_live_registered_shared_strand( + runtime: &WorldlineRuntime, + strand: &crate::strand::Strand, +) -> Result<(), SettlementError> { + let live = shared_strand(runtime.strands(), strand.strand_id)?; + if live == *strand { + Ok(()) + } else { + Err(SettlementError::StaleStrandHandle { + strand_id: strand.strand_id, + }) + } +} + fn ensure_frontier_matches_provenance( runtime: &WorldlineRuntime, provenance: &ProvenanceService, @@ -1287,10 +1432,13 @@ fn build_braid_shell( plan: &SettlementPlan, fork_basis_ref: crate::strand::ForkBasisRef, support_pins: &[crate::strand::SupportPin], + retention_posture: &RetentionPosture, policy: &SettlementPolicy, appended_imports: &[ProvenanceRef], ) -> Result { - use crate::braid_shell::{BraidShell, BraidShellMember, BraidShellOutcome, MemberVerdict}; + use crate::braid_shell::{ + BraidMemberRef, BraidShell, BraidShellMember, BraidShellOutcome, MemberVerdict, + }; let mut plural_ids = Vec::new(); let mut conflict_codes = Vec::new(); @@ -1349,9 +1497,28 @@ fn build_braid_shell( } }; + let strand_posture = retention_posture.causal_posture; + let source_identity_public = strand_posture == CausalPosture::Shared + && retention_posture.source_disclosure == SourceDisclosurePolicy::RevealFull; + let member_ref = if source_identity_public { + BraidMemberRef::Revealed(plan.strand_id) + } else { + let blinding_secret = + settlement_member_blinding(plan, retention_posture, policy.member_blinding_salt); + let blinded_commitment = BraidMemberRef::seal( + plan.strand_id, + plan.basis_report.child_worldline_id, + blinding_secret, + ); + BraidMemberRef::Sealed { + blinded_commitment, + authority: retention_posture.authority.author_domain, + } + }; + let overlap_slots = settlement_basis_overlap_slots(&plan.basis_report).unwrap_or_default(); let member = BraidShellMember { - strand_ref: plan.strand_id, + member_ref, support_pin_digest: support_pins_digest(support_pins), basis_digest: fork_basis_digest(fork_basis_ref), frontier_digest: frontier_digest(plan.basis_report.realized_parent_ref), @@ -1359,7 +1526,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( @@ -1368,10 +1535,59 @@ fn build_braid_shell( vec![member], policy.policy_id, outcome, - CausalPosture::AuthorOnly, + strand_posture, ) } +fn settlement_member_blinding( + plan: &SettlementPlan, + retention_posture: &RetentionPosture, + member_blinding_salt: MemberBlindingSalt, +) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(SETTLEMENT_MEMBER_BLINDING_DOMAIN); + hasher.update(&member_blinding_salt.0); + hasher.update(plan.strand_id.as_bytes()); + hasher.update(plan.basis_report.child_worldline_id.as_bytes()); + hasher.update(&[retention_posture.causal_posture.canonical_tag()]); + hasher.update(retention_posture.retention_contract.as_bytes()); + hasher.update( + retention_posture + .authority + .author_domain + .origin_id + .as_bytes(), + ); + hasher.update( + retention_posture + .authority + .author_domain + .domain_id + .as_bytes(), + ); + match retention_posture.admission_scope { + Some(scope) => { + hasher.update(&[1]); + hasher.update(scope.as_bytes()); + } + None => { + hasher.update(&[0]); + } + } + hasher.update(&[source_disclosure_tag(retention_posture.source_disclosure)]); + hasher.finalize().into() +} + +const fn source_disclosure_tag(source_disclosure: SourceDisclosurePolicy) -> u8 { + match source_disclosure { + SourceDisclosurePolicy::RevealNone => 0, + SourceDisclosurePolicy::RevealStub => 1, + SourceDisclosurePolicy::RevealRedacted => 2, + SourceDisclosurePolicy::RevealFull => 3, + SourceDisclosurePolicy::RevealByAuthorityOnly => 4, + } +} + fn hash_claim_source_ref(hasher: &mut Hasher, source_ref: ProvenanceRef) { hasher.update(source_ref.worldline_id.as_bytes()); hasher.update(&source_ref.worldline_tick.as_u64().to_le_bytes()); @@ -1429,6 +1645,7 @@ fn plural_draft( source_entry: &ProvenanceEntry, mut overlapping_slots: Vec, policy: &SettlementPolicy, + posture: CausalPosture, ) -> PluralAlternativeDraft { canonicalize_slots(&mut overlapping_slots); PluralAlternativeDraft { @@ -1446,7 +1663,7 @@ fn plural_draft( .collect(), overlapping_slots, policy_id: policy.policy_id, - posture: CausalPosture::AuthorOnly, + posture, } } @@ -1533,7 +1750,13 @@ fn empty_worldline_patch( } #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::large_types_passed_by_value, + clippy::redundant_clone +)] mod tests { use super::*; @@ -1542,7 +1765,12 @@ 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::strand::{ForkBasisRef, Strand}; + use crate::revelation::{ + ActorId, AdmissionScopeId, AuthorityBinding, AuthorityDomainId, AuthorityDomainRef, + CausalAuthority, OriginId, PostureDerivation, RetentionContractId, RetentionPosture, + SealStrength, SourceDisclosurePolicy, + }; + use crate::strand::{make_strand_id, ForkBasisRef, Strand}; use crate::tick_patch::{SlotId, WarpOp}; use crate::{GraphStore, WorldlineState}; @@ -1558,6 +1786,164 @@ mod tests { GlobalTick::from_raw(raw) } + 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( + posture, + 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]), + 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 test_provenance_ref(worldline: u8, tick: u64, hash: u8) -> ProvenanceRef { + ProvenanceRef { + worldline_id: wl(worldline), + worldline_tick: wt(tick), + commit_hash: [hash; 32], + } + } + + #[test] + fn sealed_member_blinding_is_stable_across_parent_frontier_movement() { + let strand_id = make_strand_id("stable-hidden-member"); + let parent = wl(1); + let child = wl(9); + let anchor_ref = test_provenance_ref(1, 0, 0x11); + let parent_to_before = test_provenance_ref(1, 2, 0x22); + let parent_to_after = test_provenance_ref(1, 5, 0x55); + let fork_basis_ref = ForkBasisRef { + source_lane_id: parent, + fork_tick: wt(0), + commit_hash: [0x11; 32], + boundary_hash: [0x12; 32], + provenance_ref: anchor_ref, + }; + let base_report = StrandBasisReport { + strand_id, + parent_anchor: fork_basis_ref, + child_worldline_id: child, + source_suffix_start_tick: wt(1), + source_suffix_end_tick: Some(wt(3)), + realized_parent_ref: parent_to_before, + owned_divergence: crate::strand::StrandDivergenceFootprint::default(), + parent_movement: crate::strand::ParentMovementFootprint::default(), + parent_revalidation: StrandRevalidationState::ParentAdvancedDisjoint { + parent_from: anchor_ref, + parent_to: parent_to_before, + }, + }; + let moved_report = StrandBasisReport { + realized_parent_ref: parent_to_after, + parent_revalidation: StrandRevalidationState::ParentAdvancedDisjoint { + parent_from: anchor_ref, + parent_to: parent_to_after, + }, + ..base_report.clone() + }; + let base_plan = SettlementPlan { + strand_id, + target_worldline: parent, + target_base_ref: parent_to_before, + basis_report: base_report, + decisions: Vec::new(), + }; + let moved_plan = SettlementPlan { + target_base_ref: parent_to_after, + basis_report: moved_report, + ..base_plan.clone() + }; + let retention_posture = author_only_retention_posture(); + let policy = plural_policy(); + + assert_eq!( + settlement_member_blinding(&base_plan, &retention_posture, policy.member_blinding_salt), + settlement_member_blinding( + &moved_plan, + &retention_posture, + policy.member_blinding_salt + ) + ); + } + + #[test] + fn sealed_member_blinding_changes_with_settlement_salt() { + let strand_id = make_strand_id("independent-hidden-member"); + let parent = wl(1); + let child = wl(9); + let parent_ref = test_provenance_ref(1, 2, 0x22); + let fork_basis_ref = ForkBasisRef { + source_lane_id: parent, + fork_tick: wt(0), + commit_hash: [0x11; 32], + boundary_hash: [0x12; 32], + provenance_ref: test_provenance_ref(1, 0, 0x11), + }; + let plan = SettlementPlan { + strand_id, + target_worldline: parent, + target_base_ref: parent_ref, + basis_report: StrandBasisReport { + strand_id, + parent_anchor: fork_basis_ref, + child_worldline_id: child, + source_suffix_start_tick: wt(1), + source_suffix_end_tick: Some(wt(3)), + realized_parent_ref: parent_ref, + owned_divergence: crate::strand::StrandDivergenceFootprint::default(), + parent_movement: crate::strand::ParentMovementFootprint::default(), + parent_revalidation: StrandRevalidationState::ParentAdvancedDisjoint { + parent_from: test_provenance_ref(1, 0, 0x11), + parent_to: parent_ref, + }, + }, + decisions: Vec::new(), + }; + let retention_posture = author_only_retention_posture(); + let first_policy = + plural_policy().with_member_blinding_salt(MemberBlindingSalt::from_bytes([0xA1; 32])); + let second_policy = + plural_policy().with_member_blinding_salt(MemberBlindingSalt::from_bytes([0xB2; 32])); + + let first_blinding = settlement_member_blinding( + &plan, + &retention_posture, + first_policy.member_blinding_salt, + ); + let second_blinding = settlement_member_blinding( + &plan, + &retention_posture, + second_policy.member_blinding_salt, + ); + + assert_ne!(first_blinding, second_blinding); + assert_ne!( + crate::braid_shell::BraidMemberRef::seal(strand_id, child, first_blinding), + crate::braid_shell::BraidMemberRef::seal(strand_id, child, second_blinding) + ); + } + fn register_head( runtime: &mut WorldlineRuntime, worldline_id: WorldlineId, @@ -1792,6 +2178,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); @@ -1852,6 +2251,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture, + _marker: std::marker::PhantomData, }; runtime.register_strand(strand).unwrap(); @@ -1909,6 +2310,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture, + _marker: std::marker::PhantomData, }) .unwrap(); } @@ -1960,6 +2363,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture, + _marker: std::marker::PhantomData, }) .unwrap(); ( @@ -2023,6 +2428,163 @@ mod tests { .is_some()); } + #[test] + fn settlement_compare_inspects_author_only_while_plan_settle_reject() { + let (runtime, provenance, strand_id, _, _) = + setup_runtime_with_strand_posture(ParentDrift::None, author_only_retention_posture()); + + let delta = SettlementService::compare(&runtime, &provenance, strand_id) + .expect("compare is revelation-only local strand inspection"); + assert_eq!(delta.strand_id, strand_id); + assert_eq!(delta.source_entries.len(), 1); + + 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(); + let settle_err = SettlementService::settle( + &mut runtime_for_settle, + &mut provenance_for_settle, + strand_id, + ) + .expect_err("settle must not bypass the planning gate"); + assert!(matches!( + settle_err, + SettlementError::NonSharedStrand { + strand_id: rejected, + posture: CausalPosture::AuthorOnly, + } if rejected == strand_id + )); + } + + #[test] + fn settlement_internals_reject_forged_shared_typestate() { + let (runtime, provenance, strand_id, _, _) = + setup_runtime_with_strand_posture(ParentDrift::None, author_only_retention_posture()); + let dynamic = runtime + .strands() + .get(&strand_id) + .expect("registered strand") + .clone(); + let forged_shared = Strand:: { + strand_id: dynamic.strand_id, + fork_basis_ref: dynamic.fork_basis_ref, + child_worldline_id: dynamic.child_worldline_id, + writer_heads: dynamic.writer_heads, + support_pins: dynamic.support_pins, + retention_posture: dynamic.retention_posture, + _marker: std::marker::PhantomData, + }; + + let plan_err = SettlementService::plan_with_policy_internal( + &runtime, + &provenance, + &forged_shared, + &SettlementPolicy::default(), + ) + .expect_err("forged Shared typestate must not bypass planning posture check"); + assert!(matches!( + plan_err, + SettlementError::NonSharedStrand { + strand_id: rejected, + posture: CausalPosture::AuthorOnly, + } if rejected == strand_id + )); + + let method_err = forged_shared + .plan(&runtime, &provenance) + .expect_err("Strand::plan must revalidate runtime posture"); + assert!(matches!( + method_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(); + let settle_err = SettlementService::settle_with_policy_internal( + &mut runtime_for_settle, + &mut provenance_for_settle, + &forged_shared, + &SettlementPolicy::default(), + ) + .expect_err("forged Shared typestate must not bypass settlement posture check"); + assert!(matches!( + settle_err, + SettlementError::NonSharedStrand { + strand_id: rejected, + posture: CausalPosture::AuthorOnly, + } if rejected == strand_id + )); + + let mut runtime_for_method_settle = runtime; + let mut provenance_for_method_settle = provenance; + let method_settle_err = forged_shared + .settle( + &mut runtime_for_method_settle, + &mut provenance_for_method_settle, + ) + .expect_err("Strand::settle must revalidate runtime posture"); + assert!(matches!( + method_settle_err, + SettlementError::NonSharedStrand { + strand_id: rejected, + posture: CausalPosture::AuthorOnly, + } if rejected == strand_id + )); + } + + #[test] + fn shared_strand_handles_require_live_registry_membership() { + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::None); + let stale_shared = shared_strand(runtime.strands(), strand_id).unwrap(); + let mut mismatched_shared = stale_shared.clone(); + mismatched_shared.child_worldline_id = wl(9); + + let stale_err = SettlementService::plan_with_policy_internal( + &runtime, + &provenance, + &mismatched_shared, + &SettlementPolicy::default(), + ) + .expect_err("mismatched Shared strand handle must not bypass live registry state"); + assert!(matches!( + stale_err, + SettlementError::StaleStrandHandle { strand_id: rejected } if rejected == strand_id + )); + + runtime + .strands_mut_for_tests() + .remove(&strand_id) + .expect("strand removed from registry"); + + let plan_err = stale_shared + .plan(&runtime, &provenance) + .expect_err("stale Shared strand handle must not bypass registry lookup"); + assert!(matches!( + plan_err, + SettlementError::StrandNotFound(rejected) if rejected == strand_id + )); + + let settle_err = stale_shared + .settle(&mut runtime, &mut provenance) + .expect_err("stale Shared strand handle must not settle outside registry"); + assert!(matches!( + settle_err, + SettlementError::StrandNotFound(rejected) if rejected == strand_id + )); + } + #[test] fn settlement_imports_child_suffix_when_parent_advanced_disjoint() { let (mut runtime, mut provenance, strand_id, base_worldline, child_worldline) = @@ -2294,7 +2856,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); @@ -2324,7 +2886,7 @@ mod tests { assert!(matches!( retained.event_kind, ProvenanceEventKind::PluralArtifact { - posture: CausalPosture::AuthorOnly, + posture: CausalPosture::Shared, .. } )); @@ -2389,9 +2951,9 @@ 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!(shell.has_revealed_member_strand(&strand_id)); assert_eq!(shell.members.len(), 1); assert_eq!( shell.members[0].verdict, @@ -2412,6 +2974,44 @@ mod tests { ); } + #[test] + fn hidden_shared_source_disclosure_seals_settlement_shell_member() { + let retention_posture = shared_retention_posture() + .with_source_disclosure(SourceDisclosurePolicy::RevealNone) + .unwrap(); + let (mut runtime, mut provenance, strand_id, _, child_worldline) = + setup_runtime_with_strand_posture(ParentDrift::OverlapDifferent, retention_posture); + + let result = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + + let shell_digest = result.braid_shell.unwrap(); + let shell = provenance.braid_shell(&shell_digest).unwrap(); + + assert!(!shell.has_revealed_member_strand(&strand_id)); + assert!(matches!( + shell.members[0].member_ref, + crate::braid_shell::BraidMemberRef::Sealed { .. } + )); + + let blinding_secret = settlement_member_blinding( + &result.plan, + &retention_posture, + plural_policy().member_blinding_salt, + ); + assert!(shell.has_member_strand_secure( + &strand_id, + &child_worldline, + &retention_posture.authority.author_domain, + &blinding_secret + )); + } + #[test] fn braid_shell_replays_after_runtime_and_histories_are_dropped() { let (mut runtime, mut provenance, strand_id, _, _) = @@ -2442,7 +3042,10 @@ mod tests { assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Plural); assert_eq!( replay.member_verdicts, - vec![(strand_id, crate::braid_shell::MemberVerdict::Plural)] + vec![( + crate::braid_shell::BraidMemberRef::Revealed(strand_id), + crate::braid_shell::MemberVerdict::Plural + )] ); assert_eq!(replay.policy_id, plural_policy().policy_id); } @@ -2606,7 +3209,9 @@ mod tests { base_worldline, plan.target_base_ref, vec![crate::braid_shell::BraidShellMember { - strand_ref: crate::strand::make_strand_id("dummy-binder"), + member_ref: crate::braid_shell::BraidMemberRef::Revealed( + crate::strand::make_strand_id("dummy-binder"), + ), support_pin_digest: [1; 32], basis_digest: [2; 32], frontier_digest: [3; 32], @@ -2703,9 +3308,9 @@ mod tests { .unwrap(); let query = crate::braid_shell::BraidShellQuery { - member_strand: Some(strand_id), + revealed_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); diff --git a/crates/warp-core/src/strand.rs b/crates/warp-core/src/strand.rs index 98ee92ed..e0ad6930 100644 --- a/crates/warp-core/src/strand.rs +++ b/crates/warp-core/src/strand.rs @@ -33,6 +33,9 @@ use thiserror::Error; use crate::clock::WorldlineTick; use crate::ident::Hash; use crate::provenance_store::{ProvenanceRef, ProvenanceService, ProvenanceStore}; +use crate::revelation::{ + CausalPostureState, DynamicPosture, PostureObstruction, RetentionPosture, Shared, +}; use crate::tick_patch::SlotId; use crate::worldline::WorldlineId; @@ -132,7 +135,7 @@ pub struct DropReceipt { /// (dropped). There is no lifecycle field — operational state is derived /// from the writer heads. #[derive(Clone, PartialEq, Eq, Debug)] -pub struct Strand { +pub struct Strand { /// Unique strand identity. pub strand_id: StrandId, /// Immutable fork coordinate. @@ -143,9 +146,13 @@ 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, + /// Phantom marker for the static causal posture typestate. + pub _marker: std::marker::PhantomData

, } -impl Strand { +impl Strand

{ /// Builds a basis-relative report for this strand against current parent history. /// /// The report is the first live-strand seam: the strand still records an @@ -158,9 +165,9 @@ impl Strand { /// /// Returns [`StrandError`] when provenance is unavailable or the fork tick /// cannot be advanced to the suffix start coordinate. - pub fn live_basis_report( + pub fn live_basis_report( &self, - provenance: &P, + provenance: &S, ) -> Result { let suffix_start = self .fork_basis_ref @@ -231,6 +238,82 @@ impl Strand { } } +impl Strand

{ + /// Erases the static posture type parameter, converting to DynamicPosture. + #[must_use] + pub fn into_dynamic(self) -> Strand { + Strand { + strand_id: self.strand_id, + fork_basis_ref: self.fork_basis_ref, + child_worldline_id: self.child_worldline_id, + writer_heads: self.writer_heads, + support_pins: self.support_pins, + retention_posture: self.retention_posture, + _marker: std::marker::PhantomData, + } + } +} + +impl Strand { + /// Attempts to cast this dynamically postured strand to a statically postured Shared strand. + /// + /// # Errors + /// + /// Returns a posture obstruction error if the strand's actual posture is not Shared. + pub fn try_into_shared(self) -> Result, StrandError> { + if self.retention_posture.causal_posture == crate::revelation::CausalPosture::Shared { + Ok(Strand { + strand_id: self.strand_id, + fork_basis_ref: self.fork_basis_ref, + child_worldline_id: self.child_worldline_id, + writer_heads: self.writer_heads, + support_pins: self.support_pins, + retention_posture: self.retention_posture, + _marker: std::marker::PhantomData, + }) + } else { + Err(StrandError::Posture(PostureObstruction::PostureMismatch { + actual: self.retention_posture.causal_posture, + expected: crate::revelation::CausalPosture::Shared, + })) + } + } +} + +impl Strand { + /// Produces a deterministic import/conflict plan for the strand suffix. + /// + /// The live registry entry is authoritative; this handle supplies only the + /// strand identity. + /// + /// # Errors + /// + /// Returns a [`crate::settlement::SettlementError`] if the plan cannot be generated. + pub fn plan( + &self, + runtime: &crate::coordinator::WorldlineRuntime, + provenance: &ProvenanceService, + ) -> Result { + crate::settlement::SettlementService::plan(runtime, provenance, self.strand_id) + } + + /// Executes the deterministic settlement plan. + /// + /// The live registry entry is authoritative; this handle supplies only the + /// strand identity. + /// + /// # Errors + /// + /// Returns a [`crate::settlement::SettlementError`] if settlement fails. + pub fn settle( + &self, + runtime: &mut crate::coordinator::WorldlineRuntime, + provenance: &mut ProvenanceService, + ) -> Result { + crate::settlement::SettlementService::settle(runtime, provenance, self.strand_id) + } +} + /// Closed optic footprint owned by a strand's local divergence. /// /// The write set records slots the child suffix produced. The read set records @@ -467,6 +550,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 +683,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/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index c47148ec..ba99e97d 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -121,7 +121,7 @@ pub struct TrustedRuntimeWalRecovery { /// Local trusted runtime host for the app-safe contract-host path. /// /// Application code should receive [`TrustedRuntimeApp`], not this type. This -/// host owns package installation, ticketed runtime ingress, scheduler passes, +/// host owns package registration, ticketed runtime ingress, scheduler passes, /// and read-only observation service access. pub struct TrustedRuntimeHost { runtime: WorldlineRuntime, @@ -213,23 +213,23 @@ impl TrustedRuntimeHost { } /// Returns the app-facing surface. This surface can submit and observe, but - /// it cannot tick, stage ticketed ingress, install packages, or recover + /// it cannot tick, stage ticketed ingress, register packages, or recover /// scheduler faults. pub fn app(&mut self) -> TrustedRuntimeApp<'_> { TrustedRuntimeApp { host: self } } - /// Installs a generated contract package through the trusted host boundary. + /// Registers a generated contract package through the trusted host boundary. /// /// # Errors /// /// Returns an installed-package error when registry verification fails or /// any handler/observer conflicts with existing runtime state. - pub fn install_contract_package<'a>( + pub fn register_contract_package<'a>( &mut self, package: InstalledContractPackage<'a>, ) -> Result> { - self.engine.install_contract_package(package) + self.engine.register_contract_package(package) } /// Stages one witnessed installed-contract submission into runtime ingress. @@ -671,7 +671,7 @@ impl TrustedRuntimeWal { /// App-facing handle for a trusted local runtime host. /// -/// This type intentionally exposes no scheduler control, package installation, +/// This type intentionally exposes no scheduler control, package registration, /// ticketed ingress staging, or fault recovery authority. pub struct TrustedRuntimeApp<'a> { host: &'a mut TrustedRuntimeHost, @@ -694,7 +694,7 @@ impl TrustedRuntimeApp<'_> { /// runtime WAL has committed the acceptance transaction. /// /// This is the ACK-boundary path for hosts that have configured a runtime - /// WAL. It does not tick, stage ticketed ingress, install packages, or + /// WAL. It does not tick, stage ticketed ingress, register packages, or /// expose WAL append authority to the application. /// /// # Errors diff --git a/crates/warp-core/tests/braid_public_api_tests.rs b/crates/warp-core/tests/braid_public_api_tests.rs new file mode 100644 index 00000000..90106d97 --- /dev/null +++ b/crates/warp-core/tests/braid_public_api_tests.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS + +//! External-consumer braid public API checks. + +use warp_core::{ + make_strand_id, AuthorityDomainId, AuthorityDomainRef, Braid, BraidError, BraidEvent, + BraidMemberRef, BraidStatus, OriginId, +}; + +fn authority_ref() -> AuthorityDomainRef { + AuthorityDomainRef::new( + OriginId::from_bytes([0x10; 32]), + AuthorityDomainId::from_bytes([0x20; 32]), + ) +} + +#[test] +fn crate_root_exports_braid_lifecycle_error_and_member_types() -> Result<(), BraidError> { + let member_ref = BraidMemberRef::Revealed(make_strand_id("public-member")); + let mut braid = Braid::new([0xAB; 32], authority_ref()); + + assert_eq!(braid.status(), BraidStatus::Active); + braid.apply(BraidEvent::MemberWoven { + member_ref, + sequence_num: 0, + })?; + assert_eq!(braid.frontier(), &[member_ref]); + + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref, + sequence_num: 1, + }), + Err(BraidError::DuplicateMember { member_ref }) + ); + Ok(()) +} diff --git a/crates/warp-core/tests/causal_wal_hardening_tests.rs b/crates/warp-core/tests/causal_wal_hardening_tests.rs index d548f46d..cb55d3ba 100644 --- a/crates/warp-core/tests/causal_wal_hardening_tests.rs +++ b/crates/warp-core/tests/causal_wal_hardening_tests.rs @@ -2,6 +2,12 @@ // © James Ross Ω FLYING•ROBOTS //! Adversarial causal WAL hardening tests. +#![allow( + clippy::needless_continue, + clippy::panic, + clippy::unnecessary_debug_formatting +)] + use warp_core::causal_wal::{ apply_committed_transaction, audit_wal_release_readiness, build_materialization_outbox_transaction, build_retained_reading_transaction, diff --git a/crates/warp-core/tests/causal_wal_tests.rs b/crates/warp-core/tests/causal_wal_tests.rs index 35d68487..d6ce4e38 100644 --- a/crates/warp-core/tests/causal_wal_tests.rs +++ b/crates/warp-core/tests/causal_wal_tests.rs @@ -2,6 +2,13 @@ // © James Ross Ω FLYING•ROBOTS //! Causal WAL foundation tests. +#![allow( + clippy::match_wild_err_arm, + clippy::needless_continue, + clippy::panic, + clippy::unnecessary_debug_formatting +)] + use warp_core::causal_wal::{ apply_committed_transaction, audit_wal_release_readiness, build_checkpoint_publication_transaction, build_materialization_outbox_transaction, diff --git a/crates/warp-core/tests/external_consumer_contract_fixture_tests.rs b/crates/warp-core/tests/external_consumer_contract_fixture_tests.rs index 87571234..d553bdb6 100644 --- a/crates/warp-core/tests/external_consumer_contract_fixture_tests.rs +++ b/crates/warp-core/tests/external_consumer_contract_fixture_tests.rs @@ -308,7 +308,7 @@ fn serious_external_consumer_fixture_proves_hosted_contract_path() { let (runtime, worldline_id) = runtime(); let mut host = TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("external package should install"); let (submission_a, submission_b) = { diff --git a/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs b/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs index 726fedd8..3f39b485 100644 --- a/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs +++ b/crates/warp-core/tests/installed_contract_intent_pipeline_tests.rs @@ -280,10 +280,10 @@ fn query_observer_plan() -> AuthoredObserverPlan { } } -fn install_contract(engine: &mut Engine) { +fn register_contract(engine: &mut Engine) { static REGISTRY: StaticRegistry = StaticRegistry; engine - .install_contract_package(warp_core::InstalledContractPackage { + .register_contract_package(warp_core::InstalledContractPackage { identity: package_identity(), registry: ®ISTRY, verification_policy: verification_policy(), @@ -367,7 +367,7 @@ fn ticketed_authority() -> TicketedRuntimeIngressAuthority { fn pipeline_runtime() -> (WorldlineRuntime, Engine, WorldlineId, WriterHeadKey) { let mut runtime = WorldlineRuntime::new(); let mut engine = empty_engine(); - install_contract(&mut engine); + register_contract(&mut engine); let worldline_id = WorldlineId::from_bytes([1; 32]); runtime .register_worldline(worldline_id, WorldlineState::empty()) diff --git a/crates/warp-core/tests/installed_contract_registry_tests.rs b/crates/warp-core/tests/installed_contract_registry_tests.rs index 5da938b4..d951e884 100644 --- a/crates/warp-core/tests/installed_contract_registry_tests.rs +++ b/crates/warp-core/tests/installed_contract_registry_tests.rs @@ -275,7 +275,7 @@ fn query_request(worldline_id: WorldlineId, query_id: u32) -> Result Result<(), String> { let mut engine = engine(); let record = engine - .install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + .register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) .map_err(|err| format!("supported package should install: {err:?}"))?; assert_eq!(record.package_name, "toy-counter"); @@ -332,7 +332,7 @@ fn installed_contract_package_binds_supported_mutation_and_query() -> Result<(), fn installed_contract_package_rejects_unknown_mutation_before_registration() -> Result<(), String> { let mut engine = engine(); - let Err(err) = engine.install_contract_package(package_with_ops(UNKNOWN_OP_ID, QUERY_OP_ID)) + let Err(err) = engine.register_contract_package(package_with_ops(UNKNOWN_OP_ID, QUERY_OP_ID)) else { return Err("unknown mutation op id must be rejected before install".to_owned()); }; @@ -360,7 +360,7 @@ fn installed_contract_package_rejects_query_kind_mismatch_before_observer_instal let mut engine = engine(); let Err(err) = - engine.install_contract_package(package_with_ops(MUTATION_OP_ID, MUTATION_OP_ID)) + engine.register_contract_package(package_with_ops(MUTATION_OP_ID, MUTATION_OP_ID)) else { return Err("query observer for mutation op id must be rejected".to_owned()); }; @@ -410,7 +410,7 @@ fn installed_contract_package_rejects_duplicate_mutation_op_id_before_registrati vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("duplicate mutation op id must be rejected".to_owned()); }; @@ -442,7 +442,7 @@ fn installed_contract_package_rejects_duplicate_query_op_id_before_registration( vec![query_observer(QUERY_OP_ID), query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("duplicate query op id must be rejected".to_owned()); }; @@ -478,7 +478,7 @@ fn installed_contract_package_rejects_duplicate_rule_id_without_partial_install( vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("duplicate rule id must be rejected".to_owned()); }; @@ -500,7 +500,7 @@ fn installed_contract_package_rejects_duplicate_rule_id_without_partial_install( ); engine - .install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + .register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) .map_err(|err| format!("failed install must not leave registered rule behind: {err:?}"))?; Ok(()) } @@ -519,7 +519,7 @@ fn installed_contract_package_rejects_rule_operation_mismatch_before_registratio vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("mismatched mutation rule op id must be rejected".to_owned()); }; @@ -553,7 +553,7 @@ fn installed_contract_package_rejects_artifact_verification_without_registration vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("artifact verification mismatch must be rejected".to_owned()); }; @@ -585,7 +585,7 @@ fn installed_contract_package_rejects_helper_api_mismatch_without_registration( vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("helper API mismatch must be rejected".to_owned()); }; @@ -621,7 +621,7 @@ fn installed_contract_package_rejects_missing_join_fn_without_registration() -> vec![query_observer(QUERY_OP_ID)], ); - let Err(err) = engine.install_contract_package(package) else { + let Err(err) = engine.register_contract_package(package) else { return Err("join policy without join fn must be rejected".to_owned()); }; @@ -641,10 +641,10 @@ fn installed_contract_package_rejects_missing_join_fn_without_registration() -> fn installed_contract_package_rejects_duplicate_package() -> Result<(), String> { let mut engine = engine(); let record = engine - .install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + .register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) .map_err(|err| format!("initial package install failed: {err:?}"))?; - let Err(err) = engine.install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + let Err(err) = engine.register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) else { return Err("duplicate package id must be rejected".to_owned()); }; @@ -661,12 +661,12 @@ fn installed_contract_package_rejects_duplicate_package() -> Result<(), String> fn installed_contract_package_rejects_duplicate_installed_operation_ids() -> Result<(), String> { let mut engine = engine(); engine - .install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + .register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) .map_err(|err| format!("initial package install failed: {err:?}"))?; let mut identity = package_identity(); identity.package_version = "0.2.0"; - let Err(err) = engine.install_contract_package(package_with_identity_ops( + let Err(err) = engine.register_contract_package(package_with_identity_ops( identity, MUTATION_OP_ID, QUERY_OP_ID, @@ -687,12 +687,12 @@ fn installed_contract_package_rejects_duplicate_installed_operation_ids() -> Res fn installed_contract_package_rejects_duplicate_installed_query_id() -> Result<(), String> { let mut engine = engine(); engine - .install_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) + .register_contract_package(package_with_ops(MUTATION_OP_ID, QUERY_OP_ID)) .map_err(|err| format!("initial package install failed: {err:?}"))?; let mut identity = package_identity(); identity.package_version = "0.2.0"; - let Err(err) = engine.install_contract_package(package_with_identity_ops( + let Err(err) = engine.register_contract_package(package_with_identity_ops( identity, SECOND_MUTATION_OP_ID, QUERY_OP_ID, diff --git a/crates/warp-core/tests/strand_contract_tests.rs b/crates/warp-core/tests/strand_contract_tests.rs index 7c30b150..c845da15 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, DynamicPosture, + 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,8 @@ fn make_test_strand( child_worldline_id: child_worldline, writer_heads: vec![head_key], support_pins: Vec::new(), + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, } } @@ -405,6 +429,8 @@ 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(), + _marker: std::marker::PhantomData::, }; let report = strand @@ -476,6 +502,8 @@ 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(), + _marker: std::marker::PhantomData::, }; let report = strand @@ -582,6 +610,8 @@ 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(), + _marker: std::marker::PhantomData, }; let err = registry.insert(strand).expect_err("INV-S8 should reject"); assert!( @@ -590,6 +620,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 +742,8 @@ fn registry_insert_rejects_duplicate_support_target() { state_hash: [2; 32], }, ], + retention_posture: test_retention_posture(), + _marker: std::marker::PhantomData, }; let err = registry .insert(owner) @@ -1008,3 +1057,20 @@ fn drop_receipt_carries_correct_fields() { assert_eq!(receipt.child_worldline_id, wl(2)); assert_eq!(receipt.final_tick, wt(10)); } + +#[test] +fn test_try_into_shared_posture_mismatch() { + let strand = make_test_strand("mismatch-test", wl(1), wl(2), wt(5)); + assert_eq!( + strand.retention_posture.causal_posture, + CausalPosture::AuthorOnly + ); + let result = strand.try_into_shared(); + assert!(matches!( + result, + Err(StrandError::Posture(PostureObstruction::PostureMismatch { + actual: CausalPosture::AuthorOnly, + expected: CausalPosture::Shared, + })) + )); +} diff --git a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs index 667a677d..21bc87e0 100644 --- a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs +++ b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs @@ -323,7 +323,7 @@ fn reference_host_loop_keeps_tick_authority_out_of_app_surface() { let (runtime, worldline_id) = runtime(); let mut host = TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let envelope = eint_envelope(worldline_id); @@ -603,7 +603,7 @@ fn runtime_wal_ack_tick_commits_receipt_transaction_before_outcome_is_observed() TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); host.enable_in_memory_runtime_wal() .expect("runtime WAL should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let envelope = eint_envelope(worldline_id); @@ -676,7 +676,7 @@ fn runtime_wal_ack_tick_failure_rolls_back_visible_outcome() { TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); host.enable_in_memory_runtime_wal() .expect("runtime WAL should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let submission = { @@ -726,7 +726,7 @@ fn runtime_wal_ack_multi_head_tick_failure_rolls_back_all_tick_records() { TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); host.enable_in_memory_runtime_wal() .expect("runtime WAL should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let submission_a = { @@ -792,7 +792,7 @@ fn runtime_wal_ack_recover_read_only_rebuilds_submission_and_receipt_indexes() { TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); host.enable_in_memory_runtime_wal() .expect("runtime WAL should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let submission = { @@ -840,7 +840,7 @@ fn runtime_wal_ack_recover_read_only_exposes_recovery_certificate() { TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); host.enable_in_memory_runtime_wal() .expect("runtime WAL should initialize"); - host.install_contract_package(package()) + host.register_contract_package(package()) .expect("host should install package"); let submission = { 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..b80c33fa 100644 --- a/crates/warp-wasm/src/warp_kernel.rs +++ b/crates/warp-wasm/src/warp_kernel.rs @@ -423,6 +423,16 @@ 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" + ), + }, + SettlementError::StaleStrandHandle { strand_id } => AbiError { + code: error_codes::INVALID_STRAND, + message: format!("stale strand handle: {strand_id:?}"), + }, _ => AbiError { code: error_codes::ENGINE_ERROR, message: err.to_string(), @@ -1187,13 +1197,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 +1221,48 @@ 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 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, @@ -1409,6 +1464,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); @@ -1477,6 +1545,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture: retention_posture.clone(), + _marker: std::marker::PhantomData, }) .unwrap(); @@ -1538,6 +1608,8 @@ mod tests { child_worldline_id: child_worldline, writer_heads: vec![child_head], support_pins: Vec::new(), + retention_posture, + _marker: std::marker::PhantomData, }) .unwrap(); ( @@ -2293,6 +2365,33 @@ mod tests { assert!(result.appended_conflicts.is_empty()); } + #[test] + fn settlement_compare_inspects_author_only_while_plan_settle_reject() { + let mut kernel = WarpKernel::new().unwrap(); + let (runtime, provenance, strand_id, base_worldline, child_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 delta = kernel.compare_settlement(request.clone()).unwrap(); + assert_eq!(delta.source_worldline_id, abi_worldline_id(child_worldline)); + assert_eq!(delta.source_entries.len(), 1); + + 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(); diff --git a/docs/design/0028-strand-typestates-proof-envelopes-and-evolving-braids/design.md b/docs/design/0028-strand-typestates-proof-envelopes-and-evolving-braids/design.md new file mode 100644 index 00000000..2830598d --- /dev/null +++ b/docs/design/0028-strand-typestates-proof-envelopes-and-evolving-braids/design.md @@ -0,0 +1,295 @@ + + + +# 0028 — Strand Typestates, Blinded References, Proof Envelopes, and Evolving Braids + +_Close the remaining gaps in the warp-core specs for AION Paper VIII / Continuum: carry causal posture through Strand typestates while keeping runtime posture validation authoritative, blind member identities in braid shells for unlinkable verification, wrap ZK/Verkle proofs in explicit verification envelopes, and implement the event log folder for evolving braids._ + +Legend: `PLATFORM` + +Status: **draft / in-review** + +> Typestates narrow the public settlement path, but the live registry and runtime posture checks remain the final admission law. Combined with proof-shaped claims and blinded references, the braid shell becomes an auditable privacy boundary. — review verdict + +## Doctrine + +AIΩN Paper VIII (Continuum): + +- **Prop 5.1 (Typestate Partitioning)** — Causal posture transitions (e.g. `Scratch` → `AuthorOnly` → `Shared`) form a one-way lattice. Executions or operations requesting global settlement must carry `Shared` posture evidence and revalidate runtime state so no stale local context leaks. +- **§3.4 (Zero-Knowledge Braid Boundaries)** — To maintain participant privacy and prevent linkability across independent braids, membership reference identities in public braid shells must be sealable. Verifiers should check the validity of a braid's members using blinded domain-separated commitments. +- **§6.2 (Verkle/ZK Envelopes)** — Any braid shell carrying zero-knowledge or Verkle-style evidence must bind that evidence through an explicit `ProofEnvelope`. The current implementation validates replay-trace envelope shape and public-input binding; zero-knowledge and vector-opening proof kinds are reserved until verifier backends exist. + +## Current state + +All four key gaps from the Echo codebase gap analysis now have current E1 surfaces, tests, and explicit limits: + +1. **Strand Typestates (`revelation.rs`, `strand.rs`):** + - Parameterized `Strand` so ordinary APIs carry posture intent at the type level while preserving runtime posture as the authoritative admission fact. + - Built infallible `into_dynamic(self)` and fallible `try_into_shared(self)` conversions. + - Exposed `Shared`-only `plan` and `settle` conveniences that re-enter the live registry path so stale, forged, or hand-built handles cannot bypass runtime posture and support validation. +2. **Blinded Member References (`braid_shell.rs`):** + - Refactored `BraidShellMember` to store a `BraidMemberRef` instead of a plain `StrandId`. + - `BraidMemberRef` supports `Revealed(StrandId)` and `Sealed { blinded_commitment, authority }` variants. + - Sealed variants commit to the `StrandId`, child worldline, and caller-supplied non-public blinding material using a domain-separated `blake3` commitment. +3. **ZK/Verkle Proof Envelopes (`proof.rs`, `braid_shell.rs`):** + - Defined `ProofKind` (`ZkSnark`, `ReplayTrace`, `VectorOpening`), `ProofEnvelope`, and `ObserverHonestyClaim`. + - Added `BraidShell::assemble_with_proof` to attach replay-trace evidence envelopes, validate shape/public-input binding, reject cryptographic proof kinds without verifier backends, and bind the proof envelope digest into shell identity. +4. **Evolving Braid Logs (`braid.rs`):** + - Created `BraidEvent` representing state transition logs (`BraidCreated`, `MemberWoven`, `SettlementFinalized`, `BraidCollapsed`). + - Implemented checked incremental application and event folding with lifecycle, duplicate-member, sequence overflow, and collapse-witness checks. + +--- + +## Technical Specifications + +### 1. Causal Posture Typestates + +We define the typestate traits and marker structs to represent the four causal posture states: + +```rust +mod posture_state_seal { + pub trait Sealed {} +} + +pub trait CausalPostureState: + Clone + std::fmt::Debug + PartialEq + Eq + posture_state_seal::Sealed +{ + fn causal_posture() -> Option; +} + +pub struct Shared; +pub struct AuthorOnly; +pub struct Scratch; +pub struct DynamicPosture; + +impl posture_state_seal::Sealed for Shared {} +impl CausalPostureState for Shared { + fn causal_posture() -> Option { + Some(CausalPosture::Shared) + } +} + +impl posture_state_seal::Sealed for AuthorOnly {} +impl CausalPostureState for AuthorOnly { + fn causal_posture() -> Option { + Some(CausalPosture::AuthorOnly) + } +} + +impl posture_state_seal::Sealed for Scratch {} +impl CausalPostureState for Scratch { + fn causal_posture() -> Option { + Some(CausalPosture::Scratch) + } +} + +impl posture_state_seal::Sealed for DynamicPosture {} +impl CausalPostureState for DynamicPosture { + fn causal_posture() -> Option { + None + } +} +``` + +The `Strand` struct is parameterized with `P: CausalPostureState`, defaulting to `DynamicPosture` to maintain backwards compatibility inside the `StrandRegistry` (which uses `BTreeMap>`): + +```rust +pub struct Strand { + pub strand_id: StrandId, + pub fork_basis_ref: ForkBasisRef, + pub child_worldline_id: WorldlineId, + pub writer_heads: Vec, + pub support_pins: Vec, + pub retention_posture: RetentionPosture, + pub _marker: std::marker::PhantomData

, +} +``` + +The `Shared`-only convenience methods narrow normal callers into the checked settlement path; they are not the sole authority. The live registry and runtime posture checks remain the final admission gate: + +```rust +impl Strand { + pub fn plan(&self, ...) -> Result { + SettlementService::plan(runtime, provenance, self.strand_id) + } + + pub fn settle(&self, ...) -> Result { + SettlementService::settle(runtime, provenance, self.strand_id) + } +} +``` + +--- + +### 2. Blinded Member References + +Member references inside the public `BraidShell` can be sealed to protect user/strand identity: + +```rust +pub enum BraidMemberRef { + Revealed(StrandId), + Sealed { + blinded_commitment: Hash, + authority: AuthorityDomainRef, + }, +} + +impl BraidMemberRef { + pub fn seal( + strand_id: StrandId, + child_worldline_id: WorldlineId, + blinding_secret: Hash, + ) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(SEALED_MEMBER_DOMAIN); + hasher.update(&blinding_secret); + hasher.update(child_worldline_id.as_bytes()); + hasher.update(strand_id.as_bytes()); + hasher.finalize().into() + } + + pub fn matches_strand( + &self, + strand_id: &StrandId, + child_worldline_id: &WorldlineId, + authority: &AuthorityDomainRef, + blinding_secret: &Hash, + ) -> bool { + match self { + Self::Revealed(_) => false, + Self::Sealed { + blinded_commitment, + authority: member_authority, + } => { + let expected = Self::seal(*strand_id, *child_worldline_id, *blinding_secret); + member_authority == authority && *blinded_commitment == expected + } + } + } +} +``` + +--- + +### 3. Proof-Shaped Envelopes + +A `ProofEnvelope` contains proof-shaped evidence bytes and the public-input +hash they claim to bind. `ObserverHonestyClaim` is a separate assertion type; +`validate_shape` admits replay-trace evidence only and rejects +`ZkSnark`/`VectorOpening` envelopes until real verifier backends exist. It does +not perform cryptographic proof verification; only envelope structure and +public-input hash binding are validated. + +```rust +pub enum ProofKind { + ZkSnark, + ReplayTrace, + VectorOpening, +} + +pub struct ProofEnvelope { + pub kind: ProofKind, + pub proof_bytes: Vec, + pub public_inputs_hash: Hash, +} + +pub struct ObserverHonestyClaim { + pub coordinate: BraidCoordinate, + pub shell_digest: Hash, + pub observer_domain: AuthorityDomainRef, +} +``` + +Replay-trace shape validation and proof-envelope digest binding occur during +shell assembly. Proof cryptographic validity is not verified; only envelope +shape and public-input binding are validated: + +```rust +impl BraidShell { + pub fn assemble_with_proof( + worldline_id: WorldlineId, + basis: ProvenanceRef, + mut members: Vec, + policy_id: Hash, + mut outcome: BraidShellOutcome, + posture: CausalPosture, + proof: Option, + ) -> Result { + // ... sorting and validation of members and posture ... + if let Some(ref p) = proof { + if let Err(err) = p.validate_shape(witness_digest) { + return Err(BraidShellError::ProofShapeValidationFailed { reason: err }); + } + } + let proof_digest = proof.as_ref().map(crate::proof::ProofEnvelope::digest); + // ... computes shell digest with proof_digest and returns Self ... + } +} +``` + +--- + +### 4. Evolving Braid Logs + +Evolving braids transition through discrete events folded sequentially: + +```rust +pub enum BraidStatus { + Active, + Finalized, + Collapsed, +} + +pub enum BraidError { + EmptyLog, + MissingCreated, + DuplicateCreated, + IncoherentSequence { + expected: u64, + actual: u64, + }, + InvalidTransition { + action: String, + status: BraidStatus, + }, + SequenceOverflow { + sequence_num: u64, + }, + DuplicateMember { + member_ref: BraidMemberRef, + }, + MixedMemberReferencePosture, + EmptySettlementFrontier, + EmptyCollapseWitness, +} + +pub enum BraidEvent { + BraidCreated { + braid_id: Hash, + creator_domain: AuthorityDomainRef, + }, + MemberWoven { + member_ref: BraidMemberRef, + sequence_num: u64, + }, + SettlementFinalized { + settlement_digest: Hash, + }, + BraidCollapsed { + collapse_witness: Hash, + outcome_digest: Hash, + }, +} + +pub struct Braid { + id: Hash, + events: Vec, + members: Vec, + member_index: BTreeSet, + next_sequence_num: u64, + latest_settlement: Option, + status: BraidStatus, +} +``` + +Checked `apply` and `fold` preserve these invariants: duplicate creation is rejected, member sequence numbers must match the expected cursor, duplicate members are refused through a deterministic membership index, revealed and sealed member references may not mix, empty member frontiers may not finalize, sequence overflow is explicit, settlement/collapse lifecycle order is enforced, and collapse witnesses must clear the `WitnessDigest` quality bar. diff --git a/docs/design/reference-trusted-runtime-host-loop.md b/docs/design/reference-trusted-runtime-host-loop.md index 934f1203..d8f17e6d 100644 --- a/docs/design/reference-trusted-runtime-host-loop.md +++ b/docs/design/reference-trusted-runtime-host-loop.md @@ -18,14 +18,14 @@ The causal status of Start, Stop, cadence, and drain commands is defined in Application code can submit canonical intent material and observe outcomes or readings through an app-facing handle, while the trusted host owns: -- generated package installation; +- generated package registration; - ticketed runtime ingress staging; - scheduler-owned tick passes; - until-idle policy; - query service access; - future trusted fault recovery. -The app-facing handle exposes no package installation, no ticketed ingress +The app-facing handle exposes no package registration, no ticketed ingress staging, no `super_tick`, no scheduler pass, and no fault recovery authority. ## Implemented Surface @@ -35,7 +35,7 @@ staging, no `super_tick`, no scheduler pass, and no fault recovery authority. - `TrustedRuntimeHost`, gated behind the trusted runtime and native bootstrap features; - `TrustedRuntimeApp`, the app-facing submit/observe/query handle; -- `TrustedRuntimeHost::install_contract_package(...)`; +- `TrustedRuntimeHost::register_contract_package(...)`; - `TrustedRuntimeHost::stage_installed_contract_submission(...)`; - `TrustedRuntimeHost::tick_once(...)`; - `TrustedRuntimeHost::run_until_idle(...)`; @@ -54,7 +54,7 @@ application -> witnessed submission handle trusted runtime host --> installs package +-> registers package -> stages ticketed ingress -> runs scheduler-owned ticks -> app observes outcome or bounded query reading @@ -77,7 +77,7 @@ scheduler-owned tick execution decide it. - `cargo test -p warp-core --features "native_rule_bootstrap trusted_runtime" --test trusted_runtime_host_loop_tests` -The witness installs a generated-style package, submits through the app handle, +The witness registers a generated-style package, submits through the app handle, stages ticketed ingress through the host, runs until idle, observes an applied intent outcome, and queries through the read-only observer service with package evidence. diff --git a/docs/quickstart-local-contract-host.md b/docs/quickstart-local-contract-host.md index c65cda7d..8e655452 100644 --- a/docs/quickstart-local-contract-host.md +++ b/docs/quickstart-local-contract-host.md @@ -36,7 +36,7 @@ The dry run prints the exact Cargo targets. The real run executes: The witness covers the current v0.1.0 local contract path: -1. Install a generated-style package through the package boundary. +1. Register a generated-style package through the package boundary. 2. Submit canonical EINT bytes through an app-facing handle. 3. Keep the submission pending until trusted runtime-owned staging. 4. Stage ticketed runtime ingress through the trusted host. @@ -59,7 +59,7 @@ let reading = app.observe(query_request)?; The app surface does not expose: -- package installation; +- package registration; - ticketed runtime ingress staging; - `super_tick`; - scheduler pass or run-until-idle control; @@ -70,7 +70,7 @@ The app surface does not expose: The trusted runtime owner uses the host surface: ```rust -host.install_contract_package(package)?; +host.register_contract_package(package)?; host.stage_installed_contract_submission(submission.submission_id, &ticket)?; host.run_until_idle(4)?; ``` @@ -80,7 +80,7 @@ logical ticks are Echo semantic history. ## Compatibility Boundary -Generated packages must fit the runtime before they install. The package +Generated packages must fit the runtime before they register. The package boundary verifies: - Echo contract ABI version; @@ -91,8 +91,8 @@ boundary verifies: - schema hash; - footprint certificate identity. -Unsupported compatibility fails closed at package install. It does not become -runtime-visible work or an accepted read. +Unsupported compatibility fails closed at package registration. It does not +become runtime-visible work or an accepted read. ## Retention Boundary diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c3a77726..0970ac10 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -6525,10 +6525,12 @@ mod tests { } #[test] - fn runtime_wal_ack_stale_claims_stay_current() { + fn runtime_wal_ack_stale_claims_stay_current() -> Result<(), Box> { let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() - .expect("xtask crate should live under repository root"); + .ok_or_else(|| { + std::io::Error::other("xtask crate should live under repository root") + })?; let checked_docs = [ "docs/BEARING.md", "docs/design/v0.1.0-jedit-release-gate.md", @@ -6545,26 +6547,27 @@ mod tests { for relative_path in checked_docs { let path = repo_root.join(relative_path); - let content = fs::read_to_string(&path) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); + let content = fs::read_to_string(&path).map_err(|err| { + std::io::Error::new( + err.kind(), + format!("failed to read {}: {err}", path.display()), + ) + })?; for stale_claim in stale_claims { assert!( !content.contains(stale_claim), - "{} contains stale runtime WAL ACK claim `{}`", - relative_path, - stale_claim + "{relative_path} contains stale runtime WAL ACK claim `{stale_claim}`", ); } } - let bearing = fs::read_to_string(repo_root.join("docs/BEARING.md")) - .expect("BEARING should be readable"); + let bearing = fs::read_to_string(repo_root.join("docs/BEARING.md"))?; assert!(bearing.contains("Runtime ACK drift gate")); assert!(bearing.contains("cargo xtask test-slice runtime-wal-ack")); - let workflows = fs::read_to_string(repo_root.join("docs/workflows.md")) - .expect("workflow docs should be readable"); + let workflows = fs::read_to_string(repo_root.join("docs/workflows.md"))?; assert!(workflows.contains("cargo xtask test-slice runtime-wal-ack")); + Ok(()) } #[test]