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;
8#[allow(dead_code, unreachable_pub)]
9pub mod ctx_cognitive;
10pub mod ctx_compile;
11pub mod ctx_compose;
12pub mod ctx_compress;
13pub mod ctx_compress_memory;
14pub mod ctx_context;
15pub mod ctx_control;
16pub mod ctx_cost;
17pub mod ctx_dedup;
18pub mod ctx_delta;
19pub mod ctx_discover;
20pub mod ctx_edit;
21pub mod ctx_execute;
22pub mod ctx_expand;
23pub mod ctx_explore;
24pub mod ctx_feedback;
25pub mod ctx_fill;
26pub mod ctx_gain;
27pub mod ctx_glob;
28pub mod ctx_graph;
29pub mod ctx_graph_diagram;
30pub mod ctx_graph_diff;
31pub mod ctx_graph_primitives;
32pub mod ctx_handoff;
33pub mod ctx_heatmap;
34pub mod ctx_impact;
35pub mod ctx_index;
36pub mod ctx_intent;
37pub mod ctx_knowledge;
38pub mod ctx_knowledge_relations;
39pub mod ctx_metrics;
40pub mod ctx_multi_read;
41pub mod ctx_multi_repo;
42pub mod ctx_outline;
43pub mod ctx_overview;
44pub mod ctx_pack;
45pub mod ctx_package;
46pub mod ctx_patch;
47pub mod ctx_plan;
48pub mod ctx_plugins;
49pub mod ctx_prefetch;
50pub mod ctx_preload;
51pub mod ctx_proof;
52pub mod ctx_provider;
53pub mod ctx_quality;
54pub mod ctx_read;
55pub mod ctx_refactor;
56pub mod ctx_repomap;
57pub mod ctx_response;
58pub mod ctx_review;
59pub mod ctx_routes;
60pub mod ctx_rules;
61pub mod ctx_search;
62pub mod ctx_semantic_search;
63pub mod ctx_session;
64pub mod ctx_share;
65pub mod ctx_shell;
66pub mod ctx_skillify;
67pub mod ctx_smart_read;
68pub mod ctx_smells;
69pub mod ctx_summary;
70pub mod ctx_symbol;
71pub mod ctx_task;
72pub mod ctx_tools;
73pub mod ctx_transcript_compact;
74pub mod ctx_tree;
75pub mod ctx_verify;
76pub mod ctx_workflow;
77pub(crate) mod edit_io;
78pub(crate) mod edit_recovery;
79pub(crate) mod graph_meta;
80pub(crate) mod knowledge_shared;
81pub(crate) mod output_format;
82pub mod registered;
83pub mod search_hook;
84pub mod search_kernel;
85pub(crate) mod walk_guard;
86
87mod server;
88mod server_lifecycle;
89mod server_metrics;
90mod server_paths;
91pub(crate) mod startup;
92
93pub use server::*;
94pub use startup::create_server;
95
96#[cfg(test)]
97mod resolve_path_tests {
98 use super::startup::canonicalize_path;
99 use super::*;
100
101 fn create_git_root(path: &std::path::Path) -> String {
102 std::fs::create_dir_all(path.join(".git")).unwrap();
103 canonicalize_path(path)
104 }
105
106 #[cfg(not(feature = "no-jail"))]
107 #[tokio::test]
108 #[allow(clippy::await_holding_lock)]
109 async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
110 let _iso = crate::core::data_dir::isolated_data_dir();
117 crate::test_env::set_var("LEAN_CTX_ALLOW_REROOT", "1");
118 let tmp = tempfile::tempdir().unwrap();
119 let stale = tmp.path().join("stale");
120 let real = tmp.path().join("real");
121 std::fs::create_dir_all(&stale).unwrap();
122 let real_root = create_git_root(&real);
123 std::fs::write(real.join("a.txt"), "ok").unwrap();
124
125 let server = LeanCtxServer::new_with_startup(
126 None,
127 Some(real.as_path()),
128 SessionMode::Personal,
129 "default",
130 "default",
131 );
132 {
133 let mut session = server.session.write().await;
134 session.project_root = Some(stale.to_string_lossy().to_string());
135 session.shell_cwd = Some(stale.to_string_lossy().to_string());
136 }
137
138 let out = server
139 .resolve_path(&real.join("a.txt").to_string_lossy())
140 .await
141 .unwrap();
142
143 assert!(out.ends_with("/a.txt"));
144
145 let session = server.session.read().await;
146 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
147 assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
148 drop(session);
149 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
150 }
151
152 #[cfg(not(feature = "no-jail"))]
153 #[tokio::test]
154 #[allow(clippy::await_holding_lock)]
155 async fn resolve_path_rejects_absolute_path_outside_trusted_startup_root() {
156 let _iso = crate::core::data_dir::isolated_data_dir();
159 let tmp = tempfile::tempdir().unwrap();
160 let stale = tmp.path().join("stale");
161 let root = tmp.path().join("root");
162 let other = tmp.path().join("other");
163 std::fs::create_dir_all(&stale).unwrap();
164 create_git_root(&root);
165 let _other_value = create_git_root(&other);
166 std::fs::write(other.join("b.txt"), "no").unwrap();
167
168 let server = LeanCtxServer::new_with_startup(
169 None,
170 Some(root.as_path()),
171 SessionMode::Personal,
172 "default",
173 "default",
174 );
175 {
176 let mut session = server.session.write().await;
177 session.project_root = Some(stale.to_string_lossy().to_string());
178 session.shell_cwd = Some(stale.to_string_lossy().to_string());
179 }
180
181 let err = server
182 .resolve_path(&other.join("b.txt").to_string_lossy())
183 .await
184 .unwrap_err();
185 assert!(err.contains("path escapes project root"));
186
187 let session = server.session.read().await;
188 assert_eq!(
189 session.project_root.as_deref(),
190 Some(stale.to_string_lossy().as_ref())
191 );
192 }
193
194 #[cfg(not(feature = "no-jail"))]
195 #[tokio::test]
196 #[allow(clippy::await_holding_lock)]
197 async fn resolve_path_auto_registers_language_cache_then_retry_succeeds() {
198 let _iso = crate::core::data_dir::isolated_data_dir();
202 let tmp = tempfile::tempdir().unwrap();
203 let root = tmp.path().join("project");
204 create_git_root(&root);
205 let dep = tmp.path().join("go/pkg/mod/example.com/lib@v1");
206 std::fs::create_dir_all(&dep).unwrap();
207 let file = dep.join("lib.go");
208 std::fs::write(&file, "package lib").unwrap();
209
210 let server = LeanCtxServer::new_with_startup(
211 None,
212 Some(root.as_path()),
213 SessionMode::Personal,
214 "default",
215 "default",
216 );
217 {
218 let mut session = server.session.write().await;
219 session.project_root = Some(root.to_string_lossy().to_string());
220 session.shell_cwd = Some(root.to_string_lossy().to_string());
221 }
222
223 let err = server
225 .resolve_path(&file.to_string_lossy())
226 .await
227 .unwrap_err();
228 assert!(
229 err.contains("Auto-detected Go module cache"),
230 "expected auto-detect hint, got: {err}"
231 );
232 assert!(err.contains("Retry"), "hint must ask for a retry: {err}");
233
234 let ok = server
236 .resolve_path(&file.to_string_lossy())
237 .await
238 .unwrap_or_else(|e| panic!("retry must resolve, got: {e}"));
239 assert!(
240 ok.ends_with("/lib.go"),
241 "retry resolves the cache file: {ok}"
242 );
243 }
244
245 #[cfg(not(feature = "no-jail"))]
246 #[tokio::test]
247 #[allow(clippy::await_holding_lock)]
248 async fn resolve_path_auto_reroots_from_agent_config_dir_without_opt_in() {
249 let _iso = crate::core::data_dir::isolated_data_dir();
255 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
256 let tmp = tempfile::tempdir().unwrap();
257 let agent = tmp.path().join(".copilot");
258 let real = tmp.path().join("repo");
259 std::fs::create_dir_all(&agent).unwrap();
260 let real_root = create_git_root(&real);
261 std::fs::write(real.join("a.txt"), "ok").unwrap();
262
263 let server = LeanCtxServer::new_with_startup(
264 None,
265 None,
266 SessionMode::Personal,
267 "default",
268 "default",
269 );
270 {
271 let mut session = server.session.write().await;
272 session.project_root = Some(agent.to_string_lossy().to_string());
273 session.shell_cwd = Some(agent.to_string_lossy().to_string());
274 }
275
276 let out = server
277 .resolve_path(&real.join("a.txt").to_string_lossy())
278 .await
279 .unwrap();
280 assert!(
281 out.ends_with("/a.txt"),
282 "agent-dir jail must auto-correct: {out}"
283 );
284
285 let session = server.session.read().await;
286 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
287 }
288
289 #[cfg(not(feature = "no-jail"))]
290 #[tokio::test]
291 #[allow(clippy::await_holding_lock)]
292 async fn resolve_path_auto_reroots_from_markerless_client_cwd_without_opt_in() {
293 let _iso = crate::core::data_dir::isolated_data_dir();
297 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
298 let tmp = tempfile::tempdir().unwrap();
299 let client_cwd = tmp.path().join("Users").join("user");
300 let real = tmp.path().join("workspaces").join("lean-ctx");
301 std::fs::create_dir_all(&client_cwd).unwrap();
302 let real_root = create_git_root(&real);
303 std::fs::write(real.join("rust.rs"), "ok").unwrap();
304
305 let server = LeanCtxServer::new_with_startup(
306 None,
307 None,
308 SessionMode::Personal,
309 "default",
310 "default",
311 );
312 {
313 let mut session = server.session.write().await;
314 session.project_root = Some(client_cwd.to_string_lossy().to_string());
315 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
316 }
317
318 let out = server
319 .resolve_path(&real.join("rust.rs").to_string_lossy())
320 .await
321 .unwrap();
322 assert!(
323 out.ends_with("/rust.rs"),
324 "markerless client cwd must auto-correct: {out}"
325 );
326
327 let session = server.session.read().await;
328 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
329 }
330
331 #[cfg(not(feature = "no-jail"))]
332 #[tokio::test]
333 #[allow(clippy::await_holding_lock)]
334 async fn resolve_path_auto_reroots_even_when_extra_root_allows_path() {
335 let _iso = crate::core::data_dir::isolated_data_dir();
336 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
337 let tmp = tempfile::tempdir().unwrap();
338 let client_cwd = tmp.path().join("Users").join("user");
339 let real = tmp.path().join("workspaces").join("lean-ctx");
340 std::fs::create_dir_all(&client_cwd).unwrap();
341 let real_root = create_git_root(&real);
342 std::fs::write(real.join("rust.rs"), "ok").unwrap();
343
344 let server = LeanCtxServer::new_with_startup(
345 None,
346 None,
347 SessionMode::Personal,
348 "default",
349 "default",
350 );
351 {
352 let mut session = server.session.write().await;
353 session.project_root = Some(client_cwd.to_string_lossy().to_string());
354 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
355 session.extra_roots.push(real_root.clone());
356 }
357
358 let out = server
359 .resolve_path(&real.join("rust.rs").to_string_lossy())
360 .await
361 .unwrap();
362 assert!(
363 out.ends_with("/rust.rs"),
364 "extra root must not suppress markerless-cwd reroot: {out}"
365 );
366
367 let session = server.session.read().await;
368 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
369 assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
370 }
371
372 #[cfg(not(feature = "no-jail"))]
373 #[tokio::test]
374 #[allow(clippy::await_holding_lock)]
375 async fn resolve_path_markerless_root_still_blocks_markerless_escape() {
376 let _iso = crate::core::data_dir::isolated_data_dir();
380 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
381 let tmp = tempfile::tempdir().unwrap();
382 let client_cwd = tmp.path().join("Users").join("user");
383 let loose = tmp.path().join("workspaces").join("loose");
384 std::fs::create_dir_all(&client_cwd).unwrap();
385 std::fs::create_dir_all(&loose).unwrap();
386 std::fs::write(loose.join("data.txt"), "no").unwrap();
387
388 let server = LeanCtxServer::new_with_startup(
389 None,
390 None,
391 SessionMode::Personal,
392 "default",
393 "default",
394 );
395 {
396 let mut session = server.session.write().await;
397 session.project_root = Some(client_cwd.to_string_lossy().to_string());
398 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
399 }
400
401 let err = server
402 .resolve_path(&loose.join("data.txt").to_string_lossy())
403 .await
404 .unwrap_err();
405 assert!(err.contains("path escapes project root"), "got: {err}");
406
407 let session = server.session.read().await;
408 assert_eq!(
409 session.project_root.as_deref(),
410 Some(client_cwd.to_string_lossy().as_ref())
411 );
412 }
413
414 #[cfg(not(feature = "no-jail"))]
415 #[tokio::test]
416 #[allow(clippy::await_holding_lock)]
417 async fn resolve_path_agent_dir_still_blocks_markerless_escape() {
418 let _iso = crate::core::data_dir::isolated_data_dir();
422 crate::test_env::remove_var("LEAN_CTX_ALLOW_REROOT");
423 let tmp = tempfile::tempdir().unwrap();
424 let agent = tmp.path().join(".copilot");
425 let loose = tmp.path().join("loose");
426 std::fs::create_dir_all(&agent).unwrap();
427 std::fs::create_dir_all(&loose).unwrap();
428 std::fs::write(loose.join("data.txt"), "no").unwrap();
429
430 let server = LeanCtxServer::new_with_startup(
431 None,
432 None,
433 SessionMode::Personal,
434 "default",
435 "default",
436 );
437 {
438 let mut session = server.session.write().await;
439 session.project_root = Some(agent.to_string_lossy().to_string());
440 session.shell_cwd = Some(agent.to_string_lossy().to_string());
441 }
442
443 let err = server
444 .resolve_path(&loose.join("data.txt").to_string_lossy())
445 .await
446 .unwrap_err();
447 assert!(err.contains("path escapes project root"), "got: {err}");
448
449 let session = server.session.read().await;
450 assert_eq!(
451 session.project_root.as_deref(),
452 Some(agent.to_string_lossy().as_ref())
453 );
454 }
455
456 #[tokio::test]
457 #[allow(clippy::await_holding_lock)]
458 async fn startup_creates_fresh_session_scoped_to_workspace() {
459 let _lock = crate::core::data_dir::test_env_lock();
460 let _data = tempfile::tempdir().unwrap();
461 let _tmp = tempfile::tempdir().unwrap();
462
463 crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
464
465 let repo_b = _tmp.path().join("repo-b");
466 let root_b = create_git_root(&repo_b);
467
468 let server = LeanCtxServer::new_with_startup(
469 None,
470 Some(repo_b.as_path()),
471 SessionMode::Personal,
472 "default",
473 "default",
474 );
475 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
476
477 let session = server.session.read().await;
478 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
482 assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
483 assert!(session.task.is_none());
484 }
485
486 #[tokio::test]
487 #[allow(clippy::await_holding_lock)]
488 async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
489 let _lock = crate::core::data_dir::test_env_lock();
490 let _data = tempfile::tempdir().unwrap();
491 let _tmp = tempfile::tempdir().unwrap();
492
493 crate::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
494
495 let repo_a = _tmp.path().join("repo-a");
496 let repo_b = _tmp.path().join("repo-b");
497 let repo_b_src = repo_b.join("src");
498 let root_a = create_git_root(&repo_a);
499 let root_b = create_git_root(&repo_b);
500 std::fs::create_dir_all(&repo_b_src).unwrap();
501 let repo_b_src_value = canonicalize_path(&repo_b_src);
502
503 let mut session_a = crate::core::session::SessionState::new();
504 session_a.project_root = Some(root_a.clone());
505 session_a.shell_cwd = Some(root_a.clone());
506 session_a.set_task("repo-a latest task", None);
507 let old_id = session_a.id.clone();
508 session_a.save().unwrap();
509
510 let server = LeanCtxServer::new_with_startup(
511 None,
512 Some(repo_b_src.as_path()),
513 SessionMode::Personal,
514 "default",
515 "default",
516 );
517 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
518
519 let session = server.session.read().await;
520 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
521 assert_eq!(
522 session.shell_cwd.as_deref(),
523 Some(repo_b_src_value.as_str())
524 );
525 assert!(session.task.is_none());
526 assert_ne!(session.id, old_id);
527 }
528
529 #[cfg(not(feature = "no-jail"))]
530 #[tokio::test]
531 #[allow(clippy::await_holding_lock)]
532 async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
533 let _iso = crate::core::data_dir::isolated_data_dir();
536 let tmp = tempfile::tempdir().unwrap();
537 let root = tmp.path().join("root");
538 let other = tmp.path().join("other");
539 let root_value = create_git_root(&root);
540 create_git_root(&other);
541 std::fs::write(other.join("b.txt"), "no").unwrap();
542
543 let root_str = root.to_string_lossy().to_string();
544 let server = LeanCtxServer::new_with_project_root(Some(&root_str));
545
546 let err = server
547 .resolve_path(&other.join("b.txt").to_string_lossy())
548 .await
549 .unwrap_err();
550 assert!(err.contains("path escapes project root"));
551
552 let session = server.session.read().await;
553 assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
554 }
555
556 #[tokio::test]
563 #[allow(clippy::await_holding_lock)]
564 async fn resolve_path_prefers_worktree_copy_after_mid_session_switch() {
565 let _iso = crate::core::data_dir::isolated_data_dir();
566 let tmp = tempfile::tempdir().unwrap();
567 let repo = tmp.path().join("repo");
568 let root_value = create_git_root(&repo);
569 let wt = repo.join(".claude/worktrees/wt");
570 std::fs::create_dir_all(wt.join("src")).unwrap();
571 std::fs::write(wt.join(".git"), "gitdir: ../../../.git/worktrees/wt\n").unwrap();
572 std::fs::create_dir_all(repo.join("src")).unwrap();
573 std::fs::write(repo.join("src/lib.rs"), "stale").unwrap();
574 std::fs::write(wt.join("src/lib.rs"), "fresh").unwrap();
575
576 let server = LeanCtxServer::new_with_project_root(Some(&root_value));
577 {
578 let mut session = server.session.write().await;
580 session.shell_cwd = Some(wt.to_string_lossy().to_string());
581 }
582
583 let out = server.resolve_path("src/lib.rs").await.unwrap();
584 assert_eq!(
585 std::fs::read_to_string(&out).unwrap(),
586 "fresh",
587 "worktree copy must win over the stale project_root copy: {out}"
588 );
589
590 {
597 let mut session = server.session.write().await;
598 session.shell_cwd = Some(root_value.clone());
599 }
600 std::fs::write(repo.join("src/back_707.rs"), "stale").unwrap();
601 std::fs::write(wt.join("src/back_707.rs"), "fresh").unwrap();
602 let out = server.resolve_path("src/back_707.rs").await.unwrap();
603 assert_eq!(std::fs::read_to_string(&out).unwrap(), "stale");
604 }
605}