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 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 #[tokio::test]
176 #[allow(clippy::await_holding_lock)]
177 async fn startup_prefers_workspace_scoped_session_over_global_latest() {
178 let _lock = crate::core::data_dir::test_env_lock();
179 let _data = tempfile::tempdir().unwrap();
180 let _tmp = tempfile::tempdir().unwrap();
181
182 crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
183
184 let repo_a = _tmp.path().join("repo-a");
185 let repo_b = _tmp.path().join("repo-b");
186 let root_a = create_git_root(&repo_a);
187 let root_b = create_git_root(&repo_b);
188
189 let mut session_b = crate::core::session::SessionState::new();
190 session_b.project_root = Some(root_b.clone());
191 session_b.shell_cwd = Some(root_b.clone());
192 session_b.set_task("repo-b task", None);
193 session_b.save().unwrap();
194
195 std::thread::sleep(std::time::Duration::from_millis(50));
196
197 let mut session_a = crate::core::session::SessionState::new();
198 session_a.project_root = Some(root_a.clone());
199 session_a.shell_cwd = Some(root_a.clone());
200 session_a.set_task("repo-a latest task", None);
201 session_a.save().unwrap();
202
203 let server = LeanCtxServer::new_with_startup(
204 None,
205 Some(repo_b.as_path()),
206 SessionMode::Personal,
207 "default",
208 "default",
209 );
210 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
211
212 let session = server.session.read().await;
213 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
214 assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
215 assert_eq!(
216 session.task.as_ref().map(|t| t.description.as_str()),
217 Some("repo-b task")
218 );
219 }
220
221 #[tokio::test]
222 #[allow(clippy::await_holding_lock)]
223 async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
224 let _lock = crate::core::data_dir::test_env_lock();
225 let _data = tempfile::tempdir().unwrap();
226 let _tmp = tempfile::tempdir().unwrap();
227
228 crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
229
230 let repo_a = _tmp.path().join("repo-a");
231 let repo_b = _tmp.path().join("repo-b");
232 let repo_b_src = repo_b.join("src");
233 let root_a = create_git_root(&repo_a);
234 let root_b = create_git_root(&repo_b);
235 std::fs::create_dir_all(&repo_b_src).unwrap();
236 let repo_b_src_value = canonicalize_path(&repo_b_src);
237
238 let mut session_a = crate::core::session::SessionState::new();
239 session_a.project_root = Some(root_a.clone());
240 session_a.shell_cwd = Some(root_a.clone());
241 session_a.set_task("repo-a latest task", None);
242 let old_id = session_a.id.clone();
243 session_a.save().unwrap();
244
245 let server = LeanCtxServer::new_with_startup(
246 None,
247 Some(repo_b_src.as_path()),
248 SessionMode::Personal,
249 "default",
250 "default",
251 );
252 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
253
254 let session = server.session.read().await;
255 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
256 assert_eq!(
257 session.shell_cwd.as_deref(),
258 Some(repo_b_src_value.as_str())
259 );
260 assert!(session.task.is_none());
261 assert_ne!(session.id, old_id);
262 }
263
264 #[cfg(not(feature = "no-jail"))]
265 #[tokio::test]
266 #[allow(clippy::await_holding_lock)]
267 async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
268 let _iso = crate::core::data_dir::isolated_data_dir();
271 let tmp = tempfile::tempdir().unwrap();
272 let root = tmp.path().join("root");
273 let other = tmp.path().join("other");
274 let root_value = create_git_root(&root);
275 create_git_root(&other);
276 std::fs::write(other.join("b.txt"), "no").unwrap();
277
278 let root_str = root.to_string_lossy().to_string();
279 let server = LeanCtxServer::new_with_project_root(Some(&root_str));
280
281 let err = server
282 .resolve_path(&other.join("b.txt").to_string_lossy())
283 .await
284 .unwrap_err();
285 assert!(err.contains("path escapes project root"));
286
287 let session = server.session.read().await;
288 assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
289 }
290}