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