use super::server::LeanCtxServer;
use super::startup::{
has_project_marker, is_suspicious_root, maybe_derive_project_root_from_absolute,
};
impl LeanCtxServer {
pub fn checkpoint_interval_effective() -> usize {
if let Ok(v) = std::env::var("LEAN_CTX_CHECKPOINT_INTERVAL")
&& let Ok(parsed) = v.trim().parse::<usize>()
{
return parsed;
}
let profile_interval = crate::core::profiles::active_profile()
.autonomy
.checkpoint_interval_effective();
if profile_interval > 0 {
return profile_interval as usize;
}
crate::core::config::Config::load().checkpoint_interval as usize
}
pub async fn resolve_path(&self, path: &str) -> Result<String, String> {
let normalized = crate::core::pathutil::normalize_tool_path(path);
if normalized.is_empty() || normalized == "." {
return Ok(normalized);
}
let p = std::path::Path::new(&normalized);
let (resolved, jail_root, extra_roots) = {
let session = self.session.read().await;
let jail_root = session
.project_root
.as_deref()
.or(session.shell_cwd.as_deref())
.unwrap_or(".")
.to_string();
let worktree_cwd = if p.is_absolute() {
None
} else {
session
.project_root
.as_deref()
.zip(session.shell_cwd.as_deref())
.filter(|(root, cwd)| {
crate::core::path_resolve::shell_cwd_is_divergent_checkout(root, cwd)
})
.map(|(_, cwd)| std::path::Path::new(cwd).join(&normalized))
};
let resolved = if let Some(overridden) = worktree_cwd {
overridden
} else if p.is_absolute() || p.exists() {
std::path::PathBuf::from(&normalized)
} else if let Some(ref root) = session.project_root {
let joined = std::path::Path::new(root).join(&normalized);
if joined.exists() {
joined
} else if let Some(ref cwd) = session.shell_cwd {
std::path::Path::new(cwd).join(&normalized)
} else {
std::path::Path::new(&jail_root).join(&normalized)
}
} else if let Some(ref cwd) = session.shell_cwd {
std::path::Path::new(cwd).join(&normalized)
} else {
std::path::Path::new(&jail_root).join(&normalized)
};
(resolved, jail_root, session.extra_roots.clone())
};
let jail_root_path = std::path::Path::new(&jail_root);
let jailed = match crate::core::pathjail::jail_path_with_roots(
&resolved,
jail_root_path,
&extra_roots,
) {
Ok(p) => self
.maybe_reroot_for_absolute_path(&resolved, jail_root_path, &extra_roots, false)
.await?
.unwrap_or(p),
Err(e) => {
if let Some((label, cache_root)) =
crate::core::pathjail::detect_language_cache_root(&resolved)
&& crate::core::pathjail::register_session_read_only_root(&cache_root)
{
return Err(format!(
"Auto-detected {label} at {} — added as a read-only root for this \
session. Retry the read.",
cache_root.display()
));
}
if let Some(jailed) = self
.maybe_reroot_for_absolute_path(&resolved, jail_root_path, &extra_roots, true)
.await?
{
jailed
} else {
return Err(e.to_string());
}
}
};
crate::core::io_boundary::check_secret_path_for_tool("resolve_path", &jailed)?;
Ok(crate::core::pathutil::normalize_tool_path(
&jailed.to_string_lossy().replace('\\', "/"),
))
}
async fn maybe_reroot_for_absolute_path(
&self,
resolved: &std::path::Path,
jail_root_path: &std::path::Path,
extra_roots: &[String],
require_opt_in_for_real_jail: bool,
) -> Result<Option<std::path::PathBuf>, String> {
if !resolved.is_absolute() {
return Ok(None);
}
let Some(new_root) = maybe_derive_project_root_from_absolute(resolved) else {
return Ok(None);
};
let candidate_under_jail = resolved.starts_with(jail_root_path);
let allow_reroot = if candidate_under_jail {
false
} else if is_suspicious_root(jail_root_path)
|| (self.startup_project_root.is_none() && !has_project_marker(jail_root_path))
{
true
} else if require_opt_in_for_real_jail {
let cfg_allow = std::env::var("LEAN_CTX_ALLOW_REROOT").map_or_else(
|_| crate::core::config::Config::load().allow_auto_reroot,
|v| v == "1" || v == "true",
);
cfg_allow
&& self
.startup_project_root
.as_ref()
.is_some_and(|trusted_root| std::path::Path::new(trusted_root) == new_root)
} else {
false
};
if !allow_reroot {
return Ok(None);
}
self.reroot_to_project(&new_root).await;
crate::core::pathjail::jail_path_with_roots(resolved, &new_root, extra_roots)
.map(Some)
.map_err(|e| e.to_string())
}
async fn reroot_to_project(&self, new_root: &std::path::Path) {
let mut session = self.session.write().await;
let new_root_str = new_root.to_string_lossy().to_string();
session.project_root = Some(new_root_str.clone());
session.shell_cwd = self
.startup_shell_cwd
.as_ref()
.filter(|cwd| std::path::Path::new(cwd).starts_with(new_root))
.cloned()
.or_else(|| Some(new_root_str.clone()));
let _ = session.save();
}
pub async fn resolve_path_or_passthrough(&self, path: &str) -> String {
self.resolve_path(path)
.await
.unwrap_or_else(|_| path.to_string())
}
}