Skip to main content

harn_vm/
persistent_state.rs

1//! Explicit persistent-state ownership for isolated VM executions.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8use crate::Vm;
9
10thread_local! {
11    static SCOPED_PERSISTENT_STATE_ROOT: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
12}
13
14/// A caller-owned persistent-state root that bypasses ambient runtime paths.
15#[derive(Clone, Copy, Debug)]
16pub struct PersistentStateRoot<'a>(&'a Path);
17
18impl<'a> PersistentStateRoot<'a> {
19    #[must_use]
20    pub fn new(path: &'a Path) -> Self {
21        Self(path)
22    }
23
24    fn as_path(self) -> &'a Path {
25        self.0
26    }
27}
28
29/// Restores the prior caller-owned persistent-state root on drop.
30///
31/// The guard is deliberately not `Send`: its scope is tied to the current
32/// thread or `LocalSet`, just like the runtime paths it overrides.
33#[derive(Debug)]
34#[must_use = "retain this guard for the isolated VM execution"]
35pub struct ScopedPersistentStateRoot {
36    previous: Option<PathBuf>,
37    _not_send: PhantomData<Rc<()>>,
38}
39
40/// Route every default persistent runtime path through one caller-owned root.
41///
42/// This is the execution-wide counterpart to
43/// [`register_persistent_state_builtins_at_root`]. It covers durable consumers
44/// such as the agent session journal that resolve their paths during execution
45/// rather than when builtins are registered.
46pub fn scope_persistent_state_root(root: PersistentStateRoot<'_>) -> ScopedPersistentStateRoot {
47    let previous =
48        SCOPED_PERSISTENT_STATE_ROOT.with(|slot| slot.replace(Some(root.as_path().to_path_buf())));
49    ScopedPersistentStateRoot {
50        previous,
51        _not_send: PhantomData,
52    }
53}
54
55impl Drop for ScopedPersistentStateRoot {
56    fn drop(&mut self) {
57        SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
58            slot.replace(self.previous.take());
59        });
60    }
61}
62
63pub(crate) fn current_persistent_state_root() -> Option<PathBuf> {
64    SCOPED_PERSISTENT_STATE_ROOT.with(|slot| slot.borrow().clone())
65}
66
67/// Register store, metadata, and checkpoint builtins at an exact state root.
68///
69/// Unlike the individual runtime registrars, this function does not consult
70/// `HARN_STATE_DIR`. Embedders use it when concurrent executions require
71/// hermetic state without mutating process-global environment variables.
72pub fn register_persistent_state_builtins_at_root(
73    vm: &mut Vm,
74    base_dir: &Path,
75    state_root: PersistentStateRoot<'_>,
76    pipeline_name: &str,
77) {
78    let state_root = state_root.as_path();
79    crate::store::register_store_builtins_at_state_root(vm, state_root);
80    crate::metadata::register_metadata_builtins_at_state_root(vm, base_dir, state_root);
81    crate::checkpoint::register_checkpoint_builtins_at_state_root(vm, state_root, pipeline_name);
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn scoped_persistent_state_root_restores_nested_owner() {
90        let outer = Path::new("/isolated/outer/.harn");
91        let inner = Path::new("/isolated/inner/.harn");
92        assert_eq!(current_persistent_state_root(), None);
93        let outer_guard = scope_persistent_state_root(PersistentStateRoot::new(outer));
94        assert_eq!(current_persistent_state_root().as_deref(), Some(outer));
95        {
96            let _inner_guard = scope_persistent_state_root(PersistentStateRoot::new(inner));
97            assert_eq!(current_persistent_state_root().as_deref(), Some(inner));
98        }
99        assert_eq!(current_persistent_state_root().as_deref(), Some(outer));
100        drop(outer_guard);
101        assert_eq!(current_persistent_state_root(), None);
102    }
103}