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