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