commit d870d32ff8f7544e096978cb4cc6627382362058 parent ad7303c14e1852a35ffd388ae9c8ead08562d897 Author: triesap <tyson@radroots.org> Date: Thu, 27 Aug 2026 23:09:14 +0000 storage: govern atomic database bootstrap Diffstat:
20 files changed, 141 insertions(+), 101 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md @@ -87,6 +87,13 @@ substitute. application-schema callback inside `radroots_service_sqlite` initialization, but must not create another pool, expose a raw connection, or reintroduce Rusqlite, Refinery, arbitrary repair, or a second migration authority. +- Storage bootstrap requires the injected runtime context's canonical state + root to exist. `RuntimeContext::state_directory_plan` is the only production + authority allowed to create the exact `services/harvestcircle/desktop` + suffix. `ServiceSqliteHost::open_or_initialize` alone selects create versus + existing state under one retained writer authority and returns the actual + verified database metadata. Do not probe paths, recursively create roots, + repair permissions, or open a raw SQLx connection during bootstrap. - Native production qualification is limited to macOS aarch64 and Linux x86_64. Do not add or claim another target without an explicit contract change and its complete platform evidence. diff --git a/README.md b/README.md @@ -61,6 +61,13 @@ close, backup, and restore mechanics. The historical `harvestcircle.sqlite3` file is legacy evidence only and is never imported, repaired, deleted, or treated as current state. +The platform or development harness must supply the existing canonical state +root. HarvestCircle then uses the runtime context's sealed provisioning plan to +create or validate only `services/harvestcircle/desktop`. The SQLite host makes +the create-versus-existing decision atomically and returns the actual verified +metadata; product storage never probes the database path, recursively creates +roots, repairs existing permissions, or opens a raw SQLx connection. + Online backup capture returns the canonical manifest in memory and writes only the governed `state.sqlite` member into a caller-selected new directory. Restore accepts only a digest-bound, identity-bound, size-bounded verified diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/architecture/MachineProvenanceTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/architecture/MachineProvenanceTest.kt @@ -26,7 +26,7 @@ class MachineProvenanceTest { ) assertTrue( provenance.contains( - "canonical_radroots_revision = \"be9db78e060ebc0000fa7827ac32efa3f6504f53\"", + "canonical_radroots_revision = \"ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb\"", ), ) assertEquals(8, Regex("(?m)^\\[\\[import]]$").findAll(provenance).count()) diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt @@ -606,7 +606,7 @@ class ConventionPluginSmokeTest { storage.schema.current=1 product.version=0.1.0-alpha package.version=1.0.0 - source.provenance_digest=daded0256c87be5a413358b346498dcecb412d4eff53d2998999d26ec20f3d40 + source.provenance_digest=40b9eccd486026128f92de8d55d002a9030f235a35f9b754c98c0b0d387bd8c0 source.foundation_baseline=c08d18ea569351dddeef70d4c1410708daf067b6 """.trimIndent() + "\n" } diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -831,7 +831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2132,7 +2132,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_blossom" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "mediatype", "serde", @@ -2144,7 +2144,7 @@ dependencies = [ [[package]] name = "radroots_core" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "rust_decimal", "serde", @@ -2153,7 +2153,7 @@ dependencies = [ [[package]] name = "radroots_event" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "hex", "jiff-tzdb", @@ -2171,7 +2171,7 @@ dependencies = [ [[package]] name = "radroots_event_codec" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "hex", "radroots_blossom", @@ -2188,7 +2188,7 @@ dependencies = [ [[package]] name = "radroots_identity" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "k256", "serde", @@ -2198,7 +2198,7 @@ dependencies = [ [[package]] name = "radroots_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "nostr 0.44.8", "radroots_event", @@ -2212,7 +2212,7 @@ dependencies = [ [[package]] name = "radroots_protocol" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "serde", ] @@ -2220,8 +2220,9 @@ dependencies = [ [[package]] name = "radroots_runtime_paths" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ + "rustix", "serde", "thiserror 1.0.69", ] @@ -2229,7 +2230,7 @@ dependencies = [ [[package]] name = "radroots_service_sqlite" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "fs2", "futures", @@ -2247,7 +2248,7 @@ dependencies = [ [[package]] name = "radroots_storage" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "radroots_event", "radroots_event_codec", @@ -2260,7 +2261,7 @@ dependencies = [ [[package]] name = "radroots_trade" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "radroots_core", "radroots_event", @@ -2270,7 +2271,7 @@ dependencies = [ [[package]] name = "radroots_transport" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "radroots_event", "radroots_identity", @@ -2281,7 +2282,7 @@ dependencies = [ [[package]] name = "radroots_transport_nostr" version = "0.1.0-alpha" -source = "git+https://github.com/radrootslabs/lib?rev=be9db78e060ebc0000fa7827ac32efa3f6504f53#be9db78e060ebc0000fa7827ac32efa3f6504f53" +source = "git+https://github.com/radrootslabs/lib?rev=ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb#ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" dependencies = [ "async-wsocket", "futures", @@ -2454,7 +2455,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2953,7 +2954,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -43,12 +43,12 @@ harvestcircle_runtime = { path = "crates/harvestcircle_runtime", version = "=0.1 harvestcircle_storage = { path = "crates/harvestcircle_storage", version = "=0.1.0-alpha" } harvestcircle_test_bridge = { path = "crates/harvestcircle_test_bridge", version = "=0.1.0-alpha" } harvestcircle_uniffi_bindgen = { path = "crates/harvestcircle_uniffi_bindgen", version = "=0.1.0-alpha" } -radroots_identity = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } -radroots_runtime_paths = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } -radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } -radroots_storage = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } -radroots_transport = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } -radroots_transport_nostr = { git = "https://github.com/radrootslabs/lib", rev = "be9db78e060ebc0000fa7827ac32efa3f6504f53", version = "=0.1.0-alpha", default-features = false } +radroots_identity = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } +radroots_runtime_paths = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } +radroots_service_sqlite = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } +radroots_storage = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } +radroots_transport = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } +radroots_transport_nostr = { git = "https://github.com/radrootslabs/lib", rev = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", version = "=0.1.0-alpha", default-features = false } getrandom = { version = "0.2", default-features = false } quote = { version = "1" } sha2 = { version = "0.10", default-features = false } diff --git a/core/compatibility/harvestcircle-ffi-v4.properties b/core/compatibility/harvestcircle-ffi-v4.properties @@ -9,5 +9,5 @@ storage.schema.minimum=1 storage.schema.current=1 product.version=0.1.0-alpha package.version=1.0.0 -source.provenance_digest=daded0256c87be5a413358b346498dcecb412d4eff53d2998999d26ec20f3d40 +source.provenance_digest=40b9eccd486026128f92de8d55d002a9030f235a35f9b754c98c0b0d387bd8c0 source.foundation_baseline=c08d18ea569351dddeef70d4c1410708daf067b6 diff --git a/core/crates/harvestcircle_ffi/src/commands.rs b/core/crates/harvestcircle_ffi/src/commands.rs @@ -951,6 +951,7 @@ pub(crate) async fn test_actor_with_nostr_timeout( }, }) .expect("runtime context"); + std::fs::create_dir_all(directory.path().join("data")).expect("state root"); let build = migration_build_identity().expect("migration build identity"); let actor = RuntimeActorHandle::open( &context, diff --git a/core/crates/harvestcircle_product/src/lib.rs b/core/crates/harvestcircle_product/src/lib.rs @@ -140,7 +140,7 @@ mod tests { let expected = provenance::digest(&source).expect("canonical provenance digest"); assert_eq!( expected, - "daded0256c87be5a413358b346498dcecb412d4eff53d2998999d26ec20f3d40" + "40b9eccd486026128f92de8d55d002a9030f235a35f9b754c98c0b0d387bd8c0" ); assert_eq!( provenance::digest(&source.replace('\n', "\r\n")).unwrap(), diff --git a/core/crates/harvestcircle_runtime/src/runtime_actor.rs b/core/crates/harvestcircle_runtime/src/runtime_actor.rs @@ -1361,13 +1361,15 @@ fn test_runtime_context(root: &std::path::Path) -> Result<RuntimeContext, SafeEr RuntimeContextSource::SafeDefault, ) .map_err(|_| invalid_runtime_evidence())?; - RuntimeContext::resolve( + let context = RuntimeContext::resolve( &resolver, bootstrap, ServiceId::new("harvestcircle").map_err(|_| invalid_runtime_evidence())?, InstanceId::new("desktop").map_err(|_| invalid_runtime_evidence())?, ) - .map_err(|_| invalid_runtime_evidence()) + .map_err(|_| invalid_runtime_evidence())?; + std::fs::create_dir_all(root.join("data")).map_err(|_| invalid_runtime_evidence())?; + Ok(context) } #[cfg(test)] diff --git a/core/crates/harvestcircle_runtime/tests/local_relay_e2e.rs b/core/crates/harvestcircle_runtime/tests/local_relay_e2e.rs @@ -46,6 +46,7 @@ async fn local_relay_e2e_imports_activates_refreshes_and_caches_profile() { InstanceId::new("desktop").expect("instance"), ) .expect("context"); + std::fs::create_dir_all(directory.path().join("data")).expect("state root"); let build = MigrationBuildIdentity::new( "0.1.0-alpha", "1111111111111111111111111111111111111111", diff --git a/core/crates/harvestcircle_runtime/tests/restart_isolation.rs b/core/crates/harvestcircle_runtime/tests/restart_isolation.rs @@ -26,7 +26,7 @@ impl Clock for FixedClock { } fn context(root: &std::path::Path) -> RuntimeContext { - RuntimeContext::resolve( + let context = RuntimeContext::resolve( &RadrootsPathResolver::new( RadrootsPlatform::current(), RadrootsHostEnvironment::default(), @@ -41,7 +41,9 @@ fn context(root: &std::path::Path) -> RuntimeContext { ServiceId::new("harvestcircle").expect("service"), InstanceId::new("desktop").expect("instance"), ) - .expect("context") + .expect("context"); + fs::create_dir_all(root.join("data")).expect("state root"); + context } fn build() -> MigrationBuildIdentity { diff --git a/core/crates/harvestcircle_storage/src/db.rs b/core/crates/harvestcircle_storage/src/db.rs @@ -1,18 +1,14 @@ use core::num::NonZeroU32; -use std::fs; -use std::path::Path; use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage}; use radroots_runtime_paths::RuntimeContext; use radroots_service_sqlite::{ - ExistingServiceDatabaseIntent, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity, OpenMode, + ExistingServiceDatabaseIntent, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity, ServiceDatabaseMetadata, ServiceSqliteConnectionOptions, ServiceSqliteErrorKind, - ServiceSqliteHost, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind, - initialize_database, + ServiceSqliteHost, ServiceSqliteInitializer, ServiceSqliteInitializerFuture, + ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind, }; use radroots_storage::event::SourceGeneration; -use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{Connection, SqliteConnection}; use crate::contract::harvestcircle_initial_schema_sql; use crate::{HARVESTCIRCLE_STATE_SCHEMA_VERSION, HarvestCircleStorageContract}; @@ -41,20 +37,10 @@ impl Database { ) -> Result<Self, SafeError> { let contract = HarvestCircleStorageContract::from_runtime_context(context) .map_err(|_| invalid_storage_contract())?; - provision_state_directory(contract.paths().state_database())?; let applied_at = MigrationAppliedAtUnixSeconds::new(applied_at_unix_s) .map_err(|_| invalid_storage_contract())?; let options = ServiceSqliteConnectionOptions::reviewed(); - if contract - .paths() - .state_database() - .try_exists() - .map_err(|_| storage_unavailable())? - { - return Self::open_existing(&contract, applied_at, build).await; - } - let mut generation = [0_u8; 32]; getrandom::getrandom(&mut generation).map_err(|_| storage_unavailable())?; let generation = SourceGeneration::new(generation).map_err(|_| storage_unavailable())?; @@ -66,28 +52,25 @@ impl Database { contract.application_id(), ) .map_err(|_| invalid_storage_contract())?; - let authority = initialize_database( + context + .state_directory_plan() + .map_err(|_| invalid_storage_contract())? + .provision() + .map_err(|_| storage_unavailable())?; + let (opened, _) = ServiceSqliteHost::open_or_initialize( contract.paths(), - OpenMode::Initialize, &metadata, - contract.schema(), - initialize_application_schema, - ) - .await - .map_err(map_service_error)?; - let (host, _) = ServiceSqliteHost::open_initialized( - contract.paths(), - &metadata.identity(), contract.migrations(), contract.schema(), options, - authority, applied_at, build, &[], + initialize_application_schema, ) .await .map_err(map_service_error)?; + let (host, metadata) = opened.into_parts(); Ok(Self { host, metadata }) } @@ -132,34 +115,18 @@ impl Database { } } -async fn initialize_application_schema(path: std::path::PathBuf) -> Result<(), sqlx::Error> { - let options = SqliteConnectOptions::new() - .filename(path) - .create_if_missing(false); - let mut connection = SqliteConnection::connect_with(&options).await?; - for statement in harvestcircle_initial_schema_sql() { - sqlx::query(*statement).execute(&mut connection).await?; - } - sqlx::query("INSERT INTO runtime_state (singleton) VALUES (1)") - .execute(&mut connection) - .await?; - connection.close().await -} - -fn provision_state_directory(database: &Path) -> Result<(), SafeError> { - let directory = database.parent().ok_or_else(invalid_storage_contract)?; - fs::create_dir_all(directory).map_err(|_| storage_unavailable())?; - let metadata = fs::symlink_metadata(directory).map_err(|_| storage_unavailable())?; - if metadata.file_type().is_symlink() || !metadata.is_dir() { - return Err(storage_unavailable()); - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(directory, fs::Permissions::from_mode(0o700)) - .map_err(|_| storage_unavailable())?; - } - Ok(()) +fn initialize_application_schema<'a>( + initializer: &'a mut ServiceSqliteInitializer<'_>, +) -> ServiceSqliteInitializerFuture<'a, sqlx::Error> { + Box::pin(async move { + for statement in harvestcircle_initial_schema_sql() { + sqlx::query(*statement).execute(&mut *initializer).await?; + } + sqlx::query("INSERT INTO runtime_state (singleton) VALUES (1)") + .execute(&mut *initializer) + .await?; + Ok(()) + }) } pub(crate) fn map_transaction_error(error: ServiceSqliteTransactionError<SafeError>) -> SafeError { diff --git a/core/crates/harvestcircle_storage/tests/backup_restore.rs b/core/crates/harvestcircle_storage/tests/backup_restore.rs @@ -22,7 +22,7 @@ fn tempdir() -> std::io::Result<TempDir> { } fn runtime_context(directory: &TempDir) -> RuntimeContext { - RuntimeContext::resolve( + let context = RuntimeContext::resolve( &RadrootsPathResolver::new( RadrootsPlatform::current(), RadrootsHostEnvironment::default(), @@ -42,7 +42,9 @@ fn runtime_context(directory: &TempDir) -> RuntimeContext { ServiceId::new("harvestcircle").expect("service"), InstanceId::new("desktop").expect("instance"), ) - .expect("runtime context") + .expect("runtime context"); + fs::create_dir_all(directory.path().join("data")).expect("state root"); + context } fn build_identity() -> MigrationBuildIdentity { diff --git a/core/crates/harvestcircle_storage/tests/redaction.rs b/core/crates/harvestcircle_storage/tests/redaction.rs @@ -34,7 +34,7 @@ fn assert_redacted(bytes: &[u8]) { } fn runtime_context(directory: &TempDir) -> RuntimeContext { - RuntimeContext::resolve( + let context = RuntimeContext::resolve( &RadrootsPathResolver::new( RadrootsPlatform::current(), RadrootsHostEnvironment::default(), @@ -54,7 +54,9 @@ fn runtime_context(directory: &TempDir) -> RuntimeContext { ServiceId::new("harvestcircle").expect("service"), InstanceId::new("desktop").expect("instance"), ) - .expect("runtime context") + .expect("runtime context"); + fs::create_dir_all(directory.path().join("data")).expect("state root"); + context } fn build_identity() -> MigrationBuildIdentity { diff --git a/core/crates/harvestcircle_storage/tests/sqlx_storage.rs b/core/crates/harvestcircle_storage/tests/sqlx_storage.rs @@ -26,7 +26,7 @@ fn tempdir() -> std::io::Result<TempDir> { tempdir_in(std::env::temp_dir().canonicalize()?) } -fn runtime_context(directory: &TempDir) -> RuntimeContext { +fn unresolved_runtime_context(directory: &TempDir) -> RuntimeContext { RuntimeContext::resolve( &RadrootsPathResolver::new( RadrootsPlatform::current(), @@ -50,6 +50,12 @@ fn runtime_context(directory: &TempDir) -> RuntimeContext { .expect("runtime context") } +fn runtime_context(directory: &TempDir) -> RuntimeContext { + let context = unresolved_runtime_context(directory); + fs::create_dir_all(directory.path().join("data")).expect("state root"); + context +} + fn build_identity() -> MigrationBuildIdentity { MigrationBuildIdentity::new( "0.1.0-alpha", @@ -85,6 +91,46 @@ fn identity(index: usize) -> NostrIdentity { } #[tokio::test] +async fn bootstrap_requires_the_existing_governed_state_root() { + let directory = tempdir().expect("directory"); + let context = unresolved_runtime_context(&directory); + let state_root = directory.path().join("data"); + let error = Database::open(&context, 1, 1, &build_identity()) + .await + .err() + .expect("missing state root must fail closed"); + + assert_eq!(error.code(), SafeErrorCode::StorageUnavailable); + assert!(!state_root.exists()); + assert!(!context.paths().state().exists()); +} + +#[test] +fn bootstrap_source_uses_only_the_governed_path_and_sqlite_boundaries() { + const SOURCE: &str = include_str!("../src/db.rs"); + + for required in [ + ".state_directory_plan()", + "ServiceSqliteHost::open_or_initialize", + "ServiceSqliteInitializer", + "opened.into_parts()", + ] { + assert!(SOURCE.contains(required), "missing `{required}`"); + } + for forbidden in [ + ".try_exists()", + "create_dir_all", + "set_permissions", + "SqliteConnectOptions", + "sqlx::SqliteConnection", + "initialize_database", + "open_initialized", + ] { + assert!(!SOURCE.contains(forbidden), "forbidden `{forbidden}`"); + } +} + +#[tokio::test] async fn canonical_database_preserves_legacy_state_and_enforces_identity_capacity() { let directory = tempdir().expect("directory"); let legacy = directory.path().join("harvestcircle.sqlite3"); diff --git a/core/crates/harvestcircle_test_bridge/src/lib.rs b/core/crates/harvestcircle_test_bridge/src/lib.rs @@ -590,13 +590,15 @@ fn runtime_context(root: &Path) -> Result<RuntimeContext, TestBridgeError> { RuntimeContextSource::SafeDefault, ) .map_err(|_| invalid_runtime_evidence())?; - RuntimeContext::resolve( + let context = RuntimeContext::resolve( &resolver, bootstrap, ServiceId::new("harvestcircle").map_err(|_| invalid_runtime_evidence())?, InstanceId::new("desktop").map_err(|_| invalid_runtime_evidence())?, ) - .map_err(|_| invalid_runtime_evidence()) + .map_err(|_| invalid_runtime_evidence())?; + fs::create_dir_all(root.join("data"))?; + Ok(context) } fn migration_build_identity() -> Result<MigrationBuildIdentity, TestBridgeError> { diff --git a/core/provenance/harvestcircle-v1.toml b/core/provenance/harvestcircle-v1.toml @@ -3,7 +3,7 @@ source_product = "HarvestCircle" source_repository = "https://github.com/radrootslabs/harvestcircle" foundation_baseline = "c08d18ea569351dddeef70d4c1410708daf067b6" canonical_radroots_repository = "https://github.com/radrootslabs/lib" -canonical_radroots_revision = "be9db78e060ebc0000fa7827ac32efa3f6504f53" +canonical_radroots_revision = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" [[import]] component = "domain" diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml @@ -1,8 +1,8 @@ schema = "radroots.lib.source-lock.v1" repository = "https://github.com/radrootslabs/lib" -revision = "be9db78e060ebc0000fa7827ac32efa3f6504f53" +revision = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb" architecture = "radroots.crates.release.v2" workspace_catalog_sha256 = "deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4" version = "0.1.0-alpha" -source_archive_sha256 = "aec2fe198b200f40af81424fbec70a9a8f22b0b38455bc6c81b7eb3be4241748" -lockfile_sha256 = "1d5187f6394e470a027d3643bc1b74beaf92c5168718783e69f2e3f67dfeb1c0" +source_archive_sha256 = "2cf12c24ed649c3c8dd48cebcb8583996646e116fc2472539a55748c803584db" +lockfile_sha256 = "4308984326ef320973bd11c78dabad499fd57ea8be8e12d5a57bbe8464196a7a" diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs @@ -952,7 +952,7 @@ fn development_integration_policy(root: &Path, findings: &mut Vec<String>) { } fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<String>) { - const LIB_REVISION: &str = "be9db78e060ebc0000fa7827ac32efa3f6504f53"; + const LIB_REVISION: &str = "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb"; const PROVENANCE_PATH: &str = "core/provenance/harvestcircle-v1.toml"; const SOURCE_LOCK_PATH: &str = "radroots.lib.source-lock.v1.toml"; let cargo = read_text(root, "core/Cargo.toml"); @@ -1003,12 +1003,12 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin let expected_source_lock = concat!( "schema = \"radroots.lib.source-lock.v1\"\n", "repository = \"https://github.com/radrootslabs/lib\"\n", - "revision = \"be9db78e060ebc0000fa7827ac32efa3f6504f53\"\n", + "revision = \"ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb\"\n", "architecture = \"radroots.crates.release.v2\"\n", "workspace_catalog_sha256 = \"deca0c080deae187ff8186c0708903e42f41ea57f77c5f91581e23aa561164a4\"\n", "version = \"0.1.0-alpha\"\n", - "source_archive_sha256 = \"aec2fe198b200f40af81424fbec70a9a8f22b0b38455bc6c81b7eb3be4241748\"\n", - "lockfile_sha256 = \"1d5187f6394e470a027d3643bc1b74beaf92c5168718783e69f2e3f67dfeb1c0\"\n", + "source_archive_sha256 = \"2cf12c24ed649c3c8dd48cebcb8583996646e116fc2472539a55748c803584db\"\n", + "lockfile_sha256 = \"4308984326ef320973bd11c78dabad499fd57ea8be8e12d5a57bbe8464196a7a\"\n", ); if read_text(root, SOURCE_LOCK_PATH) != expected_source_lock { findings.push(format!("{SOURCE_LOCK_PATH}: exact Lib source lock changed"));