use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
use crate::engine::facet::{FacetTerm, LocalLocus};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Role {
pub read_locus: LocalLocus,
pub write_locus: LocalLocus,
pub reads_secret: bool,
pub pinned: bool,
}
#[derive(Deserialize)]
struct RegionsFile {
#[serde(default)]
role: HashMap<String, RoleDef>,
#[serde(default)]
region: Vec<RegionDef>,
}
#[derive(Deserialize)]
struct RoleDef {
read_locus: String,
write_locus: String,
#[serde(default)]
reads_secret: bool,
#[serde(default)]
pinned: bool,
#[serde(default)]
#[allow(dead_code)] description: String,
}
#[derive(Deserialize)]
struct RegionDef {
path: String,
role: String,
os: Option<Vec<String>>,
#[serde(default)]
#[allow(dead_code)]
note: String,
#[serde(default)]
#[allow(dead_code)]
researched: String,
}
enum Matcher {
Exact(String),
Prefix(String),
StringPrefix(String),
Segment(String),
}
impl Matcher {
fn from_path(path: &str) -> Matcher {
if let Some(p) = path.strip_suffix('*') {
Matcher::StringPrefix(p.to_string())
} else if path.ends_with('/') {
Matcher::Prefix(path.to_string())
} else if path.starts_with('/') || path.starts_with('~') {
Matcher::Exact(path.to_string())
} else {
Matcher::Segment(path.to_string())
}
}
fn specificity(&self, path: &str, fold: bool) -> Option<usize> {
let eq = |a: &str, b: &str| if fold { a.eq_ignore_ascii_case(b) } else { a == b };
let starts = |h: &str, p: &str| if fold { ci_starts_with(h, p) } else { h.starts_with(p) };
match self {
Matcher::Exact(s) => eq(path, s).then_some(1_000_000 + s.len()),
Matcher::Prefix(s) => {
let dir = s.strip_suffix('/').unwrap_or(s);
(starts(path, s.as_str()) || eq(path, dir)).then_some(1_000 + s.len())
}
Matcher::StringPrefix(s) => starts(path, s.as_str()).then_some(1_000 + s.len()),
Matcher::Segment(seg) => path.split('/').any(|c| eq(c, seg)).then_some(seg.len()),
}
}
fn remainder<'a>(&self, path: &'a str) -> &'a str {
match self {
Matcher::Prefix(s) | Matcher::StringPrefix(s) => path.strip_prefix(s.as_str()).unwrap_or(""),
Matcher::Exact(_) => "",
Matcher::Segment(_) => path,
}
}
}
fn has_hidden_component(remainder: &str) -> bool {
remainder.split('/').any(|seg| seg.len() > 1 && seg.starts_with('.'))
}
fn ci_starts_with(haystack: &str, prefix: &str) -> bool {
haystack.len() >= prefix.len()
&& haystack.as_bytes()[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
}
fn role_is_protective(role: &Role) -> bool {
role.reads_secret
|| role.pinned
|| role.write_locus > LocalLocus::Worktree
|| role.read_locus > LocalLocus::WorktreeTrusted
}
struct Node {
matcher: Matcher,
role: Role,
os: Option<Vec<String>>,
fold: bool,
}
impl Node {
fn applies_here(&self) -> bool {
match &self.os {
None => true,
Some(list) => list.iter().any(|o| o == current_os()),
}
}
}
#[cfg(test)]
thread_local! {
static OS_OVERRIDE: std::cell::Cell<Option<&'static str>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub(crate) fn with_os<T>(os: &'static str, f: impl FnOnce() -> T) -> T {
struct Reset(Option<&'static str>);
impl Drop for Reset {
fn drop(&mut self) {
OS_OVERRIDE.with(|c| c.set(self.0));
}
}
let _reset = Reset(OS_OVERRIDE.with(|c| c.replace(Some(os))));
f()
}
fn current_os() -> &'static str {
#[cfg(test)]
if let Some(o) = OS_OVERRIDE.with(std::cell::Cell::get) {
return o;
}
std::env::consts::OS
}
struct Regions {
nodes: Vec<Node>,
worktree: Role,
unknown: Role,
}
fn parse_locus(s: &str) -> LocalLocus {
LocalLocus::from_term(s).unwrap_or_else(|| panic!("regions: unknown locus rung `{s}`"))
}
static REGIONS: LazyLock<Regions> = LazyLock::new(|| {
let src = include_str!("../../../regions/default.toml");
let file: RegionsFile = toml::from_str(src).expect("regions/default.toml is invalid TOML");
let role_of = |name: &str| -> Role {
let def = file
.role
.get(name)
.unwrap_or_else(|| panic!("regions: role `{name}` is not defined"));
Role {
read_locus: parse_locus(&def.read_locus),
write_locus: parse_locus(&def.write_locus),
reads_secret: def.reads_secret,
pinned: def.pinned,
}
};
let nodes = file
.region
.iter()
.map(|r| {
let role = role_of(&r.role);
Node {
matcher: Matcher::from_path(&r.path),
role,
os: r.os.clone(),
fold: role_is_protective(&role),
}
})
.collect();
Regions {
nodes,
worktree: role_of("worktree"),
unknown: role_of("unknown"),
}
});
struct Grant {
matcher: Matcher,
read: bool,
write: bool,
}
#[cfg(not(test))]
#[derive(Deserialize)]
struct GrantEntry {
path: String,
#[serde(default)]
read: bool,
#[serde(default)]
write: bool,
}
#[cfg(not(test))]
#[derive(Deserialize)]
struct GrantFile {
#[serde(default)]
grant: Vec<GrantEntry>,
}
#[cfg(not(test))]
fn load_user_grants() -> Vec<Grant> {
if std::env::var_os("SAFE_CHAINS_NO_LOCAL").is_some() {
return Vec::new();
}
let Some(home) = std::env::var_os("HOME").map(std::path::PathBuf::from) else {
return Vec::new();
};
let mut grants = Vec::new();
if let Ok(src) = std::fs::read_to_string(home.join(".config/safe-chains.toml")) {
grants.extend(
toml::from_str::<GrantFile>(&src)
.map(|f| f.grant)
.unwrap_or_default()
.into_iter()
.flat_map(|g| {
grant_matchers(&g.path)
.into_iter()
.map(move |m| Grant { matcher: m, read: g.read, write: g.write })
}),
);
}
grants.extend(claude_settings_read_grants(&home));
grants
}
fn translate_read_pattern(inner: &str) -> Option<String> {
let inner = inner.trim();
let base = if let Some(rest) = inner.strip_prefix("//") {
format!("/{rest}")
} else if inner == "~" || inner.starts_with("~/") {
inner.to_string()
} else {
return None;
};
let mut prefix = base.as_str();
let mut had_glob = false;
while let Some(p) = prefix.strip_suffix("/**").or_else(|| prefix.strip_suffix("/*")) {
prefix = p;
had_glob = true;
}
let prefix = prefix.strip_suffix('/').unwrap_or(prefix);
if prefix.contains(['*', '?']) || prefix.is_empty() || prefix == "/" || prefix == "~" {
return None;
}
Some(if had_glob { format!("{prefix}/") } else { prefix.to_string() })
}
fn claude_read_grant_paths(settings_json: &str) -> Vec<String> {
let Ok(value) = serde_json::from_str::<serde_json::Value>(settings_json) else {
return Vec::new();
};
let Some(arr) = value
.get("permissions")
.and_then(|v| v.get("allow"))
.and_then(|v| v.as_array())
else {
return Vec::new();
};
arr.iter()
.filter_map(|e| e.as_str())
.filter_map(|entry| entry.strip_prefix("Read(").and_then(|s| s.strip_suffix(')')))
.filter_map(translate_read_pattern)
.collect()
}
fn claude_settings_read_grants(home: &std::path::Path) -> Vec<Grant> {
let Ok(src) = std::fs::read_to_string(home.join(".claude/settings.json")) else {
return Vec::new();
};
claude_read_grant_paths(&src)
.into_iter()
.flat_map(|p| {
grant_matchers(&p)
.into_iter()
.map(|m| Grant { matcher: m, read: true, write: false })
})
.collect()
}
fn grant_matchers(path: &str) -> Vec<Matcher> {
let home = || std::env::var_os("HOME").and_then(|h| h.into_string().ok());
let mut out = vec![Matcher::from_path(path)];
if let Some(rest) = path.strip_prefix('~') {
if let Some(h) = home() {
out.push(Matcher::from_path(&format!("{h}{rest}")));
}
} else if let Some(h) = home()
&& let Some(rest) = path.strip_prefix(h.as_str())
{
out.push(Matcher::from_path(&format!("~{rest}")));
}
out
}
#[cfg(not(test))]
static USER_GRANTS: LazyLock<Vec<Grant>> = LazyLock::new(load_user_grants);
#[cfg(test)]
thread_local! {
static TEST_GRANTS: std::cell::RefCell<Vec<Grant>> = const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(test)]
pub(crate) fn with_grants<T>(grants: &[(&str, bool, bool)], f: impl FnOnce() -> T) -> T {
let parsed = grants
.iter()
.flat_map(|&(p, read, write)| grant_matchers(p).into_iter().map(move |m| Grant { matcher: m, read, write }))
.collect();
TEST_GRANTS.with(|g| *g.borrow_mut() = parsed);
let out = f();
TEST_GRANTS.with(|g| g.borrow_mut().clear());
out
}
fn best_grant(path: &str) -> Option<(bool, bool)> {
let pick = |grants: &[Grant]| {
grants
.iter()
.filter_map(|g| {
let spec = g.matcher.specificity(path, false)?;
(!has_hidden_component(g.matcher.remainder(path))).then_some((spec, g.read, g.write))
})
.max_by_key(|&(s, ..)| s)
.map(|(_, r, w)| (r, w))
};
#[cfg(test)]
{
TEST_GRANTS.with(|g| pick(&g.borrow()))
}
#[cfg(not(test))]
{
pick(&USER_GRANTS)
}
}
fn apply_grant(path: &str, base: Role) -> Role {
if base.reads_secret || base.pinned {
return base; }
let Some((read, write)) = best_grant(path) else {
return base;
};
Role {
read_locus: if read { base.read_locus.min(LocalLocus::WorktreeTrusted) } else { base.read_locus },
write_locus: if write { base.write_locus.min(LocalLocus::Worktree) } else { base.write_locus },
reads_secret: base.reads_secret,
pinned: base.pinned,
}
}
pub(crate) fn classify_region(path: &str) -> Role {
if let Some(role) = scratchpad_role(path) {
return role;
}
apply_grant(path, base_region(path))
}
fn scratchpad_role(path: &str) -> Option<Role> {
crate::pathctx::in_session_scratchpad(path).then_some(Role {
read_locus: LocalLocus::SandboxScope,
write_locus: LocalLocus::SandboxScope,
reads_secret: false,
pinned: false,
})
}
fn base_region(path: &str) -> Role {
let r = &*REGIONS;
let fold_shields = current_os() == "macos";
let mut best: Option<(usize, Role)> = None;
for node in &r.nodes {
if !node.applies_here() {
continue;
}
let Some(spec) = node.matcher.specificity(path, node.fold && fold_shields) else {
continue;
};
let take = match best {
None => true,
Some((bs, br)) => spec > bs || (spec == bs && more_restrictive(node.role, br)),
};
if take {
best = Some((spec, node.role));
}
}
if let Some((_, role)) = best {
return role;
}
if path.starts_with('/') || path.starts_with('~') {
adjacent_role(path).unwrap_or(r.unknown)
} else {
r.worktree
}
}
fn adjacent_role(path: &str) -> Option<Role> {
matches!(peer_kind(path), PeerKind::Ordinary).then_some(Role {
read_locus: LocalLocus::Adjacent,
write_locus: LocalLocus::Adjacent,
reads_secret: false,
pinned: false,
})
}
pub(crate) fn is_hidden_peer(path: &str) -> bool {
matches!(peer_kind(path), PeerKind::Hidden)
}
enum PeerKind {
Ordinary,
Hidden,
NotPeer,
}
fn peer_kind(path: &str) -> PeerKind {
let Some(home) = std::env::var("HOME").ok().filter(|h| h.starts_with('/')) else {
return PeerKind::NotPeer;
};
let Some(root_raw) = crate::pathctx::root() else {
return PeerKind::NotPeer;
};
let root = if root_raw == home {
"~".to_string()
} else if let Some(rest) = root_raw.strip_prefix(&home).filter(|r| r.starts_with('/')) {
format!("~{rest}")
} else if root_raw.starts_with('~') {
root_raw
} else {
return PeerKind::NotPeer; };
let root = root.trim_end_matches('/');
let Some(stripped) = root.strip_prefix("~/") else {
return PeerKind::NotPeer;
};
let comps: Vec<&str> = stripped.split('/').filter(|s| !s.is_empty()).collect();
let Some(last) = comps.last().filter(|_| comps.len() >= 2) else {
return PeerKind::NotPeer;
};
let parent = &root[..root.len() - last.len() - 1]; let Some(under_parent) = path.strip_prefix(parent).filter(|r| r.starts_with('/')) else {
return PeerKind::NotPeer;
};
if path == root || path.strip_prefix(root).is_some_and(|r| r.starts_with('/')) {
return PeerKind::NotPeer;
}
if has_hidden_component(under_parent.trim_start_matches('/')) {
return PeerKind::Hidden;
}
PeerKind::Ordinary
}
fn more_restrictive(a: Role, b: Role) -> bool {
(a.write_locus, a.read_locus) > (b.write_locus, b.read_locus)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_region_file_compiles_and_defaults_exist() {
let _ = classify_region("/etc/hosts");
assert_eq!(classify_region("relative/file.txt").write_locus, LocalLocus::Worktree);
assert_eq!(classify_region("/some/unmapped/path").write_locus, LocalLocus::Machine);
assert_eq!(classify_region("/some/unmapped/path").read_locus, LocalLocus::Machine);
}
#[test]
fn system_integrity_substrate_write_worst_cases_above_machine() {
for p in ["/etc/passwd", "/etc/group", "/etc/sudoers", "/etc/sudoers.d/pkg", "/etc/pam.d/sshd"] {
assert_eq!(classify_region(p).write_locus, LocalLocus::SystemIntegrity, "write {p}");
assert_eq!(classify_region(p).read_locus, LocalLocus::Machine, "read {p}");
}
assert_eq!(classify_region("/etc/nginx/nginx.conf").write_locus, LocalLocus::Machine, "ordinary /etc stays machine");
assert_eq!(classify_region("/usr/local/bin/tool").write_locus, LocalLocus::Machine, "/usr/local is admin-managed, stays machine");
}
#[test]
fn adjacent_sibling_classification() {
use crate::pathctx::{enter, PathCtx};
let ws = |root: &str, path: &str| {
let _g = enter(PathCtx { cwd: Some(root.to_string()), root: Some(root.to_string()), ..Default::default() });
classify_region(path)
};
const WS: &str = "~/projects/safe-chains";
assert_eq!(ws(WS, "~/projects/branchdiff/src/main.rs").read_locus, LocalLocus::Adjacent);
assert_eq!(ws(WS, "~/projects/branchdiff/src/main.rs").write_locus, LocalLocus::Adjacent);
assert_eq!(ws(WS, "~/projects/notes.txt").read_locus, LocalLocus::Adjacent, "a file peer to the workspace dir");
assert_ne!(ws(WS, "~/projects/branchdiff/.env").read_locus, LocalLocus::Adjacent, "peer .env stays denied");
assert_ne!(ws(WS, "~/projects/branchdiff/.npmrc").read_locus, LocalLocus::Adjacent, "peer .npmrc stays denied");
assert_eq!(ws(WS, "~/projects/branchdiff/.git/hooks/pre-commit").write_locus, LocalLocus::WorktreeTrusted, "peer .git hook stays frozen");
assert_ne!(ws("~/work", "~/.ssh/id_rsa").read_locus, LocalLocus::Adjacent, "~/.ssh is never adjacent");
assert_ne!(ws("~/work", "~/other-notes.txt").read_locus, LocalLocus::Adjacent, "depth-1 workspace has no siblings");
assert_ne!(ws("~", "~/anything.txt").read_locus, LocalLocus::Adjacent);
assert_ne!(ws(WS, "~/other/thing.txt").read_locus, LocalLocus::Adjacent, "different parent → not a sibling");
assert_eq!(ws(WS, "~/projects/safe-chains-fork/x").read_locus, LocalLocus::Adjacent);
assert_ne!(ws(WS, "~/projects/safe-chains/x").read_locus, LocalLocus::Adjacent);
assert_ne!(ws("/opt/app", "/opt/other/x").read_locus, LocalLocus::Adjacent);
assert_ne!(classify_region("~/projects/branchdiff/src/main.rs").read_locus, LocalLocus::Adjacent);
}
#[test]
fn hidden_peer_predicate_tracks_the_dot_shield() {
use crate::pathctx::{enter, PathCtx};
let hp = |root: &str, path: &str| {
let _g = enter(PathCtx { cwd: Some(root.to_string()), root: Some(root.to_string()), ..Default::default() });
is_hidden_peer(path)
};
const WS: &str = "~/projects/safe-chains";
assert!(hp(WS, "~/projects/branchdiff/.env"));
assert!(hp(WS, "~/projects/branchdiff/.github/workflows/ci.yml"));
assert!(hp(WS, "~/projects/branchdiff/sub/.config/app.toml"), "hidden component anywhere in the remainder");
assert!(!hp(WS, "~/projects/branchdiff/src/main.rs"));
assert!(!hp(WS, "~/projects/safe-chains/.env"));
assert!(!hp(WS, "~/other/.env"));
assert!(!hp("~/work", "~/.ssh/id_rsa"));
assert!(!is_hidden_peer("~/projects/branchdiff/.env"));
}
#[test]
fn most_specific_wins() {
let ssh = classify_region("~/.ssh/id_rsa");
assert_eq!(ssh.read_locus, LocalLocus::Machine);
assert!(ssh.reads_secret);
assert!(classify_region("myproj/.ssh/id_rsa").reads_secret, "segment bites a relative spelling too");
assert_eq!(classify_region("~/notes.txt").read_locus, LocalLocus::Machine);
}
#[test]
fn in_project_trusted_files_read_but_do_not_write() {
let git = classify_region(".git/config");
assert_eq!(git.read_locus, LocalLocus::WorktreeTrusted, "read is admitted at read-local");
assert_eq!(git.write_locus, LocalLocus::WorktreeTrusted, "above the worktree write ceiling → frozen");
}
#[test]
fn user_grant_widens_read_and_write() {
with_grants(&[("~/projects/", true, true)], || {
let r = classify_region("~/projects/other/src/main.rs");
assert_eq!(r.write_locus, LocalLocus::Worktree, "write admitted");
assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
});
assert_eq!(classify_region("~/projects/other/src/main.rs").write_locus, LocalLocus::Machine);
}
#[test]
fn read_only_grant_admits_read_but_not_write() {
with_grants(&[("~/.local/share/mise/", true, false)], || {
let r = classify_region("~/.local/share/mise/installs/python/bin/python");
assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
assert!(r.write_locus > LocalLocus::Worktree, "write NOT admitted");
});
}
#[test]
fn translate_read_pattern_only_honors_absolute_and_home_prefixes() {
assert_eq!(translate_read_pattern("//Users/me/x/**"), Some("/Users/me/x/".into()));
assert_eq!(translate_read_pattern("~/.gem/**"), Some("~/.gem/".into()));
assert_eq!(translate_read_pattern("//Users/me/x/*"), Some("/Users/me/x/".into()));
assert_eq!(translate_read_pattern("~/.gem"), Some("~/.gem".into()));
assert_eq!(translate_read_pattern("//etc/hosts"), Some("/etc/hosts".into()));
assert_eq!(translate_read_pattern("src/**"), None);
assert_eq!(translate_read_pattern("/logs/**"), None);
assert_eq!(translate_read_pattern("//Users/*/mise/**"), None);
assert_eq!(translate_read_pattern("~/**/*.pem"), None);
assert_eq!(translate_read_pattern("//**"), None);
assert_eq!(translate_read_pattern("~/**"), None);
assert_eq!(translate_read_pattern("~/"), None);
assert_eq!(translate_read_pattern(""), None);
}
#[test]
fn claude_read_grant_paths_extracts_only_read_allow_rules() {
let paths = claude_read_grant_paths(
r#"{"permissions":{"allow":[
"Bash(ls)","Edit(~/x/**)","Write(~/y/**)","Read(~/z/**)","WebFetch"
]}}"#,
);
assert_eq!(paths, vec!["~/z/".to_string()]);
assert!(claude_read_grant_paths("not json").is_empty());
assert!(claude_read_grant_paths("{}").is_empty());
assert!(claude_read_grant_paths(r#"{"permissions":{}}"#).is_empty());
assert!(claude_read_grant_paths(r#"{"permissions":{"deny":["Read(~/z/**)"]}}"#).is_empty());
}
#[test]
fn claude_read_rule_admits_read_but_never_write() {
let paths = claude_read_grant_paths(
r#"{"permissions":{"allow":["Read(~/.local/share/mise/**)","Edit(~/.local/share/mise/**)"]}}"#,
);
assert_eq!(paths, vec!["~/.local/share/mise/".to_string()]);
let grants: Vec<(&str, bool, bool)> = paths.iter().map(|p| (p.as_str(), true, false)).collect();
with_grants(&grants, || {
let r = classify_region("~/.local/share/mise/installs/python/bin/python");
assert!(r.read_locus <= LocalLocus::WorktreeTrusted, "read admitted");
assert!(r.write_locus > LocalLocus::Worktree, "the Edit() rule is ignored — write stays denied");
});
}
#[test]
fn a_claude_read_grant_still_respects_the_dot_rule_and_shields() {
let paths = claude_read_grant_paths(r#"{"permissions":{"allow":["Read(~/work/**)"]}}"#);
assert_eq!(paths, vec!["~/work/".to_string()]);
let grants: Vec<(&str, bool, bool)> = paths.iter().map(|p| (p.as_str(), true, false)).collect();
with_grants(&grants, || {
assert!(classify_region("~/work/notes.txt").read_locus <= LocalLocus::WorktreeTrusted, "granted read admitted");
assert_eq!(classify_region("~/work/.ssh/id_rsa").read_locus, LocalLocus::Machine, "hidden cred not widened");
});
}
#[test]
fn claude_settings_read_grants_reads_home_settings_only() {
let home = tempfile::tempdir().unwrap();
let claude = home.path().join(".claude");
std::fs::create_dir_all(&claude).unwrap();
std::fs::write(
claude.join("settings.json"),
r#"{"permissions":{"allow":["Read(~/.gem/**)","Edit(~/.gem/**)"]}}"#,
)
.unwrap();
let grants = claude_settings_read_grants(home.path());
assert!(!grants.is_empty());
assert!(grants.iter().all(|g| g.read && !g.write), "Read() rules are read-only");
let empty = tempfile::tempdir().unwrap();
assert!(claude_settings_read_grants(empty.path()).is_empty());
}
#[test]
fn shields_fold_case_on_macos_so_a_case_variant_cannot_evade_them() {
with_os("macos", || {
assert!(classify_region("~/.AWS/credentials").reads_secret, ".AWS folds to the .aws secret");
assert!(classify_region("~/.SSH/id_rsa").reads_secret, ".SSH folds to the .ssh secret");
assert_eq!(classify_region("~/.AWS/credentials").read_locus, LocalLocus::Machine);
assert_eq!(classify_region("/etc/Master.Passwd").read_locus, LocalLocus::Machine, "system secret folds");
assert!(classify_region(".GIT/hooks/pre-commit").write_locus > LocalLocus::Worktree, ".GIT write frozen");
assert!(classify_region(".Git/hooks/pre-commit").write_locus > LocalLocus::Worktree, "mixed-case .Git frozen");
assert!(classify_region(".ENVRC").write_locus > LocalLocus::Worktree, ".ENVRC write frozen");
with_grants(&[("~/.AWS/", true, false)], || {
assert_eq!(classify_region("~/.AWS/credentials").read_locus, LocalLocus::Machine, "explicit grant cannot unlock a folded secret");
});
});
}
#[test]
fn case_folding_is_macos_only_so_linux_keeps_distinct_paths() {
with_os("linux", || {
assert_eq!(classify_region(".GIT/hooks/pre-commit").write_locus, LocalLocus::Worktree, "linux: .GIT is an ordinary worktree path");
assert!(!classify_region("~/.AWS/credentials").reads_secret, "linux: .AWS is not the .aws secret");
});
for os in ["macos", "linux"] {
assert!(with_os(os, || classify_region("~/.aws/credentials").reads_secret), "{os}: canonical .aws shielded");
assert!(with_os(os, || classify_region(".git/hooks/pre-commit").write_locus > LocalLocus::Worktree), "{os}: canonical .git frozen");
}
}
#[test]
fn admit_nodes_never_fold_so_a_case_variant_is_not_widened() {
with_os("macos", || {
assert!(classify_region("/tmp/x").write_locus <= LocalLocus::Worktree, "/tmp is scratch (admitted)");
assert_eq!(classify_region("/TMP/x").write_locus, LocalLocus::Machine, "/TMP is not folded into the scratch admit");
});
}
#[test]
fn safe_chains_config_is_read_ok_write_denied_and_ungrantable() {
let cfg = "~/.config/safe-chains.toml";
assert!(classify_region(cfg).read_locus <= LocalLocus::WorktreeTrusted, "read is fine");
assert_eq!(classify_region(cfg).write_locus, LocalLocus::Machine, "write denied");
with_grants(&[("~/", true, true)], || {
assert_eq!(classify_region(cfg).write_locus, LocalLocus::Machine, "grant can't unlock the config write");
assert!(classify_region(cfg).read_locus <= LocalLocus::WorktreeTrusted);
});
}
#[test]
fn a_grant_does_not_widen_hidden_files_or_system_secrets() {
with_grants(&[("~/", true, true)], || {
assert_eq!(classify_region("~/projects/foo/main.rs").write_locus, LocalLocus::Worktree);
for p in ["~/.git-credentials", "~/.npmrc", "~/.config/gh/hosts.yml", "~/.pgpass", "~/.SSH/id_rsa"] {
assert_eq!(classify_region(p).read_locus, LocalLocus::Machine, "hidden not widened: {p}");
}
});
with_grants(&[("/", true, true)], || {
assert_eq!(classify_region("/etc/ssl/private/server.key").read_locus, LocalLocus::Machine);
assert_eq!(with_os("linux", || classify_region("/etc/shadow").read_locus), LocalLocus::Machine);
});
with_grants(&[("~/.runner-scripts/", true, true)], || {
assert_eq!(classify_region("~/.runner-scripts/deploy.sh").write_locus, LocalLocus::Worktree);
});
with_grants(&[("~/", true, true)], || {
for p in [
"~/Library/Keychains/login.keychain-db",
"~/Library/Cookies/Cookies.binarycookies",
"~/Library/Application Support/Firefox/Profiles/x.default/logins.json",
"~/Library/Application Support/Google/Chrome/Default/Login Data",
"~/.config/git/credentials",
] {
assert_eq!(with_os("macos", || classify_region(p).read_locus), LocalLocus::Machine, "shield: {p}");
}
});
}
#[test]
fn grant_never_widens_a_secret_carveout() {
with_grants(&[("~/", true, true)], || {
let r = classify_region("~/.ssh/id_rsa");
assert_eq!(r.read_locus, LocalLocus::Machine, "secret stays denied under a ~/ grant");
assert!(r.reads_secret);
});
}
#[test]
fn grant_takes_effect_end_to_end() {
with_grants(&[("~/projects/", true, true)], || {
assert!(crate::is_safe_command("cat ~/projects/sibling/notes.txt"));
assert!(crate::is_safe_command("cp ./a ~/projects/sibling/b"));
assert!(crate::is_safe_command("echo hi > ~/projects/sibling/out.txt"));
});
}
#[test]
fn a_home_grant_matches_both_tilde_and_absolute_spellings() {
let Some(home) = std::env::var_os("HOME").and_then(|h| h.into_string().ok()) else {
return;
};
with_grants(&[("~/work/", true, true)], || {
assert!(classify_region("~/work/a.txt").write_locus == LocalLocus::Worktree);
assert!(classify_region(&format!("{home}/work/a.txt")).write_locus == LocalLocus::Worktree);
});
}
#[test]
fn every_region_carries_provenance_and_a_valid_role() {
let src = include_str!("../../../regions/default.toml");
let file: RegionsFile = toml::from_str(src).expect("valid TOML");
for r in &file.region {
assert!(!r.note.trim().is_empty(), "region `{}` is missing a note", r.path);
assert!(!r.researched.trim().is_empty(), "region `{}` is missing a researched date", r.path);
assert!(file.role.contains_key(&r.role), "region `{}` names undefined role `{}`", r.path, r.role);
}
assert!(file.region.len() > 10, "region set unexpectedly small ({})", file.region.len());
}
}