//! 原子包/配置指针。指针从来都不是运行时权限。 use crate::workspace::{hash, HostError, Result}; use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct Target { pub slot: String, pub package_key: String, pub directory: String, pub tree_sha256: String, pub configuration: serde_json::Value, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Change { pub target: Target, pub expected_revision: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Active { pub target: Target, pub revision: String, pub pending_operation: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Receipt { pub operation_id: String, pub state: String, } pub fn schema(db: &Connection) -> Result<()> { db.execute_batch("CREATE TABLE IF NOT EXISTS extension_active(slot TEXT PRIMARY KEY,target TEXT NOT NULL,revision TEXT NOT NULL,pending_operation TEXT); CREATE TABLE IF NOT EXISTS extension_transactions(id TEXT PRIMARY KEY,fingerprint TEXT NOT NULL,before_state TEXT NOT NULL,after_state TEXT NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS extension_uninstalls(id TEXT PRIMARY KEY,slot TEXT NOT NULL,expected_revision TEXT NOT NULL,state TEXT NOT NULL);")?; Ok(()) } pub fn active(db: &Connection, slot: &str) -> Result> { let row: Option<(String, String, Option)> = db .query_row( "SELECT target,revision,pending_operation FROM extension_active WHERE slot=?1", [slot], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), ) .optional()?; row.map(|(target, revision, pending_operation)| { Ok(Active { target: serde_json::from_str(&target) .map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?, revision, pending_operation, }) }) .transpose() } fn encoded(value: &T) -> Result { serde_json::to_string(value).map_err(|_| HostError::new("EXTENSION_TRANSACTION_INVALID")) } pub fn switch(db: &mut Connection, operation: &str, changes: &[Change]) -> Result { switch_inner(db, operation, changes, |_| Ok(())) } fn switch_inner( db: &mut Connection, operation: &str, changes: &[Change], mut checkpoint: impl FnMut(&str) -> Result<()>, ) -> Result { if uuid::Uuid::parse_str(operation).is_err() || changes.is_empty() || changes.len() > 200 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let serialized = encoded(&changes)?; if serialized.len() > 4 * 1024 * 1024 { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let fingerprint = hash(serialized.as_bytes()); let transaction = db.transaction()?; let prior: Option<(String, String)> = transaction .query_row( "SELECT fingerprint,state FROM extension_transactions WHERE id=?1", [operation], |r| Ok((r.get(0)?, r.get(1)?)), ) .optional()?; if let Some((previous, state)) = prior { if previous != fingerprint { return Err(HostError::new("OPERATION_REUSED")); } return Ok(Receipt { operation_id: operation.into(), state, }); } let mut slots = BTreeSet::new(); let mut before = Vec::new(); for change in changes { let target = &change.target; for digest in [&target.slot, &target.package_key, &target.tree_sha256] { if digest.len() != 64 || !digest .bytes() .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } } if uuid::Uuid::parse_str(&target.directory) .map(|v| v.to_string()) .ok() .as_ref() != Some(&target.directory) || !target.configuration.is_object() || !slots.insert(&target.slot) { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } let previous = active(&transaction, &target.slot)?; if previous .as_ref() .is_some_and(|p| p.pending_operation.is_some()) { return Err(HostError::new("EXTENSION_TRANSACTION_BUSY")); } if previous.as_ref().map(|p| &p.revision) != change.expected_revision.as_ref() { return Err(HostError::new("EXTENSION_INSTALL_CONFLICT")); } before.push(previous); } transaction.execute( "INSERT INTO extension_transactions VALUES (?1,?2,?3,?4,'checking')", params![operation, fingerprint, encoded(&before)?, serialized], )?; checkpoint("journal_recorded")?; for change in changes { let target = encoded(&change.target)?; transaction.execute("INSERT INTO extension_active VALUES (?1,?2,?3,?4) ON CONFLICT(slot) DO UPDATE SET target=excluded.target,revision=excluded.revision,pending_operation=excluded.pending_operation", params![change.target.slot,target,hash(target.as_bytes()),operation])?; checkpoint("pointer_recorded")?; } transaction.commit()?; checkpoint("switch_committed")?; Ok(Receipt { operation_id: operation.into(), state: "checking".into(), }) } /// “healthy”必须来自 Host 的匹配包/配置运行状况探测。恢复称其为 false;它从不重新签发任何执行许可证。 pub fn finish(db: &mut Connection, operation: &str, healthy: bool) -> Result { let tx = db.transaction()?; let (before, after, state): (String, String, String) = tx.query_row( "SELECT before_state,after_state,state FROM extension_transactions WHERE id=?1", [operation], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), )?; if state != "checking" { return Ok(Receipt { operation_id: operation.into(), state, }); } let changes: Vec = serde_json::from_str(&after).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let previous: Vec> = serde_json::from_str(&before).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; if previous.len() != changes.len() { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } for (change, old) in changes.iter().zip(previous) { let current = active(&tx, &change.target.slot)? .ok_or_else(|| HostError::new("EXTENSION_STORE_CORRUPT"))?; if current.pending_operation.as_deref() != Some(operation) || current.target != change.target { return Err(HostError::new("EXTENSION_STORE_CORRUPT")); } if healthy { tx.execute( "UPDATE extension_active SET pending_operation=NULL WHERE slot=?1", [&change.target.slot], )?; } else if let Some(old) = old { tx.execute("UPDATE extension_active SET target=?2,revision=?3,pending_operation=NULL WHERE slot=?1", params![change.target.slot,encoded(&old.target)?,old.revision])?; } else { tx.execute( "DELETE FROM extension_active WHERE slot=?1", [&change.target.slot], )?; } } let state = if healthy { "complete" } else { "rolled_back" }; tx.execute( "UPDATE extension_transactions SET state=?2 WHERE id=?1", params![operation, state], )?; tx.commit()?; Ok(Receipt { operation_id: operation.into(), state: state.into(), }) } pub fn recover(db: &mut Connection) -> Result { let ids: Vec = db .prepare("SELECT id FROM extension_transactions WHERE state='checking' ORDER BY id")? .query_map([], |r| r.get(0))? .collect::>()?; for id in &ids { finish(db, id, false)?; } Ok(ids.len()) } /// 为已完成的升级生成回滚变更;回滚本身仍以新的 operation_id 执行和记录。 pub fn rollback_changes(db: &Connection, operation: &str) -> Result> { let (before, after, state): (String, String, String) = db .query_row( "SELECT before_state,after_state,state FROM extension_transactions WHERE id=?1", [operation], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ) .map_err(|_| HostError::new("EXTENSION_ROLLBACK_UNKNOWN"))?; if state != "complete" { return Err(HostError::new("EXTENSION_ROLLBACK_INVALID")); } let old: Vec> = serde_json::from_str(&before).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; let installed: Vec = serde_json::from_str(&after).map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?; if old.len() != installed.len() || old.iter().any(Option::is_none) { return Err(HostError::new("EXTENSION_ROLLBACK_INVALID")); } old.into_iter() .zip(installed) .map(|(previous, installed)| { let previous = previous.unwrap(); let current = active(db, &installed.target.slot)? .ok_or_else(|| HostError::new("EXTENSION_INSTALL_CONFLICT"))?; if current.pending_operation.is_some() || current.target != installed.target { return Err(HostError::new("EXTENSION_INSTALL_CONFLICT")); } Ok(Change { target: previous.target, expected_revision: Some(current.revision), }) }) .collect() } /// 实例停止后原子移除活动指针。保留已验证对象,以便审计或显式回滚;不会触碰外部目录。 pub fn uninstall( db: &mut Connection, operation: &str, slot: &str, expected_revision: &str, ) -> Result { if uuid::Uuid::parse_str(operation).is_err() || slot.len() != 64 || expected_revision.len() != 64 || [slot, expected_revision].iter().any(|value| { !value .bytes() .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) }) { return Err(HostError::new("EXTENSION_TRANSACTION_INVALID")); } db.execute_batch("CREATE TABLE IF NOT EXISTS extension_uninstalls(id TEXT PRIMARY KEY,slot TEXT NOT NULL,expected_revision TEXT NOT NULL,state TEXT NOT NULL);")?; let transaction = db.transaction()?; let prior: Option<(String, String, String)> = transaction .query_row( "SELECT slot,expected_revision,state FROM extension_uninstalls WHERE id=?1", [operation], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ) .optional()?; if let Some((old_slot, old_revision, state)) = prior { if old_slot != slot || old_revision != expected_revision { return Err(HostError::new("OPERATION_REUSED")); } return Ok(Receipt { operation_id: operation.into(), state, }); } let current = active(&transaction, slot)?.ok_or_else(|| HostError::new("EXTENSION_INSTALL_CONFLICT"))?; if current.pending_operation.is_some() || current.revision != expected_revision { return Err(HostError::new("EXTENSION_INSTALL_CONFLICT")); } transaction.execute( "INSERT INTO extension_uninstalls VALUES (?1,?2,?3,'complete')", params![operation, slot, expected_revision], )?; transaction.execute("DELETE FROM extension_active WHERE slot=?1", [slot])?; transaction.commit()?; Ok(Receipt { operation_id: operation.into(), state: "complete".into(), }) } #[cfg(test)] mod tests { use super::*; use std::{ path::Path, process::{Command, Stdio}, thread, time::{Duration, Instant}, }; fn open(path: &std::path::Path) -> Connection { let db = Connection::open(path).unwrap(); db.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;") .unwrap(); schema(&db).unwrap(); db } fn change(slot: char, version: u8, previous: Option) -> Change { Change { target: Target { slot: slot.to_string().repeat(64), package_key: format!("{version:x}").repeat(64), directory: uuid::Uuid::new_v4().to_string(), tree_sha256: "a".repeat(64), configuration: serde_json::json!({"version": version}), }, expected_revision: previous, } } fn seeded(path: &Path) -> (Vec, Vec) { let mut db = open(path); let old = vec![change('a', 1, None), change('b', 1, None)]; let id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &id, &old).unwrap(); finish(&mut db, &id, true).unwrap(); let next = old .iter() .map(|current| { change( current.target.slot.chars().next().unwrap(), 2, Some(active(&db, ¤t.target.slot).unwrap().unwrap().revision), ) }) .collect(); (old, next) } fn assert_complete_generation(db: &Connection) { let active: Vec<_> = ['a', 'b'] .into_iter() .map(|slot| active(db, &slot.to_string().repeat(64)).unwrap().unwrap()) .collect(); let versions: BTreeSet<_> = active .iter() .map(|item| item.target.configuration["version"].as_u64().unwrap()) .collect(); assert_eq!(versions.len(), 1, "package/config generations were mixed"); let version = *versions.first().unwrap(); assert!(matches!(version, 1 | 2)); for item in active { assert_eq!(item.target.package_key, format!("{version:x}").repeat(64)); assert!(item.pending_operation.is_none()); } } #[test] fn completed_upgrade_can_rollback_then_uninstall_idempotently() { let temp = tempfile::tempdir().unwrap(); let mut db = open(&temp.path().join("state.sqlite3")); let initial = change('a', 1, None); let install = uuid::Uuid::new_v4().to_string(); switch(&mut db, &install, std::slice::from_ref(&initial)).unwrap(); finish(&mut db, &install, true).unwrap(); let installed = active(&db, &initial.target.slot).unwrap().unwrap(); let upgrade = change('a', 2, Some(installed.revision)); let upgrade_id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &upgrade_id, &[upgrade]).unwrap(); finish(&mut db, &upgrade_id, true).unwrap(); let rollback = rollback_changes(&db, &upgrade_id).unwrap(); let rollback_id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &rollback_id, &rollback).unwrap(); finish(&mut db, &rollback_id, true).unwrap(); let restored = active(&db, &initial.target.slot).unwrap().unwrap(); assert_eq!(restored.target, initial.target); let uninstall_id = uuid::Uuid::new_v4().to_string(); let receipt = uninstall( &mut db, &uninstall_id, &restored.target.slot, &restored.revision, ) .unwrap(); assert_eq!(receipt.state, "complete"); assert_eq!( uninstall( &mut db, &uninstall_id, &restored.target.slot, &restored.revision, ) .unwrap(), receipt ); assert!(active(&db, &restored.target.slot).unwrap().is_none()); } #[test] fn group_switch_crashes_recover_matching_packages_and_configuration() { for boundary in ["journal_recorded", "pointer_recorded", "switch_committed"] { for _ in 0..20 { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("state.sqlite3"); let mut db = open(&path); let old = vec![change('a', 1, None), change('b', 1, None)]; let id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &id, &old).unwrap(); finish(&mut db, &id, true).unwrap(); let next: Vec<_> = old .iter() .map(|c| { change( c.target.slot.chars().next().unwrap(), 2, Some(active(&db, &c.target.slot).unwrap().unwrap().revision), ) }) .collect(); let update = uuid::Uuid::new_v4().to_string(); assert!( switch_inner(&mut db, &update, &next, |at| if at == boundary { Err(HostError::new("INJECTED")) } else { Ok(()) }) .is_err() ); drop(db); let mut db = open(&path); recover(&mut db).unwrap(); for original in &old { let current = active(&db, &original.target.slot).unwrap().unwrap(); assert_eq!(current.target, original.target); assert!(current.pending_operation.is_none()); } assert_eq!(recover(&mut db).unwrap(), 0); } } } #[test] #[ignore = "parent acceptance oracle hard-terminates this helper at a durable boundary"] fn power_cut_worker() { let Some(path) = std::env::var_os("OPENNEXUS_D03_DATABASE") else { return; }; let boundary = std::env::var("OPENNEXUS_D03_BOUNDARY").unwrap(); let marker = std::path::PathBuf::from(std::env::var_os("OPENNEXUS_D03_MARKER").unwrap()); let mut db = open(Path::new(&path)); let old: Vec<_> = ['a', 'b'] .into_iter() .map(|slot| { let current = active(&db, &slot.to_string().repeat(64)).unwrap().unwrap(); Change { target: current.target.clone(), expected_revision: Some(current.revision), } }) .collect(); let next: Vec<_> = old .iter() .map(|current| { change( current.target.slot.chars().next().unwrap(), 2, current.expected_revision.clone(), ) }) .collect(); let operation = uuid::Uuid::new_v4().to_string(); let _ = switch_inner(&mut db, &operation, &next, |at| { if at == boundary { let file = std::fs::File::create(&marker).unwrap(); file.sync_all().unwrap(); loop { thread::sleep(Duration::from_secs(60)); } } Ok(()) }); panic!("power-cut helper passed the requested boundary"); } #[test] fn group_switch_survives_hard_termination_twenty_times_per_boundary() { for boundary in ["journal_recorded", "pointer_recorded", "switch_committed"] { for round in 0..20 { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("state.sqlite3"); seeded(&path); let marker = temp.path().join(format!("{boundary}-{round}.ready")); let mut child = Command::new(std::env::current_exe().unwrap()) .args([ "--ignored", "--exact", "extension_transaction::tests::power_cut_worker", "--nocapture", ]) .env("OPENNEXUS_D03_DATABASE", &path) .env("OPENNEXUS_D03_BOUNDARY", boundary) .env("OPENNEXUS_D03_MARKER", &marker) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .unwrap(); let started = Instant::now(); while !marker.is_file() { assert!( child.try_wait().unwrap().is_none(), "helper exited before {boundary}" ); assert!( started.elapsed() < Duration::from_secs(10), "helper did not reach {boundary}" ); thread::sleep(Duration::from_millis(5)); } child.kill().unwrap(); assert!(!child.wait().unwrap().success()); let mut db = open(&path); recover(&mut db).unwrap(); assert_complete_generation(&db); assert_eq!(recover(&mut db).unwrap(), 0); } } } #[test] fn disk_full_and_configuration_migration_failures_cover_every_boundary() { let mapped: HostError = rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_FULL), None, ) .into(); assert_eq!(mapped.code, "QUOTA_EXCEEDED"); for failure in ["QUOTA_EXCEEDED", "EXTENSION_CONFIG_MIGRATION_FAILED"] { for boundary in ["journal_recorded", "pointer_recorded", "switch_committed"] { for _ in 0..20 { let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("state.sqlite3"); let (_, next) = seeded(&path); let mut db = open(&path); let update = uuid::Uuid::new_v4().to_string(); let error = switch_inner(&mut db, &update, &next, |at| { if at == boundary { Err(HostError::new(failure)) } else { Ok(()) } }) .unwrap_err(); assert_eq!(error.code, failure); drop(db); let mut db = open(&path); recover(&mut db).unwrap(); assert_complete_generation(&db); assert_eq!(recover(&mut db).unwrap(), 0); } } } } #[test] fn failed_switch_cannot_expand_an_existing_execution_permit() { use crate::extension_permit::{Authority, Claims, Environment, ExecutionKind}; use std::collections::BTreeMap; let authority = Authority::default(); let mut claims = Claims { kind: ExecutionKind::Mcp, source: "https://catalog.example/".into(), namespace: "examples".into(), package_id: "note-reviewer".into(), version: "1.0.0".into(), archive_sha256: "a".repeat(64), tree_sha256: "b".repeat(64), signer_sha256: "c".repeat(64), entry: "entry.exe".into(), arguments: vec!["--stdio".into()], environment: BTreeMap::from([( "MODE".into(), Environment::Literal("production".into()), )]), permissions: BTreeSet::from(["notes.read".into()]), vault_id: uuid::Uuid::new_v4().to_string(), platform: "windows".into(), policy_version: "1".into(), expires_at_ms: 10_000, }; let permit = authority.issue(&claims, 1).unwrap(); let temp = tempfile::tempdir().unwrap(); let path = temp.path().join("state.sqlite3"); let (_, next) = seeded(&path); let mut db = open(&path); let error = switch_inner(&mut db, &uuid::Uuid::new_v4().to_string(), &next, |at| { if at == "switch_committed" { Err(HostError::new("EXTENSION_CONFIG_MIGRATION_FAILED")) } else { Ok(()) } }) .unwrap_err(); assert_eq!(error.code, "EXTENSION_CONFIG_MIGRATION_FAILED"); recover(&mut db).unwrap(); authority.verify(&permit, &claims, 2).unwrap(); claims.permissions.insert("notes.write".into()); assert_eq!( authority.verify(&permit, &claims, 2).unwrap_err().code, "PERMISSION_CHANGED" ); assert_complete_generation(&db); } #[test] fn cas_busy_replay_and_failed_first_install() { let mut db = Connection::open_in_memory().unwrap(); schema(&db).unwrap(); let c = vec![change('a', 1, None)]; let id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &id, &c).unwrap(); assert!(switch(&mut db, &uuid::Uuid::new_v4().to_string(), &c).is_err()); assert_eq!(switch(&mut db, &id, &c).unwrap().state, "checking"); finish(&mut db, &id, false).unwrap(); assert!(active(&db, &c[0].target.slot).unwrap().is_none()); assert_eq!(switch(&mut db, &id, &c).unwrap().state, "rolled_back"); assert_eq!(finish(&mut db, &id, true).unwrap().state, "rolled_back"); let id = uuid::Uuid::new_v4().to_string(); switch(&mut db, &id, &c).unwrap(); finish(&mut db, &id, true).unwrap(); assert!(switch(&mut db, &uuid::Uuid::new_v4().to_string(), &c).is_err()); let mut different = c.clone(); different[0].target.configuration = serde_json::json!({"changed":true}); assert!(switch(&mut db, &id, &different).is_err()); } }