use crate::doctor::{BOLD, DIM, GREEN, Outcome, RED, RST, YELLOW};
pub(crate) fn shell_allowlist_outcome() -> Outcome {
if let Some(err) = crate::core::config::last_config_parse_error() {
let short = err.lines().next().unwrap_or("parse error");
return Outcome {
ok: false,
line: format!(
"{BOLD}Shell allowlist{RST} {RED}config.toml fails to parse → running on DEFAULTS{RST} {DIM}({short}){RST}"
),
};
}
match crate::core::shell_allowlist::ShellSecurity::resolve() {
crate::core::shell_allowlist::ShellSecurity::Off => {
return Outcome {
ok: true,
line: format!(
"{BOLD}Shell allowlist{RST} {YELLOW}off{RST} {DIM}(shell_security=off — gating skipped, all commands allowed){RST}"
),
};
}
crate::core::shell_allowlist::ShellSecurity::Warn => {
return Outcome {
ok: true,
line: format!(
"{BOLD}Shell allowlist{RST} {YELLOW}warn-only{RST} {DIM}(shell_security=warn — violations logged, never blocked){RST}"
),
};
}
crate::core::shell_allowlist::ShellSecurity::Enforce => {}
}
let effective = crate::core::shell_allowlist::effective_allowlist_pub();
if effective.is_empty() {
return Outcome {
ok: true,
line: format!(
"{BOLD}Shell allowlist{RST} {YELLOW}disabled{RST} {DIM}(all commands allowed){RST}"
),
};
}
Outcome {
ok: true,
line: format!(
"{BOLD}Shell allowlist{RST} {GREEN}{} command(s) enforced{RST} {DIM}(add one: lean-ctx allow <cmd>){RST}",
effective.len()
),
}
}
pub(crate) fn path_jail_outcome() -> Outcome {
if cfg!(feature = "no-jail") {
return Outcome {
ok: true,
line: format!(
"{BOLD}Path jail{RST} {YELLOW}disabled at compile time{RST} {DIM}(built with the no-jail feature){RST}"
),
};
}
let cfg = crate::core::config::Config::load();
if cfg.path_jail == Some(false) {
return Outcome {
ok: true,
line: format!(
"{BOLD}Path jail{RST} {YELLOW}disabled{RST} {DIM}(path_jail = false in config.toml — all tool paths allowed){RST}"
),
};
}
let entries: Vec<&String> = cfg
.allow_paths
.iter()
.chain(cfg.extra_roots.iter())
.collect();
let mut grants_everything = false;
let mut dead: Vec<String> = Vec::new();
for raw in &entries {
let expanded = crate::core::pathjail::expand_user_path(raw);
if expanded == std::path::Path::new("/") {
grants_everything = true;
}
if !expanded.exists() {
dead.push((*raw).clone());
}
}
if grants_everything {
return Outcome {
ok: true,
line: format!(
"{BOLD}Path jail{RST} {YELLOW}active, but allow_paths contains \"/\"{RST} {DIM}(grants everything — prefer the explicit `path_jail = false`){RST}"
),
};
}
if !dead.is_empty() {
return Outcome {
ok: false,
line: format!(
"{BOLD}Path jail{RST} {RED}{} allow_paths entr{} never match{RST} {DIM}({} — unset $VAR or missing path){RST}",
dead.len(),
if dead.len() == 1 {
"y will"
} else {
"ies will"
},
dead.join(", ")
),
};
}
let detail = if entries.is_empty() {
let cfg = crate::core::config::Config::path()
.map_or_else(|| "config.toml".to_string(), |p| p.display().to_string());
format!("project root only; extend via allow_paths in {cfg}")
} else {
format!("project root + {} configured allow path(s)", entries.len())
};
let relaxed: Vec<&str> = crate::core::pathjail::active_relaxations()
.iter()
.map(|r| r.source)
.collect();
if relaxed.is_empty() {
Outcome {
ok: true,
line: format!("{BOLD}Path jail{RST} {GREEN}active{RST} {DIM}({detail}){RST}"),
}
} else {
Outcome {
ok: true,
line: format!(
"{BOLD}Path jail{RST} {GREEN}active{RST} {YELLOW}but relaxed via {}{RST} {DIM}({detail}; relaxations widen access beyond the project root){RST}",
relaxed.join(", ")
),
}
}
}
pub(crate) fn workspace_trust_outcome() -> Outcome {
let Some(root) = crate::core::config::Config::find_project_root() else {
return Outcome {
ok: true,
line: format!("{BOLD}Workspace trust{RST} {DIM}n/a (no project root){RST}"),
};
};
let sensitive = std::fs::read_to_string(crate::core::config::Config::local_path(&root))
.ok()
.map(|c| crate::core::config::local_sensitive_overrides(&c))
.unwrap_or_default();
if sensitive.is_empty() {
return Outcome {
ok: true,
line: format!(
"{BOLD}Workspace trust{RST} {GREEN}no project-local security overrides{RST}"
),
};
}
if crate::core::workspace_trust::is_trusted(std::path::Path::new(&root)) {
Outcome {
ok: true,
line: format!(
"{BOLD}Workspace trust{RST} {GREEN}trusted{RST} {DIM}({} sensitive override(s) honoured: {}){RST}",
sensitive.len(),
sensitive.join(", ")
),
}
} else {
Outcome {
ok: true,
line: format!(
"{BOLD}Workspace trust{RST} {YELLOW}untrusted — {} sensitive override(s) withheld{RST} {DIM}(run `lean-ctx trust`: {}){RST}",
sensitive.len(),
sensitive.join(", ")
),
}
}
}
pub(crate) fn secret_detection_outcome() -> Outcome {
let cfg = crate::core::config::Config::load();
let sd = &cfg.secret_detection;
if !sd.enabled {
return Outcome {
ok: true,
line: format!(
"{BOLD}Secret redaction{RST} {YELLOW}off{RST} {DIM}(secret_detection.enabled=false — .env/API keys can reach the provider; re-enable: lean-ctx security secrets on){RST}"
),
};
}
if !sd.redact {
return Outcome {
ok: true,
line: format!(
"{BOLD}Secret redaction{RST} {YELLOW}detect-only{RST} {DIM}(secrets flagged but not masked — set secret_detection.redact=true to mask){RST}"
),
};
}
let custom = if sd.custom_patterns.is_empty() {
String::new()
} else {
format!(" + {} custom pattern(s)", sd.custom_patterns.len())
};
Outcome {
ok: true,
line: format!(
"{BOLD}Secret redaction{RST} {GREEN}on{RST} {DIM}(.env/API keys masked before the model sees them{custom}){RST}"
),
}
}
pub(crate) fn permission_inheritance_outcome() -> Outcome {
use crate::core::config::{Config, PermissionInheritance};
let cfg = Config::load();
if cfg.permission_inheritance_effective() != PermissionInheritance::On {
return Outcome {
ok: true,
line: format!(
"{BOLD}Permission inheritance{RST} {YELLOW}off{RST} {DIM}(enable: lean-ctx config set permission_inheritance on → ctx_shell honors your IDE's bash/rm rules){RST}"
),
};
}
let policy = dirs::home_dir()
.map(|home| crate::core::ide_permissions::load_opencode(&home, None))
.unwrap_or_default();
let detail = if policy.is_empty() {
"on, but no OpenCode permission rules found yet".to_string()
} else {
format!(
"mirroring {} OpenCode permission rule(s)",
policy.rule_count()
)
};
Outcome {
ok: true,
line: format!("{BOLD}Permission inheritance{RST} {GREEN}on{RST} {DIM}({detail}){RST}"),
}
}