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