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