use std::cell::RefCell;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::Vm;
thread_local! {
static SCOPED_PERSISTENT_STATE_ROOT: RefCell<Option<PersistentStateContext>> = const { RefCell::new(None) };
}
#[derive(Clone, Debug)]
pub(crate) struct PersistentStateContext {
state_root: PathBuf,
workspace: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct SessionStoreScope {
workspace: PathBuf,
state_root: PathBuf,
}
impl SessionStoreScope {
pub fn resolve(
cwd: &Path,
launcher: &crate::security::LauncherEnvironment,
) -> std::io::Result<Self> {
let workspace = cwd.canonicalize()?;
if !workspace.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"session workspace must be a directory",
));
}
let state_root = crate::runtime_paths::captured_state_root(&workspace, launcher);
Ok(Self {
workspace,
state_root,
})
}
pub fn workspace(&self) -> &Path {
&self.workspace
}
pub fn state_root(&self) -> &Path {
&self.state_root
}
pub fn enter(&self) -> ScopedPersistentStateRoot {
replace_context(PersistentStateContext {
state_root: self.state_root.clone(),
workspace: Some(self.workspace.clone()),
})
}
pub async fn run<F: std::future::Future>(&self, future: F) -> F::Output {
crate::orchestration::scope_persistent_state_context(
PersistentStateContext {
state_root: self.state_root.clone(),
workspace: Some(self.workspace.clone()),
},
future,
)
.await
}
}
#[derive(Clone, Copy, Debug)]
pub struct PersistentStateRoot<'a>(&'a Path);
impl<'a> PersistentStateRoot<'a> {
#[must_use]
pub fn new(path: &'a Path) -> Self {
Self(path)
}
fn as_path(self) -> &'a Path {
self.0
}
}
#[derive(Debug)]
#[must_use = "retain this guard for the isolated VM execution"]
pub struct ScopedPersistentStateRoot {
previous: Option<PersistentStateContext>,
_not_send: PhantomData<Rc<()>>,
}
pub fn scope_persistent_state_root(root: PersistentStateRoot<'_>) -> ScopedPersistentStateRoot {
replace_context(PersistentStateContext {
state_root: root.as_path().to_path_buf(),
workspace: None,
})
}
fn replace_context(context: PersistentStateContext) -> ScopedPersistentStateRoot {
let previous = SCOPED_PERSISTENT_STATE_ROOT.with(|slot| slot.replace(Some(context)));
ScopedPersistentStateRoot {
previous,
_not_send: PhantomData,
}
}
impl Drop for ScopedPersistentStateRoot {
fn drop(&mut self) {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
slot.replace(self.previous.take());
});
}
}
pub(crate) fn current_persistent_state_root() -> Option<PathBuf> {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
slot.borrow()
.as_ref()
.map(|context| context.state_root.clone())
})
}
pub(crate) fn current_session_workspace() -> Option<PathBuf> {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
slot.borrow()
.as_ref()
.and_then(|context| context.workspace.clone())
})
}
pub(crate) fn session_matches_current_workspace(meta: &harn_session_store::SessionMeta) -> bool {
session_matches_workspace(meta, current_session_workspace())
}
fn session_matches_workspace(
meta: &harn_session_store::SessionMeta,
workspace: Option<PathBuf>,
) -> bool {
workspace.is_none_or(|workspace| {
let expected = workspace.to_string_lossy();
meta.project_scope.as_deref() == Some(expected.as_ref())
&& meta.cwd.as_deref() == Some(expected.as_ref())
})
}
pub(crate) fn validate_session_store_workspace(
state_root: &Path,
meta: &harn_session_store::SessionMeta,
) -> Result<(), crate::VmError> {
if session_matches_workspace(meta, session_workspace_for_state(state_root)) {
Ok(())
} else {
Err(crate::VmError::Runtime(format!(
"session_store: session '{}' belongs to another workspace",
meta.id
)))
}
}
pub(crate) fn session_workspace_for_state(state_root: &Path) -> Option<PathBuf> {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
slot.borrow()
.as_ref()
.filter(|context| context.state_root == state_root)
.and_then(|context| context.workspace.clone())
})
}
pub(crate) fn canonical_store_root(workspace: &Path) -> PathBuf {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| {
slot.borrow()
.as_ref()
.filter(|context| context.workspace.as_deref() == Some(workspace))
.map(|context| context.state_root.clone())
.unwrap_or_else(|| workspace.join(".harn"))
})
}
pub(crate) fn swap_persistent_state_context(
replacement: Option<PersistentStateContext>,
) -> Option<PersistentStateContext> {
SCOPED_PERSISTENT_STATE_ROOT.with(|slot| slot.replace(replacement))
}
pub fn register_persistent_state_builtins_at_root(
vm: &mut Vm,
base_dir: &Path,
state_root: PersistentStateRoot<'_>,
pipeline_name: &str,
) {
let state_root = state_root.as_path();
crate::store::register_store_builtins_at_state_root(vm, state_root);
crate::metadata::register_metadata_builtins_at_state_root(vm, base_dir, state_root);
crate::checkpoint::register_checkpoint_builtins_at_state_root(vm, state_root, pipeline_name);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scoped_persistent_state_root_restores_nested_owner() {
let outer = Path::new("/isolated/outer/.harn");
let inner = Path::new("/isolated/inner/.harn");
assert_eq!(current_persistent_state_root(), None);
let outer_guard = scope_persistent_state_root(PersistentStateRoot::new(outer));
assert_eq!(current_persistent_state_root().as_deref(), Some(outer));
{
let _inner_guard = scope_persistent_state_root(PersistentStateRoot::new(inner));
assert_eq!(current_persistent_state_root().as_deref(), Some(inner));
}
assert_eq!(current_persistent_state_root().as_deref(), Some(outer));
drop(outer_guard);
assert_eq!(current_persistent_state_root(), None);
}
#[tokio::test(flavor = "current_thread")]
async fn scoped_store_row_validation_preserves_explicit_root_and_refuses_foreign_rows() {
use harn_session_store::{CreateSession, SessionStore, SqliteSessionStore};
let dir = tempfile::tempdir().unwrap();
let launcher = crate::security::LauncherEnvironment::from_snapshot(Default::default());
let scope = SessionStoreScope::resolve(dir.path(), &launcher).unwrap();
let store = SqliteSessionStore::open_in_memory().unwrap();
let workspace = scope.workspace().to_string_lossy().into_owned();
let mut meta = store
.create(CreateSession {
cwd: Some(workspace.clone()),
project_scope: Some(workspace),
..CreateSession::default()
})
.await
.unwrap();
let _guard = scope.enter();
assert!(validate_session_store_workspace(scope.state_root(), &meta).is_ok());
meta.cwd = Some("foreign workspace".to_string());
assert!(validate_session_store_workspace(scope.state_root(), &meta).is_err());
meta.cwd = None;
meta.project_scope = None;
assert!(validate_session_store_workspace(scope.state_root(), &meta).is_err());
assert!(
validate_session_store_workspace(&dir.path().join("explicit-other-state"), &meta)
.is_ok()
);
}
#[tokio::test(flavor = "current_thread")]
async fn interleaved_session_store_scopes_restore_and_follow_child_tasks() {
let left_dir = tempfile::tempdir().expect("left workspace");
let right_dir = tempfile::tempdir().expect("right workspace");
let launcher = crate::security::LauncherEnvironment::from_snapshot(Default::default());
let left = SessionStoreScope::resolve(left_dir.path(), &launcher).unwrap();
let right = SessionStoreScope::resolve(right_dir.path(), &launcher).unwrap();
let check = |scope: SessionStoreScope| async move {
scope
.run(async {
for _ in 0..3 {
assert_eq!(
current_persistent_state_root().as_deref(),
Some(scope.state_root())
);
assert_eq!(
current_session_workspace().as_deref(),
Some(scope.workspace())
);
tokio::task::yield_now().await;
}
let expected = scope.state_root().to_path_buf();
crate::orchestration::run_blocking_with_ambient(move || {
assert_eq!(
current_persistent_state_root().as_deref(),
Some(expected.as_path())
);
})
.await
.expect("captured child host task");
crate::orchestration::scope_ambient(
crate::orchestration::AmbientExecutionScope::capture_inherited(),
async {
tokio::task::yield_now().await;
assert_eq!(
current_persistent_state_root().as_deref(),
Some(scope.state_root())
);
assert_eq!(
current_session_workspace().as_deref(),
Some(scope.workspace())
);
},
)
.await;
})
.await;
};
tokio::join!(check(left), check(right));
assert_eq!(current_persistent_state_root(), None);
assert_eq!(current_session_workspace(), None);
}
}