pub mod admitted_sources;
pub mod autonomy;
pub mod ctx_agent;
pub mod ctx_architecture;
pub mod ctx_artifacts;
pub mod ctx_callgraph;
#[allow(dead_code, unreachable_pub)]
pub mod ctx_compile;
pub mod ctx_compose;
pub mod ctx_compress;
pub mod ctx_compress_memory;
pub mod ctx_context;
pub mod ctx_control;
pub mod ctx_cost;
pub mod ctx_dedup;
pub mod ctx_delta;
pub mod ctx_discover;
pub mod ctx_doc;
pub mod ctx_edit;
pub mod ctx_execute;
pub mod ctx_expand;
pub mod ctx_explore;
pub mod ctx_feedback;
pub mod ctx_gain;
pub mod ctx_glob;
pub mod ctx_graph;
pub mod ctx_graph_diagram;
pub mod ctx_graph_diff;
pub mod ctx_graph_primitives;
pub mod ctx_handoff;
pub mod ctx_heatmap;
pub mod ctx_impact;
pub mod ctx_index;
pub mod ctx_knowledge;
pub mod ctx_knowledge_relations;
pub mod ctx_metrics;
pub mod ctx_multi_read;
pub mod ctx_multi_repo;
pub mod ctx_outline;
pub mod ctx_overview;
pub mod ctx_pack;
pub mod ctx_package;
pub mod ctx_patch;
pub mod ctx_plan;
pub mod ctx_prefetch;
pub mod ctx_preload;
pub mod ctx_proof;
pub mod ctx_provider;
pub mod ctx_read;
pub mod ctx_refactor;
pub mod ctx_response;
pub mod ctx_search;
pub mod ctx_semantic_search;
pub mod ctx_session;
pub mod ctx_share;
pub mod ctx_shell;
pub mod ctx_skillify;
pub mod ctx_smart_read;
pub mod ctx_smells;
pub mod ctx_summary;
pub mod ctx_symbol;
pub mod ctx_tools;
pub mod ctx_transcript_compact;
pub mod ctx_tree;
pub mod ctx_verify;
pub mod ctx_work_graph;
pub(crate) mod edit_io;
pub(crate) mod edit_recovery;
pub(crate) mod graph_meta;
pub mod knowledge_shared;
pub(crate) mod output_format;
pub mod registered;
pub use lean_ctx::core::search_hook;
pub use lean_ctx::core::search_kernel;
#[cfg(test)]
mod store_admission_tests;
pub mod walk_guard;
mod server;
mod server_lifecycle;
pub use server_lifecycle::shutdown_stage;
mod server_metrics;
mod server_paths;
pub(crate) mod startup;
pub use server::*;
pub use startup::create_server;
#[cfg(test)]
mod resolve_path_tests {
use super::startup::canonicalize_path;
use super::*;
fn create_git_root(path: &std::path::Path) -> String {
std::fs::create_dir_all(path.join(".git")).unwrap();
canonicalize_path(path)
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let stale = tmp.path().join("stale");
let real = tmp.path().join("real");
std::fs::create_dir_all(&stale).unwrap();
let real_root = create_git_root(&real);
std::fs::write(real.join("a.txt"), "ok").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
Some(real.as_path()),
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(stale.to_string_lossy().to_string());
session.shell_cwd = Some(stale.to_string_lossy().to_string());
}
let out = server
.resolve_path(&real.join("a.txt").to_string_lossy())
.await
.unwrap();
assert!(out.ends_with("/a.txt"));
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
drop(session);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_reroots_from_markerless_stale_root_to_real_project() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let stale = tmp.path().join("stale");
let root = tmp.path().join("root");
let other = tmp.path().join("other");
std::fs::create_dir_all(&stale).unwrap();
create_git_root(&root);
let _other_value = create_git_root(&other);
std::fs::write(other.join("b.txt"), "no").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
Some(root.as_path()),
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(stale.to_string_lossy().to_string());
session.shell_cwd = Some(stale.to_string_lossy().to_string());
}
let resolved = server
.resolve_path(&other.join("b.txt").to_string_lossy())
.await
.unwrap();
assert!(
resolved.contains("b.txt"),
"should resolve after reroot: {resolved}"
);
let session = server.session.read().await;
let actual = std::path::Path::new(session.project_root.as_deref().unwrap_or(""))
.canonicalize()
.unwrap_or_default();
let expected = other.canonicalize().unwrap_or(other.clone());
assert_eq!(actual, expected, "should have rerooted to real project");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[allow(clippy::await_holding_lock)]
async fn a_reroot_moves_the_call_and_leaves_the_stale_sessions_data_behind() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let stale = tmp.path().join("stale");
std::fs::create_dir_all(&stale).unwrap();
let repo = tmp.path().join("repo");
let repo_root = create_git_root(&repo);
std::fs::write(repo.join("a.txt"), "a").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(stale.to_string_lossy().to_string());
session.shell_cwd = Some(stale.to_string_lossy().to_string());
session.record_manual_evidence("ap04:stale", None);
}
let call = async {
server.session.pin();
server
.resolve_path(&repo.join("a.txt").to_string_lossy())
.await
.unwrap();
let project = lean_ctx::core::policy::runtime::REQUEST_PROJECT
.with(|slot| slot.borrow().clone())
.map(|project| project.to_string_lossy().into_owned());
let bound = server.session.read().await;
assert_eq!(bound.project_root.as_deref(), Some(repo_root.as_str()));
assert_eq!(project, bound.project_root);
assert!(bound.evidence.iter().all(|e| e.key != "ap04:stale"));
};
server
.session
.scope(
lean_ctx::core::policy::runtime::REQUEST_PROJECT
.scope(std::cell::RefCell::new(Some(stale.clone())), call),
)
.await;
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_never_reroots_a_shared_session() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let markerless = tmp.path().join("shared");
std::fs::create_dir_all(&markerless).unwrap();
let repo = tmp.path().join("repo");
create_git_root(&repo);
std::fs::write(repo.join("a.txt"), "a").unwrap();
let server =
LeanCtxServer::new_shared_with_context(&markerless.to_string_lossy(), "ap04-s3", "s3");
let root = server.session.read().await.project_root.clone();
assert!(root.is_some());
let _ = server
.resolve_path(&repo.join("a.txt").to_string_lossy())
.await;
assert_eq!(server.session.read().await.project_root, root);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_never_reroots_to_the_home_directory() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("home");
create_git_root(&home);
let home = home.canonicalize().unwrap();
let stale = tmp.path().join("stale");
std::fs::create_dir_all(&stale).unwrap();
std::fs::create_dir_all(home.join("notes")).unwrap();
std::fs::write(home.join("notes/a.md"), "notes").unwrap();
let home_guard = lean_ctx::test_env::VarGuard::set("HOME", &home);
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
let stale = stale.to_string_lossy().to_string();
{
let mut session = server.session.write().await;
session.project_root = Some(stale.clone());
session.shell_cwd = Some(stale.clone());
}
let _ = server
.resolve_path(&home.join("notes/a.md").to_string_lossy())
.await;
drop(home_guard);
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(stale.as_str()));
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_auto_registers_language_cache_and_serves_the_same_call() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("project");
create_git_root(&root);
let dep = tmp.path().join("go/pkg/mod/example.com/lib@v1");
std::fs::create_dir_all(&dep).unwrap();
let file = dep.join("lib.go");
std::fs::write(&file, "package lib").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
Some(root.as_path()),
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(root.to_string_lossy().to_string());
session.shell_cwd = Some(root.to_string_lossy().to_string());
}
let ok = server
.resolve_path(&file.to_string_lossy())
.await
.unwrap_or_else(|e| panic!("first read of a cache file must resolve, got: {e}"));
assert!(ok.ends_with("/lib.go"), "resolves the cache file: {ok}");
assert!(
lean_ctx::core::pathjail::is_read_only_path(&file),
"the auto-registered cache stays read-only"
);
let again = server
.resolve_path(&file.to_string_lossy())
.await
.unwrap_or_else(|e| panic!("second read must resolve, got: {e}"));
assert_eq!(again, ok);
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_auto_reroots_from_agent_config_dir_without_opt_in() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let agent = tmp.path().join(".copilot");
let real = tmp.path().join("repo");
std::fs::create_dir_all(&agent).unwrap();
let real_root = create_git_root(&real);
std::fs::write(real.join("a.txt"), "ok").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(agent.to_string_lossy().to_string());
session.shell_cwd = Some(agent.to_string_lossy().to_string());
}
let out = server
.resolve_path(&real.join("a.txt").to_string_lossy())
.await
.unwrap();
assert!(
out.ends_with("/a.txt"),
"agent-dir jail must auto-correct: {out}"
);
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_auto_reroots_from_markerless_client_cwd_without_opt_in() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let client_cwd = tmp.path().join("Users").join("user");
let real = tmp.path().join("workspaces").join("lean-ctx");
std::fs::create_dir_all(&client_cwd).unwrap();
let real_root = create_git_root(&real);
std::fs::write(real.join("rust.rs"), "ok").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(client_cwd.to_string_lossy().to_string());
session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
}
let out = server
.resolve_path(&real.join("rust.rs").to_string_lossy())
.await
.unwrap();
assert!(
out.ends_with("/rust.rs"),
"markerless client cwd must auto-correct: {out}"
);
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_keeps_a_pinned_markerless_root_inside_a_checkout() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let checkout = tmp.path().join("checkout");
create_git_root(&checkout);
let pinned = checkout.join("runs").join("one");
std::fs::create_dir_all(&pinned).unwrap();
std::fs::write(pinned.join("note.md"), "ok").unwrap();
std::fs::write(checkout.join("outside.md"), "no").unwrap();
let pinned_root = pinned.to_string_lossy().to_string();
lean_ctx::test_env::set_var("LEAN_CTX_PROJECT_ROOT", &pinned_root);
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(pinned_root.clone());
session.shell_cwd = Some(pinned_root.clone());
}
let inside = server
.resolve_path(&pinned.join("note.md").to_string_lossy())
.await;
let outside = server
.resolve_path(&checkout.join("outside.md").to_string_lossy())
.await;
let root = server.session.read().await.project_root.clone();
lean_ctx::test_env::remove_var("LEAN_CTX_PROJECT_ROOT");
assert!(inside.is_ok_and(|path| path.ends_with("/note.md")));
assert!(outside.is_err(), "a path above the pin is refused");
assert_eq!(root.as_deref(), Some(pinned_root.as_str()));
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_auto_reroots_even_when_extra_root_allows_path() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let client_cwd = tmp.path().join("Users").join("user");
let real = tmp.path().join("workspaces").join("lean-ctx");
std::fs::create_dir_all(&client_cwd).unwrap();
let real_root = create_git_root(&real);
std::fs::write(real.join("rust.rs"), "ok").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(client_cwd.to_string_lossy().to_string());
session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
session.extra_roots.push(real_root.clone());
}
let out = server
.resolve_path(&real.join("rust.rs").to_string_lossy())
.await
.unwrap();
assert!(
out.ends_with("/rust.rs"),
"extra root must not suppress markerless-cwd reroot: {out}"
);
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_markerless_root_still_blocks_markerless_escape() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let client_cwd = tmp.path().join("Users").join("user");
let loose = tmp.path().join("workspaces").join("loose");
std::fs::create_dir_all(&client_cwd).unwrap();
std::fs::create_dir_all(&loose).unwrap();
std::fs::write(loose.join("data.txt"), "no").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(client_cwd.to_string_lossy().to_string());
session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
}
let err = server
.resolve_path(&loose.join("data.txt").to_string_lossy())
.await
.unwrap_err();
assert!(err.contains("path escapes project root"), "got: {err}");
let session = server.session.read().await;
assert_eq!(
session.project_root.as_deref(),
Some(client_cwd.to_string_lossy().as_ref())
);
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_agent_dir_still_blocks_markerless_escape() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let agent = tmp.path().join(".copilot");
let loose = tmp.path().join("loose");
std::fs::create_dir_all(&agent).unwrap();
std::fs::create_dir_all(&loose).unwrap();
std::fs::write(loose.join("data.txt"), "no").unwrap();
let server = LeanCtxServer::new_with_startup(
None,
None,
SessionMode::Personal,
"default",
"default",
);
{
let mut session = server.session.write().await;
session.project_root = Some(agent.to_string_lossy().to_string());
session.shell_cwd = Some(agent.to_string_lossy().to_string());
}
let err = server
.resolve_path(&loose.join("data.txt").to_string_lossy())
.await
.unwrap_err();
assert!(err.contains("path escapes project root"), "got: {err}");
let session = server.session.read().await;
assert_eq!(
session.project_root.as_deref(),
Some(agent.to_string_lossy().as_ref())
);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn startup_creates_fresh_session_scoped_to_workspace() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let _data = tempfile::tempdir().unwrap();
let _tmp = tempfile::tempdir().unwrap();
lean_ctx::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
let repo_b = _tmp.path().join("repo-b");
let root_b = create_git_root(&repo_b);
let server = LeanCtxServer::new_with_startup(
None,
Some(repo_b.as_path()),
SessionMode::Personal,
"default",
"default",
);
lean_ctx::test_env::remove_var("LEAN_CTX_DATA_DIR");
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
assert!(session.task.is_none());
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
let _lock = lean_ctx::core::data_dir::test_env_lock();
let _data = tempfile::tempdir().unwrap();
let _tmp = tempfile::tempdir().unwrap();
lean_ctx::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
let repo_a = _tmp.path().join("repo-a");
let repo_b = _tmp.path().join("repo-b");
let repo_b_src = repo_b.join("src");
let root_a = create_git_root(&repo_a);
let root_b = create_git_root(&repo_b);
std::fs::create_dir_all(&repo_b_src).unwrap();
let repo_b_src_value = canonicalize_path(&repo_b_src);
let mut session_a = lean_ctx::core::session::SessionState::new();
session_a.project_root = Some(root_a.clone());
session_a.shell_cwd = Some(root_a.clone());
session_a.set_task("repo-a latest task", None);
let old_id = session_a.id.clone();
session_a.save().unwrap();
let server = LeanCtxServer::new_with_startup(
None,
Some(repo_b_src.as_path()),
SessionMode::Personal,
"default",
"default",
);
lean_ctx::test_env::remove_var("LEAN_CTX_DATA_DIR");
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
assert_eq!(
session.shell_cwd.as_deref(),
Some(repo_b_src_value.as_str())
);
assert!(session.task.is_none());
assert_ne!(session.id, old_id);
}
#[cfg(not(feature = "no-jail"))]
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("root");
let other = tmp.path().join("other");
let root_value = create_git_root(&root);
create_git_root(&other);
std::fs::write(other.join("b.txt"), "no").unwrap();
let root_str = root.to_string_lossy().to_string();
let server = LeanCtxServer::new_with_project_root(Some(&root_str));
let err = server
.resolve_path(&other.join("b.txt").to_string_lossy())
.await
.unwrap_err();
assert!(err.contains("path escapes project root"));
let session = server.session.read().await;
assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn resolve_path_prefers_worktree_copy_after_mid_session_switch() {
let _iso = lean_ctx::core::data_dir::isolated_data_dir();
let tmp = tempfile::tempdir().unwrap();
let repo = tmp.path().join("repo");
let root_value = create_git_root(&repo);
let wt = repo.join(".claude/worktrees/wt");
std::fs::create_dir_all(wt.join("src")).unwrap();
std::fs::write(wt.join(".git"), "gitdir: ../../../.git/worktrees/wt\n").unwrap();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(repo.join("src/lib.rs"), "stale").unwrap();
std::fs::write(wt.join("src/lib.rs"), "fresh").unwrap();
let server = LeanCtxServer::new_with_project_root(Some(&root_value));
{
let mut session = server.session.write().await;
session.shell_cwd = Some(wt.to_string_lossy().to_string());
}
let out = server.resolve_path("src/lib.rs").await.unwrap();
assert_eq!(
std::fs::read_to_string(&out).unwrap(),
"fresh",
"worktree copy must win over the stale project_root copy: {out}"
);
{
let mut session = server.session.write().await;
session.shell_cwd = Some(root_value.clone());
}
std::fs::write(repo.join("src/back_707.rs"), "stale").unwrap();
std::fs::write(wt.join("src/back_707.rs"), "fresh").unwrap();
let out = server.resolve_path("src/back_707.rs").await.unwrap();
assert_eq!(std::fs::read_to_string(&out).unwrap(), "stale");
}
}