use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct Manifest {
#[serde(default, rename = "agent")]
pub agents: Vec<AgentManifest>,
}
impl Manifest {
pub fn only_agents(&self, agents: &std::collections::HashSet<String>) -> Manifest {
Manifest {
agents: self
.agents
.iter()
.filter(|a| agents.contains(&a.name))
.cloned()
.collect(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AgentManifest {
pub name: String,
#[serde(default, rename = "path")]
pub paths: Vec<AgentPath>,
#[serde(default)]
pub exclude: Option<ExcludeBlock>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfigScope {
Enterprise,
Personal,
Project,
Local,
}
impl ConfigScope {
pub fn as_str(self) -> &'static str {
match self {
ConfigScope::Enterprise => "enterprise",
ConfigScope::Personal => "personal",
ConfigScope::Project => "project",
ConfigScope::Local => "local",
}
}
pub fn is_project_rooted(self) -> bool {
matches!(self, ConfigScope::Project | ConfigScope::Local)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AgentPath {
pub kind: String,
#[serde(default)]
pub scope: Option<ConfigScope>,
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub paths_relative: Vec<String>,
#[serde(default)]
pub paths_glob: Option<String>,
#[serde(default)]
pub paths_glob_relative: Vec<String>,
#[serde(default)]
pub json_slice_paths: Vec<JsonSlicePath>,
pub watch_strategy: WatchStrategy,
#[serde(default)]
pub slice_kind_subpath: bool,
#[serde(default)]
pub opaque: bool,
#[serde(default)]
pub projected: bool,
#[serde(default)]
pub claude_plugins: bool,
#[serde(default)]
pub opencode_select: bool,
#[serde(default)]
pub projection: Option<Projection>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Projection {
OpencodeConfig,
OpencodeMcp,
}
impl AgentPath {
pub fn is_project_scoped(&self) -> bool {
self.scope.is_some_and(|s| s.is_project_rooted())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct JsonSlicePath {
pub path: String,
pub json_pointer: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ExcludeBlock {
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WatchStrategy {
ExactFile,
ExactFileWithSlice,
Glob,
ExactFileAndGlob,
}
#[derive(thiserror::Error, Debug)]
pub enum ManifestError {
#[error("manifest read failed at {0}: {1}")]
ReadFailed(String, std::io::Error),
#[error("manifest parse failed at {0}: {1}")]
ParseFailed(String, String),
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum PathExpansionError {
#[error("home directory not found")]
HomeNotFound,
#[error("app-data directory not found")]
AppDataNotFound,
#[error("project-scope path used without a project root")]
ProjectScopeWithoutRoot,
#[error("unknown variable: {0}")]
UnknownVariable(String),
#[error("env var missing: {0}")]
EnvVarMissing(String),
#[error("unterminated variable expansion in: {0}")]
UnterminatedVariable(String),
#[error("variable unset: {0}")]
Unset(String),
}
const EMBEDDED_MANIFEST: &str = include_str!("../../../manifests/agent_configs.toml");
static EMBEDDED_PARSED: std::sync::LazyLock<Result<Manifest, String>> =
std::sync::LazyLock::new(|| {
toml::from_str::<Manifest>(EMBEDDED_MANIFEST).map_err(|e| e.to_string())
});
pub fn load_embedded() -> Result<Manifest, ManifestError> {
#[cfg(any(test, feature = "insecure-test-keys"))]
if let Ok(path) = std::env::var("OPENLATCH_INVENTORY_MANIFEST_PATH") {
if !path.is_empty() {
let raw = std::fs::read_to_string(&path)
.map_err(|e| ManifestError::ReadFailed(path.clone(), e))?;
return parse_str(&raw, &path);
}
}
EMBEDDED_PARSED
.clone()
.map_err(|msg| ManifestError::ParseFailed("embedded".to_string(), msg))
}
#[cfg(any(test, feature = "insecure-test-keys"))]
fn parse_str(raw: &str, source: &str) -> Result<Manifest, ManifestError> {
toml::from_str::<Manifest>(raw)
.map_err(|e| ManifestError::ParseFailed(source.to_string(), e.to_string()))
}
pub fn expand_path(
input: &str,
project_root: Option<&Path>,
) -> Result<PathBuf, PathExpansionError> {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '$' && chars.peek() == Some(&'{') {
chars.next();
let mut var = String::new();
let mut closed = false;
for c in chars.by_ref() {
if c == '}' {
closed = true;
break;
}
var.push(c);
}
if !closed {
return Err(PathExpansionError::UnterminatedVariable(input.to_string()));
}
out.push_str(&resolve_var(&var, project_root)?);
} else {
out.push(c);
}
}
Ok(PathBuf::from(out))
}
pub fn expand_path_all(input: &str, project_root: Option<&Path>) -> Vec<PathBuf> {
let multi = [
(
"${OPENCODE_ENV_DIR}",
super::opencode_select::env_dirs as fn() -> Vec<PathBuf>,
),
(
"${OPENCODE_ENV_CONFIG}",
super::opencode_select::env_config_files,
),
(
"${OPENCODE_MANAGED_PREFS}",
super::opencode_select::managed_prefs_dirs,
),
];
for (token, values) in multi {
if input.contains(token) {
return values()
.into_iter()
.filter_map(|v| {
let v = v.display().to_string();
if v.contains("${") {
return None;
}
expand_path(&input.replace(token, &v), project_root).ok()
})
.collect();
}
}
expand_path(input, project_root).into_iter().collect()
}
fn resolve_var(name: &str, project_root: Option<&Path>) -> Result<String, PathExpansionError> {
match name {
"CLAUDE_CONFIG_DIR" => crate::hooks::claude_code::config_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CLAUDE_STATE_DIR" => crate::hooks::claude_code::state_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CODEX_HOME" => crate::hooks::codex_cli::config_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CLINE_DIR" => crate::hooks::cline::store_root()
.map(|(p, _)| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CLINE_ASSETS_DIR" => crate::hooks::cline::asset_root()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CLINE_DATA_DIR" => crate::hooks::cline::data_root()
.map(|(p, _)| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"CURSOR_DIR" => crate::hooks::cursor::root()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"OPENCODE_DIR" => crate::hooks::opencode::root()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"OPENCODE_USER_HOME" => super::opencode_select::user_home()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"OPENCODE_HOME_DIR" => super::opencode_select::user_home()
.map(|p| p.join(".opencode").display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"OPENCODE_ENV_DIR" => super::opencode_select::env_dirs()
.into_iter()
.next()
.map(|p| p.display().to_string())
.ok_or_else(|| PathExpansionError::Unset(name.into())),
"OPENCODE_ENV_CONFIG" => super::opencode_select::env_config_files()
.into_iter()
.next()
.map(|p| p.display().to_string())
.ok_or_else(|| PathExpansionError::Unset(name.into())),
"OPENCODE_MANAGED_PREFS" => super::opencode_select::managed_prefs_dirs()
.into_iter()
.next()
.map(|p| p.display().to_string())
.ok_or_else(|| PathExpansionError::Unset(name.into())),
"OPENCODE_MANAGED_DIR" => crate::hooks::opencode::managed_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::AppDataNotFound),
"CURSOR_ENTERPRISE_DIR" => crate::hooks::cursor::enterprise_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::AppDataNotFound),
"HOME" => dirs::home_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::HomeNotFound),
"APPDATA" => dirs::data_dir()
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::AppDataNotFound),
"VSCODE_USER" => dirs::config_dir()
.map(|p| p.join("Code").join("User").display().to_string())
.ok_or(PathExpansionError::AppDataNotFound),
"CURSOR_USER" => dirs::config_dir()
.map(|p| p.join("Cursor").join("User").display().to_string())
.ok_or(PathExpansionError::AppDataNotFound),
"OPENLATCH_DIR" => Ok(crate::config::openlatch_dir().display().to_string()),
"SYSTEM_CLAUDE_DIR" => Ok(system_claude_dir()),
"PROJECT" => project_root
.map(|p| p.display().to_string())
.ok_or(PathExpansionError::ProjectScopeWithoutRoot),
other if other.starts_with("env:") => {
let env_name = &other[4..];
std::env::var(env_name).map_err(|_| PathExpansionError::EnvVarMissing(env_name.into()))
}
other => Err(PathExpansionError::UnknownVariable(other.into())),
}
}
fn system_claude_dir() -> String {
#[cfg(target_os = "linux")]
{
return "/etc/claude-code".to_string();
}
#[cfg(target_os = "macos")]
{
return "/Library/Application Support/Claude".to_string();
}
#[cfg(target_os = "windows")]
{
let base = std::env::var("ProgramData").unwrap_or_else(|_| "C:\\ProgramData".to_string());
return format!("{base}\\Claude");
}
#[allow(unreachable_code)]
String::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_manifest_parses() {
let m = load_embedded().expect("embedded manifest must parse");
assert!(!m.agents.is_empty(), "embedded manifest declares agents");
let claude = m
.agents
.iter()
.find(|a| a.name == "claude-code")
.expect("claude-code agent present");
assert!(
claude.paths.iter().any(|p| p.kind == "mcp"),
"claude-code includes an mcp path entry"
);
}
fn claude_code_absolute_templates() -> Vec<String> {
let m = load_embedded().expect("embedded manifest must parse");
let agent = m
.agents
.iter()
.find(|a| a.name == "claude-code")
.expect("claude-code agent present");
let mut out = Vec::new();
for ap in &agent.paths {
out.extend(ap.paths.iter().cloned());
out.extend(ap.paths_glob.iter().cloned());
out.extend(ap.json_slice_paths.iter().map(|s| s.path.clone()));
}
out.extend(
agent
.exclude
.iter()
.flat_map(|e| e.patterns.iter().cloned()),
);
out
}
#[test]
fn embedded_manifest_follows_a_relocated_config_dir() {
let _lock = crate::daemon::identity::ENV_LOCK.blocking_lock();
let env = crate::daemon::identity::test_support::EnvGuard::clear();
let relocated = tempfile::tempdir().expect("temp dir");
env.set("CLAUDE_CONFIG_DIR", relocated.path());
let home = dirs::home_dir().expect("home dir");
let default_config_dir = home.join(".claude");
let default_state_file = home.join(".claude.json");
let templates = claude_code_absolute_templates();
assert!(
!templates.is_empty(),
"the sweep must actually see the manifest's templates"
);
let mut under_relocated = 0usize;
for template in &templates {
let expanded = expand_path(template, None)
.unwrap_or_else(|e| panic!("{template} must expand: {e}"));
assert!(
!expanded.starts_with(&default_config_dir),
"{template} resolved to {} — inside the default config directory \
while $CLAUDE_CONFIG_DIR points at {}",
expanded.display(),
relocated.path().display()
);
assert_ne!(
expanded,
default_state_file,
"{template} resolved to the default state file while \
$CLAUDE_CONFIG_DIR points at {}",
relocated.path().display()
);
if expanded.starts_with(relocated.path()) {
under_relocated += 1;
}
}
assert_eq!(
under_relocated,
templates.len() - 1,
"exactly one template (the SYSTEM_CLAUDE_DIR one) may sit outside \
the relocated directory"
);
}
#[test]
fn claude_dir_variables_track_the_env_var() {
let _lock = crate::daemon::identity::ENV_LOCK.blocking_lock();
let env = crate::daemon::identity::test_support::EnvGuard::clear();
let home = dirs::home_dir().expect("home dir");
assert_eq!(
expand_path("${CLAUDE_CONFIG_DIR}/settings.json", None).unwrap(),
home.join(".claude").join("settings.json")
);
assert_eq!(
expand_path("${CLAUDE_STATE_DIR}/.claude.json", None).unwrap(),
home.join(".claude.json")
);
let relocated = tempfile::tempdir().expect("temp dir");
env.set("CLAUDE_CONFIG_DIR", relocated.path());
assert_eq!(
expand_path("${CLAUDE_CONFIG_DIR}/settings.json", None).unwrap(),
relocated.path().join("settings.json")
);
assert_eq!(
expand_path("${CLAUDE_STATE_DIR}/.claude.json", None).unwrap(),
relocated.path().join(".claude.json")
);
env.set("CLAUDE_CONFIG_DIR", "");
assert_eq!(
expand_path("${CLAUDE_CONFIG_DIR}/settings.json", None).unwrap(),
home.join(".claude").join("settings.json")
);
}
#[test]
fn codex_home_resolver_falls_back_when_unset() {
let _lock = crate::hooks::codex_cli::CONFIG_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let previous = std::env::var_os("CODEX_HOME");
std::env::remove_var("CODEX_HOME");
let resolved = resolve_var("CODEX_HOME", None);
let env_form = resolve_var("env:CODEX_HOME", None);
let expanded = expand_path("${CODEX_HOME}/hooks.json", None);
match previous {
Some(v) => std::env::set_var("CODEX_HOME", v),
None => std::env::remove_var("CODEX_HOME"),
}
let home = dirs::home_dir().expect("home dir");
assert_eq!(
resolved.expect("the resolver answers with the variable unset"),
home.join(".codex").display().to_string()
);
assert_eq!(
env_form.unwrap_err(),
PathExpansionError::EnvVarMissing("CODEX_HOME".into()),
"the `env:` form is why this arm exists — it fails when unset"
);
assert_eq!(
expanded.expect("the manifest's own template expands"),
home.join(".codex").join("hooks.json")
);
}
#[test]
fn embedded_manifest_declares_the_codex_hooks_file() {
let m = load_embedded().expect("embedded manifest must parse");
let codex = m
.agents
.iter()
.find(|a| a.name == "codex-cli")
.expect("codex-cli agent present");
let hooks: Vec<&AgentPath> = codex.paths.iter().filter(|p| p.kind == "hooks").collect();
let paths: Vec<&String> = hooks.iter().flat_map(|h| h.paths.iter()).collect();
assert_eq!(
paths,
vec![
&"${CODEX_HOME}/hooks.json".to_string(),
&"${CODEX_HOME}/config.toml".to_string(),
],
"both Codex perimeter files are watched, got {paths:?}"
);
for h in &hooks {
assert_eq!(h.watch_strategy, WatchStrategy::ExactFile);
assert!(
h.json_slice_paths.is_empty(),
"neither file has a JSON slice to point at — the no-slice shape is what \
makes hash_for_kind hash the file itself"
);
}
}
#[test]
fn opencode_reads_exactly_the_c4_paths() {
let m = load_embedded().expect("embedded manifest must parse");
let opencode = m
.agents
.iter()
.find(|a| a.name == "opencode")
.expect("opencode agent present");
let mut templates = Vec::new();
for ap in &opencode.paths {
assert!(
ap.opencode_select,
"every OpenCode entry is selected: {ap:?}"
);
let all: Vec<String> = ap
.paths
.iter()
.chain(ap.paths_relative.iter())
.chain(ap.paths_glob.iter())
.chain(ap.paths_glob_relative.iter())
.cloned()
.collect();
for t in &all {
for banned in [
"auth.json",
"storage",
"opencode.db",
"/log",
"cache",
".dat",
"well-known",
] {
assert!(!t.contains(banned), "{t} reaches {banned}");
}
if t.contains("opencode.json")
|| t.ends_with("config.json")
|| t == "${OPENCODE_ENV_CONFIG}"
|| t.ends_with(".plist")
{
let want = if ap.kind == "mcp" {
Projection::OpencodeMcp
} else {
Projection::OpencodeConfig
};
assert_eq!(ap.projection, Some(want), "{t} must be projected");
}
if t.contains("plugin") || t.ends_with("package.json") {
assert!(ap.opaque, "{t} must be opaque");
}
if t.contains("command") {
assert_eq!(ap.kind, "command", "{t} is a command");
}
if t.contains("agent/") || t.contains("agents/") || t.contains("mode") {
assert_eq!(ap.kind, "skill", "{t} is filed as a skill");
}
}
templates.extend(all);
}
let mut expected: Vec<String> = [
"${OPENCODE_DIR}/config.json",
"${OPENCODE_DIR}/opencode.json",
"${OPENCODE_DIR}/opencode.jsonc",
"${OPENCODE_ENV_CONFIG}",
"${OPENCODE_HOME_DIR}/opencode.json",
"${OPENCODE_ENV_DIR}/opencode.jsonc",
"opencode.json",
".opencode/opencode.jsonc",
"${OPENCODE_MANAGED_DIR}/opencode.json",
"${OPENCODE_MANAGED_PREFS}/ai.opencode.managed.plist",
".opencode/package.json",
".opencode/plugins/*.[jt]s",
".opencode/plugin/*.[jt]s",
"${OPENCODE_DIR}/AGENTS.md",
"${OPENCODE_ENV_DIR}/AGENTS.md",
"${OPENCODE_USER_HOME}/.claude/CLAUDE.md",
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md",
"${OPENCODE_USER_HOME}/.claude/skills/**/SKILL.md",
"${OPENCODE_USER_HOME}/.agents/skills/**/SKILL.md",
".opencode/skills/**/SKILL.md",
".opencode/skill/**/SKILL.md",
".opencode/agent/**/*.md",
".opencode/mode/*.md",
".opencode/commands/**/*.md",
".opencode/command/**/*.md",
".claude/skills/**/SKILL.md",
".agents/skills/**/SKILL.md",
]
.map(String::from)
.to_vec();
for dir in [
"${OPENCODE_DIR}",
"${OPENCODE_HOME_DIR}",
"${OPENCODE_ENV_DIR}",
] {
for rel in [
"package.json",
"plugins/*.[jt]s",
"plugin/*.[jt]s",
"skills/**/SKILL.md",
"skill/**/SKILL.md",
"agents/**/*.md",
"agent/**/*.md",
"modes/*.md",
"mode/*.md",
"commands/**/*.md",
"command/**/*.md",
] {
expected.push(format!("{dir}/{rel}"));
}
}
for e in &expected {
assert!(
templates.iter().any(|t| t == e),
"{e} missing from {templates:?}"
);
}
for ap in opencode.paths.iter().filter(|ap| ap.kind == "mcp") {
assert!(ap.projected, "OpenCode MCP servers carry credentials");
assert!(ap.slice_kind_subpath, "one item per server");
assert_eq!(ap.projection, Some(Projection::OpencodeMcp));
}
assert!(opencode
.paths
.iter()
.all(|ap| ap.projected == (ap.kind == "mcp")));
let settings_files = |pred: &dyn Fn(&AgentPath) -> bool| -> Vec<String> {
let mut v: Vec<String> = opencode
.paths
.iter()
.filter(|ap| pred(ap))
.flat_map(|ap| ap.paths.iter().chain(ap.paths_relative.iter()).cloned())
.collect();
v.sort();
v
};
let hooks_settings = settings_files(&|ap: &AgentPath| {
ap.kind == "hooks" && ap.projection == Some(Projection::OpencodeConfig)
});
let mcp_files =
settings_files(&|ap: &AgentPath| ap.projection == Some(Projection::OpencodeMcp));
assert_eq!(
mcp_files, hooks_settings,
"one mcp artifact per settings file"
);
let scope_of = |template: &str| {
opencode
.paths
.iter()
.filter(|ap| ap.kind == "mcp")
.find(|ap| {
ap.paths
.iter()
.chain(ap.paths_relative.iter())
.any(|t| t == template)
})
.and_then(|ap| ap.scope)
};
for (template, scope) in [
("${OPENCODE_DIR}/opencode.json", ConfigScope::Personal),
("${OPENCODE_DIR}/config.json", ConfigScope::Personal),
("${OPENCODE_HOME_DIR}/opencode.jsonc", ConfigScope::Personal),
("${OPENCODE_ENV_DIR}/opencode.json", ConfigScope::Personal),
("${OPENCODE_ENV_CONFIG}", ConfigScope::Personal),
("opencode.json", ConfigScope::Project),
(".opencode/opencode.jsonc", ConfigScope::Project),
(
"${OPENCODE_MANAGED_DIR}/opencode.json",
ConfigScope::Enterprise,
),
(
"${OPENCODE_MANAGED_PREFS}/ai.opencode.managed.plist",
ConfigScope::Enterprise,
),
] {
assert_eq!(scope_of(template), Some(scope), "{template}");
}
}
#[test]
fn opencode_managed_prefs_paths_put_the_user_first() {
use super::super::opencode_select::{managed_prefs_dirs_for, MANAGED_PLIST_NAME};
let root = PathBuf::from("/Library/Managed Preferences");
assert_eq!(
managed_prefs_dirs_for(Some("alice")),
vec![root.join("alice"), root.clone()]
);
for odd in [None, Some(""), Some(".."), Some("a/b"), Some("a\\b")] {
assert_eq!(managed_prefs_dirs_for(odd), vec![root.clone()], "{odd:?}");
}
assert_eq!(MANAGED_PLIST_NAME, "ai.opencode.managed.plist");
if !cfg!(target_os = "macos") {
assert!(
expand_path_all("${OPENCODE_MANAGED_PREFS}/ai.opencode.managed.plist", None)
.is_empty(),
"managed preferences are read on macOS only"
);
assert_eq!(
expand_path("${OPENCODE_MANAGED_PREFS}/x", None),
Err(PathExpansionError::Unset("OPENCODE_MANAGED_PREFS".into()))
);
} else {
let got = expand_path_all("${OPENCODE_MANAGED_PREFS}/ai.opencode.managed.plist", None);
assert_eq!(got.last(), Some(&root.join(MANAGED_PLIST_NAME)));
}
}
#[test]
fn opencode_env_dir_expands_once_per_kept_report() {
let _serial = super::super::opencode_select::TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
super::super::opencode_select::reset_reports();
let a = std::env::temp_dir().join("oc-a");
let b = std::env::temp_dir().join("oc-b");
for dir in [&a, &b] {
super::super::opencode_select::record(super::super::opencode_select::LoaderEnv {
config_dir: Some(dir.display().to_string()),
..Default::default()
});
}
let got = expand_path_all("${OPENCODE_ENV_DIR}/AGENTS.md", None);
assert_eq!(got, vec![b.join("AGENTS.md"), a.join("AGENTS.md")]);
assert_eq!(
expand_path("${OPENCODE_ENV_DIR}/x", None).unwrap(),
b.join("x"),
"the single expansion is the most recent report"
);
super::super::opencode_select::reset_reports();
}
#[test]
fn only_agents_keeps_the_named_agents_whole() {
let m = load_embedded().expect("embedded manifest must parse");
let keep: std::collections::HashSet<String> = ["claude-code".to_string()].into();
let narrowed = m.only_agents(&keep);
assert_eq!(narrowed.agents.len(), 1);
assert_eq!(narrowed.agents[0].name, "claude-code");
let full = m.agents.iter().find(|a| a.name == "claude-code").unwrap();
assert_eq!(narrowed.agents[0].paths.len(), full.paths.len());
assert!(m.only_agents(&Default::default()).agents.is_empty());
}
#[test]
fn embedded_manifest_declares_cline_hooks_rules_workflows() {
let m = load_embedded().expect("embedded manifest must parse");
let cline = m
.agents
.iter()
.find(|a| a.name == "cline")
.expect("cline agent present");
let count = |kind: &str| cline.paths.iter().filter(|p| p.kind == kind).count();
let hooks: Vec<&AgentPath> = cline.paths.iter().filter(|p| p.kind == "hooks").collect();
assert_eq!(hooks.len(), 1);
assert!(hooks[0].opaque, "Cline hook scripts must be opaque");
assert_eq!(
count("rules"),
4,
"store, asset root, global extras, workspace"
);
assert_eq!(
count("command"),
3,
"store, asset root, workspace workflows"
);
assert_eq!(count("skill"), 3, "~/.cline, ~/.agents, workspace");
let mcp: Vec<&str> = cline
.paths
.iter()
.filter(|p| p.kind == "mcp")
.flat_map(|p| p.paths.iter().map(String::as_str))
.collect();
assert_eq!(
mcp,
[
"${CLINE_DATA_DIR}/settings/cline_mcp_settings.json",
"${VSCODE_USER}/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
"${CURSOR_USER}/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
],
"the store's data root and the editor extension's"
);
for ap in &cline.paths {
let templates = ap
.paths
.iter()
.chain(ap.paths_glob.iter())
.chain(ap.paths_glob_relative.iter());
for template in templates {
for denied in [
"secrets.json",
"providers.json",
"/sessions",
"/tasks",
"/db",
"/hooks/",
"/plugins",
] {
assert!(
!template.contains(denied),
"{template} reaches {denied}, outside PRD C-6's allow-list"
);
}
if template.contains("CLINE_DATA_DIR") || template.contains("/data") {
assert!(ap.projected, "{template} is under data/ unprojected");
}
}
assert!(ap.json_slice_paths.is_empty());
assert_eq!(ap.opaque, ap.kind == "hooks");
assert_eq!(ap.projected, ap.kind == "mcp");
}
}
#[test]
fn cline_resolvers_track_the_seams() {
let root = tempfile::tempdir().expect("seam root");
let seams = crate::hooks::cline::absent_seams(root.path());
let _seam = crate::hooks::cline::cline_isolated(
seams
.iter()
.map(|(k, v)| (*k, Some(v.clone().into_os_string()))),
);
assert_eq!(
expand_path("${CLINE_DIR}/rules/a.md", None).unwrap(),
seams[0].1.join("rules/a.md")
);
assert_eq!(
expand_path("${CLINE_ASSETS_DIR}/Hooks/PreToolUse", None).unwrap(),
seams[2].1.join("Hooks/PreToolUse")
);
assert_eq!(
expand_path("${CLINE_DATA_DIR}/settings/x.json", None).unwrap(),
seams[1].1.join("settings/x.json")
);
}
#[test]
fn cursor_resolver_tracks_the_seam() {
let root = tempfile::tempdir().expect("seam root");
let seams = crate::hooks::cline::absent_seams(root.path());
let _seam = crate::hooks::cline::cline_isolated(
seams
.iter()
.map(|(k, v)| (*k, Some(v.clone().into_os_string()))),
);
assert_eq!(
expand_path("${CURSOR_DIR}/hooks.json", None).unwrap(),
crate::hooks::cursor::absent_seam(root.path())
.1
.join("hooks.json")
);
}
fn cursor_block() -> AgentManifest {
load_embedded()
.expect("embedded manifest must parse")
.agents
.into_iter()
.find(|a| a.name == "cursor")
.expect("cursor agent present")
}
#[test]
fn embedded_manifest_declares_the_cursor_block() {
let cursor = cursor_block();
let count = |kind: &str| cursor.paths.iter().filter(|p| p.kind == kind).count();
assert_eq!(count("hooks"), 2, "user + enterprise, and project");
assert_eq!(
count("mcp"),
4,
"personal, project, local plugins and Claude Code plugins"
);
assert_eq!(
count("rules"),
3,
".cursor/rules/*.mdc, .cursorrules and local plugins"
);
assert_eq!(
count("skill"),
7,
"~/.cursor, built-in, ~/.agents, ~/.claude, ~/.codex, project, local plugins"
);
assert_eq!(cursor.paths.len(), 16, "no kind outside the five");
let hooks = cursor
.paths
.iter()
.find(|p| p.kind == "hooks" && !p.is_project_scoped())
.expect("user hooks entry");
assert_eq!(
hooks.paths,
[
"${CURSOR_DIR}/hooks.json",
"${CURSOR_ENTERPRISE_DIR}/hooks.json"
]
);
for ap in &cursor.paths {
assert_eq!(ap.projected, ap.kind == "mcp", "MCP carries credentials");
assert!(ap.json_slice_paths.is_empty());
assert!(!ap.opaque);
}
for ap in cursor.paths.iter().filter(|p| p.kind == "mcp") {
assert!(ap.slice_kind_subpath);
assert!(
ap.scope.is_some(),
"MCP entries declare their precedence tier"
);
}
}
#[test]
fn workspace_agents_md_is_declared_by_every_reader() {
let m = load_embedded().unwrap();
let readers: Vec<&str> = m
.agents
.iter()
.filter(|a| {
a.paths.iter().any(|p| {
p.is_project_scoped() && p.paths_relative.iter().any(|r| r == "AGENTS.md")
})
})
.map(|a| a.name.as_str())
.collect();
assert_eq!(
readers,
["claude-code", "codex-cli", "cline", "cursor", "opencode"]
);
}
#[test]
fn embedded_manifest_declares_codex_rules_skills_and_mcp() {
let m = load_embedded().unwrap();
let codex = m.agents.iter().find(|a| a.name == "codex-cli").unwrap();
let declares = |kind: &str, template: &str| {
codex.paths.iter().any(|p| {
p.kind == kind
&& (p.paths.iter().any(|t| t == template)
|| p.paths_glob.as_deref() == Some(template)
|| p.paths_relative.iter().any(|t| t == template)
|| p.paths_glob_relative.iter().any(|t| t == template))
})
};
assert!(declares("rules", "${CODEX_HOME}/AGENTS.md"));
assert!(declares("rules", "${CODEX_HOME}/AGENTS.override.md"));
assert!(declares("rules", "AGENTS.override.md"));
assert!(declares("skill", "${HOME}/.agents/skills/*/SKILL.md"));
assert!(declares("skill", "/etc/codex/skills/*/SKILL.md"));
assert!(declares("skill", ".agents/skills/*/SKILL.md"));
assert!(declares("mcp", "${CODEX_HOME}/config.toml"));
assert!(declares("mcp", ".codex/config.toml"));
assert!(declares("skill", "${CODEX_HOME}/skills/*/SKILL.md"));
assert!(declares("skill", "${CODEX_HOME}/skills/.system/*/SKILL.md"));
assert!(declares(
"skill",
"${CODEX_HOME}/plugins/cache/*/*/*/skills/*/SKILL.md"
));
assert!(declares(
"mcp",
"${CODEX_HOME}/plugins/cache/*/*/*/.mcp.json"
));
assert!(declares("rules", "${CODEX_HOME}/rules/*.rules"));
assert!(declares("rules", ".codex/rules/*.rules"));
for ap in codex.paths.iter().filter(|p| p.kind == "mcp") {
assert!(ap.projected, "Codex MCP servers carry credentials");
assert!(ap.slice_kind_subpath);
}
}
#[test]
fn cursor_relative_blocks_are_project_scoped() {
for ap in &cursor_block().paths {
if !ap.paths_relative.is_empty() || !ap.paths_glob_relative.is_empty() {
assert!(
ap.is_project_scoped(),
"{:?} is relative but not project-scoped",
ap.paths_relative
);
assert!(ap.paths.is_empty() && ap.paths_glob.is_none());
}
}
}
#[test]
fn cursor_manifest_never_resolves_transcripts() {
let root = tempfile::tempdir().expect("seam root");
let seams = crate::hooks::cline::absent_seams(root.path());
let _seam = crate::hooks::cline::cline_isolated(
seams
.iter()
.map(|(k, v)| (*k, Some(v.clone().into_os_string()))),
);
let cursor_root = crate::hooks::cursor::absent_seam(root.path()).1;
let project = root.path().join("project");
let denied = [
cursor_root.join("projects"),
cursor_root.join("chats"),
project.join(".cursor").join("projects"),
project.join(".cursor").join("chats"),
];
let mut resolved: Vec<PathBuf> = Vec::new();
for ap in &cursor_block().paths {
for template in ap.paths.iter().chain(ap.paths_glob.iter()) {
if let Ok(p) = expand_path(template, None) {
resolved.push(p);
}
}
for rel in ap
.paths_relative
.iter()
.chain(ap.paths_glob_relative.iter())
{
resolved.push(project.join(rel));
}
}
assert!(
resolved.iter().any(|p| p.starts_with(&cursor_root)),
"the premise: the block resolves under the seam: {resolved:?}"
);
for path in &resolved {
for deny in &denied {
assert!(
!path.starts_with(deny),
"{} is under {}",
path.display(),
deny.display()
);
}
let text = path.to_string_lossy();
assert!(!text.contains("transcript"), "{text}");
}
}
#[test]
fn cursor_enterprise_resolver_is_the_one_definition() {
match crate::hooks::cursor::enterprise_dir() {
Some(dir) => assert_eq!(
expand_path("${CURSOR_ENTERPRISE_DIR}/hooks.json", None).unwrap(),
dir.join("hooks.json")
),
None => assert!(expand_path("${CURSOR_ENTERPRISE_DIR}/hooks.json", None).is_err()),
}
}
#[test]
fn claude_config_dir_resolver_is_not_the_env_form() {
let _lock = crate::daemon::identity::ENV_LOCK.blocking_lock();
let _env = crate::daemon::identity::test_support::EnvGuard::clear();
assert!(expand_path("${CLAUDE_CONFIG_DIR}/settings.json", None).is_ok());
assert_eq!(
expand_path("${env:CLAUDE_CONFIG_DIR}/settings.json", None).unwrap_err(),
PathExpansionError::EnvVarMissing("CLAUDE_CONFIG_DIR".into())
);
}
#[test]
fn expand_path_home() {
let p = expand_path("${HOME}/.claude/.mcp.json", None).unwrap();
let home = dirs::home_dir().unwrap();
assert_eq!(p, home.join(".claude").join(".mcp.json"));
}
#[test]
fn expand_path_cursor_user() {
let p = expand_path("${CURSOR_USER}/globalStorage", None).unwrap();
let config = dirs::config_dir().unwrap();
assert_eq!(p, config.join("Cursor").join("User").join("globalStorage"));
}
#[test]
fn expand_path_no_vars_returns_input() {
let p = expand_path("/etc/openlatch/config.toml", None).unwrap();
assert_eq!(p, PathBuf::from("/etc/openlatch/config.toml"));
}
#[test]
fn expand_path_project_without_root_errors() {
let err = expand_path("${PROJECT}/.claude/CLAUDE.md", None).unwrap_err();
assert_eq!(err, PathExpansionError::ProjectScopeWithoutRoot);
}
#[test]
fn expand_path_project_with_root() {
let root = PathBuf::from("/tmp/myproject");
let p = expand_path("${PROJECT}/.claude/CLAUDE.md", Some(&root)).unwrap();
assert_eq!(p, root.join(".claude").join("CLAUDE.md"));
}
#[test]
fn expand_path_env_var_present() {
let key = "OPENLATCH_TEST_EXPAND_PATH_PRESENT";
std::env::set_var(key, "/x/y");
let p = expand_path(&format!("${{env:{key}}}/file"), None).unwrap();
std::env::remove_var(key);
assert_eq!(p, PathBuf::from("/x/y/file"));
}
#[test]
fn expand_path_env_var_missing() {
let key = "OPENLATCH_TEST_EXPAND_PATH_MISSING";
std::env::remove_var(key);
let err = expand_path(&format!("${{env:{key}}}/file"), None).unwrap_err();
assert_eq!(err, PathExpansionError::EnvVarMissing(key.into()));
}
#[test]
fn expand_path_unknown_variable() {
let err = expand_path("${NOPE}/file", None).unwrap_err();
assert_eq!(err, PathExpansionError::UnknownVariable("NOPE".into()));
}
#[test]
fn expand_path_unterminated_variable() {
let err = expand_path("${HOME/file", None).unwrap_err();
assert!(matches!(err, PathExpansionError::UnterminatedVariable(_)));
}
#[test]
fn expand_path_openlatch_dir() {
let p = expand_path("${OPENLATCH_DIR}/state.json", None).unwrap();
let expected = crate::config::openlatch_dir().join("state.json");
assert_eq!(p, expected);
}
#[test]
fn parse_str_invalid_toml_reports_source() {
let err = parse_str("not toml = =", "fixture").unwrap_err();
match err {
ManifestError::ParseFailed(source, _) => assert_eq!(source, "fixture"),
_ => panic!("expected ParseFailed"),
}
}
#[test]
fn watch_strategy_round_trip() {
let raw = r#"
[[agent]]
name = "demo"
[[agent.path]]
kind = "mcp"
paths = ["/tmp/x.json"]
watch_strategy = "exact_file"
"#;
let m: Manifest = toml::from_str(raw).unwrap();
assert_eq!(
m.agents[0].paths[0].watch_strategy,
WatchStrategy::ExactFile
);
}
#[test]
fn config_scope_round_trip() {
for (variant, wire) in [
(ConfigScope::Enterprise, "enterprise"),
(ConfigScope::Personal, "personal"),
(ConfigScope::Project, "project"),
(ConfigScope::Local, "local"),
] {
let s = serde_json::to_string(&variant).unwrap();
assert_eq!(s, format!("\"{wire}\""));
let parsed: ConfigScope = serde_json::from_str(&s).unwrap();
assert_eq!(parsed, variant);
assert_eq!(variant.as_str(), wire);
}
}
#[test]
fn config_scope_parsed_from_toml() {
let raw = r#"
[[agent]]
name = "demo"
[[agent.path]]
kind = "mcp"
scope = "personal"
paths = ["/tmp/x.json"]
watch_strategy = "exact_file"
"#;
let m: Manifest = toml::from_str(raw).unwrap();
assert_eq!(m.agents[0].paths[0].scope, Some(ConfigScope::Personal));
}
#[test]
fn config_scope_rejects_unknown_variant() {
let raw = r#"
[[agent]]
name = "demo"
[[agent.path]]
kind = "mcp"
scope = "bogus"
paths = ["/tmp/x.json"]
watch_strategy = "exact_file"
"#;
let err = parse_str(raw, "fixture").unwrap_err();
assert!(matches!(err, ManifestError::ParseFailed(_, _)));
}
#[test]
fn expand_path_system_claude_dir() {
let p = expand_path("${SYSTEM_CLAUDE_DIR}/mcp.json", None).unwrap();
assert_eq!(p.file_name().and_then(|s| s.to_str()), Some("mcp.json"));
let parent = p
.parent()
.expect("system claude dir resolves to a non-empty path");
#[cfg(target_os = "linux")]
assert_eq!(parent, std::path::Path::new("/etc/claude-code"));
#[cfg(target_os = "macos")]
assert_eq!(
parent,
std::path::Path::new("/Library/Application Support/Claude")
);
#[cfg(target_os = "windows")]
{
assert_eq!(
parent.file_name().and_then(|s| s.to_str()),
Some("Claude"),
"{}",
parent.display()
);
}
}
}