Skip to main content

lean_ctx/tools/
mod.rs

1pub mod autonomy;
2pub mod ctx_agent;
3pub mod ctx_analyze;
4pub mod ctx_architecture;
5pub mod ctx_artifacts;
6pub mod ctx_benchmark;
7pub mod ctx_callgraph;
8pub mod ctx_compile;
9pub mod ctx_compose;
10pub mod ctx_compress;
11pub mod ctx_compress_memory;
12pub mod ctx_context;
13pub mod ctx_control;
14pub mod ctx_cost;
15pub mod ctx_dedup;
16pub mod ctx_delta;
17pub mod ctx_discover;
18pub mod ctx_edit;
19pub mod ctx_execute;
20pub mod ctx_expand;
21pub mod ctx_feedback;
22pub mod ctx_fill;
23pub mod ctx_gain;
24pub mod ctx_graph;
25pub mod ctx_graph_diagram;
26pub mod ctx_handoff;
27pub mod ctx_heatmap;
28pub mod ctx_impact;
29pub mod ctx_index;
30pub mod ctx_intent;
31pub mod ctx_knowledge;
32pub mod ctx_knowledge_relations;
33pub mod ctx_metrics;
34pub mod ctx_multi_read;
35pub mod ctx_outline;
36pub mod ctx_overview;
37pub mod ctx_pack;
38pub mod ctx_plan;
39pub mod ctx_prefetch;
40pub mod ctx_preload;
41pub mod ctx_proof;
42pub mod ctx_provider;
43pub mod ctx_read;
44pub mod ctx_refactor;
45pub mod ctx_response;
46pub mod ctx_review;
47pub mod ctx_routes;
48pub mod ctx_search;
49pub mod ctx_semantic_search;
50pub mod ctx_session;
51pub mod ctx_share;
52pub mod ctx_shell;
53pub mod ctx_smart_read;
54pub mod ctx_smells;
55pub mod ctx_symbol;
56pub mod ctx_task;
57pub mod ctx_tree;
58pub mod ctx_verify;
59pub mod ctx_workflow;
60pub(crate) mod knowledge_shared;
61pub mod registered;
62
63mod server;
64mod server_lifecycle;
65mod server_metrics;
66mod server_paths;
67pub(crate) mod startup;
68
69pub use server::*;
70pub use startup::create_server;
71
72#[cfg(test)]
73mod resolve_path_tests {
74    use super::startup::canonicalize_path;
75    use super::*;
76
77    fn create_git_root(path: &std::path::Path) -> String {
78        std::fs::create_dir_all(path.join(".git")).unwrap();
79        canonicalize_path(path)
80    }
81
82    #[cfg(not(feature = "no-jail"))]
83    #[tokio::test]
84    async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
85        std::env::set_var("LEAN_CTX_ALLOW_REROOT", "1");
86        let tmp = tempfile::tempdir().unwrap();
87        let stale = tmp.path().join("stale");
88        let real = tmp.path().join("real");
89        std::fs::create_dir_all(&stale).unwrap();
90        let real_root = create_git_root(&real);
91        std::fs::write(real.join("a.txt"), "ok").unwrap();
92
93        let server = LeanCtxServer::new_with_startup(
94            None,
95            Some(real.as_path()),
96            SessionMode::Personal,
97            "default",
98            "default",
99        );
100        {
101            let mut session = server.session.write().await;
102            session.project_root = Some(stale.to_string_lossy().to_string());
103            session.shell_cwd = Some(stale.to_string_lossy().to_string());
104        }
105
106        let out = server
107            .resolve_path(&real.join("a.txt").to_string_lossy())
108            .await
109            .unwrap();
110
111        assert!(out.ends_with("/a.txt"));
112
113        let session = server.session.read().await;
114        assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
115        assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
116    }
117
118    #[cfg(not(feature = "no-jail"))]
119    #[tokio::test]
120    async fn resolve_path_rejects_absolute_path_outside_trusted_startup_root() {
121        let tmp = tempfile::tempdir().unwrap();
122        let stale = tmp.path().join("stale");
123        let root = tmp.path().join("root");
124        let other = tmp.path().join("other");
125        std::fs::create_dir_all(&stale).unwrap();
126        create_git_root(&root);
127        let _other_value = create_git_root(&other);
128        std::fs::write(other.join("b.txt"), "no").unwrap();
129
130        let server = LeanCtxServer::new_with_startup(
131            None,
132            Some(root.as_path()),
133            SessionMode::Personal,
134            "default",
135            "default",
136        );
137        {
138            let mut session = server.session.write().await;
139            session.project_root = Some(stale.to_string_lossy().to_string());
140            session.shell_cwd = Some(stale.to_string_lossy().to_string());
141        }
142
143        let err = server
144            .resolve_path(&other.join("b.txt").to_string_lossy())
145            .await
146            .unwrap_err();
147        assert!(err.contains("path escapes project root"));
148
149        let session = server.session.read().await;
150        assert_eq!(
151            session.project_root.as_deref(),
152            Some(stale.to_string_lossy().as_ref())
153        );
154    }
155
156    #[tokio::test]
157    #[allow(clippy::await_holding_lock)]
158    async fn startup_prefers_workspace_scoped_session_over_global_latest() {
159        let _lock = crate::core::data_dir::test_env_lock();
160        let _data = tempfile::tempdir().unwrap();
161        let _tmp = tempfile::tempdir().unwrap();
162
163        std::env::set_var("LEAN_CTX_DATA_DIR", _data.path());
164
165        let repo_a = _tmp.path().join("repo-a");
166        let repo_b = _tmp.path().join("repo-b");
167        let root_a = create_git_root(&repo_a);
168        let root_b = create_git_root(&repo_b);
169
170        let mut session_b = crate::core::session::SessionState::new();
171        session_b.project_root = Some(root_b.clone());
172        session_b.shell_cwd = Some(root_b.clone());
173        session_b.set_task("repo-b task", None);
174        session_b.save().unwrap();
175
176        std::thread::sleep(std::time::Duration::from_millis(50));
177
178        let mut session_a = crate::core::session::SessionState::new();
179        session_a.project_root = Some(root_a.clone());
180        session_a.shell_cwd = Some(root_a.clone());
181        session_a.set_task("repo-a latest task", None);
182        session_a.save().unwrap();
183
184        let server = LeanCtxServer::new_with_startup(
185            None,
186            Some(repo_b.as_path()),
187            SessionMode::Personal,
188            "default",
189            "default",
190        );
191        std::env::remove_var("LEAN_CTX_DATA_DIR");
192
193        let session = server.session.read().await;
194        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
195        assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
196        assert_eq!(
197            session.task.as_ref().map(|t| t.description.as_str()),
198            Some("repo-b task")
199        );
200    }
201
202    #[tokio::test]
203    #[allow(clippy::await_holding_lock)]
204    async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
205        let _lock = crate::core::data_dir::test_env_lock();
206        let _data = tempfile::tempdir().unwrap();
207        let _tmp = tempfile::tempdir().unwrap();
208
209        std::env::set_var("LEAN_CTX_DATA_DIR", _data.path());
210
211        let repo_a = _tmp.path().join("repo-a");
212        let repo_b = _tmp.path().join("repo-b");
213        let repo_b_src = repo_b.join("src");
214        let root_a = create_git_root(&repo_a);
215        let root_b = create_git_root(&repo_b);
216        std::fs::create_dir_all(&repo_b_src).unwrap();
217        let repo_b_src_value = canonicalize_path(&repo_b_src);
218
219        let mut session_a = crate::core::session::SessionState::new();
220        session_a.project_root = Some(root_a.clone());
221        session_a.shell_cwd = Some(root_a.clone());
222        session_a.set_task("repo-a latest task", None);
223        let old_id = session_a.id.clone();
224        session_a.save().unwrap();
225
226        let server = LeanCtxServer::new_with_startup(
227            None,
228            Some(repo_b_src.as_path()),
229            SessionMode::Personal,
230            "default",
231            "default",
232        );
233        std::env::remove_var("LEAN_CTX_DATA_DIR");
234
235        let session = server.session.read().await;
236        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
237        assert_eq!(
238            session.shell_cwd.as_deref(),
239            Some(repo_b_src_value.as_str())
240        );
241        assert!(session.task.is_none());
242        assert_ne!(session.id, old_id);
243    }
244
245    #[cfg(not(feature = "no-jail"))]
246    #[tokio::test]
247    async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
248        let tmp = tempfile::tempdir().unwrap();
249        let root = tmp.path().join("root");
250        let other = tmp.path().join("other");
251        let root_value = create_git_root(&root);
252        create_git_root(&other);
253        std::fs::write(other.join("b.txt"), "no").unwrap();
254
255        let root_str = root.to_string_lossy().to_string();
256        let server = LeanCtxServer::new_with_project_root(Some(&root_str));
257
258        let err = server
259            .resolve_path(&other.join("b.txt").to_string_lossy())
260            .await
261            .unwrap_err();
262        assert!(err.contains("path escapes project root"));
263
264        let session = server.session.read().await;
265        assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
266    }
267}