rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

services_hardening_state_host.rs (24549B)


      1 #![forbid(unsafe_code)]
      2 #![cfg(any(target_os = "linux", target_os = "macos"))]
      3 
      4 use std::{error::Error, fs, os::unix::fs::PermissionsExt, path::Path};
      5 
      6 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
      7 use radroots_storage::event::SourceGeneration;
      8 use rhi::{
      9     RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigProfile,
     10     RhiStateHostErrorKind, RhiStateHostMode, RhiStateMetadata, RhiStateRepositoryKind,
     11     initialize_rhi_state, open_rhi_state_inspection, open_rhi_state_read_write,
     12     parse_rhi_cli_v1_from, parse_rhi_config_v1, resolve_rhi_runtime_context,
     13 };
     14 use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions};
     15 
     16 const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     17 
     18 const HOST_SOURCE: &str = include_str!("../src/state_host.rs");
     19 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     20 
     21 fn runtime(root: &Path, instance: &str) -> rhi::RhiRuntimeContext {
     22     let root = root.to_str().expect("UTF-8 temporary root");
     23     let invocation = parse_rhi_cli_v1_from([
     24         "rhi",
     25         "--profile",
     26         "repo-local",
     27         "--instance",
     28         instance,
     29         "--repo-local-root",
     30         root,
     31         "run",
     32     ])
     33     .expect("valid test invocation");
     34     resolve_rhi_runtime_context(
     35         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
     36         &invocation,
     37     )
     38     .expect("runtime context")
     39 }
     40 
     41 fn prepare_state_directory(runtime: &rhi::RhiRuntimeContext) {
     42     let directory = runtime.context().paths().state();
     43     fs::create_dir_all(directory).expect("state directory");
     44     fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode");
     45 }
     46 
     47 fn prepare_state_root(runtime: &rhi::RhiRuntimeContext) {
     48     let root = runtime
     49         .context()
     50         .paths()
     51         .state()
     52         .ancestors()
     53         .nth(3)
     54         .expect("state root");
     55     fs::create_dir_all(root).expect("state root");
     56     fs::set_permissions(root, fs::Permissions::from_mode(0o700)).expect("state root mode");
     57 }
     58 
     59 fn metadata(runtime: &rhi::RhiRuntimeContext) -> RhiStateMetadata {
     60     let configuration = parse_rhi_config_v1(EXAMPLE.as_bytes(), RhiConfigProfile::RepoLocal)
     61         .expect("configuration");
     62     RhiStateMetadata::new(
     63         runtime,
     64         &configuration,
     65         SourceGeneration::new([0x5a; 32]).expect("generation"),
     66         1_725_000_000_000,
     67     )
     68     .expect("metadata")
     69 }
     70 
     71 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) {
     72     let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time");
     73     let build = MigrationBuildIdentity::new(
     74         env!("CARGO_PKG_VERSION"),
     75         "1111111111111111111111111111111111111111",
     76         "053d0c750bf9cd683c6ea37cefe7e79617ba629f",
     77         "rustc-test",
     78         "test-target",
     79         "service-host",
     80         1,
     81         rhi::RHI_STATE_SCHEMA_VERSION,
     82         1,
     83         1,
     84         1,
     85     )
     86     .expect("build identity");
     87     (applied_at, build)
     88 }
     89 
     90 async fn offline_connection(runtime: &rhi::RhiRuntimeContext) -> SqliteConnection {
     91     let options = SqliteConnectOptions::new()
     92         .filename(runtime.artifacts().state_database())
     93         .create_if_missing(false)
     94         .foreign_keys(false);
     95     SqliteConnection::connect_with(&options)
     96         .await
     97         .expect("offline fixture connection")
     98 }
     99 
    100 async fn downgrade_fixture_to_schema_v7(runtime: &rhi::RhiRuntimeContext) {
    101     let mut connection = offline_connection(runtime).await;
    102     let metadata_guard: String = sqlx::query_scalar(
    103         "SELECT sql FROM sqlite_schema WHERE type = 'trigger' AND name = 'radroots_service_metadata_guard_update'",
    104     )
    105     .fetch_one(&mut connection)
    106     .await
    107     .expect("metadata guard SQL");
    108     let migration_no_update: String = sqlx::query_scalar(
    109         "SELECT sql FROM sqlite_schema WHERE type = 'trigger' AND name = 'schema_migrations_no_update'",
    110     )
    111     .fetch_one(&mut connection)
    112     .await
    113     .expect("migration update guard SQL");
    114     let migration_no_delete: String = sqlx::query_scalar(
    115         "SELECT sql FROM sqlite_schema WHERE type = 'trigger' AND name = 'schema_migrations_no_delete'",
    116     )
    117     .fetch_one(&mut connection)
    118     .await
    119     .expect("migration delete guard SQL");
    120     for statement in [
    121         "DROP TRIGGER rhi_admin_operations_guard_update",
    122         "DROP TABLE rhi_admin_operations",
    123         "DROP TABLE presence_attempts",
    124         "DROP TABLE presence_targets",
    125         "DROP TABLE presence_outbox",
    126         "DROP TRIGGER presence_desired_state_guard_insert",
    127         "DROP TRIGGER presence_desired_state_guard_update",
    128         "DROP TRIGGER presence_desired_state_no_delete",
    129         "DROP TABLE presence_desired_state",
    130         "DROP TRIGGER reconciliation_jobs_shape_guard_insert",
    131         "DROP TRIGGER reconciliation_jobs_shape_guard_update",
    132         "DROP TRIGGER radroots_service_metadata_guard_update",
    133         "DROP TRIGGER schema_migrations_no_update",
    134         "DROP TRIGGER schema_migrations_no_delete",
    135         "UPDATE radroots_service_metadata SET state_schema_version = 7 WHERE singleton = 1",
    136         "DELETE FROM schema_migrations WHERE version IN (8, 9, 10, 11)",
    137     ] {
    138         sqlx::query(statement)
    139             .execute(&mut connection)
    140             .await
    141             .expect("downgrade exact v8 fixture state");
    142     }
    143     for statement in [metadata_guard, migration_no_update, migration_no_delete] {
    144         sqlx::query(sqlx::AssertSqlSafe(statement.as_str()))
    145             .execute(&mut connection)
    146             .await
    147             .expect("restore shared immutable guard");
    148     }
    149     connection.close().await.expect("downgrade fixture close");
    150 }
    151 
    152 async fn insert_historical_reconciliation_job(
    153     runtime: &rhi::RhiRuntimeContext,
    154     state: &str,
    155     next_attempt_unix_ms: Option<i64>,
    156     lease_owner: Option<Vec<u8>>,
    157     lease_expires_unix_ms: Option<i64>,
    158 ) {
    159     let mut connection = offline_connection(runtime).await;
    160     let trade_id = [0x51_u8; 16];
    161     sqlx::query(
    162         "INSERT INTO trade_dirty_generations (trade_id, generation, evidence_policy_sha256, updated_at_unix_s) VALUES (?, 1, ?, 1)",
    163     )
    164     .bind(trade_id.as_slice())
    165     .bind([0x52_u8; 32].as_slice())
    166     .execute(&mut connection)
    167     .await
    168     .expect("historical dirty generation");
    169     sqlx::query(
    170         r#"INSERT INTO reconciliation_jobs (
    171             job_id, trade_id, input_generation, evidence_policy_sha256,
    172             state, revision, attempt_count, failure_count, max_attempts,
    173             lease_duration_ms, lease_renewal_ms, initial_backoff_ms,
    174             maximum_backoff_ms, next_attempt_unix_ms, lease_owner,
    175             lease_expires_unix_ms, created_at_unix_ms, updated_at_unix_ms
    176         ) VALUES (?, ?, 1, ?, ?, 1, ?, 0, 3, 30000, 10000, 100,
    177             1000, ?, ?, ?, 1, 1)"#,
    178     )
    179     .bind([0x53_u8; 32].as_slice())
    180     .bind(trade_id.as_slice())
    181     .bind([0x52_u8; 32].as_slice())
    182     .bind(state)
    183     .bind(i64::from(state == "leased"))
    184     .bind(next_attempt_unix_ms)
    185     .bind(lease_owner)
    186     .bind(lease_expires_unix_ms)
    187     .execute(&mut connection)
    188     .await
    189     .expect("historical nullable reconciliation row admitted by schema v7");
    190     connection.close().await.expect("historical fixture close");
    191 }
    192 
    193 #[tokio::test]
    194 async fn initialize_is_create_new_and_both_existing_open_modes_close_explicitly() {
    195     let directory = tempfile::tempdir().expect("temporary root");
    196     let runtime = runtime(directory.path(), "primary");
    197     prepare_state_root(&runtime);
    198     let metadata = metadata(&runtime);
    199     let state = runtime.artifacts().state_database();
    200     let lock = runtime.artifacts().state_lock();
    201 
    202     assert!(!runtime.context().paths().state().exists());
    203     assert!(!state.exists());
    204     let (applied_at, build) = migration_evidence();
    205     initialize_rhi_state(&runtime, &metadata, applied_at, &build)
    206         .await
    207         .expect("create-new initialization");
    208     assert!(state.is_file());
    209     assert!(lock.is_file());
    210     assert_eq!(
    211         fs::metadata(state).unwrap().permissions().mode() & 0o777,
    212         0o600
    213     );
    214     assert_eq!(
    215         fs::metadata(lock).unwrap().permissions().mode() & 0o777,
    216         0o600
    217     );
    218 
    219     let duplicate = initialize_rhi_state(&runtime, &metadata, applied_at, &build)
    220         .await
    221         .expect_err("second initialization must fail");
    222     assert_eq!(duplicate.kind(), RhiStateHostErrorKind::Initialize);
    223 
    224     let writer = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build)
    225         .await
    226         .expect("existing writable state");
    227     assert_eq!(writer.mode(), RhiStateHostMode::ReadWriteExisting);
    228     assert_eq!(
    229         format!("{writer:?}"),
    230         "RhiStateHost { mode: ReadWriteExisting, state: \"[sealed]\" }"
    231     );
    232 
    233     let contended = open_rhi_state_inspection(&runtime, &metadata)
    234         .await
    235         .expect_err("inspection must not bypass active writer authority");
    236     assert_eq!(contended.kind(), RhiStateHostErrorKind::InspectionOpen);
    237     writer.close().await.expect("writer close");
    238     writer.close().await.expect("idempotent writer close");
    239 
    240     let inspection = open_rhi_state_inspection(&runtime, &metadata)
    241         .await
    242         .expect("existing inspection state");
    243     assert_eq!(inspection.mode(), RhiStateHostMode::ReadOnlyInspection);
    244     let repositories = inspection.repositories();
    245     assert_eq!(
    246         format!("{repositories:?}"),
    247         "RhiStateRepositories { mode: ReadOnlyInspection, state: \"[sealed]\" }"
    248     );
    249     assert_eq!(
    250         repositories.sources().kind(),
    251         RhiStateRepositoryKind::Source
    252     );
    253     assert_eq!(
    254         repositories.source_cursors().kind(),
    255         RhiStateRepositoryKind::SourceCursor
    256     );
    257     assert_eq!(
    258         repositories.source_completions().kind(),
    259         RhiStateRepositoryKind::SourceCompletion
    260     );
    261     assert_eq!(
    262         repositories.signed_events().kind(),
    263         RhiStateRepositoryKind::SignedEvent
    264     );
    265     assert_eq!(
    266         repositories.mutations().kind(),
    267         RhiStateRepositoryKind::Mutation
    268     );
    269     assert_eq!(
    270         repositories.provenance().kind(),
    271         RhiStateRepositoryKind::Provenance
    272     );
    273     assert_eq!(
    274         repositories.dirty_trades().kind(),
    275         RhiStateRepositoryKind::DirtyTrade
    276     );
    277     assert_eq!(
    278         repositories.reconciliation_jobs().kind(),
    279         RhiStateRepositoryKind::ReconciliationJob
    280     );
    281     assert_eq!(
    282         repositories.reconciliation_attempts().kind(),
    283         RhiStateRepositoryKind::ReconciliationAttempt
    284     );
    285     assert_eq!(
    286         repositories.evidence_manifests().kind(),
    287         RhiStateRepositoryKind::EvidenceManifest
    288     );
    289     assert_eq!(
    290         repositories.projections().kind(),
    291         RhiStateRepositoryKind::Projection
    292     );
    293     assert_eq!(
    294         repositories.reports().kind(),
    295         RhiStateRepositoryKind::Report
    296     );
    297     assert_eq!(
    298         repositories.supersessions().kind(),
    299         RhiStateRepositoryKind::Supersession
    300     );
    301     assert_eq!(
    302         repositories.signed_attestation_events().kind(),
    303         RhiStateRepositoryKind::SignedAttestationEvent
    304     );
    305     assert_eq!(
    306         repositories.publication_outbox().kind(),
    307         RhiStateRepositoryKind::PublicationOutbox
    308     );
    309     assert_eq!(
    310         repositories.publication_targets().kind(),
    311         RhiStateRepositoryKind::PublicationTarget
    312     );
    313     assert_eq!(
    314         repositories.publication_attempts().kind(),
    315         RhiStateRepositoryKind::PublicationAttempt
    316     );
    317     assert_eq!(
    318         repositories.desired_presence().kind(),
    319         RhiStateRepositoryKind::DesiredPresence
    320     );
    321     inspection.close().await.expect("inspection close");
    322 
    323     let writer = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build)
    324         .await
    325         .expect("authority reacquisition after explicit close");
    326     writer.close().await.expect("reopened writer close");
    327 }
    328 
    329 #[tokio::test]
    330 async fn publication_schema_rejects_null_state_holes_and_accepted_target_mutation() {
    331     let directory = tempfile::tempdir().expect("temporary root");
    332     let runtime = runtime(directory.path(), "publication-schema");
    333     prepare_state_directory(&runtime);
    334     let metadata = metadata(&runtime);
    335     let (applied_at, build) = migration_evidence();
    336     initialize_rhi_state(&runtime, &metadata, applied_at, &build)
    337         .await
    338         .expect("state initialization");
    339 
    340     let options = SqliteConnectOptions::new()
    341         .filename(runtime.artifacts().state_database())
    342         .create_if_missing(false)
    343         .foreign_keys(false);
    344     let mut connection = SqliteConnection::connect_with(&options)
    345         .await
    346         .expect("offline fixture connection");
    347 
    348     let outbox_id = [0x21_u8; 32];
    349     let event_id = [0x22_u8; 32];
    350     let event_sha256 = [0x23_u8; 32];
    351     let authority_sha256 = [0x24_u8; 32];
    352     let target_set_sha256 = [0x25_u8; 32];
    353     let missing_pending_schedule = sqlx::query(
    354         r#"INSERT INTO publication_outbox (
    355             outbox_id, event_id, event_sha256, publication_authority_sha256,
    356             target_set_sha256, target_count, required_target_count,
    357             max_attempts, initial_backoff_ms, maximum_backoff_ms,
    358             attempt_deadline_ms, state, revision, next_attempt_unix_ms,
    359             lease_owner, lease_expires_unix_ms, created_at_unix_ms,
    360             updated_at_unix_ms
    361         ) VALUES (?, ?, ?, ?, ?, 1, 1, 3, 100, 1000, 5000,
    362             'pending', 1, NULL, NULL, NULL, 10, 10)"#,
    363     )
    364     .bind(outbox_id.as_slice())
    365     .bind(event_id.as_slice())
    366     .bind(event_sha256.as_slice())
    367     .bind(authority_sha256.as_slice())
    368     .bind(target_set_sha256.as_slice())
    369     .execute(&mut connection)
    370     .await;
    371     assert!(
    372         missing_pending_schedule.is_err(),
    373         "pending outbox rows require a concrete next-attempt time"
    374     );
    375 
    376     sqlx::query(
    377         r#"INSERT INTO publication_outbox (
    378             outbox_id, event_id, event_sha256, publication_authority_sha256,
    379             target_set_sha256, target_count, required_target_count,
    380             max_attempts, initial_backoff_ms, maximum_backoff_ms,
    381             attempt_deadline_ms, state, revision, next_attempt_unix_ms,
    382             lease_owner, lease_expires_unix_ms, created_at_unix_ms,
    383             updated_at_unix_ms
    384         ) VALUES (?, ?, ?, ?, ?, 1, 1, 3, 100, 1000, 5000,
    385             'pending', 1, 10, NULL, NULL, 10, 10)"#,
    386     )
    387     .bind(outbox_id.as_slice())
    388     .bind(event_id.as_slice())
    389     .bind(event_sha256.as_slice())
    390     .bind(authority_sha256.as_slice())
    391     .bind(target_set_sha256.as_slice())
    392     .execute(&mut connection)
    393     .await
    394     .expect("valid pending outbox row");
    395 
    396     sqlx::query(
    397         r#"INSERT INTO publication_targets (
    398             outbox_id, target_ordinal, relay_id, required, state, revision,
    399             attempt_count, next_attempt_unix_ms, last_attempt_id,
    400             updated_at_unix_ms
    401         ) VALUES (?, 0, 'relay_a', 1, 'accepted', 1, 0, NULL, NULL, 10)"#,
    402     )
    403     .bind(outbox_id.as_slice())
    404     .execute(&mut connection)
    405     .await
    406     .expect("accepted target fixture");
    407     let accepted_mutation = sqlx::query(
    408         r#"UPDATE publication_targets
    409         SET revision = 2, updated_at_unix_ms = 11
    410         WHERE outbox_id = ? AND target_ordinal = 0"#,
    411     )
    412     .bind(outbox_id.as_slice())
    413     .execute(&mut connection)
    414     .await;
    415     assert!(
    416         accepted_mutation.is_err(),
    417         "accepted publication targets are terminal"
    418     );
    419 
    420     connection.close().await.expect("fixture connection close");
    421 }
    422 
    423 #[tokio::test]
    424 async fn schema_v8_scans_historical_nullable_job_state_and_installs_permanent_guards() {
    425     for (instance, state, next_attempt, owner, expiry) in [
    426         ("missing-ready-time", "ready", None, None, None),
    427         ("missing-lease-owner", "leased", None, None, Some(30_001)),
    428         (
    429             "missing-lease-expiry",
    430             "leased",
    431             None,
    432             Some(vec![0x61; 16]),
    433             None,
    434         ),
    435     ] {
    436         let directory = tempfile::tempdir().expect("temporary root");
    437         let runtime = runtime(directory.path(), instance);
    438         prepare_state_directory(&runtime);
    439         let metadata = metadata(&runtime);
    440         let (applied_at, build) = migration_evidence();
    441         initialize_rhi_state(&runtime, &metadata, applied_at, &build)
    442             .await
    443             .expect("current-schema initialization");
    444         downgrade_fixture_to_schema_v7(&runtime).await;
    445         insert_historical_reconciliation_job(&runtime, state, next_attempt, owner, expiry).await;
    446 
    447         let error = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build)
    448             .await
    449             .expect_err("invalid historical row must block migration");
    450         assert_eq!(error.kind(), RhiStateHostErrorKind::ReadWriteOpen);
    451         let mut connection = offline_connection(&runtime).await;
    452         let durable: (i64, i64, i64, i64) = sqlx::query_as(
    453             r#"SELECT
    454                 (SELECT state_schema_version FROM radroots_service_metadata WHERE singleton = 1),
    455                 (SELECT COUNT(*) FROM schema_migrations WHERE version = 8),
    456                 (SELECT COUNT(*) FROM sqlite_schema WHERE type = 'trigger'
    457                     AND name IN ('reconciliation_jobs_shape_guard_insert',
    458                                  'reconciliation_jobs_shape_guard_update')),
    459                 (SELECT COUNT(*) FROM reconciliation_jobs)"#,
    460         )
    461         .fetch_one(&mut connection)
    462         .await
    463         .expect("failed migration state");
    464         assert_eq!(durable, (7, 0, 0, 1));
    465         connection.close().await.expect("failed fixture close");
    466     }
    467 
    468     let directory = tempfile::tempdir().expect("temporary root");
    469     let runtime = runtime(directory.path(), "valid-schema-v7");
    470     prepare_state_directory(&runtime);
    471     let metadata = metadata(&runtime);
    472     let (applied_at, build) = migration_evidence();
    473     initialize_rhi_state(&runtime, &metadata, applied_at, &build)
    474         .await
    475         .expect("current-schema initialization");
    476     downgrade_fixture_to_schema_v7(&runtime).await;
    477     insert_historical_reconciliation_job(
    478         &runtime,
    479         "leased",
    480         None,
    481         Some(vec![0x62; 16]),
    482         Some(30_001),
    483     )
    484     .await;
    485     let writer = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build)
    486         .await
    487         .expect("valid schema-v7 prefix migrates");
    488     writer.close().await.expect("migrated writer close");
    489 
    490     let mut connection = offline_connection(&runtime).await;
    491     let migrated: (i64, i64, i64, i64) = sqlx::query_as(
    492         r#"SELECT
    493             (SELECT state_schema_version FROM radroots_service_metadata WHERE singleton = 1),
    494             (SELECT COUNT(*) FROM schema_migrations WHERE version = 8),
    495             (SELECT COUNT(*) FROM sqlite_schema WHERE type = 'trigger'
    496                 AND name IN ('reconciliation_jobs_shape_guard_insert',
    497                              'reconciliation_jobs_shape_guard_update')),
    498             (SELECT COUNT(*) FROM sqlite_schema
    499                 WHERE name = 'reconciliation_jobs_shape_scan_v1')"#,
    500     )
    501     .fetch_one(&mut connection)
    502     .await
    503     .expect("migrated schema state");
    504     assert_eq!(migrated, (11, 1, 2, 0));
    505 
    506     let invalid_insert = sqlx::query(
    507         r#"INSERT INTO reconciliation_jobs (
    508             job_id, trade_id, input_generation, evidence_policy_sha256,
    509             state, revision, attempt_count, failure_count, max_attempts,
    510             lease_duration_ms, lease_renewal_ms, initial_backoff_ms,
    511             maximum_backoff_ms, next_attempt_unix_ms, lease_owner,
    512             lease_expires_unix_ms, created_at_unix_ms, updated_at_unix_ms
    513         ) VALUES (?, ?, 1, ?, 'ready', 1, 0, 0, 3, 30000, 10000,
    514             100, 1000, NULL, NULL, NULL, 1, 1)"#,
    515     )
    516     .bind([0x63_u8; 32].as_slice())
    517     .bind([0x64_u8; 16].as_slice())
    518     .bind([0x65_u8; 32].as_slice())
    519     .execute(&mut connection)
    520     .await;
    521     assert!(
    522         invalid_insert.is_err(),
    523         "insert guard rejects a ready NULL hole"
    524     );
    525 
    526     let invalid_update = sqlx::query(
    527         r#"UPDATE reconciliation_jobs
    528         SET revision = revision + 1, updated_at_unix_ms = updated_at_unix_ms + 1,
    529             lease_owner = NULL
    530         WHERE job_id = ?"#,
    531     )
    532     .bind([0x53_u8; 32].as_slice())
    533     .execute(&mut connection)
    534     .await;
    535     assert!(
    536         invalid_update.is_err(),
    537         "update guard rejects a leased NULL hole"
    538     );
    539     connection.close().await.expect("guard fixture close");
    540 }
    541 
    542 #[tokio::test]
    543 async fn missing_state_and_mismatched_evidence_fail_before_database_creation() {
    544     let invalid_directory = tempfile::tempdir().expect("invalid temporary root");
    545     let invalid_runtime = runtime(invalid_directory.path(), "invalid");
    546     let invalid_metadata = metadata(&invalid_runtime);
    547     let (applied_at, build) = migration_evidence();
    548     let invalid_build = MigrationBuildIdentity::new(
    549         env!("CARGO_PKG_VERSION"),
    550         "1111111111111111111111111111111111111111",
    551         "053d0c750bf9cd683c6ea37cefe7e79617ba629f",
    552         "rustc-test",
    553         "test-target",
    554         "service-host",
    555         2,
    556         1,
    557         1,
    558         1,
    559         1,
    560     )
    561     .expect("structurally valid mismatched build");
    562     let invalid = initialize_rhi_state(
    563         &invalid_runtime,
    564         &invalid_metadata,
    565         applied_at,
    566         &invalid_build,
    567     )
    568     .await
    569     .expect_err("migration build must match RHI policy before I/O");
    570     assert_eq!(invalid.kind(), RhiStateHostErrorKind::InvalidEvidence);
    571     assert!(!invalid_runtime.context().paths().state().exists());
    572 
    573     let missing_directory = tempfile::tempdir().expect("missing temporary root");
    574     let missing_runtime = runtime(missing_directory.path(), "missing");
    575     prepare_state_root(&missing_runtime);
    576     let missing_metadata = metadata(&missing_runtime);
    577     let missing =
    578         open_rhi_state_read_write(&missing_runtime, &missing_metadata, applied_at, &build)
    579             .await
    580             .expect_err("existing-only open never provisions the service suffix");
    581     assert_eq!(missing.kind(), RhiStateHostErrorKind::ReadWriteOpen);
    582     assert!(!missing_runtime.context().paths().state().exists());
    583 
    584     let directory = tempfile::tempdir().expect("temporary root");
    585     let primary = runtime(directory.path(), "primary");
    586     let secondary = runtime(directory.path(), "secondary");
    587     prepare_state_directory(&primary);
    588     let primary_metadata = metadata(&primary);
    589     let missing = open_rhi_state_read_write(&primary, &primary_metadata, applied_at, &build)
    590         .await
    591         .expect_err("missing state is never created by open");
    592     assert_eq!(missing.kind(), RhiStateHostErrorKind::ReadWriteOpen);
    593     assert!(!primary.artifacts().state_database().exists());
    594 
    595     let mismatch = initialize_rhi_state(&secondary, &primary_metadata, applied_at, &build)
    596         .await
    597         .expect_err("cross-instance metadata");
    598     assert_eq!(mismatch.kind(), RhiStateHostErrorKind::InvalidEvidence);
    599     assert_eq!(mismatch.code(), "state_evidence_invalid");
    600     assert!(Error::source(&mismatch).is_none());
    601     let rendered = format!("{mismatch} {mismatch:?}");
    602     assert!(!rendered.contains(directory.path().to_string_lossy().as_ref()));
    603     assert!(!rendered.contains("state.sqlite"));
    604     assert!(!secondary.artifacts().state_database().exists());
    605 }
    606 
    607 #[test]
    608 fn public_lifecycle_source_is_sealed() {
    609     assert!(LIB_SOURCE.contains("mod state_host;"));
    610     assert!(!LIB_SOURCE.contains("pub mod state_host;"));
    611     assert!(HOST_SOURCE.contains("host: ServiceSqliteHost"));
    612     assert!(!HOST_SOURCE.contains("pub host:"));
    613     for required in [
    614         "ServiceSqliteInitializer",
    615         "ServiceSqliteInitializerFuture",
    616         ".state_directory_plan()",
    617         ".and_then(|plan| plan.provision())",
    618     ] {
    619         assert!(
    620             HOST_SOURCE.contains(required),
    621             "missing sealed initialization boundary `{required}`"
    622         );
    623     }
    624     for forbidden in [
    625         "pub fn transaction",
    626         "pub async fn transaction",
    627         "pub fn pool",
    628         "pub fn connection",
    629         "pub fn into_inner",
    630         "pub fn executor",
    631         "MigrationDescriptor::",
    632         "raw_sql",
    633         "CREATE TABLE",
    634         "PRAGMA application_id",
    635         "PathBuf",
    636         "use sqlx::",
    637         "SqliteConnectOptions",
    638         "ConnectOptions",
    639         "create_dir_all",
    640         "try_exists",
    641     ] {
    642         assert!(
    643             !HOST_SOURCE.contains(forbidden),
    644             "found forbidden lifecycle authority `{forbidden}`"
    645         );
    646     }
    647 }