sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit f3008c7bf40e5e116dba6bc7dc4b02a5509ee388
parent 16bdb7f65ec8ff902041d5736e6773e1684a5560
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 11:44:55 +0000

sdk: migrate reliability operations

- delegate backup restore integrity and status to canonical storage contracts
- preserve optimistic staging interruption and explicit finalization semantics
- expose passive diagnostics with only native redacted status values
- verify memory behavior and guard against duplicate or Studio representations

Diffstat:
Mcrates/sdk/src/client.rs | 5+++++
Mcrates/sdk/src/diagnostics.rs | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/sdk/src/storage.rs | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/package_boundary.rs | 37+++++++++++++++++++++++++++++++++++++
4 files changed, 341 insertions(+), 1 deletion(-)

diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -214,6 +214,11 @@ impl Client { Ok(self.inner.storage.as_ref()) } + /// Returns backend-neutral backup, restore, status, and integrity operations. + pub fn storage_operations(&self) -> Result<crate::storage::Operations<'_>> { + Ok(crate::storage::Operations::new(self.storage()?)) + } + /// Returns the injected signer, when outbound authoring is enabled. pub fn signer(&self) -> Result<Option<&dyn Signer>> { self.require_open()?; diff --git a/crates/sdk/src/diagnostics.rs b/crates/sdk/src/diagnostics.rs @@ -1 +1,59 @@ -//! Redacted client diagnostics. +//! Redacted client diagnostics composed from canonical capability and storage status. + +/// One passive diagnostics snapshot containing only lower-owned status types. +#[derive(Clone, Debug)] +pub struct Report { + capabilities: crate::capability::CapabilityReport, + storage: radroots_storage::StorageStatus, +} + +impl Report { + /// Returns the side-effect-free SDK capability report. + #[must_use] + pub const fn capabilities(&self) -> &crate::capability::CapabilityReport { + &self.capabilities + } + + /// Returns the canonical backend status without implementation details. + #[must_use] + pub const fn storage(&self) -> radroots_storage::StorageStatus { + self.storage + } +} + +/// Captures passive, secret-safe diagnostics without filesystem paths, SQL, +/// connection handles, private artifacts, or recovery side effects. +pub async fn inspect(client: &crate::Client) -> crate::Result<Report> { + Ok(Report { + capabilities: client.capabilities(), + storage: client.storage_status().await?, + }) +} + +#[cfg(all(test, feature = "memory"))] +mod tests { + use std::sync::Arc; + + use radroots_storage::{ + event::SourceGeneration, memory::MemoryStorage, status::StorageBackend, + }; + + use crate::ClientBuilder; + + #[tokio::test] + async fn report_is_passive_native_and_redacted() { + let client = ClientBuilder::new() + .storage(Arc::new(MemoryStorage::new( + SourceGeneration::new([9; 32]).expect("generation"), + ))) + .build() + .expect("client"); + let report = super::inspect(&client).await.expect("report"); + + assert_eq!(report.storage().backend(), StorageBackend::Memory); + assert!(report.capabilities().iter().all(|status| { + !status.id().as_str().contains('/') && !status.id().as_str().contains('\\') + })); + assert!(!format!("{report:?}").contains("sqlite")); + } +} diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs @@ -17,3 +17,243 @@ pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode; /// Validated SQLite-owned paths; this contains no backend handle. #[cfg(feature = "sqlite")] pub type SqlitePaths = radroots_storage_sqlite::Paths; + +use radroots_storage::backup::{ + BackupId, BackupOperation, BackupPlan, BackupTransition, ReliabilityRevision, RestoreOperation, + RestorePlan, RestoreTransition, StorageReliability, +}; + +/// Borrowed reliability operations over the canonical backend-neutral SPI. +#[derive(Clone, Copy)] +pub struct Operations<'a> { + storage: &'a dyn radroots_storage::Storage, +} + +impl<'a> Operations<'a> { + pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self { + Self { storage } + } + + /// Begins or resumes one idempotent backup plan. + pub async fn begin_backup( + &self, + plan: BackupPlan, + ) -> Result<BackupOperation, radroots_storage::Error> { + StorageReliability::begin_backup(self.storage, plan).await + } + + /// Applies one optimistic backup transition. + pub async fn transition_backup( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: BackupTransition, + at_unix_ms: u64, + ) -> Result<BackupOperation, radroots_storage::Error> { + StorageReliability::transition_backup( + self.storage, + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + .await + } + + /// Begins or resumes one staged restore plan. + pub async fn begin_restore( + &self, + plan: RestorePlan, + ) -> Result<RestoreOperation, radroots_storage::Error> { + StorageReliability::begin_restore(self.storage, plan).await + } + + /// Applies one optimistic staged restore transition. + pub async fn transition_restore( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: RestoreTransition, + at_unix_ms: u64, + ) -> Result<RestoreOperation, radroots_storage::Error> { + StorageReliability::transition_restore( + self.storage, + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + .await + } + + /// Returns passive backend status without initiating recovery work. + pub async fn status(&self) -> Result<Status, radroots_storage::Error> { + StorageReliability::status(self.storage).await + } + + /// Runs backend-owned integrity inspection. + pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> { + StorageReliability::integrity(self.storage).await + } +} + +impl std::fmt::Debug for Operations<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Operations") + .field("storage", &"<borrowed canonical storage>") + .finish() + } +} + +#[cfg(all(test, feature = "memory"))] +mod tests { + use std::sync::Arc; + + use radroots_storage::{ + backup::{ + BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind, + BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus, + RestoreStage, + }, + event::SourceGeneration, + memory::MemoryStorage, + status::IntegrityHealth, + }; + + use crate::ClientBuilder; + + use super::*; + + fn manifest(backup_id: BackupId) -> BackupManifest { + BackupManifest::new( + BackupFormatVersion::V1, + backup_id, + 1_800_000_000_100, + BackupSecretPolicy::ExcludeProtectedStorage, + vec![ + BackupMember::new( + "runtime/events.bin", + BackupMemberKind::Runtime, + 16, + MemberDigest::new([3; 32]), + ) + .expect("member"), + ], + ) + .expect("manifest") + } + + #[tokio::test] + async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([8; 32]).expect("generation"), + )); + let client = ClientBuilder::new() + .storage(storage) + .build() + .expect("client"); + let operations = client.storage_operations().expect("operations"); + let backup_id = BackupId::new([9; 16]).expect("backup id"); + let plan = BackupPlan::new( + backup_id, + BackupFormatVersion::V1, + BackupSecretPolicy::ExcludeProtectedStorage, + 1_800_000_000_000, + ) + .expect("plan"); + + drop(operations.begin_backup(plan.clone())); + let planned = operations.begin_backup(plan).await.expect("planned"); + assert_eq!(planned.stage(), BackupStage::Planned); + let captured = operations + .transition_backup( + backup_id, + planned.revision(), + BackupTransition::Captured(manifest(backup_id)), + 1_800_000_000_200, + ) + .await + .expect("captured"); + let verified = operations + .transition_backup( + backup_id, + captured.revision(), + BackupTransition::Verified, + 1_800_000_000_300, + ) + .await + .expect("verified"); + let finalized = operations + .transition_backup( + backup_id, + verified.revision(), + BackupTransition::Finalize, + 1_800_000_000_400, + ) + .await + .expect("finalized"); + assert_eq!(finalized.stage(), BackupStage::Finalized); + + let restore_plan = RestorePlan::new( + manifest(backup_id), + BackupSecretPolicy::ExcludeProtectedStorage, + 1_800_000_001_000, + ) + .expect("restore plan"); + let staging = operations + .begin_restore(restore_plan.clone()) + .await + .expect("staging"); + assert_eq!(staging.stage(), RestoreStage::Staging); + let replayed = operations + .begin_restore(restore_plan) + .await + .expect("resume staging"); + assert_eq!(replayed, staging); + let verifying = operations + .transition_restore( + backup_id, + staging.revision(), + RestoreTransition::Staged, + 1_800_000_001_100, + ) + .await + .expect("verifying"); + let finalizing = operations + .transition_restore( + backup_id, + verifying.revision(), + RestoreTransition::Verified(vec![ + RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified) + .expect("member status"), + ]), + 1_800_000_001_200, + ) + .await + .expect("finalizing"); + let restored = operations + .transition_restore( + backup_id, + finalizing.revision(), + RestoreTransition::Finalize, + 1_800_000_001_300, + ) + .await + .expect("restored"); + assert_eq!(restored.stage(), RestoreStage::Finalized); + assert_eq!( + operations.integrity().await.expect("integrity").health(), + IntegrityHealth::Healthy + ); + assert_eq!( + operations + .status() + .await + .expect("status") + .integrity() + .health(), + IntegrityHealth::Healthy + ); + } +} diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -6,6 +6,8 @@ const CLIENT: &str = include_str!("../src/client.rs"); const FARM: &str = include_str!("../src/farm.rs"); const LISTING: &str = include_str!("../src/listing.rs"); const TRADE: &str = include_str!("../src/trade.rs"); +const STORAGE: &str = include_str!("../src/storage.rs"); +const DIAGNOSTICS: &str = include_str!("../src/diagnostics.rs"); const SYNC: &str = include_str!("../src/sync.rs"); const TRANSPORT: &str = include_str!("../src/transport.rs"); @@ -308,6 +310,41 @@ fn trade_operations_use_canonical_workflow_storage_sync_and_projection_types() { ); } +#[test] +fn reliability_and_diagnostics_return_canonical_storage_contracts() { + for required in [ + "StorageReliability::begin_backup", + "StorageReliability::transition_backup", + "StorageReliability::begin_restore", + "StorageReliability::transition_restore", + "StorageReliability::integrity", + "StorageReliability::status", + ] { + assert!( + STORAGE.contains(required), + "missing storage delegation `{required}`" + ); + } + for forbidden in [ + "struct BackupManifest", + "struct IntegrityStatus", + "Studio", + "sqlx::", + ] { + assert!( + !STORAGE.contains(forbidden), + "storage source duplicates `{forbidden}`" + ); + } + assert!(DIAGNOSTICS.contains("radroots_storage::StorageStatus")); + for forbidden in ["path:", "SqlitePool", "private_artifact"] { + assert!( + !DIAGNOSTICS.contains(forbidden), + "diagnostics leaks `{forbidden}`" + ); + } +} + fn dependency_names(manifest: &str) -> BTreeSet<&str> { let dependencies = manifest .split_once("[dependencies]")