diff --git a/CHANGELOG.md b/CHANGELOG.md index 3258c586..ecdb0056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ### Added +- `warp-core` trusted runtime hosts now configure runtime WAL through + `TrustedRuntimeWalConfig`, with `TrustedRuntimeWalStoreKind` exposing the + configured adapter kind as host-owned read-only evidence while + `TrustedRuntimeApp` remains limited to submit and observe surfaces. - Added an Echo 1.0 release contract that records the four binary release gates, compatibility policy, evidence requirements, and GitHub Project boundary without carrying live roadmap state in the repository. diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index a89c73fb..3072d695 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -390,7 +390,7 @@ pub use tick_patch::{ #[cfg(all(feature = "native_rule_bootstrap", feature = "trusted_runtime"))] pub use trusted_runtime_host::{ TrustedRuntimeApp, TrustedRuntimeHost, TrustedRuntimeHostError, TrustedRuntimeHostRunReport, - TrustedRuntimeWal, TrustedRuntimeWalError, + TrustedRuntimeWal, TrustedRuntimeWalConfig, TrustedRuntimeWalError, TrustedRuntimeWalStoreKind, }; pub use tx::TxId; pub use warp_state::{WarpInstance, WarpState}; diff --git a/crates/warp-core/src/trusted_runtime_host.rs b/crates/warp-core/src/trusted_runtime_host.rs index ba99e97d..4b8cf4b5 100644 --- a/crates/warp-core/src/trusted_runtime_host.rs +++ b/crates/warp-core/src/trusted_runtime_host.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::{ causal_wal::{ build_recovery_certificate, build_submission_acceptance_transaction, - build_tick_transaction, recover_in_memory_store, recover_receipt_index, + build_tick_transaction, recover_from_frames_and_commits, recover_receipt_index, recover_submission_index, recovered_submission_receipt_index_root, AffectedFrontier, AffectedFrontierKind, InMemoryWalStore, Lsn, PayloadCodecId, PayloadSchemaId, RecoveredReceiptIndex, RecoveredSubmissionIndex, RecoveryAccessMode, RecoveryCertificate, @@ -118,6 +118,45 @@ pub struct TrustedRuntimeWalRecovery { pub receipts: RecoveredReceiptIndex, } +/// Store kind configured for the trusted runtime WAL adapter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum TrustedRuntimeWalStoreKind { + /// Deterministic process-local store used by fast tests. + InMemory, +} + +/// Host-owned runtime WAL adapter configuration. +#[derive(Clone, Debug)] +pub struct TrustedRuntimeWalConfig { + store: TrustedRuntimeWalStore, + next_lsn: Lsn, +} + +impl TrustedRuntimeWalConfig { + /// Builds the deterministic in-memory runtime WAL configuration. + #[must_use] + pub fn in_memory() -> Self { + Self { + store: TrustedRuntimeWalStore::in_memory(), + next_lsn: Lsn::from_raw(0), + } + } + + /// Returns the configured store kind as read-only evidence. + #[must_use] + pub fn store_kind(&self) -> TrustedRuntimeWalStoreKind { + self.store.kind() + } + + /// Returns this configuration with a caller-supplied next LSN. + #[must_use] + pub fn with_next_lsn(mut self, next_lsn: Lsn) -> Self { + self.next_lsn = next_lsn; + self + } +} + /// Local trusted runtime host for the app-safe contract-host path. /// /// Application code should receive [`TrustedRuntimeApp`], not this type. This @@ -195,7 +234,20 @@ impl TrustedRuntimeHost { /// /// Returns a WAL error when the writer epoch cannot be acquired. pub fn enable_in_memory_runtime_wal(&mut self) -> Result<(), TrustedRuntimeHostError> { - self.runtime_wal = Some(TrustedRuntimeWal::new_in_memory()?); + self.enable_runtime_wal(TrustedRuntimeWalConfig::in_memory()) + } + + /// Enables a host-owned runtime WAL adapter configuration. + /// + /// # Errors + /// + /// Returns a WAL error when the configured store cannot acquire the runtime + /// writer epoch. + pub fn enable_runtime_wal( + &mut self, + config: TrustedRuntimeWalConfig, + ) -> Result<(), TrustedRuntimeHostError> { + self.runtime_wal = Some(TrustedRuntimeWal::from_config(config)?); Ok(()) } @@ -357,7 +409,7 @@ impl TrustedRuntimeHost { /// Minimal trusted-runtime WAL adapter for ACK-boundary integration tests. #[derive(Clone, Debug)] pub struct TrustedRuntimeWal { - store: InMemoryWalStore, + store: TrustedRuntimeWalStore, writer_epoch: WriterEpochId, segment_id: WalSegmentId, next_lsn: Lsn, @@ -375,11 +427,19 @@ pub struct TrustedRuntimeWal { impl TrustedRuntimeWal { /// Builds a WAL adapter backed by an in-memory store. pub fn new_in_memory() -> Result { - Self::new_in_memory_at_lsn(Lsn::from_raw(0)) + Self::from_config(TrustedRuntimeWalConfig::in_memory()) } fn new_in_memory_at_lsn(next_lsn: Lsn) -> Result { - let mut store = InMemoryWalStore::new(); + Self::from_config(TrustedRuntimeWalConfig::in_memory().with_next_lsn(next_lsn)) + } + + /// Builds a WAL adapter from host-owned configuration. + pub fn from_config(config: TrustedRuntimeWalConfig) -> Result { + let TrustedRuntimeWalConfig { + mut store, + next_lsn, + } = config; let writer_epoch = WriterEpochId::from_hash(trusted_runtime_wal_digest("writer-epoch")); store.acquire_writer_epoch(WriterEpochRequest { epoch_id: writer_epoch, @@ -414,6 +474,12 @@ impl TrustedRuntimeWal { }) } + /// Returns the configured store kind as read-only evidence. + #[must_use] + pub fn store_kind(&self) -> TrustedRuntimeWalStoreKind { + self.store.kind() + } + /// Returns committed WAL markers recorded by the adapter. #[must_use] pub fn commits(&self) -> Vec { @@ -429,14 +495,13 @@ impl TrustedRuntimeWal { /// Returns a clone of the underlying in-memory store for recovery tests. #[must_use] pub fn cloned_store(&self) -> InMemoryWalStore { - self.store.clone() + self.store.cloned_in_memory_store() } /// Recovers submission and receipt indexes from committed WAL transactions /// without scheduler callbacks. pub fn recover_read_only(&self) -> Result { - let mut store = self.cloned_store(); - let report = recover_in_memory_store(&mut store, RecoveryAccessMode::ReadOnly)?; + let report = recover_runtime_wal_store_read_only(&self.store)?; let submissions = recover_submission_index(&report).map_err(WalRecoveryError::from)?; let receipts = recover_receipt_index(&report).map_err(WalRecoveryError::from)?; Ok(TrustedRuntimeWalRecovery { @@ -616,6 +681,131 @@ impl TrustedRuntimeWal { } } +#[derive(Clone, Debug)] +enum TrustedRuntimeWalStore { + InMemory(InMemoryWalStore), +} + +impl TrustedRuntimeWalStore { + fn in_memory() -> Self { + Self::InMemory(InMemoryWalStore::new()) + } + + fn kind(&self) -> TrustedRuntimeWalStoreKind { + match self { + Self::InMemory(_) => TrustedRuntimeWalStoreKind::InMemory, + } + } + + fn append_transaction( + &mut self, + transaction: WalCommittedTransaction, + ) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.append_transaction(transaction), + } + } + + fn append_uncommitted_frame( + &mut self, + epoch_id: WriterEpochId, + frame: crate::causal_wal::WalFrame, + ) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.append_uncommitted_frame(epoch_id, frame), + } + } + + fn cloned_in_memory_store(&self) -> InMemoryWalStore { + match self { + Self::InMemory(store) => store.clone(), + } + } +} + +impl WalStorePort for TrustedRuntimeWalStore { + fn acquire_writer_epoch( + &mut self, + request: WriterEpochRequest, + ) -> Result { + match self { + Self::InMemory(store) => store.acquire_writer_epoch(request), + } + } + + fn append_frame( + &mut self, + epoch_id: WriterEpochId, + frame: crate::causal_wal::WalFrame, + ) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.append_frame(epoch_id, frame), + } + } + + fn flush_commit( + &mut self, + epoch_id: WriterEpochId, + commit: WalTransactionCommit, + ) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.flush_commit(epoch_id, commit), + } + } + + fn read_frames(&self) -> Vec { + match self { + Self::InMemory(store) => store.read_frames(), + } + } + + fn read_commits(&self) -> Vec { + match self { + Self::InMemory(store) => store.read_commits(), + } + } + + fn seal_segment( + &mut self, + epoch_id: WriterEpochId, + segment_id: WalSegmentId, + ) -> Result { + match self { + Self::InMemory(store) => store.seal_segment(epoch_id, segment_id), + } + } + + fn truncate_tail_after(&mut self, after_lsn: Lsn) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.truncate_tail_after(after_lsn), + } + } + + fn publish_manifest( + &mut self, + epoch_id: WriterEpochId, + manifest: crate::causal_wal::WalManifest, + ) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.publish_manifest(epoch_id, manifest), + } + } + + fn close_epoch(&mut self, epoch_id: WriterEpochId) -> Result<(), WalStoreError> { + match self { + Self::InMemory(store) => store.close_epoch(epoch_id), + } + } +} + +fn recover_runtime_wal_store_read_only( + store: &impl WalStorePort, +) -> Result { + let frames = store.read_frames(); + let commits = store.read_commits(); + recover_from_frames_and_commits(&frames, &commits, RecoveryAccessMode::ReadOnly) +} + #[cfg(any(test, feature = "host_test"))] impl TrustedRuntimeWal { /// Builds an in-memory WAL at a caller-supplied LSN for overflow tests. 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 21bc87e0..bccce302 100644 --- a/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs +++ b/crates/warp-core/tests/trusted_runtime_host_loop_tests.rs @@ -21,9 +21,9 @@ use warp_core::{ ObservationCoordinate, ObservationFrame, ObservationPayload, ObservationProjection, ObservationReadBudget, ObservationRequest, ObserverPlanId, OpticAdmissionTicket, OpticArtifactHandle, PatternGraph, PlaybackMode, SchedulerKind, TickDelta, TrustedRuntimeHost, - TrustedRuntimeHostError, TrustedRuntimeWal, TrustedRuntimeWalError, WarpOp, WorldlineId, - WorldlineRuntime, WorldlineState, WriterHead, WriterHeadKey, OPTIC_ADMISSION_TICKET_KIND, - OPTIC_ARTIFACT_HANDLE_KIND, + TrustedRuntimeHostError, TrustedRuntimeWal, TrustedRuntimeWalConfig, TrustedRuntimeWalError, + TrustedRuntimeWalStoreKind, WarpOp, WorldlineId, WorldlineRuntime, WorldlineState, WriterHead, + WriterHeadKey, OPTIC_ADMISSION_TICKET_KIND, OPTIC_ARTIFACT_HANDLE_KIND, }; const SCHEMA_SHA256_HEX: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; @@ -427,6 +427,40 @@ fn runtime_wal_ack_submit_commits_acceptance_before_returning_handle() { assert_eq!(entry.posture, RecoveredSubmissionPosture::AcceptedPending); } +#[test] +fn runtime_wal_ack_adapter_is_configured_by_trusted_host_boundary() { + let (runtime, worldline_id) = runtime(); + let mut host = + TrustedRuntimeHost::new(runtime, empty_engine()).expect("trusted host should initialize"); + + host.enable_runtime_wal(TrustedRuntimeWalConfig::in_memory()) + .expect("host should own runtime WAL adapter configuration"); + assert_eq!( + host.runtime_wal() + .expect("runtime WAL should be configured") + .store_kind(), + TrustedRuntimeWalStoreKind::InMemory + ); + + let submission = { + let mut app = host.app(); + app.submit_intent_with_runtime_wal_ack(eint_envelope(worldline_id)) + .expect("app should only see ACK submission surface") + }; + + assert_eq!( + host.runtime_wal() + .expect("runtime WAL should remain host-owned") + .recover_read_only() + .expect("runtime WAL should recover through adapter boundary") + .submissions + .get(&submission.submission_id) + .expect("submission should recover") + .posture, + RecoveredSubmissionPosture::AcceptedPending + ); +} + #[test] fn runtime_wal_ack_duplicate_submit_does_not_append_second_acceptance() { let (runtime, worldline_id) = runtime(); diff --git a/docs/design/causal-wal-hardening-matrix.md b/docs/design/causal-wal-hardening-matrix.md index f19be9c3..fc9b70c3 100644 --- a/docs/design/causal-wal-hardening-matrix.md +++ b/docs/design/causal-wal-hardening-matrix.md @@ -910,11 +910,13 @@ boundary without giving the application append authority. Acceptance criteria: - `TrustedRuntimeHost` owns the runtime WAL adapter. +- `TrustedRuntimeHost` configures the adapter through `TrustedRuntimeWalConfig`. - `TrustedRuntimeApp` exposes no WAL append or tick authority. - The adapter can be inspected by host tests as read-only evidence. Test plan: +- `runtime_wal_ack_adapter_is_configured_by_trusted_host_boundary` - `runtime_wal_ack_submit_commits_acceptance_before_returning_handle` ## Slice 87: Submission Acceptance Transaction Wiring diff --git a/docs/design/reference-trusted-runtime-host-loop.md b/docs/design/reference-trusted-runtime-host-loop.md index d8f17e6d..8a6b4eb3 100644 --- a/docs/design/reference-trusted-runtime-host-loop.md +++ b/docs/design/reference-trusted-runtime-host-loop.md @@ -39,6 +39,10 @@ staging, no `super_tick`, no scheduler pass, and no fault recovery authority. - `TrustedRuntimeHost::stage_installed_contract_submission(...)`; - `TrustedRuntimeHost::tick_once(...)`; - `TrustedRuntimeHost::run_until_idle(...)`; +- `TrustedRuntimeWalConfig`, which the trusted host uses to configure the + runtime WAL adapter before app-facing ACK submission; +- `TrustedRuntimeWalStoreKind`, which exposes the configured store kind as + host-owned read-only evidence; - `TrustedRuntimeHostRunReport`, which records scheduler passes and committed step count. @@ -54,6 +58,7 @@ application -> witnessed submission handle trusted runtime host +-> configures runtime WAL adapter -> registers package -> stages ticketed ingress -> runs scheduler-owned ticks @@ -64,6 +69,11 @@ The host loop does not make application dispatch synchronous. A submitted intent remains pending until trusted runtime-owned ingress staging and scheduler-owned tick execution decide it. +The runtime WAL adapter is configured through the trusted host, not through +`TrustedRuntimeApp`. The app-facing handle can request the WAL-backed ACK path, +but it never receives WAL append, flush, truncate, manifest, tick, or recovery +authority. + ## Non-Goals - No production daemon. @@ -76,6 +86,7 @@ scheduler-owned tick execution decide it. ## Evidence - `cargo test -p warp-core --features "native_rule_bootstrap trusted_runtime" --test trusted_runtime_host_loop_tests` +- `cargo test -p warp-core --features "native_rule_bootstrap trusted_runtime host_test" --test trusted_runtime_host_loop_tests runtime_wal_ack` The witness registers a generated-style package, submits through the app handle, stages ticketed ingress through the host, runs until idle, observes an applied