//! Signed, account-scoped HoloLake module package runtime. //! //! A repository is never executable input. The runtime accepts only an immutable //! detached-minisign package, validates a declarative manifest, persists lifecycle //! state in SQLite and writes a hash-chained receipt for every mutation. Package //! mount does not load native code or arbitrary webview JavaScript. use base64::{engine::general_purpose::STANDARD, Engine as _}; use minisign_verify::{PublicKey, Signature}; use ring::digest::{digest, SHA256}; use rusqlite::{params, Connection, OptionalExtension, Transaction}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::BTreeSet; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::AppHandle; use uuid::Uuid; const CONTRACT_RAW: &str = include_str!("../../contracts/module-package-runtime.json"); const RELEASE_TRUST_RAW: &str = include_str!("../release-trust.json"); const CONTRACT_SCHEMA: &str = "hololake.module-package-runtime/v1"; const PACKAGE_SCHEMA: &str = "hololake.module-package/v1"; const HOST_VERSION: &str = "0.5.0"; const MAX_PACKAGE_BYTES: u64 = 16 * 1024 * 1024; const NATIVE_COMPOSITION_NUMBER: &str = "HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001"; const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod" ); const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig" ); const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001"; const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod" ); const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig" ); const PERSONA_CHANNEL_BODY_NUMBER: &str = "HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001"; const PERSONA_CHANNEL_BODY_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod" ); const PERSONA_CHANNEL_BODY_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-LOCAL-PERSONA-CHANNEL-BODY-0001-0.1.0.ghmod.sig" ); const EDUCATION_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001"; const EDUCATION_WORKBENCH_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod" ); const EDUCATION_WORKBENCH_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001-0.1.0.ghmod.sig" ); const WEB_NOVEL_WORKBENCH_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001"; const WEB_NOVEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod" ); const WEB_NOVEL_WORKBENCH_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-WORKBENCH-0001-0.1.0.ghmod.sig" ); const WEB_NOVEL_OUTLINE_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001"; const WEB_NOVEL_OUTLINE_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod" ); const WEB_NOVEL_OUTLINE_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-OUTLINE-0001-0.1.0.ghmod.sig" ); const WEB_NOVEL_GRID_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001"; const WEB_NOVEL_GRID_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod" ); const WEB_NOVEL_GRID_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-GRID-0001-0.1.0.ghmod.sig" ); const WEB_NOVEL_STORYWORLD_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001"; const WEB_NOVEL_STORYWORLD_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod" ); const WEB_NOVEL_STORYWORLD_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-STORYWORLD-0001-0.1.0.ghmod.sig" ); const WEB_NOVEL_DELIVERY_NUMBER: &str = "HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001"; const WEB_NOVEL_DELIVERY_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod" ); const WEB_NOVEL_DELIVERY_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-WEB-NOVEL-DELIVERY-0001-0.1.0.ghmod.sig" ); const MOBILE_SYNC_NUMBER: &str = "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001"; const MOBILE_SYNC_PACKAGE: &[u8] = include_bytes!("../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod"); const MOBILE_SYNC_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-MOBILE-SYNC-0001-0.1.0.ghmod.sig" ); const DYNAMIC_WORLD_SURFACE_NUMBER: &str = "HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001"; const DYNAMIC_WORLD_SURFACE_PACKAGE: &[u8] = include_bytes!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod" ); const DYNAMIC_WORLD_SURFACE_SIGNATURE: &str = include_str!( "../../fixtures/module-packages/HLP-MOD-OFFICIAL-DYNAMIC-WORLD-SURFACE-0001-0.1.0.ghmod.sig" ); struct BundledModuleSource { module_number: &'static str, package: &'static [u8], signature: &'static str, } const BUNDLED_MODULES: &[BundledModuleSource] = &[ BundledModuleSource { module_number: NATIVE_COMPOSITION_NUMBER, package: NATIVE_COMPOSITION_PACKAGE, signature: NATIVE_COMPOSITION_SIGNATURE, }, BundledModuleSource { module_number: CHANNEL_WORKBENCH_NUMBER, package: CHANNEL_WORKBENCH_PACKAGE, signature: CHANNEL_WORKBENCH_SIGNATURE, }, BundledModuleSource { module_number: PERSONA_CHANNEL_BODY_NUMBER, package: PERSONA_CHANNEL_BODY_PACKAGE, signature: PERSONA_CHANNEL_BODY_SIGNATURE, }, BundledModuleSource { module_number: EDUCATION_WORKBENCH_NUMBER, package: EDUCATION_WORKBENCH_PACKAGE, signature: EDUCATION_WORKBENCH_SIGNATURE, }, BundledModuleSource { module_number: WEB_NOVEL_WORKBENCH_NUMBER, package: WEB_NOVEL_WORKBENCH_PACKAGE, signature: WEB_NOVEL_WORKBENCH_SIGNATURE, }, BundledModuleSource { module_number: WEB_NOVEL_OUTLINE_NUMBER, package: WEB_NOVEL_OUTLINE_PACKAGE, signature: WEB_NOVEL_OUTLINE_SIGNATURE, }, BundledModuleSource { module_number: WEB_NOVEL_GRID_NUMBER, package: WEB_NOVEL_GRID_PACKAGE, signature: WEB_NOVEL_GRID_SIGNATURE, }, BundledModuleSource { module_number: WEB_NOVEL_STORYWORLD_NUMBER, package: WEB_NOVEL_STORYWORLD_PACKAGE, signature: WEB_NOVEL_STORYWORLD_SIGNATURE, }, BundledModuleSource { module_number: WEB_NOVEL_DELIVERY_NUMBER, package: WEB_NOVEL_DELIVERY_PACKAGE, signature: WEB_NOVEL_DELIVERY_SIGNATURE, }, BundledModuleSource { module_number: MOBILE_SYNC_NUMBER, package: MOBILE_SYNC_PACKAGE, signature: MOBILE_SYNC_SIGNATURE, }, BundledModuleSource { module_number: DYNAMIC_WORLD_SURFACE_NUMBER, package: DYNAMIC_WORLD_SURFACE_PACKAGE, signature: DYNAMIC_WORLD_SURFACE_SIGNATURE, }, ]; #[derive(Debug, Deserialize)] struct RuntimeContract { schema: String, record_id: String, package_schema: String, host: HostContract, registered_adapters: Vec, lifecycle: Vec, } #[derive(Debug, Deserialize)] struct HostContract { version: String, maximum_package_bytes: u64, arbitrary_native_code: bool, arbitrary_webview_javascript: bool, declarative_payload_only: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ModulePackage { schema: String, manifest: ModuleManifest, payload: Value, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ModuleManifest { module_number: String, registration_class: String, display_name: String, version: String, minimum_host_version: String, adapter: String, content_digest: String, permissions: Vec, user_data_schema: String, self_test: ModuleSelfTest, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ModuleSelfTest { kind: String, expected_content_digest: String, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct VerifyModulePackageInput { pub(crate) package_path: String, pub(crate) signature_path: String, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct InstallModulePackageInput { pub(crate) package_path: String, pub(crate) signature_path: String, pub(crate) expected_package_sha256: String, pub(crate) human_confirmed_permission_expansion: bool, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ModuleNumberInput { pub(crate) module_number: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct VerifiedModulePackage { pub(crate) state: String, pub(crate) package_sha256: String, pub(crate) module_number: String, pub(crate) display_name: String, pub(crate) version: String, pub(crate) adapter: String, pub(crate) permissions: Vec, pub(crate) signature_verified: bool, pub(crate) package_code_executed: bool, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct InstalledModule { pub(crate) module_number: String, pub(crate) display_name: String, pub(crate) version: String, pub(crate) adapter: String, pub(crate) state: String, pub(crate) package_sha256: String, pub(crate) previous_package_sha256: Option, pub(crate) permissions: Vec, pub(crate) user_data_preserved: bool, pub(crate) updated_at_unix_ms: u64, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ModuleRuntimeSnapshot { pub(crate) schema: &'static str, pub(crate) state: &'static str, pub(crate) host_version: &'static str, pub(crate) package_code_executed: bool, pub(crate) modules: Vec, pub(crate) receipt_count: u64, pub(crate) latest_receipt_hash: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ModuleMutationReceipt { receipt_id: String, event: String, module_number: String, state: String, package_sha256: String, previous_receipt_hash: String, receipt_hash: String, observed_at_unix_ms: u64, user_data_preserved: bool, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ActivateBundledModuleInput { module_number: String, human_confirmed_permission_expansion: bool, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BundledModuleDescriptor { module_number: String, display_name: String, version: String, adapter: String, permissions: Vec, package_sha256: String, installed_state: String, signature_verified: bool, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BundledModuleActivation { schema: &'static str, state: String, module_number: String, receipts: Vec, snapshot: ModuleRuntimeSnapshot, } fn now_unix_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis() as u64) .unwrap_or(0) } fn sha256_hex(bytes: &[u8]) -> String { digest(&SHA256, bytes) .as_ref() .iter() .map(|byte| format!("{byte:02x}")) .collect() } fn is_sha256(value: &str) -> bool { value.len() == 64 && value.chars().all(|item| item.is_ascii_hexdigit()) } fn canonical_value(value: &Value) -> Value { match value { Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()), Value::Object(object) => { let mut keys = object.keys().collect::>(); keys.sort(); let mut canonical = Map::new(); for key in keys { canonical.insert(key.clone(), canonical_value(&object[key])); } Value::Object(canonical) } _ => value.clone(), } } fn payload_digest(value: &Value) -> Result { let bytes = serde_json::to_vec(&canonical_value(value)) .map_err(|error| format!("HOLOLAKE_MODULE_PAYLOAD_INVALID: {error}"))?; Ok(sha256_hex(&bytes)) } fn contract() -> Result { let contract: RuntimeContract = serde_json::from_str(CONTRACT_RAW) .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_CONTRACT_INVALID: {error}"))?; if contract.schema != CONTRACT_SCHEMA || contract.record_id != "HLP-MODULE-PACKAGE-RUNTIME-001" || contract.package_schema != PACKAGE_SCHEMA || contract.host.version != HOST_VERSION || contract.host.maximum_package_bytes != MAX_PACKAGE_BYTES || contract.host.arbitrary_native_code || contract.host.arbitrary_webview_javascript || !contract.host.declarative_payload_only || contract.registered_adapters.len() != 7 || contract.lifecycle.len() != 6 { return Err("HOLOLAKE_MODULE_RUNTIME_CONTRACT_BOUNDARY_INVALID".into()); } Ok(contract) } fn decode_outer_base64(value: &str, label: &str) -> Result { let bytes = STANDARD .decode(value.trim()) .map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?; String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) } fn verify_signature_raw( package: &[u8], signature_raw: &str, trust_raw: &str, ) -> Result<(), String> { let trust: Value = serde_json::from_str(trust_raw) .map_err(|error| format!("HOLOLAKE_MODULE_TRUST_INVALID: {error}"))?; if trust.get("state").and_then(Value::as_str) != Some("PROVISIONED") { return Err("HOLOLAKE_MODULE_TRUST_UNPROVISIONED".into()); } let encoded_key = trust .get("publicKey") .and_then(Value::as_str) .ok_or("HOLOLAKE_MODULE_TRUST_KEY_MISSING")?; let public_key = PublicKey::decode(&decode_outer_base64(encoded_key, "HOLOLAKE_MODULE_KEY")?) .map_err(|error| format!("HOLOLAKE_MODULE_KEY_INVALID: {error}"))?; let signature = Signature::decode(&decode_outer_base64( signature_raw, "HOLOLAKE_MODULE_SIGNATURE", )?) .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_INVALID: {error}"))?; public_key .verify(package, &signature, true) .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_REJECTED: {error}")) } fn valid_number(value: &str, registration_class: &str) -> bool { let prefix = match registration_class { "OFFICIAL_LIGHTHOUSE" => "HLP-MOD-OFFICIAL-", "PRIVATE_CHANNEL_LOCAL" => "HLP-MOD-LOCAL-", _ => return false, }; value.starts_with(prefix) && value.len() <= 96 && value .chars() .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-') } fn version_triplet(value: &str) -> Option<(u64, u64, u64)> { let parts = value.split('.').collect::>(); if parts.len() != 3 { return None; } Some(( parts[0].parse().ok()?, parts[1].parse().ok()?, parts[2].parse().ok()?, )) } fn validate_package(package: &ModulePackage, contract: &RuntimeContract) -> Result<(), String> { let manifest = &package.manifest; let permissions = manifest.permissions.iter().collect::>(); if package.schema != PACKAGE_SCHEMA || !valid_number(&manifest.module_number, &manifest.registration_class) || manifest.display_name.trim().is_empty() || manifest.display_name.len() > 120 || version_triplet(&manifest.version).is_none() || version_triplet(&manifest.minimum_host_version).is_none() || version_triplet(&manifest.minimum_host_version) > version_triplet(HOST_VERSION) || !contract.registered_adapters.contains(&manifest.adapter) || !is_sha256(&manifest.content_digest) || permissions.len() != manifest.permissions.len() || manifest.permissions.len() > 32 || manifest.permissions.iter().any(|permission| { permission.is_empty() || permission.len() > 80 || !permission .chars() .all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '_') }) || manifest.user_data_schema.trim().is_empty() || manifest.self_test.kind != "DECLARATIVE_SCHEMA_V1" || manifest.self_test.expected_content_digest != manifest.content_digest || !package.payload.is_object() || payload_digest(&package.payload)? != manifest.content_digest { return Err("HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID".into()); } Ok(()) } fn read_verified_package( package_path: &Path, signature_path: &Path, trust_raw: &str, ) -> Result<(Vec, ModulePackage, String), String> { let metadata = fs::symlink_metadata(package_path) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_METADATA_FAILED: {error}"))?; let signature_metadata = fs::symlink_metadata(signature_path) .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_METADATA_FAILED: {error}"))?; if metadata.file_type().is_symlink() || signature_metadata.file_type().is_symlink() || !metadata.is_file() || !signature_metadata.is_file() || metadata.len() == 0 || metadata.len() > MAX_PACKAGE_BYTES || signature_metadata.len() == 0 || signature_metadata.len() > 16 * 1024 { return Err("HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID".into()); } let bytes = fs::read(package_path) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_READ_FAILED: {error}"))?; let signature_raw = fs::read_to_string(signature_path) .map_err(|error| format!("HOLOLAKE_MODULE_SIGNATURE_READ_FAILED: {error}"))?; read_verified_package_bytes(&bytes, &signature_raw, trust_raw) } fn read_verified_package_bytes( bytes: &[u8], signature_raw: &str, trust_raw: &str, ) -> Result<(Vec, ModulePackage, String), String> { verify_signature_raw(bytes, signature_raw, trust_raw)?; let package: ModulePackage = serde_json::from_slice(bytes) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_JSON_INVALID: {error}"))?; validate_package(&package, &contract()?)?; let package_sha256 = sha256_hex(bytes); Ok((bytes.to_vec(), package, package_sha256)) } fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> { BUNDLED_MODULES .iter() .find(|source| source.module_number == module_number) .map(|source| (source.package, source.signature)) .ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()) } fn runtime_root(app: &AppHandle) -> Result { crate::authenticated_storage::account_storage_root(app, "module-runtime-v1") } fn open_db(root: &Path) -> Result { fs::create_dir_all(root) .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_ROOT_FAILED: {error}"))?; let connection = Connection::open(root.join("module-runtime.sqlite3")) .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_OPEN_FAILED: {error}"))?; connection .busy_timeout(std::time::Duration::from_secs(5)) .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_TIMEOUT_FAILED: {error}"))?; connection .execute_batch( "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS installed_modules( module_number TEXT PRIMARY KEY, display_name TEXT NOT NULL, version TEXT NOT NULL, adapter TEXT NOT NULL, state TEXT NOT NULL, package_sha256 TEXT NOT NULL, previous_package_sha256 TEXT, permissions_json TEXT NOT NULL, manifest_json TEXT NOT NULL, updated_at_unix_ms INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS module_receipts( sequence INTEGER PRIMARY KEY AUTOINCREMENT, receipt_id TEXT NOT NULL UNIQUE, event TEXT NOT NULL, module_number TEXT NOT NULL, state TEXT NOT NULL, package_sha256 TEXT NOT NULL, previous_receipt_hash TEXT NOT NULL, receipt_hash TEXT NOT NULL, observed_at_unix_ms INTEGER NOT NULL );", ) .map_err(|error| format!("HOLOLAKE_MODULE_RUNTIME_DB_SCHEMA_FAILED: {error}"))?; Ok(connection) } fn known_state(state: &str) -> bool { matches!( state, "INSTALLED_DORMANT" | "MOUNTED_PENDING_SELF_TEST" | "ACTIVE" | "DORMANT" | "ROLLBACK_PENDING_SELF_TEST" | "FAILED_CLOSED" ) } fn verify_receipt_chain(connection: &Connection) -> Result<(), String> { let mut statement = connection .prepare( "SELECT receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM module_receipts ORDER BY sequence", ) .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_PREPARE_FAILED: {error}"))?; let rows = statement .query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, String>(5)?, row.get::<_, String>(6)?, row.get::<_, u64>(7)?, )) }) .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_QUERY_FAILED: {error}"))?; let mut previous = "0".repeat(64); for row in rows { let (receipt_id, event, module_number, state, package_sha256, linked, stored, observed) = row.map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_VERIFY_ROW_FAILED: {error}"))?; let expected = sha256_hex( format!( "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{linked}\n{observed}" ) .as_bytes(), ); if linked != previous || stored != expected || !is_sha256(&stored) || !is_sha256(&package_sha256) || !known_state(&state) { return Err("HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID".into()); } previous = stored; } Ok(()) } fn verify_persisted_modules(connection: &Connection, root: &Path) -> Result<(), String> { let mut statement = connection .prepare( "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json FROM installed_modules ORDER BY module_number", ) .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_PREPARE_FAILED: {error}"))?; let rows = statement .query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, String>(4)?, row.get::<_, String>(5)?, row.get::<_, Option>(6)?, row.get::<_, String>(7)?, row.get::<_, String>(8)?, )) }) .map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_QUERY_FAILED: {error}"))?; for row in rows { let ( number, display, version, adapter, state, digest, previous, permissions_raw, manifest_raw, ) = row.map_err(|error| format!("HOLOLAKE_MODULE_PERSISTED_VERIFY_ROW_FAILED: {error}"))?; if !known_state(&state) || !is_sha256(&digest) { return Err("HOLOLAKE_MODULE_PERSISTED_STATE_INVALID".into()); } let manifest: ModuleManifest = serde_json::from_str(&manifest_raw) .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID".to_string())?; let permissions: Vec = serde_json::from_str(&permissions_raw) .map_err(|_| "HOLOLAKE_MODULE_PERSISTED_PERMISSIONS_INVALID".to_string())?; let (_, package, verified_digest) = read_verified_package( &package_file(root, &digest), &signature_file(root, &digest), RELEASE_TRUST_RAW, )?; if verified_digest != digest || package.manifest != manifest || manifest.module_number != number || manifest.display_name != display || manifest.version != version || manifest.adapter != adapter || manifest.permissions != permissions { return Err("HOLOLAKE_MODULE_PERSISTED_IDENTITY_INVALID".into()); } if let Some(previous) = previous { if !is_sha256(&previous) { return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_INVALID".into()); } let (_, previous_package, previous_digest) = read_verified_package( &package_file(root, &previous), &signature_file(root, &previous), RELEASE_TRUST_RAW, )?; if previous_digest != previous || previous_package.manifest.module_number != number { return Err("HOLOLAKE_MODULE_PREVIOUS_PACKAGE_IDENTITY_INVALID".into()); } } } Ok(()) } fn package_file(root: &Path, digest: &str) -> PathBuf { root.join("packages").join(format!("{digest}.ghmod")) } fn signature_file(root: &Path, digest: &str) -> PathBuf { root.join("packages").join(format!("{digest}.ghmod.sig")) } fn copy_atomic(source: &Path, target: &Path) -> Result<(), String> { let parent = target .parent() .ok_or("HOLOLAKE_MODULE_PACKAGE_TARGET_INVALID")?; fs::create_dir_all(parent) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_STORE_FAILED: {error}"))?; if target.exists() { return Ok(()); } let temporary = parent.join(format!(".install-{}", Uuid::new_v4())); fs::copy(source, &temporary) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COPY_FAILED: {error}"))?; fs::rename(&temporary, target) .map_err(|error| format!("HOLOLAKE_MODULE_PACKAGE_COMMIT_FAILED: {error}")) } fn previous_receipt_hash(transaction: &Transaction<'_>) -> Result { transaction .query_row( "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", [], |row| row.get(0), ) .optional() .map(|value| value.unwrap_or_else(|| "0".repeat(64))) .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_READ_FAILED: {error}")) } fn append_receipt( transaction: &Transaction<'_>, event: &str, module_number: &str, state: &str, package_sha256: &str, observed_at_unix_ms: u64, ) -> Result { let receipt_id = format!("HLP-MOD-RCPT-{}", Uuid::new_v4()); let previous = previous_receipt_hash(transaction)?; let receipt_hash = sha256_hex( format!( "HLP-MODULE-RECEIPT-v1\n{receipt_id}\n{event}\n{module_number}\n{state}\n{package_sha256}\n{previous}\n{observed_at_unix_ms}" ) .as_bytes(), ); transaction .execute( "INSERT INTO module_receipts(receipt_id,event,module_number,state,package_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", params![receipt_id, event, module_number, state, package_sha256, previous, receipt_hash, observed_at_unix_ms], ) .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_WRITE_FAILED: {error}"))?; Ok(ModuleMutationReceipt { receipt_id, event: event.into(), module_number: module_number.into(), state: state.into(), package_sha256: package_sha256.into(), previous_receipt_hash: previous, receipt_hash, observed_at_unix_ms, user_data_preserved: true, }) } fn permissions_from_json(raw: &str) -> BTreeSet { serde_json::from_str::>(raw) .unwrap_or_default() .into_iter() .collect() } fn materialize_bundled_source( root: &Path, module_number: &str, package: &[u8], signature: &str, ) -> Result<(PathBuf, PathBuf), String> { let source_root = root.join("bundled-sources"); fs::create_dir_all(&source_root) .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_ROOT_FAILED: {error}"))?; let digest = sha256_hex(package); let package_path = source_root.join(format!("{module_number}-{digest}.ghmod")); let signature_path = source_root.join(format!("{module_number}-{digest}.ghmod.sig")); for (path, bytes) in [ (&package_path, package), (&signature_path, signature.as_bytes()), ] { if path.exists() { let current = fs::read(path) .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_READ_FAILED: {error}"))?; if current != bytes { return Err("HOLOLAKE_BUNDLED_MODULE_SOURCE_TAMPERED".into()); } continue; } let temporary = source_root.join(format!(".bundled-{}", Uuid::new_v4())); fs::write(&temporary, bytes) .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_WRITE_FAILED: {error}"))?; fs::rename(&temporary, path) .map_err(|error| format!("HOLOLAKE_BUNDLED_MODULE_SOURCE_COMMIT_FAILED: {error}"))?; } Ok((package_path, signature_path)) } pub async fn get_bundled_module_catalog( app: AppHandle, ) -> Result, String> { let root = runtime_root(&app)?; let snapshot = snapshot_at(&root)?; BUNDLED_MODULES .iter() .map(|source| { let (_, package, package_sha256) = read_verified_package_bytes(source.package, source.signature, RELEASE_TRUST_RAW)?; if package.manifest.module_number != source.module_number { return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); } let installed_state = snapshot .modules .iter() .find(|module| module.module_number == source.module_number) .map(|module| module.state.clone()) .unwrap_or_else(|| "NOT_INSTALLED".into()); Ok(BundledModuleDescriptor { module_number: package.manifest.module_number, display_name: package.manifest.display_name, version: package.manifest.version, adapter: package.manifest.adapter, permissions: package.manifest.permissions, package_sha256, installed_state, signature_verified: true, }) }) .collect() } pub async fn activate_bundled_module( app: AppHandle, input: ActivateBundledModuleInput, ) -> Result { let root = runtime_root(&app)?; let (bytes, signature) = bundled_package(&input.module_number)?; let (_, package, package_sha256) = read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?; if package.manifest.module_number != input.module_number { return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into()); } let mut receipts = Vec::new(); let before = snapshot_at(&root)?; let installed = before .modules .iter() .find(|module| module.module_number == input.module_number); if installed .is_some_and(|module| module.state == "ACTIVE" && module.package_sha256 == package_sha256) { return Ok(BundledModuleActivation { schema: "hololake.bundled-module-activation/v1", state: "ALREADY_ACTIVE".into(), module_number: input.module_number, receipts, snapshot: before, }); } let needs_install = installed .map(|module| module.package_sha256 != package_sha256 || module.state == "FAILED_CLOSED") .unwrap_or(true); if needs_install { let (package_path, signature_path) = materialize_bundled_source(&root, &input.module_number, bytes, signature)?; receipts.push(install_module_package_at( &root, &InstallModulePackageInput { package_path: package_path.to_string_lossy().into_owned(), signature_path: signature_path.to_string_lossy().into_owned(), expected_package_sha256: package_sha256, human_confirmed_permission_expansion: input.human_confirmed_permission_expansion, }, RELEASE_TRUST_RAW, now_unix_ms(), )?); } let current = snapshot_at(&root)? .modules .into_iter() .find(|module| module.module_number == input.module_number) .ok_or("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_MISSING")?; match current.state.as_str() { "INSTALLED_DORMANT" | "DORMANT" => { receipts.push(transition( &root, &input.module_number, &["INSTALLED_DORMANT", "DORMANT"], "MOUNTED_PENDING_SELF_TEST", "MOUNT", )?); receipts.push(self_test_module_at( &root, &input.module_number, now_unix_ms(), )?); } "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" => { receipts.push(self_test_module_at( &root, &input.module_number, now_unix_ms(), )?); } "ACTIVE" => {} _ => return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_STATE_INVALID".into()), } let snapshot = snapshot_at(&root)?; if !snapshot.modules.iter().any(|module| { module.module_number == input.module_number && module.adapter == package.manifest.adapter && module.state == "ACTIVE" }) { return Err("HOLOLAKE_BUNDLED_MODULE_ACTIVATION_NOT_ACTIVE".into()); } Ok(BundledModuleActivation { schema: "hololake.bundled-module-activation/v1", state: "ACTIVE".into(), module_number: input.module_number, receipts, snapshot, }) } pub(crate) fn require_active_module_adapter( app: &AppHandle, module_number: &str, adapter: &str, ) -> Result<(), String> { let snapshot = snapshot_at(&runtime_root(app)?)?; if snapshot.modules.iter().any(|module| { module.module_number == module_number && module.adapter == adapter && module.state == "ACTIVE" }) { Ok(()) } else { Err("HOLOLAKE_MODULE_NOT_ACTIVE".into()) } } pub async fn verify_module_package( app: AppHandle, input: VerifyModulePackageInput, ) -> Result { let _ = runtime_root(&app)?; let (_, package, package_sha256) = read_verified_package( Path::new(&input.package_path), Path::new(&input.signature_path), RELEASE_TRUST_RAW, )?; Ok(VerifiedModulePackage { state: "VERIFIED_NOT_INSTALLED".into(), package_sha256, module_number: package.manifest.module_number, display_name: package.manifest.display_name, version: package.manifest.version, adapter: package.manifest.adapter, permissions: package.manifest.permissions, signature_verified: true, package_code_executed: false, }) } pub async fn install_module_package( app: AppHandle, input: InstallModulePackageInput, ) -> Result { install_module_package_at( &runtime_root(&app)?, &input, RELEASE_TRUST_RAW, now_unix_ms(), ) } fn install_module_package_at( root: &Path, input: &InstallModulePackageInput, trust_raw: &str, observed: u64, ) -> Result { if !is_sha256(&input.expected_package_sha256) { return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_INVALID".into()); } let (bytes, package, digest) = read_verified_package( Path::new(&input.package_path), Path::new(&input.signature_path), trust_raw, )?; if digest != input.expected_package_sha256 { return Err("HOLOLAKE_MODULE_EXPECTED_DIGEST_MISMATCH".into()); } let mut connection = open_db(root)?; let existing: Option<(String, String)> = connection .query_row( "SELECT package_sha256, permissions_json FROM installed_modules WHERE module_number=?1", params![package.manifest.module_number], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional() .map_err(|error| format!("HOLOLAKE_MODULE_EXISTING_READ_FAILED: {error}"))?; let requested = package .manifest .permissions .iter() .cloned() .collect::>(); let expansion = existing .as_ref() .map(|(_, raw)| !requested.is_subset(&permissions_from_json(raw))) .unwrap_or(!requested.is_empty()); if expansion && !input.human_confirmed_permission_expansion { return Err("HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION".into()); } copy_atomic(Path::new(&input.package_path), &package_file(root, &digest))?; copy_atomic( Path::new(&input.signature_path), &signature_file(root, &digest), )?; if fs::read(package_file(root, &digest)) .map_err(|error| format!("HOLOLAKE_MODULE_STORED_PACKAGE_READ_FAILED: {error}"))? != bytes { return Err("HOLOLAKE_MODULE_STORED_PACKAGE_MISMATCH".into()); } if fs::read(signature_file(root, &digest)) .map_err(|error| format!("HOLOLAKE_MODULE_STORED_SIGNATURE_READ_FAILED: {error}"))? != fs::read(&input.signature_path) .map_err(|error| format!("HOLOLAKE_MODULE_SOURCE_SIGNATURE_READ_FAILED: {error}"))? { return Err("HOLOLAKE_MODULE_STORED_SIGNATURE_MISMATCH".into()); } fs::create_dir_all(root.join("user-data").join(&package.manifest.module_number)) .map_err(|error| format!("HOLOLAKE_MODULE_USER_DATA_ROOT_FAILED: {error}"))?; let transaction = connection .transaction() .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_TRANSACTION_FAILED: {error}"))?; let permissions_json = serde_json::to_string(&package.manifest.permissions) .map_err(|error| format!("HOLOLAKE_MODULE_PERMISSIONS_INVALID: {error}"))?; let manifest_json = serde_json::to_string(&package.manifest) .map_err(|error| format!("HOLOLAKE_MODULE_MANIFEST_INVALID: {error}"))?; let previous = existing.as_ref().map(|(value, _)| value.as_str()); transaction .execute( "INSERT INTO installed_modules(module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,manifest_json,updated_at_unix_ms) VALUES(?1,?2,?3,?4,'INSTALLED_DORMANT',?5,?6,?7,?8,?9) ON CONFLICT(module_number) DO UPDATE SET display_name=excluded.display_name,version=excluded.version,adapter=excluded.adapter,state='INSTALLED_DORMANT',package_sha256=excluded.package_sha256,previous_package_sha256=installed_modules.package_sha256,permissions_json=excluded.permissions_json,manifest_json=excluded.manifest_json,updated_at_unix_ms=excluded.updated_at_unix_ms", params![package.manifest.module_number, package.manifest.display_name, package.manifest.version, package.manifest.adapter, digest, previous, permissions_json, manifest_json, observed], ) .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_WRITE_FAILED: {error}"))?; let receipt = append_receipt( &transaction, "INSTALL", &package.manifest.module_number, "INSTALLED_DORMANT", &digest, observed, )?; transaction .commit() .map_err(|error| format!("HOLOLAKE_MODULE_INSTALL_COMMIT_FAILED: {error}"))?; Ok(receipt) } fn transition( root: &Path, module_number: &str, allowed_from: &[&str], target: &str, event: &str, ) -> Result { let mut connection = open_db(root)?; let row: (String, String) = connection .query_row( "SELECT state,package_sha256 FROM installed_modules WHERE module_number=?1", params![module_number], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional() .map_err(|error| format!("HOLOLAKE_MODULE_STATE_READ_FAILED: {error}"))? .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; if !allowed_from.contains(&row.0.as_str()) { return Err("HOLOLAKE_MODULE_LIFECYCLE_TRANSITION_INVALID".into()); } let observed = now_unix_ms(); let transaction = connection .transaction() .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_BEGIN_FAILED: {error}"))?; transaction .execute( "UPDATE installed_modules SET state=?1,updated_at_unix_ms=?2 WHERE module_number=?3 AND state=?4", params![target, observed, module_number, row.0], ) .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_WRITE_FAILED: {error}"))?; let receipt = append_receipt(&transaction, event, module_number, target, &row.1, observed)?; transaction .commit() .map_err(|error| format!("HOLOLAKE_MODULE_TRANSITION_COMMIT_FAILED: {error}"))?; Ok(receipt) } pub async fn mount_module( app: AppHandle, input: ModuleNumberInput, ) -> Result { transition( &runtime_root(&app)?, &input.module_number, &["INSTALLED_DORMANT", "DORMANT"], "MOUNTED_PENDING_SELF_TEST", "MOUNT", ) } pub async fn self_test_module( app: AppHandle, input: ModuleNumberInput, ) -> Result { self_test_module_at(&runtime_root(&app)?, &input.module_number, now_unix_ms()) } fn self_test_module_at( root: &Path, module_number: &str, observed: u64, ) -> Result { let mut connection = open_db(root)?; let row: (String, String, Option) = connection .query_row( "SELECT state,package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", params![module_number], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ) .optional() .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_STATE_FAILED: {error}"))? .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; if !matches!( row.0.as_str(), "MOUNTED_PENDING_SELF_TEST" | "ROLLBACK_PENDING_SELF_TEST" ) { return Err("HOLOLAKE_MODULE_SELF_TEST_STATE_INVALID".into()); } let verification = read_verified_package( &package_file(root, &row.1), &signature_file(root, &row.1), RELEASE_TRUST_RAW, ); let success = verification.as_ref().is_ok_and(|(_, package, digest)| { digest == &row.1 && package.manifest.module_number == module_number }); let rollback_package = row.2.as_ref().and_then(|previous| { read_verified_package( &package_file(root, previous), &signature_file(root, previous), RELEASE_TRUST_RAW, ) .ok() .filter(|(_, package, digest)| { digest == previous && package.manifest.module_number == module_number }) }); let transaction = connection .transaction() .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_BEGIN_FAILED: {error}"))?; if success { transaction .execute( "UPDATE installed_modules SET state='ACTIVE',updated_at_unix_ms=?1 WHERE module_number=?2", params![observed, module_number], ) .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_WRITE_FAILED: {error}"))?; let receipt = append_receipt( &transaction, "SELF_TEST_PASS", module_number, "ACTIVE", &row.1, observed, )?; transaction .commit() .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_COMMIT_FAILED: {error}"))?; return Ok(receipt); } let (target_state, target_digest) = if let Some((_, previous_package, previous)) = rollback_package { let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; let manifest_json = serde_json::to_string(&previous_package.manifest) .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; transaction .execute( "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=NULL,permissions_json=?5,manifest_json=?6,updated_at_unix_ms=?7 WHERE module_number=?8", params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, permissions_json, manifest_json, observed, module_number], ) .map_err(|error| format!("HOLOLAKE_MODULE_AUTO_ROLLBACK_WRITE_FAILED: {error}"))?; ("ROLLBACK_PENDING_SELF_TEST", previous) } else { transaction .execute( "UPDATE installed_modules SET state='FAILED_CLOSED',updated_at_unix_ms=?1 WHERE module_number=?2", params![observed, module_number], ) .map_err(|error| format!("HOLOLAKE_MODULE_FAIL_CLOSED_WRITE_FAILED: {error}"))?; ("FAILED_CLOSED", row.1) }; let _ = append_receipt( &transaction, "SELF_TEST_FAIL", module_number, target_state, &target_digest, observed, )?; transaction .commit() .map_err(|error| format!("HOLOLAKE_MODULE_SELF_TEST_FAILURE_COMMIT_FAILED: {error}"))?; Err(verification .err() .unwrap_or_else(|| "HOLOLAKE_MODULE_SELF_TEST_IDENTITY_MISMATCH".into())) } pub async fn unmount_module( app: AppHandle, input: ModuleNumberInput, ) -> Result { if input.module_number == MOBILE_SYNC_NUMBER { crate::mobile_sync::stop_for_unmount(&app)?; } transition( &runtime_root(&app)?, &input.module_number, &["ACTIVE", "MOUNTED_PENDING_SELF_TEST", "FAILED_CLOSED"], "DORMANT", "UNMOUNT", ) } pub async fn rollback_module( app: AppHandle, input: ModuleNumberInput, ) -> Result { let root = runtime_root(&app)?; let mut connection = open_db(&root)?; let row: (String, Option) = connection .query_row( "SELECT package_sha256,previous_package_sha256 FROM installed_modules WHERE module_number=?1", params![input.module_number], |row| Ok((row.get(0)?, row.get(1)?)), ) .optional() .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_READ_FAILED: {error}"))? .ok_or("HOLOLAKE_MODULE_NOT_INSTALLED")?; let previous = row.1.ok_or("HOLOLAKE_MODULE_ROLLBACK_NOT_AVAILABLE")?; let (_, previous_package, verified_previous) = read_verified_package( &package_file(&root, &previous), &signature_file(&root, &previous), RELEASE_TRUST_RAW, )?; if verified_previous != previous || previous_package.manifest.module_number != input.module_number { return Err("HOLOLAKE_MODULE_ROLLBACK_PACKAGE_IDENTITY_INVALID".into()); } let permissions_json = serde_json::to_string(&previous_package.manifest.permissions) .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_PERMISSIONS_INVALID: {error}"))?; let manifest_json = serde_json::to_string(&previous_package.manifest) .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_MANIFEST_INVALID: {error}"))?; let observed = now_unix_ms(); let transaction = connection .transaction() .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_BEGIN_FAILED: {error}"))?; transaction .execute( "UPDATE installed_modules SET display_name=?1,version=?2,adapter=?3,state='ROLLBACK_PENDING_SELF_TEST',package_sha256=?4,previous_package_sha256=?5,permissions_json=?6,manifest_json=?7,updated_at_unix_ms=?8 WHERE module_number=?9", params![previous_package.manifest.display_name, previous_package.manifest.version, previous_package.manifest.adapter, previous, row.0, permissions_json, manifest_json, observed, input.module_number], ) .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_WRITE_FAILED: {error}"))?; let receipt = append_receipt( &transaction, "ROLLBACK", &input.module_number, "ROLLBACK_PENDING_SELF_TEST", &previous, observed, )?; transaction .commit() .map_err(|error| format!("HOLOLAKE_MODULE_ROLLBACK_COMMIT_FAILED: {error}"))?; Ok(receipt) } pub async fn get_module_runtime_snapshot(app: AppHandle) -> Result { let root = runtime_root(&app)?; snapshot_at(&root) } fn snapshot_at(root: &Path) -> Result { let connection = open_db(root)?; verify_receipt_chain(&connection)?; verify_persisted_modules(&connection, root)?; let mut statement = connection .prepare( "SELECT module_number,display_name,version,adapter,state,package_sha256,previous_package_sha256,permissions_json,updated_at_unix_ms FROM installed_modules ORDER BY module_number", ) .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_PREPARE_FAILED: {error}"))?; let modules = statement .query_map([], |row| { let permissions_json: String = row.get(7)?; Ok(InstalledModule { module_number: row.get(0)?, display_name: row.get(1)?, version: row.get(2)?, adapter: row.get(3)?, state: row.get(4)?, package_sha256: row.get(5)?, previous_package_sha256: row.get(6)?, permissions: serde_json::from_str(&permissions_json).unwrap_or_default(), user_data_preserved: true, updated_at_unix_ms: row.get(8)?, }) }) .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_QUERY_FAILED: {error}"))? .collect::, _>>() .map_err(|error| format!("HOLOLAKE_MODULE_SNAPSHOT_ROW_FAILED: {error}"))?; let receipt_count: u64 = connection .query_row("SELECT COUNT(*) FROM module_receipts", [], |row| row.get(0)) .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_COUNT_FAILED: {error}"))?; let latest_receipt_hash = connection .query_row( "SELECT receipt_hash FROM module_receipts ORDER BY sequence DESC LIMIT 1", [], |row| row.get(0), ) .optional() .map_err(|error| format!("HOLOLAKE_MODULE_RECEIPT_TAIL_FAILED: {error}"))? .unwrap_or_else(|| "0".repeat(64)); Ok(ModuleRuntimeSnapshot { schema: "hololake.module-runtime-snapshot/v1", state: "ACTIVE_SIGNED_DECLARATIVE_PACKAGES_ONLY", host_version: HOST_VERSION, package_code_executed: false, modules, receipt_count, latest_receipt_hash, }) } pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> { let _ = contract()?; let root = runtime_root(app); match root { Ok(root) => { let _ = snapshot_at(&root)?; Ok(()) } Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()), Err(error) => Err(error), } } #[cfg(test)] mod tests { use super::*; fn signed_fixture() -> (PathBuf, PathBuf, String) { let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../fixtures/module-packages/HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001-0.1.0.ghmod"); let signature = package.with_extension("ghmod.sig"); let digest = sha256_hex(&fs::read(&package).unwrap()); (package, signature, digest) } fn install_fixture(root: &Path, observed: u64) -> String { let (package, signature, digest) = signed_fixture(); install_module_package_at( root, &InstallModulePackageInput { package_path: package.to_string_lossy().into_owned(), signature_path: signature.to_string_lossy().into_owned(), expected_package_sha256: digest.clone(), human_confirmed_permission_expansion: true, }, RELEASE_TRUST_RAW, observed, ) .unwrap(); digest } #[test] fn contract_keeps_packages_declarative_and_lifecycle_closed() { let contract = contract().unwrap(); assert_eq!(contract.registered_adapters.len(), 7); assert!(!contract.host.arbitrary_native_code); assert!(!contract.host.arbitrary_webview_javascript); assert!(contract.host.declarative_payload_only); } #[test] fn package_number_classes_and_content_digest_are_exact() { let payload = serde_json::json!({"views":[],"entry":"home"}); let digest = payload_digest(&payload).unwrap(); let package = ModulePackage { schema: PACKAGE_SCHEMA.into(), manifest: ModuleManifest { module_number: "HLP-MOD-LOCAL-0001".into(), registration_class: "PRIVATE_CHANNEL_LOCAL".into(), display_name: "Local module".into(), version: "1.0.0".into(), minimum_host_version: "0.5.0".into(), adapter: "native-composition-v1".into(), content_digest: digest.clone(), permissions: vec!["KNOWLEDGE_READ".into()], user_data_schema: "hololake.module-data/local/v1".into(), self_test: ModuleSelfTest { kind: "DECLARATIVE_SCHEMA_V1".into(), expected_content_digest: digest, }, }, payload, }; let runtime_contract = contract().unwrap(); validate_package(&package, &runtime_contract).unwrap(); let mut invalid = package; invalid.manifest.module_number = "HLP-DONOR-CAND-0001".into(); assert_eq!( validate_package(&invalid, &runtime_contract).unwrap_err(), "HOLOLAKE_MODULE_PACKAGE_CONTRACT_INVALID" ); } #[test] fn tampered_persisted_manifest_fails_closed_without_touching_user_data() { let temporary = tempfile::tempdir().unwrap(); let root = temporary.path(); let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; install_fixture(root, 1); fs::write( root.join("user-data") .join(module_number) .join("human-data.json"), b"preserve", ) .unwrap(); let connection = open_db(root).unwrap(); connection .execute( "UPDATE installed_modules SET manifest_json='{}' WHERE module_number=?1", params![module_number], ) .unwrap(); drop(connection); assert!(snapshot_at(root) .unwrap_err() .starts_with("HOLOLAKE_MODULE_PERSISTED_MANIFEST_INVALID")); assert_eq!( fs::read( root.join("user-data") .join(module_number) .join("human-data.json") ) .unwrap(), b"preserve" ); } #[test] fn tampered_receipt_chain_fails_closed_on_restart() { let temporary = tempfile::tempdir().unwrap(); install_fixture(temporary.path(), 1); let connection = open_db(temporary.path()).unwrap(); connection .execute( "UPDATE module_receipts SET receipt_hash=?1 WHERE sequence=1", params!["f".repeat(64)], ) .unwrap(); drop(connection); assert_eq!( snapshot_at(temporary.path()).unwrap_err(), "HOLOLAKE_MODULE_RECEIPT_CHAIN_INVALID" ); } #[test] fn real_product_signed_fixture_verifies_without_executing_package_code() { let (fixture, signature, _) = signed_fixture(); let (_, package, package_sha256) = read_verified_package(&fixture, &signature, RELEASE_TRUST_RAW).unwrap(); assert_eq!( package.manifest.module_number, "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001" ); assert!(is_sha256(&package_sha256)); } #[test] fn signed_package_install_mount_self_test_restart_and_unmount_are_receipted() { let (fixture, signature, expected) = signed_fixture(); let temporary = tempfile::tempdir().unwrap(); let module_number = "HLP-MOD-LOCAL-RUNTIME-ACCEPTANCE-0001"; let mut input = InstallModulePackageInput { package_path: fixture.to_string_lossy().into_owned(), signature_path: signature.to_string_lossy().into_owned(), expected_package_sha256: expected, human_confirmed_permission_expansion: false, }; assert_eq!( install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 100) .unwrap_err(), "HOLOLAKE_MODULE_PERMISSION_EXPANSION_REQUIRES_HUMAN_CONFIRMATION" ); input.human_confirmed_permission_expansion = true; install_module_package_at(temporary.path(), &input, RELEASE_TRUST_RAW, 101).unwrap(); transition( temporary.path(), module_number, &["INSTALLED_DORMANT"], "MOUNTED_PENDING_SELF_TEST", "MOUNT", ) .unwrap(); self_test_module_at(temporary.path(), module_number, 103).unwrap(); let user_data = temporary .path() .join("user-data") .join(module_number) .join("human-data.json"); fs::write(&user_data, b"preserve-after-unmount").unwrap(); transition( temporary.path(), module_number, &["ACTIVE"], "DORMANT", "UNMOUNT", ) .unwrap(); let reopened = snapshot_at(temporary.path()).unwrap(); assert_eq!(reopened.modules.len(), 1); assert_eq!(reopened.modules[0].state, "DORMANT"); assert_eq!(reopened.receipt_count, 4); assert_eq!(fs::read(user_data).unwrap(), b"preserve-after-unmount"); } #[test] fn bundled_native_composition_package_completes_installed_lifecycle() { let temporary = tempfile::tempdir().unwrap(); let (_, package, digest) = read_verified_package_bytes( NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, NATIVE_COMPOSITION_NUMBER); assert_eq!(package.manifest.adapter, "native-composition-v1"); let (package_path, signature_path) = materialize_bundled_source( temporary.path(), NATIVE_COMPOSITION_NUMBER, NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE, ) .unwrap(); install_module_package_at( temporary.path(), &InstallModulePackageInput { package_path: package_path.to_string_lossy().into_owned(), signature_path: signature_path.to_string_lossy().into_owned(), expected_package_sha256: digest, human_confirmed_permission_expansion: true, }, RELEASE_TRUST_RAW, 200, ) .unwrap(); transition( temporary.path(), NATIVE_COMPOSITION_NUMBER, &["INSTALLED_DORMANT"], "MOUNTED_PENDING_SELF_TEST", "MOUNT", ) .unwrap(); self_test_module_at(temporary.path(), NATIVE_COMPOSITION_NUMBER, 202).unwrap(); let snapshot = snapshot_at(temporary.path()).unwrap(); assert!(snapshot.modules.iter().any(|module| { module.module_number == NATIVE_COMPOSITION_NUMBER && module.adapter == "native-composition-v1" && module.state == "ACTIVE" })); assert_eq!(snapshot.receipt_count, 3); } #[test] fn bundled_channel_workbench_has_an_independent_verified_identity() { let (_, package, digest) = read_verified_package_bytes( CHANNEL_WORKBENCH_PACKAGE, CHANNEL_WORKBENCH_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER); assert_eq!(package.manifest.adapter, "channel-workbench-v1"); assert_eq!(package.manifest.permissions.len(), 4); assert!(is_sha256(&digest)); assert_eq!(BUNDLED_MODULES.len(), 11); } #[test] fn bundled_persona_channel_body_is_signed_without_claiming_persona_binding() { let (_, package, digest) = read_verified_package_bytes( PERSONA_CHANNEL_BODY_PACKAGE, PERSONA_CHANNEL_BODY_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, PERSONA_CHANNEL_BODY_NUMBER); assert_eq!(package.manifest.adapter, "persona-channel-body-v1"); assert_eq!(package.manifest.permissions.len(), 7); assert_eq!( package.payload["adapterConfig"]["personaBindingClaimed"], false ); assert!(is_sha256(&digest)); } #[test] fn bundled_education_workbench_is_an_official_signed_module() { let (_, package, digest) = read_verified_package_bytes( EDUCATION_WORKBENCH_PACKAGE, EDUCATION_WORKBENCH_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, EDUCATION_WORKBENCH_NUMBER); assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); assert_eq!(package.manifest.adapter, "education-workbench-v1"); assert_eq!(package.manifest.permissions.len(), 8); assert_eq!( package.payload["adapterConfig"]["importDefaultScope"], "UNASSIGNED" ); assert!(is_sha256(&digest)); } #[test] fn bundled_web_novel_family_uses_one_signed_lifecycle_and_exact_numbers() { let expected = [ ( WEB_NOVEL_WORKBENCH_NUMBER, WEB_NOVEL_WORKBENCH_PACKAGE, WEB_NOVEL_WORKBENCH_SIGNATURE, 8usize, ), ( WEB_NOVEL_OUTLINE_NUMBER, WEB_NOVEL_OUTLINE_PACKAGE, WEB_NOVEL_OUTLINE_SIGNATURE, 2usize, ), ( WEB_NOVEL_GRID_NUMBER, WEB_NOVEL_GRID_PACKAGE, WEB_NOVEL_GRID_SIGNATURE, 2usize, ), ( WEB_NOVEL_STORYWORLD_NUMBER, WEB_NOVEL_STORYWORLD_PACKAGE, WEB_NOVEL_STORYWORLD_SIGNATURE, 2usize, ), ( WEB_NOVEL_DELIVERY_NUMBER, WEB_NOVEL_DELIVERY_PACKAGE, WEB_NOVEL_DELIVERY_SIGNATURE, 2usize, ), ]; for (number, bytes, signature, permission_count) in expected { let (_, package, digest) = read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW).unwrap(); assert_eq!(package.manifest.module_number, number); assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); assert_eq!(package.manifest.adapter, "web-novel-workbench-v1"); assert_eq!(package.manifest.permissions.len(), permission_count); assert!(is_sha256(&digest)); } } #[test] fn bundled_mobile_sync_is_signed_and_keeps_the_desktop_as_root_node() { let (_, package, digest) = read_verified_package_bytes( MOBILE_SYNC_PACKAGE, MOBILE_SYNC_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, MOBILE_SYNC_NUMBER); assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); assert_eq!(package.manifest.adapter, "mobile-sync-v1"); assert_eq!(package.manifest.permissions.len(), 5); assert_eq!( package.payload["adapterConfig"]["desktopRole"], "USER_LOCAL_COMPUTER_TERMINAL_ROOT_NODE" ); assert_eq!( package.payload["adapterConfig"]["iosClientPackaging"], "PENDING_SEPARATE_ADMISSION" ); assert!(is_sha256(&digest)); } #[test] fn bundled_dynamic_world_surface_is_signed_and_cannot_change_routes() { let (_, package, digest) = read_verified_package_bytes( DYNAMIC_WORLD_SURFACE_PACKAGE, DYNAMIC_WORLD_SURFACE_SIGNATURE, RELEASE_TRUST_RAW, ) .unwrap(); assert_eq!(package.manifest.module_number, DYNAMIC_WORLD_SURFACE_NUMBER); assert_eq!(package.manifest.registration_class, "OFFICIAL_LIGHTHOUSE"); assert_eq!( package.manifest.adapter, "dynamic-language-world-surface-v1" ); assert_eq!(package.manifest.permissions.len(), 2); assert_eq!( package.payload["adapterConfig"]["routingPermissionOrFactMutation"], false ); assert_eq!( package.payload["adapterConfig"]["legacyTraditionalWorkbench"], "REJECTED_DUPLICATE_SHELL_AND_SIMULATED_STATE" ); assert!(is_sha256(&digest)); } #[cfg(unix)] #[test] fn symlinked_package_input_is_rejected_before_signature_processing() { use std::os::unix::fs::symlink; let temporary = tempfile::tempdir().unwrap(); let source = temporary.path().join("source.ghmod"); let link = temporary.path().join("link.ghmod"); let signature = temporary.path().join("dummy.sig"); fs::write(&source, b"{}").unwrap(); fs::write(&signature, b"not-a-signature").unwrap(); symlink(&source, &link).unwrap(); assert_eq!( read_verified_package(&link, &signature, RELEASE_TRUST_RAW).unwrap_err(), "HOLOLAKE_MODULE_PACKAGE_SIZE_INVALID" ); } }