Skip to main content

lean_ctx/tools/
server_paths.rs

1use super::server::LeanCtxServer;
2use super::startup::{
3    has_project_marker, is_suspicious_root, maybe_derive_project_root_from_absolute,
4};
5
6impl LeanCtxServer {
7    pub fn checkpoint_interval_effective() -> usize {
8        if let Ok(v) = std::env::var("LEAN_CTX_CHECKPOINT_INTERVAL") {
9            if let Ok(parsed) = v.trim().parse::<usize>() {
10                return parsed;
11            }
12        }
13        let profile_interval = crate::core::profiles::active_profile()
14            .autonomy
15            .checkpoint_interval_effective();
16        if profile_interval > 0 {
17            return profile_interval as usize;
18        }
19        crate::core::config::Config::load().checkpoint_interval as usize
20    }
21
22    /// Resolves a (possibly relative) tool path against the session's project_root.
23    /// Absolute paths and "." are returned as-is. Relative paths like "src/main.rs"
24    /// are joined with project_root so tools work regardless of the server's cwd.
25    pub async fn resolve_path(&self, path: &str) -> Result<String, String> {
26        let normalized = crate::core::pathutil::normalize_tool_path(path);
27        if normalized.is_empty() || normalized == "." {
28            return Ok(normalized);
29        }
30        let p = std::path::Path::new(&normalized);
31
32        let (resolved, jail_root) = {
33            let session = self.session.read().await;
34            let jail_root = session
35                .project_root
36                .as_deref()
37                .or(session.shell_cwd.as_deref())
38                .unwrap_or(".")
39                .to_string();
40
41            let resolved = if p.is_absolute() || p.exists() {
42                std::path::PathBuf::from(&normalized)
43            } else if let Some(ref root) = session.project_root {
44                let joined = std::path::Path::new(root).join(&normalized);
45                if joined.exists() {
46                    joined
47                } else if let Some(ref cwd) = session.shell_cwd {
48                    std::path::Path::new(cwd).join(&normalized)
49                } else {
50                    std::path::Path::new(&jail_root).join(&normalized)
51                }
52            } else if let Some(ref cwd) = session.shell_cwd {
53                std::path::Path::new(cwd).join(&normalized)
54            } else {
55                std::path::Path::new(&jail_root).join(&normalized)
56            };
57
58            (resolved, jail_root)
59        };
60
61        let jail_root_path = std::path::Path::new(&jail_root);
62        let jailed = match crate::core::pathjail::jail_path(&resolved, jail_root_path) {
63            Ok(p) => p,
64            Err(e) => {
65                if p.is_absolute() {
66                    if let Some(new_root) = maybe_derive_project_root_from_absolute(&resolved) {
67                        let candidate_under_jail = resolved.starts_with(jail_root_path);
68                        let allow_reroot = if candidate_under_jail {
69                            false
70                        } else if let Some(ref trusted_root) = self.startup_project_root {
71                            std::path::Path::new(trusted_root) == new_root.as_path()
72                        } else {
73                            !has_project_marker(jail_root_path)
74                                || is_suspicious_root(jail_root_path)
75                        };
76
77                        if allow_reroot {
78                            let mut session = self.session.write().await;
79                            let new_root_str = new_root.to_string_lossy().to_string();
80                            session.project_root = Some(new_root_str.clone());
81                            session.shell_cwd = self
82                                .startup_shell_cwd
83                                .as_ref()
84                                .filter(|cwd| std::path::Path::new(cwd).starts_with(&new_root))
85                                .cloned()
86                                .or_else(|| Some(new_root_str.clone()));
87                            let _ = session.save();
88
89                            crate::core::pathjail::jail_path(&resolved, &new_root)?
90                        } else {
91                            return Err(e);
92                        }
93                    } else {
94                        return Err(e);
95                    }
96                } else {
97                    return Err(e);
98                }
99            }
100        };
101
102        crate::core::io_boundary::check_secret_path_for_tool("resolve_path", &jailed)?;
103
104        Ok(crate::core::pathutil::normalize_tool_path(
105            &jailed.to_string_lossy().replace('\\', "/"),
106        ))
107    }
108
109    /// Like `resolve_path`, but returns the original path on failure instead of an error.
110    pub async fn resolve_path_or_passthrough(&self, path: &str) -> String {
111        self.resolve_path(path)
112            .await
113            .unwrap_or_else(|_| path.to_string())
114    }
115}