use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub const RUNTIME_DIR_ENV: &str = "WEFTOS_RUNTIME_DIR";
pub const CHAIN_CHECKPOINT_FILE: &str = "chain.json";
pub const SOCKET_NAME: &str = "kernel.sock";
pub const PID_FILE_NAME: &str = "kernel.pid";
pub const LOG_FILE_NAME: &str = "kernel.log";
pub const LOCK_FILE_NAME: &str = "kernel.lock";
pub const REFUSED_FILE_NAME: &str = "REFUSED";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootSource {
Env,
Project(PathBuf),
LegacyHome,
User,
Child {
id: String,
project_root: PathBuf,
trust_root: PathBuf,
},
}
mod child;
pub use child::{
OVERLAY_FILE, PARENT_POLICY_FILE, PROJECT_CERT_FILE, PROJECT_KEY_FILE, REVOKED_FILE,
SPAWN_JSON_FILE, STATE_JSON_FILE, child_profile, child_run_dir, revoked_marker,
set_child_profile,
};
static USER_PROFILE: RwLock<Option<Option<PathBuf>>> = RwLock::new(None);
pub fn absolutize(path: &Path) -> PathBuf {
std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
}
pub fn set_user_profile(on: bool) {
let state = on.then(|| capture_runtime_dir(std::env::var(RUNTIME_DIR_ENV).ok().as_deref()));
*USER_PROFILE.write().unwrap_or_else(|e| e.into_inner()) = state;
}
pub fn set_user_profile_at(root: &Path) {
*USER_PROFILE.write().unwrap_or_else(|e| e.into_inner()) = Some(Some(absolutize(root)));
}
fn capture_runtime_dir(env: Option<&str>) -> Option<PathBuf> {
env.map(str::trim)
.filter(|v| !v.is_empty())
.map(|v| absolutize(Path::new(v)))
}
pub fn runtime_dir_env() -> Option<PathBuf> {
let user = USER_PROFILE.read().unwrap_or_else(|e| e.into_inner()).clone();
match user {
Some(captured) => captured,
None => capture_none_if_blank(std::env::var(RUNTIME_DIR_ENV).ok().as_deref()),
}
}
fn capture_none_if_blank(env: Option<&str>) -> Option<PathBuf> {
env.filter(|v| !v.is_empty()).map(PathBuf::from)
}
pub fn user_profile_active() -> bool {
USER_PROFILE
.read()
.unwrap_or_else(|e| e.into_inner())
.is_some()
}
pub fn is_test_process(cfg_test: bool, exe: Option<&Path>) -> bool {
if cfg_test {
return true;
}
let Some(exe) = exe else { return false };
exe.parent().and_then(|p| p.file_name()).is_some_and(|d| d == "deps")
|| exe.components().any(|c| c.as_os_str().to_string_lossy().starts_with("rustdoctest"))
}
pub fn refuse_real_runtime_in_tests(root: &Path, overridden: bool) {
if !cfg!(debug_assertions)
|| overridden
|| std::env::var_os("WEFTOS_ALLOW_REAL_HOME_IN_TESTS").is_some()
|| !is_test_process(cfg!(test), std::env::current_exe().ok().as_deref())
{
return;
}
let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let tmp = std::env::temp_dir();
if !(root.starts_with(&tmp) || canon(root).starts_with(canon(&tmp))) {
panic!(
"a test resolved the runtime root {} without a WEFTOS_RUNTIME_DIR override; \
that is a real runtime dir. Run tests through `scripts/build.sh test`, pin the root \
(WEFTOS_RUNTIME_DIR, set_user_profile_at, or an explicit RuntimePaths), or point HOME \
at a tempdir. WEFTOS_ALLOW_REAL_HOME_IN_TESTS=1 opts out",
root.display()
);
}
}
pub fn user_weftos_dir(home: &Path) -> PathBuf {
home.join(".weftos")
}
pub fn user_runtime_root(home: &Path) -> PathBuf {
user_weftos_dir(home).join("run")
}
pub fn user_chain_checkpoint(home: &Path) -> PathBuf {
user_weftos_dir(home)
.join("chain")
.join(CHAIN_CHECKPOINT_FILE)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimePaths {
root: PathBuf,
source: RootSource,
}
fn same_dir(a: &Path, b: &Path) -> bool {
a == b
|| match (a.canonicalize(), b.canonicalize()) {
(Ok(x), Ok(y)) => x == y,
_ => false,
}
}
fn is_project_dir(dir: &Path) -> bool {
let weftos = dir.join(".weftos");
if !weftos.is_dir() {
return false;
}
weftos.join("project.toml").is_file()
|| weftos.join("weave.toml").is_file()
|| dir.join("weave.toml").is_file()
|| weftos.join("runtime").is_dir()
|| dir.join(".git").exists()
}
pub fn find_project_dir(cwd: &Path, home: Option<&Path>) -> Option<PathBuf> {
for dir in cwd.ancestors() {
if home.is_some_and(|h| same_dir(dir, h)) {
return None;
}
if is_project_dir(dir) {
return Some(dir.to_path_buf());
}
}
None
}
pub fn resolve_root(
env: Option<&str>,
cwd: Option<&Path>,
home: Option<&Path>,
) -> (PathBuf, RootSource) {
if let Some(dir) = env.map(str::trim).filter(|d| !d.is_empty()) {
return (PathBuf::from(dir), RootSource::Env);
}
if let Some(project) = cwd.and_then(|c| find_project_dir(c, home)) {
return (
project.join(".weftos").join("runtime"),
RootSource::Project(project),
);
}
if let Some(user) = home.and_then(prefer_user_root) {
return (user, RootSource::User);
}
let legacy = home
.map(Path::to_path_buf)
.unwrap_or_else(std::env::temp_dir)
.join(".clawft");
(legacy, RootSource::LegacyHome)
}
fn socket_answers(root: &Path) -> bool {
#[cfg(unix)]
{
std::os::unix::net::UnixStream::connect(root.join(SOCKET_NAME)).is_ok()
}
#[cfg(not(unix))]
{
let _ = root;
false
}
}
pub fn prefer_user_root(home: &Path) -> Option<PathBuf> {
let user = user_runtime_root(home);
let legacy = home.join(".clawft");
if socket_answers(&user) {
return Some(user);
}
if socket_answers(&legacy) {
return None;
}
let user_marked = user.join(SOCKET_NAME).exists() || user.join(LOCK_FILE_NAME).exists();
(user_marked && !legacy.join(SOCKET_NAME).exists()).then_some(user)
}
impl RuntimePaths {
pub fn at(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
source: RootSource::Env,
}
}
pub fn resolve_with(env: Option<&str>, cwd: Option<&Path>, home: Option<&Path>) -> Self {
let (root, source) = resolve_root(env, cwd, home);
Self { root, source }
}
pub fn user_with(env: Option<&str>, home: Option<&Path>) -> Self {
if let Some(dir) = env.map(str::trim).filter(|d| !d.is_empty()) {
return Self {
root: absolutize(Path::new(dir)),
source: RootSource::User,
};
}
let home = home
.map(Path::to_path_buf)
.unwrap_or_else(std::env::temp_dir);
Self {
root: user_runtime_root(&home),
source: RootSource::User,
}
}
pub fn resolve() -> Self {
if let Some(child) = child_profile() {
return child;
}
let env = std::env::var(RUNTIME_DIR_ENV).ok();
let user = USER_PROFILE.read().unwrap_or_else(|e| e.into_inner()).clone();
if let Some(captured) = user {
let captured = captured.as_ref().and_then(|p| p.to_str());
let paths = Self::user_with(captured, home_dir().as_deref());
paths.refuse_real_home_in_tests(captured.is_some());
return paths;
}
let cwd = std::env::current_dir().ok();
let home = home_dir();
let paths = Self::resolve_with(env.as_deref(), cwd.as_deref(), home.as_deref());
paths.refuse_real_home_in_tests(env.as_deref().is_some_and(|e| !e.trim().is_empty()));
paths
}
fn refuse_real_home_in_tests(&self, overridden: bool) {
refuse_real_runtime_in_tests(&self.root, overridden);
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn source(&self) -> &RootSource {
&self.source
}
fn file(&self, name: &str) -> PathBuf {
self.root.join(name)
}
pub fn socket(&self) -> PathBuf {
self.file(SOCKET_NAME)
}
pub fn pid(&self) -> PathBuf {
self.file(PID_FILE_NAME)
}
pub fn log(&self) -> PathBuf {
self.file(LOG_FILE_NAME)
}
pub fn lock(&self) -> PathBuf {
self.file(LOCK_FILE_NAME)
}
pub fn refused(&self) -> PathBuf {
self.file(REFUSED_FILE_NAME)
}
pub fn node_key(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join(PROJECT_KEY_FILE);
}
self.file("node.key")
}
pub fn chain_checkpoint(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join("chain").join(CHAIN_CHECKPOINT_FILE);
}
self.file(CHAIN_CHECKPOINT_FILE)
}
pub fn chain_rvf(&self) -> PathBuf {
self.chain_checkpoint().with_extension("rvf")
}
pub fn chain_key(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join(PROJECT_KEY_FILE);
}
self.chain_checkpoint().with_extension("key")
}
pub fn chain_tree(&self) -> PathBuf {
self.chain_checkpoint().with_extension("tree.json")
}
pub fn chain_dir(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join("chain");
}
self.file("chain")
}
pub fn anchors_ledger(&self) -> PathBuf {
self.chain_dir().join("anchors.jsonl")
}
pub fn workloads(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join("state").join("workloads.json");
}
self.file("workloads.json")
}
pub fn cluster_peers(&self) -> PathBuf {
self.file("cluster_peers.json")
}
pub fn apps(&self) -> PathBuf {
if let Some(d) = self.child_weftos_dir() {
return d.join("state").join("apps.json");
}
self.file("apps.json")
}
pub fn revoked_hosts(&self) -> PathBuf {
self.file("revoked_hosts.json")
}
pub fn paired_hosts(&self) -> PathBuf {
self.file("paired_hosts.json")
}
}
pub fn home_dir() -> Option<PathBuf> {
#[cfg(feature = "native")]
{
dirs::home_dir()
}
#[cfg(not(feature = "native"))]
{
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|h| !h.is_empty())
.map(PathBuf::from)
}
}
pub const MIGRATED_FROM_FILE: &str = "MIGRATED_FROM.json";
pub const LEGACY_MIGRATED_MARKER: &str = "MIGRATED-TO-WEFTOS.txt";
pub fn user_chain_root(home: &Path) -> PathBuf {
user_weftos_dir(home).join("chain")
}
pub fn legacy_migration_marker(legacy_root: &Path) -> Option<(PathBuf, Option<String>)> {
let marker = legacy_root.join(LEGACY_MIGRATED_MARKER);
let text = std::fs::read_to_string(&marker).ok()?;
let dest = text
.lines()
.find_map(|l| l.strip_prefix("migrated-to:"))
.map(|d| d.trim().to_string());
Some((marker, dest))
}
pub fn legacy_chain_left_behind(paths: &RuntimePaths, home: Option<&Path>) -> Option<PathBuf> {
if !matches!(paths.source(), RootSource::Project(_)) {
return None;
}
let legacy = home?.join(".clawft").join(CHAIN_CHECKPOINT_FILE);
let resolved = paths.chain_checkpoint();
let has_chain = |p: &Path| p.exists() || p.with_extension("rvf").exists();
(!has_chain(&resolved) && has_chain(&legacy)).then_some(legacy)
}
#[cfg(test)]
mod tests {
fn at(root: &str) -> RuntimePaths {
RuntimePaths { root: PathBuf::from(root), source: RootSource::User }
}
#[test]
#[should_panic(expected = "a test resolved the runtime root")]
fn a_test_resolving_a_real_home_root_panics() {
at("/definitely-not-temp/home/.weftos/run").refuse_real_home_in_tests(false);
}
#[test]
fn only_a_deps_or_doctest_executable_is_a_test_process() {
let p = |s: &str| Some(PathBuf::from(s));
assert!(is_test_process(false, p("/w/target/debug/deps/user_daemon-1a2b").as_deref()));
assert!(is_test_process(false, p("/tmp/rustdoctestAbC/rust_out").as_deref()));
assert!(is_test_process(true, None));
assert!(!is_test_process(false, p("/usr/local/bin/weaver").as_deref()));
assert!(!is_test_process(false, p("/w/target/debug/weaver").as_deref()));
assert!(!is_test_process(false, p("/w/target/release/weft").as_deref()));
assert!(!is_test_process(false, None));
}
#[test]
fn temp_roots_and_overrides_are_allowed_in_tests() {
let t = tempfile::tempdir().unwrap();
at(&t.path().join("home/.weftos/run").display().to_string()).refuse_real_home_in_tests(false);
at(&std::env::temp_dir().join("x").display().to_string()).refuse_real_home_in_tests(false);
at("/definitely-not-temp/run").refuse_real_home_in_tests(true);
}
use super::*;
use std::fs;
fn mk(dir: &Path, rel: &str) {
let p = dir.join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, "").unwrap();
}
#[test]
fn user_root_is_weftos_run_and_ignores_projects() {
let t = tempfile::tempdir().unwrap();
let home = t.path().join("home");
let p = RuntimePaths::user_with(None, Some(&home));
assert_eq!(p.root(), home.join(".weftos/run"));
assert_eq!(p.source(), &RootSource::User);
assert_eq!(p.socket(), home.join(".weftos/run/kernel.sock"));
assert_eq!(p.lock(), home.join(".weftos/run/kernel.lock"));
assert_eq!(
user_chain_checkpoint(&home),
home.join(".weftos/chain/chain.json")
);
}
#[test]
fn captured_runtime_dir_is_absolute_and_blank_is_unset() {
assert_eq!(capture_runtime_dir(None), None);
assert_eq!(capture_runtime_dir(Some(" ")), None);
let rel = capture_runtime_dir(Some("rel/run")).unwrap();
assert_eq!(rel, std::env::current_dir().unwrap().join("rel/run"));
assert_eq!(
capture_runtime_dir(Some("/abs/run")),
Some(PathBuf::from("/abs/run"))
);
}
#[test]
fn user_root_env_override_is_made_absolute() {
let p = RuntimePaths::user_with(Some("rel/run"), Some(Path::new("/h")));
assert!(p.root().is_absolute(), "{:?}", p.root());
assert!(p.root().ends_with("rel/run"));
assert_eq!(
p.root(),
std::env::current_dir().unwrap().join("rel/run")
);
}
#[test]
fn user_root_env_override_and_blank_env() {
let home = Path::new("/h");
let p = RuntimePaths::user_with(Some("/run/probe"), Some(home));
assert_eq!(p.root(), Path::new("/run/probe"));
assert_eq!(p.source(), &RootSource::User);
let p = RuntimePaths::user_with(Some(" "), Some(home));
assert_eq!(p.root(), Path::new("/h/.weftos/run"));
}
#[test]
fn env_wins_and_empty_env_is_unset() {
let t = tempfile::tempdir().unwrap();
mk(t.path(), ".weftos/project.toml");
let p = RuntimePaths::resolve_with(Some("/run/probe"), Some(t.path()), None);
assert_eq!(p.root(), Path::new("/run/probe"));
assert_eq!(p.chain_checkpoint(), PathBuf::from("/run/probe/chain.json"));
assert_eq!(p.chain_rvf(), PathBuf::from("/run/probe/chain.rvf"));
assert_eq!(p.chain_key(), PathBuf::from("/run/probe/chain.key"));
assert_eq!(p.chain_tree(), PathBuf::from("/run/probe/chain.tree.json"));
assert_eq!(
p.anchors_ledger(),
PathBuf::from("/run/probe/chain/anchors.jsonl")
);
let p = RuntimePaths::resolve_with(Some(" "), Some(t.path()), None);
assert!(matches!(p.source(), RootSource::Project(_)));
}
#[test]
fn walks_up_to_project_marker() {
let t = tempfile::tempdir().unwrap();
mk(t.path(), "proj/.weftos/weave.toml");
fs::create_dir_all(t.path().join("proj/a/b")).unwrap();
let cwd = t.path().join("proj/a/b");
let p = RuntimePaths::resolve_with(None, Some(&cwd), None);
assert_eq!(p.root(), t.path().join("proj/.weftos/runtime"));
assert_eq!(
p.socket(),
t.path().join("proj/.weftos/runtime/kernel.sock")
);
assert_eq!(p.node_key(), t.path().join("proj/.weftos/runtime/node.key"));
}
#[test]
fn accepts_weaver_init_layout() {
let t = tempfile::tempdir().unwrap();
mk(t.path(), "proj/weave.toml");
fs::create_dir_all(t.path().join("proj/.weftos/runtime")).unwrap();
let p = RuntimePaths::resolve_with(None, Some(&t.path().join("proj")), None);
assert_eq!(p.root(), t.path().join("proj/.weftos/runtime"));
}
#[test]
fn existing_runtime_dir_marks_a_project() {
let t = tempfile::tempdir().unwrap();
fs::create_dir_all(t.path().join("proj/.weftos/runtime")).unwrap();
fs::create_dir_all(t.path().join("proj/src")).unwrap();
let home = t.path().join("home");
let p = RuntimePaths::resolve_with(None, Some(&t.path().join("proj/src")), Some(&home));
assert_eq!(p.root(), t.path().join("proj/.weftos/runtime"));
}
#[test]
fn git_worktree_with_tracked_weftos_is_its_own_project() {
let t = tempfile::tempdir().unwrap();
fs::create_dir_all(t.path().join("main/.git")).unwrap();
mk(t.path(), "main/.weftos/SESSION_HANDOFF.md");
mk(t.path(), "main/.claude/worktrees/w1/.git");
mk(
t.path(),
"main/.claude/worktrees/w1/.weftos/SESSION_HANDOFF.md",
);
let cwd = t.path().join("main/.claude/worktrees/w1");
let p = RuntimePaths::resolve_with(None, Some(&cwd), None);
assert_eq!(p.root(), cwd.join(".weftos/runtime"));
let main = RuntimePaths::resolve_with(None, Some(&t.path().join("main")), None);
assert_eq!(main.root(), t.path().join("main/.weftos/runtime"));
assert_ne!(p.root(), main.root());
}
#[test]
fn bare_weftos_dir_without_any_marker_is_not_a_project() {
let t = tempfile::tempdir().unwrap();
mk(t.path(), "proj/.weftos/notes.md");
let home = t.path().to_path_buf();
let p = RuntimePaths::resolve_with(None, Some(&t.path().join("proj")), Some(&home));
assert_eq!(p.source(), &RootSource::LegacyHome);
fs::create_dir_all(t.path().join("repo/.git")).unwrap();
let p = RuntimePaths::resolve_with(None, Some(&t.path().join("repo")), Some(&home));
assert_eq!(p.source(), &RootSource::LegacyHome);
}
#[test]
fn never_resolves_to_home_even_with_markers() {
let t = tempfile::tempdir().unwrap();
let home = t.path().join("home");
mk(&home, ".weftos/weave.toml");
fs::create_dir_all(home.join("sub/deep")).unwrap();
for cwd in [home.clone(), home.join("sub/deep")] {
let p = RuntimePaths::resolve_with(None, Some(&cwd), Some(&home));
assert_eq!(p.source(), &RootSource::LegacyHome);
assert_eq!(p.root(), home.join(".clawft"));
}
}
#[test]
fn project_below_home_still_found() {
let t = tempfile::tempdir().unwrap();
let home = t.path().join("home");
mk(&home, ".weftos/weave.toml");
mk(&home, "work/p/.weftos/project.toml");
let p = RuntimePaths::resolve_with(None, Some(&home.join("work/p")), Some(&home));
assert_eq!(p.root(), home.join("work/p/.weftos/runtime"));
}
#[test]
fn legacy_chain_notice_only_for_project_roots() {
let t = tempfile::tempdir().unwrap();
let home = t.path().join("home");
mk(&home, ".clawft/chain.json");
mk(t.path(), "proj/.weftos/project.toml");
let cwd = t.path().join("proj");
let project = RuntimePaths::resolve_with(None, Some(&cwd), Some(&home));
assert_eq!(
legacy_chain_left_behind(&project, Some(&home)),
Some(home.join(".clawft/chain.json"))
);
mk(&cwd, ".weftos/runtime/chain.json");
assert_eq!(legacy_chain_left_behind(&project, Some(&home)), None);
let env = RuntimePaths::resolve_with(Some("/x"), Some(&cwd), Some(&home));
assert_eq!(legacy_chain_left_behind(&env, Some(&home)), None);
let leg = RuntimePaths::resolve_with(None, None, Some(&home));
assert_eq!(legacy_chain_left_behind(&leg, Some(&home)), None);
}
#[cfg(unix)]
fn short_home() -> tempfile::TempDir {
tempfile::Builder::new()
.prefix("rp")
.tempdir_in("/tmp")
.unwrap()
}
#[cfg(unix)]
fn listen(root: &Path) -> std::os::unix::net::UnixListener {
fs::create_dir_all(root).unwrap();
std::os::unix::net::UnixListener::bind(root.join(SOCKET_NAME)).unwrap()
}
#[cfg(unix)]
fn resolve_in(home: &Path, cwd: &Path) -> RuntimePaths {
RuntimePaths::resolve_with(None, Some(cwd), Some(home))
}
#[cfg(unix)]
#[test]
fn both_present_only_user_live_resolves_user() {
let t = short_home();
let home = t.path();
let _user = listen(&user_runtime_root(home));
let legacy = home.join(".clawft");
drop(listen(&legacy));
let p = resolve_in(home, home);
assert_eq!(p.source(), &RootSource::User);
assert_eq!(p.root(), user_runtime_root(home));
}
#[cfg(unix)]
#[test]
fn both_present_only_legacy_live_resolves_legacy() {
let t = short_home();
let home = t.path();
drop(listen(&user_runtime_root(home)));
let _legacy = listen(&home.join(".clawft"));
let p = resolve_in(home, home);
assert_eq!(p.source(), &RootSource::LegacyHome);
assert_eq!(p.root(), home.join(".clawft"));
}
#[cfg(unix)]
#[test]
fn both_live_prefers_user() {
let t = short_home();
let home = t.path();
let _u = listen(&user_runtime_root(home));
let _l = listen(&home.join(".clawft"));
assert_eq!(resolve_in(home, home).source(), &RootSource::User);
}
#[cfg(unix)]
#[test]
fn neither_live_defaults_to_legacy_unless_only_user_has_signs() {
let t = short_home();
let home = t.path();
fs::create_dir_all(home.join(".clawft")).unwrap();
assert_eq!(resolve_in(home, home).source(), &RootSource::LegacyHome);
drop(listen(&user_runtime_root(home)));
drop(listen(&home.join(".clawft")));
assert_eq!(resolve_in(home, home).source(), &RootSource::LegacyHome);
let t2 = short_home();
mk(&user_runtime_root(t2.path()), "kernel.lock");
fs::create_dir_all(t2.path().join(".clawft")).unwrap();
assert_eq!(resolve_in(t2.path(), t2.path()).source(), &RootSource::User);
}
#[cfg(unix)]
#[test]
fn env_and_project_still_beat_a_live_user_daemon() {
let t = short_home();
let home = t.path();
let _u = listen(&user_runtime_root(home));
let env = RuntimePaths::resolve_with(Some("/x/rt"), Some(home), Some(home));
assert_eq!(env.source(), &RootSource::Env);
mk(home, "work/p/.weftos/project.toml");
let p = resolve_in(home, &home.join("work/p"));
assert_eq!(p.root(), home.join("work/p/.weftos/runtime"));
}
#[test]
fn no_home_falls_back_to_temp_legacy() {
let p = RuntimePaths::resolve_with(None, None, None);
assert_eq!(p.source(), &RootSource::LegacyHome);
assert!(p.root().ends_with(".clawft"));
}
}