commit 07480ee68e9f3ee9357527d4c382f1c36e3520bd
parent ded5c32f19d3f8e4ece5c6111bdb3d5238bbe0b3
Author: triesap <tyson@radroots.org>
Date: Sun, 23 Aug 2026 20:31:25 +0000
state: freeze RHI SQLite catalogs
Diffstat:
6 files changed, 379 insertions(+), 0 deletions(-)
diff --git a/README b/README
@@ -80,6 +80,22 @@ injected host environment defined by `radroots_runtime_paths`; repo-local uses
one explicit absolute base and the same `services/rhi/<instance>` namespace.
Path resolution performs no directory creation or filesystem I/O.
+## Governed SQLite catalog
+
+RHI owns one `state.sqlite` per service instance. Its clean-slate baseline is
+schema version one and contains only the six shared immutable service-metadata
+and append-only migration-ledger objects supplied by `radroots_service_sqlite`.
+The future migration catalog is empty at this checkpoint. Exact literal
+SHA-256 values bind the migration history, schema-v1 object snapshot, and the
+schema catalog that joins those two identities. RHI validates all three before
+they can become database authority.
+
+This catalog layer performs no filesystem or SQLite I/O and owns no pool,
+connection, transaction, query, or migration executor. Explicit create-new
+initialization and existing-only host lifecycle remain separate boundaries.
+Service-owned evidence and attestation tables and their ordered migrations are
+introduced only by their owning later checkpoints.
+
Validate the standalone crate through extbuild:
```text
diff --git a/src/lib.rs b/src/lib.rs
@@ -7,6 +7,7 @@ pub mod features;
pub mod host_identity;
pub mod identity_storage;
mod runtime_context;
+mod state_catalog;
pub use cli_v1::{
RhiBootstrapProfileV1, RhiCliInvocationV1, RhiCliOutputModeV1, RhiCliV1Error,
@@ -29,3 +30,9 @@ pub use runtime_context::{
RhiRuntimeContext, RhiRuntimeContextError, RhiRuntimeContextErrorKind,
resolve_rhi_runtime_context,
};
+pub use state_catalog::{
+ RHI_MIGRATION_CATALOG_SHA256, RHI_STATE_SCHEMA_CATALOG_SHA256, RHI_STATE_SCHEMA_VERSION,
+ RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_1_SHA256,
+ RhiStateCatalogError, RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
+ validate_rhi_state_catalogs,
+};
diff --git a/src/state_catalog.rs b/src/state_catalog.rs
@@ -0,0 +1,155 @@
+//! Immutable RHI schema and migration catalog identity.
+
+use core::fmt;
+use std::error::Error;
+
+use radroots_service_sqlite::{
+ MigrationCatalog, SchemaCatalog, SchemaDigest, SchemaVersionCatalog,
+};
+
+/// The clean-slate RHI baseline schema version.
+pub const RHI_STATE_SCHEMA_VERSION: u32 = 1;
+
+/// The shared metadata and migration-ledger objects present at schema v1.
+pub const RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT: u32 = 6;
+
+/// SHA-256 identity of the empty schema-v1 migration catalog.
+pub const RHI_MIGRATION_CATALOG_SHA256: [u8; 32] = [
+ 0xec, 0x89, 0xdc, 0x8f, 0x7b, 0x6c, 0x2a, 0x11, 0xb9, 0x67, 0xe3, 0x38, 0x08, 0xe4, 0x03, 0x1e,
+ 0x29, 0xb3, 0x97, 0x0f, 0xfe, 0xe4, 0x95, 0x9b, 0xff, 0x9b, 0xad, 0x35, 0x28, 0x77, 0xee, 0x9b,
+];
+
+/// SHA-256 identity of the exact schema-v1 object snapshot.
+pub const RHI_STATE_SCHEMA_VERSION_1_SHA256: [u8; 32] = [
+ 0x94, 0xdc, 0x66, 0xfb, 0xca, 0x60, 0x16, 0x79, 0x61, 0x5c, 0x05, 0x52, 0x29, 0xdc, 0x0d, 0xb6,
+ 0x11, 0x9f, 0x5b, 0xd9, 0x2b, 0x04, 0x39, 0x0c, 0x67, 0xf6, 0x98, 0xa0, 0x36, 0xfa, 0x78, 0xae,
+];
+
+/// SHA-256 identity of the schema catalog bound to the migration catalog.
+pub const RHI_STATE_SCHEMA_CATALOG_SHA256: [u8; 32] = [
+ 0x23, 0x09, 0x15, 0x3f, 0x3b, 0x49, 0x75, 0x48, 0x87, 0xc5, 0x48, 0xa7, 0x45, 0x9b, 0x3e, 0x09,
+ 0x09, 0x9c, 0x60, 0xf7, 0x14, 0x6b, 0x37, 0x3c, 0x8f, 0x96, 0x70, 0x6c, 0x67, 0x68, 0xd7, 0x91,
+];
+
+/// Stable classes for invalid embedded RHI catalog definitions.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum RhiStateCatalogErrorKind {
+ MigrationCatalog,
+ SchemaCatalog,
+ CatalogMismatch,
+}
+
+impl RhiStateCatalogErrorKind {
+ /// Returns the stable machine-readable classification.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::MigrationCatalog => "migration_catalog_invalid",
+ Self::SchemaCatalog => "schema_catalog_invalid",
+ Self::CatalogMismatch => "state_catalog_mismatch",
+ }
+ }
+}
+
+/// Source-free failure to construct or validate the embedded RHI catalogs.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct RhiStateCatalogError {
+ kind: RhiStateCatalogErrorKind,
+}
+
+impl RhiStateCatalogError {
+ const fn new(kind: RhiStateCatalogErrorKind) -> Self {
+ Self { kind }
+ }
+
+ /// Returns the stable failure class.
+ #[must_use]
+ pub const fn kind(self) -> RhiStateCatalogErrorKind {
+ self.kind
+ }
+
+ /// Returns the stable machine-readable code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Display for RhiStateCatalogError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ RhiStateCatalogErrorKind::MigrationCatalog => {
+ "RHI migration catalog definition is invalid"
+ }
+ RhiStateCatalogErrorKind::SchemaCatalog => "RHI schema catalog definition is invalid",
+ RhiStateCatalogErrorKind::CatalogMismatch => {
+ "RHI state catalogs do not match the governed identity"
+ }
+ })
+ }
+}
+
+impl fmt::Debug for RhiStateCatalogError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RhiStateCatalogError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl Error for RhiStateCatalogError {}
+
+/// Constructs the exact schema-v1 migration catalog.
+pub fn rhi_migration_catalog() -> Result<MigrationCatalog, RhiStateCatalogError> {
+ let catalog = MigrationCatalog::new([])
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::MigrationCatalog))?;
+ if catalog.current_version() != RHI_STATE_SCHEMA_VERSION
+ || !catalog.descriptors().is_empty()
+ || catalog.digest().as_bytes() != &RHI_MIGRATION_CATALOG_SHA256
+ {
+ return Err(RhiStateCatalogError::new(
+ RhiStateCatalogErrorKind::CatalogMismatch,
+ ));
+ }
+ Ok(catalog)
+}
+
+/// Constructs the exact RHI schema catalog bound to the migration catalog.
+pub fn rhi_schema_catalog() -> Result<SchemaCatalog, RhiStateCatalogError> {
+ let migrations = rhi_migration_catalog()?;
+ let version = SchemaVersionCatalog::new(
+ RHI_STATE_SCHEMA_VERSION,
+ [],
+ SchemaDigest::from_bytes(RHI_STATE_SCHEMA_VERSION_1_SHA256),
+ )
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
+ let catalog = SchemaCatalog::new(&migrations, [version])
+ .map_err(|_| RhiStateCatalogError::new(RhiStateCatalogErrorKind::SchemaCatalog))?;
+ validate_rhi_state_catalogs(&migrations, &catalog)?;
+ Ok(catalog)
+}
+
+/// Independently validates exact catalog versions, counts, and digests.
+pub fn validate_rhi_state_catalogs(
+ migrations: &MigrationCatalog,
+ schema: &SchemaCatalog,
+) -> Result<(), RhiStateCatalogError> {
+ let versions = schema.versions();
+ let valid = migrations.current_version() == RHI_STATE_SCHEMA_VERSION
+ && migrations.descriptors().is_empty()
+ && migrations.digest().as_bytes() == &RHI_MIGRATION_CATALOG_SHA256
+ && schema.migration_catalog_digest() == migrations.digest()
+ && versions.len() == 1
+ && versions[0].version() == RHI_STATE_SCHEMA_VERSION
+ && versions[0].object_count() == RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT
+ && versions[0].digest().as_bytes() == &RHI_STATE_SCHEMA_VERSION_1_SHA256
+ && schema.digest().as_bytes() == &RHI_STATE_SCHEMA_CATALOG_SHA256;
+ if valid {
+ Ok(())
+ } else {
+ Err(RhiStateCatalogError::new(
+ RhiStateCatalogErrorKind::CatalogMismatch,
+ ))
+ }
+}
diff --git a/tests/build_policy.rs b/tests/build_policy.rs
@@ -49,6 +49,19 @@ fn shared_host_packages_are_exactly_source_locked() {
}
#[test]
+fn shared_service_sqlite_is_the_only_catalog_authority() {
+ assert!(MANIFEST.contains(
+ "radroots_service_sqlite = { git = \"https://github.com/radrootslabs/lib\", rev = \"7d7b454b4c9ed86569671993bd03ca868b676665\", version = \"=0.1.0-alpha\" }"
+ ));
+ for forbidden in ["rusqlite", "libsqlite3-sys"] {
+ assert!(
+ !MANIFEST.contains(forbidden),
+ "RHI must not introduce alternate SQLite authority `{forbidden}`"
+ );
+ }
+}
+
+#[test]
fn source_lock_binds_the_current_cargo_lock() {
let digest = lower_hex(&Sha256::digest(include_bytes!("../Cargo.lock")));
assert!(SOURCE_LOCK.starts_with(
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -42,6 +42,23 @@ fn shared_host_implementations_do_not_escape_the_public_api() {
}
#[test]
+fn state_catalog_module_is_private_and_root_api_is_curated() {
+ assert!(ROOT.contains("mod state_catalog;"));
+ assert!(!ROOT.contains("pub mod state_catalog;"));
+ for required in [
+ "rhi_migration_catalog",
+ "rhi_schema_catalog",
+ "validate_rhi_state_catalogs",
+ "RhiStateCatalogError",
+ ] {
+ assert!(
+ ROOT.contains(required),
+ "RHI root API is missing {required}"
+ );
+ }
+}
+
+#[test]
fn human_verification_contract_is_extbuild_only_through_rcld_170() {
for required in [
"cargo extbuild doctor",
diff --git a/tests/services_hardening_state_catalog.rs b/tests/services_hardening_state_catalog.rs
@@ -0,0 +1,171 @@
+#![forbid(unsafe_code)]
+
+use std::error::Error;
+
+use radroots_service_sqlite::{
+ MigrationCatalog, MigrationChecksum, MigrationDescriptor, SchemaCatalog, SchemaObject,
+ SchemaObjectKind, SchemaVersionCatalog,
+};
+use rhi::{
+ RHI_MIGRATION_CATALOG_SHA256, RHI_STATE_SCHEMA_CATALOG_SHA256, RHI_STATE_SCHEMA_VERSION,
+ RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT, RHI_STATE_SCHEMA_VERSION_1_SHA256,
+ RhiStateCatalogErrorKind, rhi_migration_catalog, rhi_schema_catalog,
+ validate_rhi_state_catalogs,
+};
+
+const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
+const LIB_SOURCE: &str = include_str!("../src/lib.rs");
+const MANIFEST: &str = include_str!("../Cargo.toml");
+
+#[test]
+fn schema_v1_and_empty_migration_catalog_have_exact_literal_identities() {
+ let migrations = rhi_migration_catalog().expect("RHI migration catalog");
+ let schema = rhi_schema_catalog().expect("RHI schema catalog");
+
+ assert_eq!(RHI_STATE_SCHEMA_VERSION, 1);
+ assert!(migrations.descriptors().is_empty());
+ assert_eq!(migrations.current_version(), 1);
+ assert_eq!(
+ migrations.digest().as_bytes(),
+ &RHI_MIGRATION_CATALOG_SHA256
+ );
+
+ assert_eq!(schema.versions().len(), 1);
+ let version = schema.versions()[0];
+ assert_eq!(version.version(), 1);
+ assert_eq!(
+ version.object_count(),
+ RHI_STATE_SCHEMA_VERSION_1_OBJECT_COUNT
+ );
+ assert_eq!(version.object_count(), 6);
+ assert_eq!(
+ version.digest().as_bytes(),
+ &RHI_STATE_SCHEMA_VERSION_1_SHA256
+ );
+ assert_eq!(schema.digest().as_bytes(), &RHI_STATE_SCHEMA_CATALOG_SHA256);
+ assert_eq!(schema.migration_catalog_digest(), migrations.digest());
+ validate_rhi_state_catalogs(&migrations, &schema).expect("exact catalogs");
+
+ assert_eq!(
+ lower_hex(&RHI_MIGRATION_CATALOG_SHA256),
+ "ec89dc8f7b6c2a11b967e33808e4031e29b3970ffee4959bff9bad352877ee9b"
+ );
+ assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_VERSION_1_SHA256),
+ "94dc66fbca601679615c055229dc0db6119f5bd92b04390c67f698a036fa78ae"
+ );
+ assert_eq!(
+ lower_hex(&RHI_STATE_SCHEMA_CATALOG_SHA256),
+ "2309153f3b49754887c548a7459b3e09099c60f7146b373c8f96706c6768d791"
+ );
+}
+
+#[test]
+fn independent_validator_rejects_migration_or_schema_drift() {
+ const SQL: &str = "CREATE TABLE unexpected (value INTEGER NOT NULL) STRICT";
+ let migration =
+ MigrationDescriptor::sql(2, "unexpected_schema", SQL, MigrationChecksum::for_sql(SQL))
+ .expect("valid drift fixture");
+ let migrations = MigrationCatalog::new([migration]).expect("drift migration catalog");
+ let expected_schema = rhi_schema_catalog().expect("expected schema");
+ assert_eq!(
+ validate_rhi_state_catalogs(&migrations, &expected_schema)
+ .expect_err("migration drift")
+ .kind(),
+ RhiStateCatalogErrorKind::CatalogMismatch
+ );
+
+ let empty_migrations = rhi_migration_catalog().expect("empty migrations");
+ let object_digest =
+ SchemaObject::computed_digest(SchemaObjectKind::Table, "unexpected", "unexpected", SQL)
+ .expect("object digest");
+ let object = SchemaObject::new(
+ SchemaObjectKind::Table,
+ "unexpected",
+ "unexpected",
+ SQL,
+ object_digest,
+ )
+ .expect("schema object");
+ let snapshot_digest =
+ SchemaVersionCatalog::computed_digest(1, [object.clone()]).expect("snapshot digest");
+ let version = SchemaVersionCatalog::new(1, [object], snapshot_digest).expect("version");
+ let schema = SchemaCatalog::new(&empty_migrations, [version]).expect("drift schema catalog");
+ assert_eq!(
+ validate_rhi_state_catalogs(&empty_migrations, &schema)
+ .expect_err("schema drift")
+ .kind(),
+ RhiStateCatalogErrorKind::CatalogMismatch
+ );
+}
+
+#[test]
+fn catalog_errors_are_stable_source_free_and_redacted() {
+ let migrations = rhi_migration_catalog().expect("migration catalog");
+ let object_digest = SchemaObject::computed_digest(
+ SchemaObjectKind::Table,
+ "secret_table",
+ "secret_table",
+ "secret SQL text",
+ )
+ .expect("object digest");
+ let object = SchemaObject::new(
+ SchemaObjectKind::Table,
+ "secret_table",
+ "secret_table",
+ "secret SQL text",
+ object_digest,
+ )
+ .expect("object");
+ let snapshot =
+ SchemaVersionCatalog::computed_digest(1, [object.clone()]).expect("snapshot digest");
+ let version = SchemaVersionCatalog::new(1, [object], snapshot).expect("version");
+ let schema = SchemaCatalog::new(&migrations, [version]).expect("schema catalog");
+ let error = validate_rhi_state_catalogs(&migrations, &schema).expect_err("mismatch");
+
+ assert_eq!(error.kind(), RhiStateCatalogErrorKind::CatalogMismatch);
+ assert_eq!(error.code(), "state_catalog_mismatch");
+ assert!(Error::source(&error).is_none());
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains("secret"));
+ assert!(!rendered.contains(&lower_hex(snapshot.as_bytes())));
+}
+
+#[test]
+fn catalog_source_is_pure_pinned_and_uses_only_the_shared_authority() {
+ assert!(MANIFEST.contains(
+ "radroots_service_sqlite = { git = \"https://github.com/radrootslabs/lib\", rev = \"7d7b454b4c9ed86569671993bd03ca868b676665\", version = \"=0.1.0-alpha\" }"
+ ));
+ assert!(LIB_SOURCE.contains("mod state_catalog;"));
+ assert!(!LIB_SOURCE.contains("pub mod state_catalog;"));
+ assert!(CATALOG_SOURCE.contains("MigrationCatalog::new([])"));
+ assert!(CATALOG_SOURCE.contains("SchemaDigest::from_bytes("));
+ assert!(!CATALOG_SOURCE.contains("computed_digest"));
+ for forbidden in [
+ "sqlx::",
+ "rusqlite",
+ "libsqlite3_sys",
+ "CREATE TABLE",
+ "raw_sql",
+ "std::fs",
+ "std::path",
+ "Connection",
+ "Transaction",
+ "MigrationDescriptor",
+ ] {
+ assert!(
+ !CATALOG_SOURCE.contains(forbidden),
+ "found forbidden catalog authority `{forbidden}`"
+ );
+ }
+}
+
+fn lower_hex(bytes: &[u8]) -> String {
+ const DIGITS: &[u8; 16] = b"0123456789abcdef";
+ let mut output = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ output.push(char::from(DIGITS[usize::from(byte >> 4)]));
+ output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
+ }
+ output
+}