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_explore;
22pub mod ctx_feedback;
23pub mod ctx_fill;
24pub mod ctx_gain;
25pub mod ctx_glob;
26pub mod ctx_graph;
27pub mod ctx_graph_diagram;
28pub mod ctx_graph_diff;
29pub mod ctx_graph_primitives;
30pub mod ctx_handoff;
31pub mod ctx_heatmap;
32pub mod ctx_impact;
33pub mod ctx_index;
34pub mod ctx_intent;
35pub mod ctx_knowledge;
36pub mod ctx_knowledge_relations;
37pub mod ctx_metrics;
38pub mod ctx_multi_read;
39pub mod ctx_multi_repo;
40pub mod ctx_outline;
41pub mod ctx_overview;
42pub mod ctx_pack;
43pub mod ctx_package;
44pub mod ctx_plan;
45pub mod ctx_plugins;
46pub mod ctx_prefetch;
47pub mod ctx_preload;
48pub mod ctx_proof;
49pub mod ctx_provider;
50pub mod ctx_read;
51pub mod ctx_refactor;
52pub mod ctx_repomap;
53pub mod ctx_response;
54pub mod ctx_review;
55pub mod ctx_routes;
56pub mod ctx_rules;
57pub mod ctx_search;
58pub mod ctx_semantic_search;
59pub mod ctx_session;
60pub mod ctx_share;
61pub mod ctx_shell;
62pub mod ctx_skillify;
63pub mod ctx_smart_read;
64pub mod ctx_smells;
65pub mod ctx_summary;
66pub mod ctx_symbol;
67pub mod ctx_task;
68pub mod ctx_tools;
69pub mod ctx_transcript_compact;
70pub mod ctx_tree;
71pub mod ctx_verify;
72pub mod ctx_workflow;
73pub(crate) mod edit_recovery;
74pub(crate) mod knowledge_shared;
75pub mod registered;
76pub(crate) mod walk_guard;
77
78mod server;
79mod server_lifecycle;
80mod server_metrics;
81mod server_paths;
82pub(crate) mod startup;
83
84pub use server::*;
85pub use startup::create_server;
86
87#[cfg(test)]
88mod resolve_path_tests {
89    use super::startup::canonicalize_path;
90    use super::*;
91
92    fn create_git_root(path: &std::path::Path) -> String {
93        std::fs::create_dir_all(path.join(".git")).unwrap();
94        canonicalize_path(path)
95    }
96
97    #[cfg(not(feature = "no-jail"))]
98    #[tokio::test]
99    async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
100        crate::test_env::set_var("LEAN_CTX_ALLOW_REROOT", "1");
101        let tmp = tempfile::tempdir().unwrap();
102        let stale = tmp.path().join("stale");
103        let real = tmp.path().join("real");
104        std::fs::create_dir_all(&stale).unwrap();
105        let real_root = create_git_root(&real);
106        std::fs::write(real.join("a.txt"), "ok").unwrap();
107
108        let server = LeanCtxServer::new_with_startup(
109            None,
110            Some(real.as_path()),
111            SessionMode::Personal,
112            "default",
113            "default",
114        );
115        {
116            let mut session = server.session.write().await;
117            session.project_root = Some(stale.to_string_lossy().to_string());
118            session.shell_cwd = Some(stale.to_string_lossy().to_string());
119        }
120
121        let out = server
122            .resolve_path(&real.join("a.txt").to_string_lossy())
123            .await
124            .unwrap();
125
126        assert!(out.ends_with("/a.txt"));
127
128        let session = server.session.read().await;
129        assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
130        assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
131    }
132
133    #[cfg(not(feature = "no-jail"))]
134    #[tokio::test]
135    #[allow(clippy::await_holding_lock)]
136    async fn resolve_path_rejects_absolute_path_outside_trusted_startup_root() {
137        // Hermetic config + serialized via test_env_lock so a parallel test that
138        // flips `path_jail` cannot disable this jail-enforcement assertion (#406).
139        let _iso = crate::core::data_dir::isolated_data_dir();
140        let tmp = tempfile::tempdir().unwrap();
141        let stale = tmp.path().join("stale");
142        let root = tmp.path().join("root");
143        let other = tmp.path().join("other");
144        std::fs::create_dir_all(&stale).unwrap();
145        create_git_root(&root);
146        let _other_value = create_git_root(&other);
147        std::fs::write(other.join("b.txt"), "no").unwrap();
148
149        let server = LeanCtxServer::new_with_startup(
150            None,
151            Some(root.as_path()),
152            SessionMode::Personal,
153            "default",
154            "default",
155        );
156        {
157            let mut session = server.session.write().await;
158            session.project_root = Some(stale.to_string_lossy().to_string());
159            session.shell_cwd = Some(stale.to_string_lossy().to_string());
160        }
161
162        let err = server
163            .resolve_path(&other.join("b.txt").to_string_lossy())
164            .await
165            .unwrap_err();
166        assert!(err.contains("path escapes project root"));
167
168        let session = server.session.read().await;
169        assert_eq!(
170            session.project_root.as_deref(),
171            Some(stale.to_string_lossy().as_ref())
172        );
173    }
174
175    #[cfg(not(feature = "no-jail"))]
176    #[tokio::test]
177    #[allow(clippy::await_holding_lock)]
178    async fn resolve_path_auto_reroots_from_agent_config_dir_without_opt_in() {
179        // #580: the MCP server is launched from an agent/IDE config dir
180        // (~/.copilot-style) and wrongly adopts it as the root. With no
181        // `allow_auto_reroot` opt-in and no trusted startup root, the first
182        // absolute path into a real project must still correct the root — an
183        // agent config dir is never a real jail boundary.
184        let _iso = crate::core::data_dir::isolated_data_dir();
185        crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
186        let tmp = tempfile::tempdir().unwrap();
187        let agent = tmp.path().join(".copilot");
188        let real = tmp.path().join("repo");
189        std::fs::create_dir_all(&agent).unwrap();
190        let real_root = create_git_root(&real);
191        std::fs::write(real.join("a.txt"), "ok").unwrap();
192
193        let server = LeanCtxServer::new_with_startup(
194            None,
195            None,
196            SessionMode::Personal,
197            "default",
198            "default",
199        );
200        {
201            let mut session = server.session.write().await;
202            session.project_root = Some(agent.to_string_lossy().to_string());
203            session.shell_cwd = Some(agent.to_string_lossy().to_string());
204        }
205
206        let out = server
207            .resolve_path(&real.join("a.txt").to_string_lossy())
208            .await
209            .unwrap();
210        assert!(
211            out.ends_with("/a.txt"),
212            "agent-dir jail must auto-correct: {out}"
213        );
214
215        let session = server.session.read().await;
216        assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
217    }
218
219    #[cfg(not(feature = "no-jail"))]
220    #[tokio::test]
221    #[allow(clippy::await_holding_lock)]
222    async fn resolve_path_agent_dir_still_blocks_markerless_escape() {
223        // The agent-dir bypass only reroots to a *real* project (one carrying a
224        // marker). A markerless absolute path outside the jail stays blocked —
225        // PathJail enforcement is unchanged, only the root *choice* is corrected.
226        let _iso = crate::core::data_dir::isolated_data_dir();
227        crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
228        let tmp = tempfile::tempdir().unwrap();
229        let agent = tmp.path().join(".copilot");
230        let loose = tmp.path().join("loose");
231        std::fs::create_dir_all(&agent).unwrap();
232        std::fs::create_dir_all(&loose).unwrap();
233        std::fs::write(loose.join("data.txt"), "no").unwrap();
234
235        let server = LeanCtxServer::new_with_startup(
236            None,
237            None,
238            SessionMode::Personal,
239            "default",
240            "default",
241        );
242        {
243            let mut session = server.session.write().await;
244            session.project_root = Some(agent.to_string_lossy().to_string());
245            session.shell_cwd = Some(agent.to_string_lossy().to_string());
246        }
247
248        let err = server
249            .resolve_path(&loose.join("data.txt").to_string_lossy())
250            .await
251            .unwrap_err();
252        assert!(err.contains("path escapes project root"), "got: {err}");
253
254        let session = server.session.read().await;
255        assert_eq!(
256            session.project_root.as_deref(),
257            Some(agent.to_string_lossy().as_ref())
258        );
259    }
260
261    #[tokio::test]
262    #[allow(clippy::await_holding_lock)]
263    async fn startup_prefers_workspace_scoped_session_over_global_latest() {
264        let _lock = crate::core::data_dir::test_env_lock();
265        let _data = tempfile::tempdir().unwrap();
266        let _tmp = tempfile::tempdir().unwrap();
267
268        crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
269
270        let repo_a = _tmp.path().join("repo-a");
271        let repo_b = _tmp.path().join("repo-b");
272        let root_a = create_git_root(&repo_a);
273        let root_b = create_git_root(&repo_b);
274
275        let mut session_b = crate::core::session::SessionState::new();
276        session_b.project_root = Some(root_b.clone());
277        session_b.shell_cwd = Some(root_b.clone());
278        session_b.set_task("repo-b task", None);
279        session_b.save().unwrap();
280
281        std::thread::sleep(std::time::Duration::from_millis(50));
282
283        let mut session_a = crate::core::session::SessionState::new();
284        session_a.project_root = Some(root_a.clone());
285        session_a.shell_cwd = Some(root_a.clone());
286        session_a.set_task("repo-a latest task", None);
287        session_a.save().unwrap();
288
289        let server = LeanCtxServer::new_with_startup(
290            None,
291            Some(repo_b.as_path()),
292            SessionMode::Personal,
293            "default",
294            "default",
295        );
296        crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
297
298        let session = server.session.read().await;
299        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
300        assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
301        assert_eq!(
302            session.task.as_ref().map(|t| t.description.as_str()),
303            Some("repo-b task")
304        );
305    }
306
307    #[tokio::test]
308    #[allow(clippy::await_holding_lock)]
309    async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
310        let _lock = crate::core::data_dir::test_env_lock();
311        let _data = tempfile::tempdir().unwrap();
312        let _tmp = tempfile::tempdir().unwrap();
313
314        crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
315
316        let repo_a = _tmp.path().join("repo-a");
317        let repo_b = _tmp.path().join("repo-b");
318        let repo_b_src = repo_b.join("src");
319        let root_a = create_git_root(&repo_a);
320        let root_b = create_git_root(&repo_b);
321        std::fs::create_dir_all(&repo_b_src).unwrap();
322        let repo_b_src_value = canonicalize_path(&repo_b_src);
323
324        let mut session_a = crate::core::session::SessionState::new();
325        session_a.project_root = Some(root_a.clone());
326        session_a.shell_cwd = Some(root_a.clone());
327        session_a.set_task("repo-a latest task", None);
328        let old_id = session_a.id.clone();
329        session_a.save().unwrap();
330
331        let server = LeanCtxServer::new_with_startup(
332            None,
333            Some(repo_b_src.as_path()),
334            SessionMode::Personal,
335            "default",
336            "default",
337        );
338        crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
339
340        let session = server.session.read().await;
341        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
342        assert_eq!(
343            session.shell_cwd.as_deref(),
344            Some(repo_b_src_value.as_str())
345        );
346        assert!(session.task.is_none());
347        assert_ne!(session.id, old_id);
348    }
349
350    #[cfg(not(feature = "no-jail"))]
351    #[tokio::test]
352    #[allow(clippy::await_holding_lock)]
353    async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
354        // Hermetic config + serialized via test_env_lock so a parallel test that
355        // flips `path_jail` cannot disable this jail-enforcement assertion (#406).
356        let _iso = crate::core::data_dir::isolated_data_dir();
357        let tmp = tempfile::tempdir().unwrap();
358        let root = tmp.path().join("root");
359        let other = tmp.path().join("other");
360        let root_value = create_git_root(&root);
361        create_git_root(&other);
362        std::fs::write(other.join("b.txt"), "no").unwrap();
363
364        let root_str = root.to_string_lossy().to_string();
365        let server = LeanCtxServer::new_with_project_root(Some(&root_str));
366
367        let err = server
368            .resolve_path(&other.join("b.txt").to_string_lossy())
369            .await
370            .unwrap_err();
371        assert!(err.contains("path escapes project root"));
372
373        let session = server.session.read().await;
374        assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
375    }
376}