#![allow(dead_code)]
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::error::{MindError, Result};
use crate::source::Pin;
#[cfg(target_os = "macos")]
const SYSTEM_PATH_FIXED: &str = "/Library/Application Support/mind/policy.toml";
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
const SYSTEM_PATH_FIXED: &str = "/etc/mind/policy.toml";
fn system_path() -> PathBuf {
#[cfg(target_os = "windows")]
{
let base = std::env::var_os("PROGRAMDATA")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
return base.join("mind").join("policy.toml");
}
#[cfg(not(target_os = "windows"))]
{
PathBuf::from(SYSTEM_PATH_FIXED)
}
}
const ENV_VAR: &str = "MIND_POLICY_FILE";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AutoMeld {
pub repo: String,
pub pin: Pin,
}
#[derive(Debug, Clone)]
pub struct Policy {
allow: Vec<String>,
lock: bool,
pinned: bool,
auto_meld: Vec<AutoMeld>,
lobes_lock: bool,
lobes_targets: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPolicy {
#[serde(default)]
sources: RawSources,
#[serde(default)]
lobes: RawLobes,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSources {
#[serde(default)]
allow: Vec<String>,
#[serde(default)]
lock: bool,
#[serde(default)]
pinned: bool,
#[serde(default)]
auto_meld: Vec<RawAutoMeld>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLobes {
#[serde(default)]
lock: bool,
#[serde(default)]
targets: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAutoMeld {
repo: String,
#[serde(default)]
tag: Option<String>,
#[serde(default, rename = "ref")]
ref_: Option<String>,
#[serde(default)]
follow_branch: Option<String>,
}
impl RawAutoMeld {
fn into_auto_meld(self, path: &Path) -> Result<AutoMeld> {
let pins = self.tag.is_some() as u8
+ self.ref_.is_some() as u8
+ self.follow_branch.is_some() as u8;
if pins > 1 {
return Err(MindError::InvalidPolicy {
path: path.display().to_string(),
reason: format!(
"auto_meld entry '{}' declares more than one of tag/ref/follow_branch; supply at most one",
self.repo
),
});
}
let pin = if let Some(tag) = self.tag {
Pin::Tag(tag)
} else if let Some(r) = self.ref_ {
Pin::Ref(r)
} else if let Some(branch) = self.follow_branch {
Pin::FollowBranch(branch)
} else {
Pin::DefaultBranch
};
Ok(AutoMeld {
repo: self.repo,
pin,
})
}
}
impl Policy {
pub fn load() -> Result<Option<Policy>> {
let env = std::env::var_os(ENV_VAR).map(PathBuf::from);
match locate_existing(&system_path(), env.as_deref()) {
Some(path) => Ok(Some(load_file(&path)?)),
None => Ok(None),
}
}
pub fn allow_matches(&self, identity: &str) -> bool {
self.allow.iter().any(|p| glob_match(p, identity))
}
pub fn validate(&self) -> Result<()> {
self.validate_at(&system_path())
}
fn validate_at(&self, path: &Path) -> Result<()> {
if self.pinned {
for am in &self.auto_meld {
let is_pinned = matches!(am.pin, Pin::Tag(_) | Pin::Ref(_));
if !is_pinned {
return Err(MindError::InvalidPolicy {
path: path.display().to_string(),
reason: format!(
"auto_meld entry '{}' must declare a tag or ref because [sources].pinned is true",
am.repo
),
});
}
}
}
if self.lock {
for am in &self.auto_meld {
let identity = crate::source::parse_spec(&am.repo)
.map(|s| s.name)
.map_err(|_| MindError::InvalidPolicy {
path: path.display().to_string(),
reason: format!("auto_meld entry '{}' is not a valid repo spec", am.repo),
})?;
if !self.allow_matches(&identity) {
return Err(MindError::InvalidPolicy {
path: path.display().to_string(),
reason: format!(
"auto_meld entry '{}' (identity '{identity}') is outside [sources].allow but [sources].lock is true",
am.repo
),
});
}
}
}
for target in &self.lobes_targets {
if target.trim().is_empty() {
return Err(MindError::InvalidPolicy {
path: path.display().to_string(),
reason: "[lobes].targets contains an empty or whitespace-only entry; \
each target must be a non-empty path"
.to_string(),
});
}
}
Ok(())
}
pub fn lock(&self) -> bool {
self.lock
}
pub fn pinned(&self) -> bool {
self.pinned
}
pub fn auto_meld(&self) -> &[AutoMeld] {
&self.auto_meld
}
pub fn lobes_lock(&self) -> bool {
self.lobes_lock
}
pub fn lobes_targets(&self) -> &[String] {
&self.lobes_targets
}
pub fn allow(&self) -> &[String] {
&self.allow
}
}
fn locate_with(system: Option<&Path>, env: Option<&Path>) -> Option<PathBuf> {
if let Some(s) = system {
return Some(s.to_path_buf());
}
env.map(|e| e.to_path_buf())
}
fn locate_existing(system: &Path, env: Option<&Path>) -> Option<PathBuf> {
let system = system.exists().then(|| system.to_path_buf());
let env = env.filter(|p| p.exists());
locate_with(system.as_deref(), env)
}
pub fn load_file(path: &Path) -> Result<Policy> {
let text = std::fs::read_to_string(path).map_err(|e| MindError::io(path, e))?;
parse_str(&text, path)
}
fn parse_str(text: &str, path: &Path) -> Result<Policy> {
let raw: RawPolicy = toml::from_str(text).map_err(|e| MindError::InvalidPolicy {
path: path.display().to_string(),
reason: e.to_string(),
})?;
let auto_meld = raw
.sources
.auto_meld
.into_iter()
.map(|am| am.into_auto_meld(path))
.collect::<Result<Vec<_>>>()?;
let policy = Policy {
allow: raw.sources.allow,
lock: raw.sources.lock,
pinned: raw.sources.pinned,
auto_meld,
lobes_lock: raw.lobes.lock,
lobes_targets: raw.lobes.targets,
};
policy.validate_at(path)?;
Ok(policy)
}
fn glob_match(pattern: &str, identity: &str) -> bool {
let pat: Vec<&str> = pattern.split('/').collect();
let id: Vec<&str> = identity.split('/').collect();
if pat.len() != id.len() {
return false;
}
pat.iter().zip(id.iter()).all(|(p, s)| segment_match(p, s))
}
fn segment_match(pattern: &str, segment: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let seg: Vec<char> = segment.chars().collect();
let (mut pi, mut si) = (0usize, 0usize);
let (mut star, mut mark) = (None::<usize>, 0usize);
while si < seg.len() {
if pi < pat.len() && pat[pi] == '*' {
star = Some(pi);
mark = si;
pi += 1;
} else if pi < pat.len() && pat[pi] == seg[si] {
pi += 1;
si += 1;
} else if let Some(s) = star {
pi = s + 1;
mark += 1;
si = mark;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == '*' {
pi += 1;
}
pi == pat.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Policy> {
parse_str(text, Path::new("test-policy.toml"))
}
#[test]
fn locate_with_precedence() {
let system = Path::new("/etc/mind/policy.toml");
let env = Path::new("/tmp/custom-policy.toml");
assert_eq!(
locate_with(Some(system), Some(env)),
Some(system.to_path_buf()),
"system file is authoritative; env must be ignored"
);
assert_eq!(
locate_with(None, Some(env)),
Some(env.to_path_buf()),
"env is honored only when no system file exists"
);
assert_eq!(locate_with(Some(system), None), Some(system.to_path_buf()));
assert_eq!(locate_with(None, None), None);
}
#[test]
fn no_policy_is_inert() {
assert_eq!(locate_with(None, None), None);
}
#[test]
fn unknown_key_is_error() {
let err = parse("[sources]\nallowed = []\n").unwrap_err();
assert!(
matches!(err, MindError::InvalidPolicy { .. }),
"got {err:?}"
);
}
#[test]
fn malformed_toml_is_error() {
let err = parse("[sources\nallow = [").unwrap_err();
assert!(
matches!(err, MindError::InvalidPolicy { .. }),
"got {err:?}"
);
}
#[test]
fn two_pins_is_error() {
let text = r#"
[[sources.auto_meld]]
repo = "github.com/acme/a"
tag = "v1"
ref = "abc123"
"#;
let err = parse(text).unwrap_err();
match err {
MindError::InvalidPolicy { reason, .. } => {
assert!(reason.contains("more than one"), "reason: {reason}");
assert!(
reason.contains("github.com/acme/a"),
"names entry: {reason}"
);
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
}
#[test]
fn allow_matches_segment_globbing() {
let text = r#"
[sources]
allow = ["github.com/acme/*", "github.example.com/platform/agents"]
"#;
let p = parse(text).unwrap();
assert!(p.allow_matches("github.com/acme/repo"));
assert!(p.allow_matches("github.com/acme/agent-baseline"));
assert!(p.allow_matches("github.example.com/platform/agents"));
assert!(!p.allow_matches("github.com/acme/group/repo"));
assert!(!p.allow_matches("github.com/other/repo"));
assert!(!p.allow_matches("gitlab.com/acme/repo"));
assert!(!p.allow_matches("github.example.com/platform/other"));
}
#[test]
fn allow_matches_partial_segment() {
let text = r#"
[sources]
allow = ["github.com/acme/agent-*"]
"#;
let p = parse(text).unwrap();
assert!(p.allow_matches("github.com/acme/agent-baseline"));
assert!(p.allow_matches("github.com/acme/agent-"));
assert!(!p.allow_matches("github.com/acme/baseline"));
assert!(!p.allow_matches("github.com/acme/agent-x/y"));
}
#[test]
fn pinned_requires_tag_or_ref() {
let unpinned = r#"
[sources]
pinned = true
[[sources.auto_meld]]
repo = "github.com/acme/a"
"#;
let err = parse(unpinned).unwrap_err();
match err {
MindError::InvalidPolicy { reason, .. } => {
assert!(reason.contains("github.com/acme/a"), "reason: {reason}");
assert!(reason.contains("tag or ref"), "reason: {reason}");
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
let branch = r#"
[sources]
pinned = true
[[sources.auto_meld]]
repo = "github.com/acme/a"
follow_branch = "main"
"#;
assert!(matches!(
parse(branch).unwrap_err(),
MindError::InvalidPolicy { .. }
));
let ok = r#"
[sources]
pinned = true
[[sources.auto_meld]]
repo = "github.com/acme/a"
tag = "v1.0.0"
[[sources.auto_meld]]
repo = "github.com/acme/b"
ref = "9f3a1c2e"
"#;
assert!(parse(ok).is_ok());
}
#[test]
fn auto_meld_parses_each_pin_variant() {
let text = r#"
[[sources.auto_meld]]
repo = "github.com/acme/tagged"
tag = "v1.4.0"
[[sources.auto_meld]]
repo = "github.com/acme/reffed"
ref = "9f3a1c2e"
[[sources.auto_meld]]
repo = "github.com/acme/branched"
follow_branch = "release"
[[sources.auto_meld]]
repo = "github.com/acme/floating"
"#;
let p = parse(text).unwrap();
let am = p.auto_meld();
assert_eq!(am.len(), 4);
assert_eq!(
am[0],
AutoMeld {
repo: "github.com/acme/tagged".into(),
pin: Pin::Tag("v1.4.0".into()),
}
);
assert_eq!(
am[1],
AutoMeld {
repo: "github.com/acme/reffed".into(),
pin: Pin::Ref("9f3a1c2e".into()),
}
);
assert_eq!(
am[2],
AutoMeld {
repo: "github.com/acme/branched".into(),
pin: Pin::FollowBranch("release".into()),
}
);
assert_eq!(
am[3],
AutoMeld {
repo: "github.com/acme/floating".into(),
pin: Pin::DefaultBranch,
}
);
}
#[test]
fn lock_requires_auto_meld_in_allow() {
let outside = r#"
[sources]
lock = true
allow = ["github.com/acme/*"]
[[sources.auto_meld]]
repo = "other/x"
"#;
let err = parse(outside).unwrap_err();
match err {
MindError::InvalidPolicy { reason, .. } => {
assert!(reason.contains("other/x"), "reason: {reason}");
assert!(reason.contains("allow"), "reason: {reason}");
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
let inside = r#"
[sources]
lock = true
allow = ["github.com/acme/*"]
[[sources.auto_meld]]
repo = "acme/baseline"
"#;
assert!(parse(inside).is_ok());
}
#[test]
fn empty_policy_has_controls_off() {
let p = parse("").unwrap();
assert!(!p.lock());
assert!(!p.pinned());
assert!(!p.lobes_lock());
assert!(p.auto_meld().is_empty());
assert!(p.lobes_targets().is_empty());
assert!(p.allow().is_empty());
}
#[test]
fn load_file_reads_a_real_file() {
let dir = std::env::temp_dir();
let path = dir.join(format!("mind-policy-test-{}.toml", std::process::id()));
std::fs::write(&path, "[lobes]\nlock = true\ntargets = [\"~/.claude\"]\n").unwrap();
let p = load_file(&path).unwrap();
std::fs::remove_file(&path).ok();
assert!(p.lobes_lock());
assert_eq!(p.lobes_targets(), &["~/.claude".to_string()]);
}
#[test]
fn locate_with_is_the_complete_precedence_surface() {
let system = Path::new("/etc/mind/policy.toml");
let env = Path::new("/tmp/x.toml");
assert_eq!(locate_with(None, None), None);
assert_eq!(locate_with(None, Some(env)), Some(env.to_path_buf()));
assert_eq!(locate_with(Some(system), None), Some(system.to_path_buf()));
assert_eq!(
locate_with(Some(system), Some(env)),
Some(system.to_path_buf())
);
}
#[test]
fn locate_existing_ignores_missing_files() {
let dir = std::env::temp_dir();
let present = dir.join(format!("mind-policy-present-{}.toml", std::process::id()));
std::fs::write(&present, "").unwrap();
let missing = dir.join(format!("mind-policy-missing-{}.toml", std::process::id()));
let _ = std::fs::remove_file(&missing);
let absent_system = dir.join(format!("mind-policy-no-system-{}.toml", std::process::id()));
let _ = std::fs::remove_file(&absent_system);
assert_eq!(locate_existing(&absent_system, Some(&missing)), None);
assert_eq!(
locate_existing(&absent_system, Some(&present)),
Some(present.clone())
);
assert_eq!(
locate_existing(&present, Some(&present)),
Some(present.clone())
);
assert_eq!(locate_existing(&absent_system, None), None);
std::fs::remove_file(&present).ok();
}
#[test]
fn glob_match_segment_count_alignment() {
assert!(!glob_match("a/b/c", "a/b"));
assert!(!glob_match("a/b", "a/b/c"));
assert!(!glob_match("*", "a/b"));
assert!(glob_match("*", "anything"));
assert!(glob_match("*/*/*", "github.com/acme/repo"));
assert!(!glob_match("*/*", "github.com/acme/repo"));
assert!(!glob_match("github.com/*", "github.com/acme/repo"));
}
#[test]
fn segment_match_wildcard_edges() {
assert!(segment_match("", ""));
assert!(!segment_match("", "x"));
assert!(segment_match("*", ""));
assert!(segment_match("*", "anything"));
assert!(segment_match("*baseline", "agent-baseline"));
assert!(segment_match("*baseline", "baseline"));
assert!(!segment_match("*baseline", "baseline-x"));
assert!(segment_match("agent-*", "agent-baseline"));
assert!(segment_match("agent-*", "agent-"));
assert!(!segment_match("agent-*", "baseline"));
assert!(segment_match("a*b*c", "axbyc"));
assert!(segment_match("a*b*c", "abc"));
assert!(segment_match("*a*", "xay"));
assert!(!segment_match("a*b*c", "axby"));
assert!(segment_match("a**c", "axyzc"));
assert!(!segment_match("a**c", "axyz"));
assert!(!segment_match("*z", "abc"));
}
#[test]
fn segment_match_unicode() {
assert!(segment_match("café-*", "café-baseline"));
assert!(segment_match("*-café", "acme-café"));
assert!(segment_match("*", "日本語"));
assert!(segment_match("日*語", "日本語"));
assert!(!segment_match("café", "cafe"));
let p = parse("[sources]\nallow = [\"github.com/café/*\"]\n").unwrap();
assert!(p.allow_matches("github.com/café/repo"));
assert!(!p.allow_matches("github.com/cafe/repo"));
}
#[test]
fn deny_unknown_fields_on_each_table() {
assert!(matches!(
parse("[bogus]\nx = 1\n").unwrap_err(),
MindError::InvalidPolicy { .. }
));
assert!(matches!(
parse("[sources]\nalloww = []\n").unwrap_err(),
MindError::InvalidPolicy { .. }
));
assert!(matches!(
parse("[lobes]\nlocked = true\n").unwrap_err(),
MindError::InvalidPolicy { .. }
));
let am = r#"
[[sources.auto_meld]]
repo = "github.com/acme/a"
branch = "main"
"#;
assert!(matches!(
parse(am).unwrap_err(),
MindError::InvalidPolicy { .. }
));
}
#[test]
fn pinned_and_lock_violation_is_deterministic() {
let text = r#"
[sources]
pinned = true
lock = true
allow = ["github.com/acme/*"]
[[sources.auto_meld]]
repo = "github.com/other/x"
"#;
for _ in 0..5 {
match parse(text).unwrap_err() {
MindError::InvalidPolicy { reason, .. } => {
assert!(reason.contains("github.com/other/x"), "reason: {reason}");
assert!(
reason.contains("tag or ref"),
"expected the pinned error to win deterministically: {reason}"
);
assert!(
reason.contains("pinned"),
"names the offending control: {reason}"
);
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
}
}
#[test]
fn pinned_and_lock_with_no_auto_meld_is_valid() {
let text = r#"
[sources]
pinned = true
lock = true
allow = ["github.com/acme/*"]
"#;
let p = parse(text).unwrap();
assert!(p.pinned());
assert!(p.lock());
assert!(p.auto_meld().is_empty());
assert!(p.validate().is_ok());
}
#[test]
fn invalid_policy_names_the_file() {
let path = Path::new("/org/policies/acme.toml");
match parse_str("[sources]\nnope = 1\n", path).unwrap_err() {
MindError::InvalidPolicy { path: p, .. } => {
assert_eq!(p, "/org/policies/acme.toml");
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
let unpinned = r#"
[sources]
pinned = true
[[sources.auto_meld]]
repo = "github.com/acme/a"
"#;
match parse_str(unpinned, path).unwrap_err() {
MindError::InvalidPolicy { path: p, reason } => {
assert_eq!(p, "/org/policies/acme.toml");
assert!(reason.contains("github.com/acme/a"), "reason: {reason}");
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
let two = r#"
[[sources.auto_meld]]
repo = "github.com/acme/a"
tag = "v1"
ref = "abc"
"#;
match parse_str(two, path).unwrap_err() {
MindError::InvalidPolicy { path: p, .. } => {
assert_eq!(p, "/org/policies/acme.toml");
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
}
#[test]
fn auto_meld_mixed_pins_round_trip_in_order() {
let text = r#"
[[sources.auto_meld]]
repo = "github.com/acme/floating"
[[sources.auto_meld]]
repo = "github.com/acme/branched"
follow_branch = "release"
[[sources.auto_meld]]
repo = "github.com/acme/reffed"
ref = "9f3a1c2e"
[[sources.auto_meld]]
repo = "github.com/acme/tagged"
tag = "v2.0.0"
"#;
let p = parse(text).unwrap();
let expected = vec![
AutoMeld {
repo: "github.com/acme/floating".into(),
pin: Pin::DefaultBranch,
},
AutoMeld {
repo: "github.com/acme/branched".into(),
pin: Pin::FollowBranch("release".into()),
},
AutoMeld {
repo: "github.com/acme/reffed".into(),
pin: Pin::Ref("9f3a1c2e".into()),
},
AutoMeld {
repo: "github.com/acme/tagged".into(),
pin: Pin::Tag("v2.0.0".into()),
},
];
assert_eq!(p.auto_meld(), expected.as_slice());
}
#[test]
fn invalid_policy_display() {
let e = MindError::InvalidPolicy {
path: "/etc/mind/policy.toml".into(),
reason: "auto_meld entry 'x' must declare a tag or ref".into(),
};
assert_eq!(
e.to_string(),
"invalid managed policy at /etc/mind/policy.toml: \
auto_meld entry 'x' must declare a tag or ref"
);
}
#[test]
fn enforcement_error_display() {
let not_allowed = MindError::SourceNotAllowed {
identity: "gitlab.com/x/y".into(),
};
assert_eq!(
not_allowed.to_string(),
"source 'gitlab.com/x/y' is not permitted by the managed policy's allowlist"
);
let unpinned = MindError::UnpinnedSourceForbidden {
identity: "github.com/acme/a".into(),
};
assert_eq!(
unpinned.to_string(),
"source 'github.com/acme/a' must be pinned to a tag or ref: \
the managed policy forbids floating branches"
);
}
#[test]
fn lobes_targets_empty_entry_is_invalid() {
let empty = r#"
[lobes]
targets = [""]
"#;
let err = parse(empty).unwrap_err();
match err {
MindError::InvalidPolicy { reason, .. } => {
assert!(
reason.contains("empty or whitespace"),
"reason should name the problem: {reason}"
);
assert!(
reason.contains("[lobes].targets"),
"reason should name the field: {reason}"
);
}
other => panic!("expected InvalidPolicy, got {other:?}"),
}
let whitespace = "[lobes]\ntargets = [\" \"]\n";
let err = parse(whitespace).unwrap_err();
assert!(
matches!(err, MindError::InvalidPolicy { .. }),
"whitespace-only target must also be rejected: {err:?}"
);
let mixed = "[lobes]\ntargets = [\"~/.claude\", \"\"]\n";
let err = parse(mixed).unwrap_err();
assert!(
matches!(err, MindError::InvalidPolicy { .. }),
"mixed list with an empty entry must be rejected: {err:?}"
);
let ok = "[lobes]\ntargets = [\"~/.claude\"]\n";
let p = parse(ok).expect("non-empty target must be valid");
assert_eq!(p.lobes_targets(), &["~/.claude".to_string()]);
let multi = "[lobes]\ntargets = [\"~/.claude\", \"/opt/agent-home\"]\n";
let p = parse(multi).expect("multiple non-empty targets must be valid");
assert_eq!(p.lobes_targets().len(), 2);
}
}