use clap::Args;
use std::fs;
use std::path::{Path, PathBuf};
use super::resolve_cwd;
use crate::config::Config;
use leviath_core::manifest::parse_manifest;
#[derive(Args)]
pub struct ListArgs {
#[arg(short, long, default_value = "all")]
pub filter: String,
}
struct AgentInfo {
name: String,
version: String,
description: String,
read_paths: Option<String>,
}
fn read_agent_info(manifest_path: &Path, config: &Config, cwd: &Path) -> Option<AgentInfo> {
let content = fs::read_to_string(manifest_path).ok()?;
let blueprint = parse_manifest(&content).ok()?;
let read_paths = read_path_summary(&blueprint, config, cwd);
Some(AgentInfo {
name: blueprint.name,
version: blueprint.version,
description: blueprint.description,
read_paths,
})
}
fn read_path_summary(
blueprint: &leviath_core::Blueprint,
config: &Config,
cwd: &Path,
) -> Option<String> {
match crate::read_path_report::build(blueprint, config, cwd)? {
Ok(report) if report.has_ungranted() => Some(format!(
"read_paths: {} - `lev validate` shows which",
report.summary()
)),
Ok(report) => Some(format!("read_paths: {}", report.summary())),
Err(e) => Some(format!("read_paths: {e}")),
}
}
fn scan_directory_for_agents(dir: &Path, config: &Config, cwd: &Path) -> Vec<(PathBuf, AgentInfo)> {
let mut agents = Vec::new();
if !dir.exists() {
return agents;
}
let direct_manifest = dir.join("agent.leviath");
if direct_manifest.exists()
&& let Some(info) = read_agent_info(&direct_manifest, config, cwd)
{
agents.push((dir.to_path_buf(), info));
}
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("agent.leviath");
if manifest_path.exists()
&& let Some(info) = read_agent_info(&manifest_path, config, cwd)
{
agents.push((path, info));
}
}
}
}
agents
}
fn print_agent(info: &AgentInfo) {
let desc = if info.description.is_empty() {
String::new()
} else {
format!(" - {}", info.description)
};
println!(" {} (v{}){}", info.name, info.version, desc);
if let Some(read_paths) = &info.read_paths {
println!(" {read_paths}");
}
}
pub async fn execute(_args: ListArgs) -> anyhow::Result<()> {
let config = Config::load()?;
let agents_dir = get_agents_dir()?;
let cwd = resolve_cwd().unwrap_or_default();
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
print_agent_listing(&agents_dir, &cwd, exe_dir.as_deref(), &config)
}
fn print_agent_listing(
agents_dir: &Path,
cwd: &Path,
exe_dir: Option<&Path>,
config: &Config,
) -> anyhow::Result<()> {
let mut found_runnable = false;
let installed = scan_directory_for_agents(agents_dir, config, cwd);
if !installed.is_empty() {
found_runnable = true;
println!("Installed agents (~/.leviath/agents/):");
for (_path, info) in &installed {
print_agent(info);
}
println!();
}
let local_manifest = cwd.join("agent.leviath");
if local_manifest.exists()
&& let Some(info) = read_agent_info(&local_manifest, config, cwd)
{
found_runnable = true;
println!("Local (current directory):");
print_agent(&info);
println!();
}
let mut config_agents = Vec::new();
for agent_path in &config.agent_paths {
let found = scan_directory_for_agents(agent_path, config, cwd);
config_agents.extend(found);
}
if !config_agents.is_empty() {
found_runnable = true;
println!("From configured paths:");
for (_path, info) in &config_agents {
print_agent(info);
}
println!();
}
let mut builtin_names: Vec<String> = crate::bundled::BUNDLED_AGENTS
.iter()
.map(|a| format!("{} (v{})", a.name, a.version))
.collect();
if let Some(exe_dir) = exe_dir {
for (_path, info) in scan_directory_for_agents(&exe_dir.join("agents"), config, cwd) {
let entry = format!("{} (v{})", info.name, info.version);
if !builtin_names.contains(&entry) {
builtin_names.push(entry);
}
}
}
println!("Bundled agents (install with `lev setup`):");
println!(" {}", builtin_names.join(", "));
println!();
if !found_runnable {
println!("No agents installed yet.");
println!();
println!("To install the bundled agents:");
println!(" lev setup");
println!();
println!("To create your own:");
println!(" lev create my-agent");
}
Ok(())
}
fn get_agents_dir_or_error(dir: Option<PathBuf>) -> anyhow::Result<PathBuf> {
dir.ok_or(anyhow::anyhow!("Could not determine home directory"))
}
fn get_agents_dir() -> anyhow::Result<PathBuf> {
#[cfg(test)]
if FORCE_AGENTS_DIR_ERROR.with(|f| f.get()) {
anyhow::bail!("Could not determine home directory");
}
get_agents_dir_or_error(leviath_core::paths::agents_dir())
}
#[cfg(test)]
thread_local! {
static FORCE_AGENTS_DIR_ERROR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::write_test_agent;
fn write_manifest(dir: &Path, name: &str) {
write_manifest_with_description(dir, name, "Test agent");
}
fn read_agent_info(manifest_path: &Path) -> Option<AgentInfo> {
super::read_agent_info(manifest_path, &Config::default(), Path::new("/work"))
}
fn scan_directory_for_agents(dir: &Path) -> Vec<(PathBuf, AgentInfo)> {
super::scan_directory_for_agents(dir, &Config::default(), Path::new("/work"))
}
fn write_manifest_with_description(dir: &Path, name: &str, description: &str) {
let content = format!(
r#"[agent]
name = "{}"
version = "1.0.0"
description = "{}"
[stages.main]
mode = "autonomous"
model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
description = "Main"
max_iterations = 5
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
"#,
name, description
);
write_test_agent(dir, content);
}
fn write_read_paths_manifest(dir: &Path, name: &str) {
let content = format!(
r#"[agent]
name = "{name}"
version = "1.0.0"
description = "Test agent"
[stages.main]
mode = "autonomous"
model = {{ provider = "anthropic", model = "claude-sonnet-4-6" }}
description = "Main"
max_iterations = 5
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
[read_paths]
allow = ["/data/runs"]
"#
);
write_test_agent(dir, content);
}
fn info_with_config(dir: &Path, config: &Config) -> AgentInfo {
super::read_agent_info(&dir.join("agent.leviath"), config, Path::new("/work"))
.expect("manifest parses")
}
#[test]
fn an_ungranted_read_paths_declaration_is_listed_as_such() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let summary = info_with_config(dir.path(), &Config::default())
.read_paths
.expect("declares read paths");
assert!(summary.contains("1 declared, 0 granted"), "{summary}");
assert!(summary.contains("lev validate"), "{summary}");
}
#[test]
fn a_granted_read_paths_declaration_needs_no_pointer() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let mut config = Config::default();
config.security.read_paths = vec!["/data/runs".to_string()];
let summary = info_with_config(dir.path(), &config)
.read_paths
.expect("declares read paths");
assert_eq!(summary, "read_paths: 1 declared, 1 granted");
}
#[test]
fn a_broken_grant_list_is_reported_on_the_agent() {
let dir = tempfile::tempdir().unwrap();
write_read_paths_manifest(dir.path(), "cto");
let mut config = Config::default();
config.security.read_paths = vec!["regex:relative/.*".to_string()];
let summary = info_with_config(dir.path(), &config)
.read_paths
.expect("declares read paths");
assert!(summary.contains("config.toml"), "{summary}");
}
#[test]
fn an_agent_declaring_no_read_paths_gets_no_line() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "plain");
assert!(
info_with_config(dir.path(), &Config::default())
.read_paths
.is_none()
);
}
#[test]
fn read_agent_info_valid_manifest() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "my-agent");
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.name, "my-agent");
assert_eq!(info.version, "1.0.0");
assert_eq!(info.description, "Test agent");
}
#[test]
fn read_agent_info_missing_file_returns_none() {
let result = read_agent_info(Path::new("/nonexistent/agent.leviath"));
assert!(result.is_none());
}
#[test]
fn read_agent_info_invalid_toml_returns_none() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let result = read_agent_info(&dir.path().join("agent.leviath"));
assert!(result.is_none());
}
#[test]
fn scan_directory_nonexistent_returns_empty() {
let agents = scan_directory_for_agents(Path::new("/nonexistent/path"));
assert!(agents.is_empty());
}
#[test]
fn scan_directory_path_is_a_file_returns_empty() {
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("not-a-directory.txt");
fs::write(&file_path, "hello").unwrap();
let agents = scan_directory_for_agents(&file_path);
assert!(agents.is_empty());
}
#[test]
fn scan_directory_direct_manifest_invalid_is_skipped() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn scan_directory_with_direct_manifest() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "direct-agent");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.name, "direct-agent");
}
#[test]
fn scan_directory_with_subdirectories() {
let dir = tempfile::tempdir().unwrap();
let sub1 = dir.path().join("agent-a");
let sub2 = dir.path().join("agent-b");
fs::create_dir_all(&sub1).unwrap();
fs::create_dir_all(&sub2).unwrap();
write_manifest(&sub1, "agent-a");
write_manifest(&sub2, "agent-b");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 2);
let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
assert!(names.contains(&"agent-a"));
assert!(names.contains(&"agent-b"));
}
#[test]
fn scan_directory_ignores_subdirs_without_manifest() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("no-manifest");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("readme.txt"), "not a manifest").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn list_args_default_filter() {
let args = ListArgs {
filter: "all".to_string(),
};
assert_eq!(args.filter, "all");
}
#[test]
fn read_agent_info_extracts_description() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "my-agent");
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.description, "Test agent");
assert_eq!(info.version, "1.0.0");
}
#[test]
fn scan_directory_with_both_direct_and_subdirs() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "root-agent");
let sub = dir.path().join("child");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "child-agent");
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 2);
let names: Vec<&str> = agents.iter().map(|a| a.1.name.as_str()).collect();
assert!(names.contains(&"root-agent"));
assert!(names.contains(&"child-agent"));
}
#[test]
fn scan_directory_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn scan_directory_subdir_with_invalid_manifest() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("bad-agent");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("agent.leviath"), "invalid toml {{{{").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert!(agents.is_empty());
}
#[test]
fn get_agents_dir_returns_path_with_agents() {
let dir = get_agents_dir().unwrap();
assert!(dir.to_str().unwrap().contains(".leviath"));
assert!(dir.to_str().unwrap().ends_with("agents"));
}
#[test]
fn get_agents_dir_or_error_some_returns_path() {
let dir = PathBuf::from("/home/testuser/.leviath/agents");
assert_eq!(get_agents_dir_or_error(Some(dir.clone())).unwrap(), dir);
}
#[test]
fn get_agents_dir_or_error_none_returns_error() {
let err = get_agents_dir_or_error(None).unwrap_err();
assert!(
err.to_string()
.contains("Could not determine home directory")
);
}
#[test]
fn read_agent_info_minimal_manifest() {
let dir = tempfile::tempdir().unwrap();
let content = r#"[agent]
name = "minimal"
version = "0.0.1"
description = ""
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_test_agent(dir.path(), content);
let info = read_agent_info(&dir.path().join("agent.leviath")).unwrap();
assert_eq!(info.name, "minimal");
assert_eq!(info.description, "");
}
#[tokio::test]
async fn execute_runs_without_error() {
crate::config::with_isolated_config_path_async("list-runs-ok", |_fake_dir| async move {
let args = ListArgs {
filter: "all".to_string(),
};
let result = execute(args).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_returns_err_when_agents_dir_unresolvable() {
crate::config::with_isolated_config_path_async("list-dir-err", |_fake_dir| async move {
FORCE_AGENTS_DIR_ERROR.with(|f| f.set(true));
let args = ListArgs {
filter: "all".to_string(),
};
let result = execute(args).await;
FORCE_AGENTS_DIR_ERROR.with(|f| f.set(false));
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("Could not determine home directory")
);
})
.await;
}
#[cfg(unix)]
#[tokio::test]
async fn execute_falls_back_to_default_cwd_when_current_dir_is_gone() {
crate::config::with_isolated_config_path_async("list-cwd-gone", |_fake_dir| async move {
let _guard = crate::config::isolate_cwd_for_test();
let dir = std::env::temp_dir().join("lev-test-list-cwd-gone");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
let args = ListArgs {
filter: "all".to_string(),
};
let result = execute(args).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_falls_back_to_default_cwd_via_forced_error() {
crate::config::with_isolated_config_path_async("list-cwd-forced", |_fake_dir| async move {
crate::commands::force_cwd_error(true);
let args = ListArgs {
filter: "all".to_string(),
};
let result = execute(args).await;
crate::commands::force_cwd_error(false);
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn execute_fails_loudly_on_a_broken_config() {
crate::config::with_isolated_config_path_async(
"list-broken-config",
|fake_dir| async move {
std::fs::write(fake_dir.join("config.toml"), "not = valid = toml").unwrap();
let args = ListArgs {
filter: "all".to_string(),
};
let err = execute(args).await.expect_err("broken config must error");
assert!(err.to_string().contains("parse"), "{err}");
},
)
.await;
}
#[test]
fn print_agent_listing_nothing_installed() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let config = Config::default();
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_installed_agent() {
let agents_dir = tempfile::tempdir().unwrap();
let sub = agents_dir.path().join("installed-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "installed-agent");
let cwd = tempfile::tempdir().unwrap();
let config = Config::default();
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_local_manifest() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
write_manifest(cwd.path(), "local-agent");
let config = Config::default();
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_local_manifest_invalid_is_skipped() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
fs::write(cwd.path().join("agent.leviath"), "not valid toml {{{{").unwrap();
let config = Config::default();
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_configured_path_agent() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let configured = tempfile::tempdir().unwrap();
let sub = configured.path().join("configured-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "configured-agent");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_finds_builtin_agents() {
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let exe_dir = tempfile::tempdir().unwrap();
let builtin_dir = exe_dir.path().join("agents");
let sub = builtin_dir.join("builtin-agent");
fs::create_dir_all(&sub).unwrap();
write_manifest(&sub, "builtin-agent");
let config = Config::default();
let result =
print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_carries_the_read_paths_line() {
let agents_dir = tempfile::tempdir().unwrap();
let agent = agents_dir.path().join("cto");
fs::create_dir_all(&agent).unwrap();
write_read_paths_manifest(&agent, "cto");
let cwd = tempfile::tempdir().unwrap();
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &Config::default());
assert!(result.is_ok());
assert!(
info_with_config(&agent, &Config::default())
.read_paths
.is_some()
);
}
#[test]
fn print_agent_listing_does_not_list_a_bundled_agent_twice() {
let bundled = &crate::bundled::BUNDLED_AGENTS[0];
let agents_dir = tempfile::tempdir().unwrap();
let cwd = tempfile::tempdir().unwrap();
let exe_dir = tempfile::tempdir().unwrap();
let sub = exe_dir.path().join("agents").join(bundled.name);
fs::create_dir_all(&sub).unwrap();
crate::bundled::install_bundled(bundled, &exe_dir.path().join("agents")).unwrap();
let config = Config::default();
let result =
print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
assert!(result.is_ok());
let entry = format!("{} (v{})", bundled.name, bundled.version);
let names: Vec<String> = crate::bundled::BUNDLED_AGENTS
.iter()
.map(|a| format!("{} (v{})", a.name, a.version))
.collect();
assert_eq!(names.iter().filter(|n| **n == entry).count(), 1);
}
#[test]
fn print_agent_listing_all_sources_populated() {
let agents_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
write_manifest(&agents_dir.path().join("installed"), "installed");
let cwd = tempfile::tempdir().unwrap();
write_manifest(cwd.path(), "local");
let configured = tempfile::tempdir().unwrap();
fs::create_dir_all(configured.path().join("configured")).unwrap();
write_manifest(&configured.path().join("configured"), "configured");
let exe_dir = tempfile::tempdir().unwrap();
let builtin_sub = exe_dir.path().join("agents").join("builtin");
fs::create_dir_all(&builtin_sub).unwrap();
write_manifest(&builtin_sub, "builtin");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result =
print_agent_listing(agents_dir.path(), cwd.path(), Some(exe_dir.path()), &config);
assert!(result.is_ok());
}
#[test]
fn print_agent_listing_empty_descriptions_across_all_sources() {
let agents_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(agents_dir.path().join("installed")).unwrap();
write_manifest_with_description(&agents_dir.path().join("installed"), "installed", "");
let cwd = tempfile::tempdir().unwrap();
write_manifest_with_description(cwd.path(), "local", "");
let configured = tempfile::tempdir().unwrap();
fs::create_dir_all(configured.path().join("configured")).unwrap();
write_manifest_with_description(&configured.path().join("configured"), "configured", "");
let config = Config {
agent_paths: vec![configured.path().to_path_buf()],
..Config::default()
};
let result = print_agent_listing(agents_dir.path(), cwd.path(), None, &config);
assert!(result.is_ok());
}
#[test]
fn scan_directory_agent_with_empty_description() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("my-agent");
fs::create_dir_all(&sub).unwrap();
let content = r#"[agent]
name = "my-agent"
version = "2.0.0"
description = ""
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_test_agent(sub, content);
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.description, "");
}
#[test]
fn scan_directory_mixed_valid_and_invalid() {
let dir = tempfile::tempdir().unwrap();
let good = dir.path().join("good");
let bad = dir.path().join("bad");
let empty = dir.path().join("empty");
fs::create_dir_all(&good).unwrap();
fs::create_dir_all(&bad).unwrap();
fs::create_dir_all(&empty).unwrap();
write_manifest(&good, "good-agent");
fs::write(bad.join("agent.leviath"), "bad {{ toml").unwrap();
let agents = scan_directory_for_agents(dir.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].1.name, "good-agent");
}
}