use std::fs;
use std::path::Path;
use openmoat_core::Policy;
use openmoat_hosts::Host;
use crate::hangs;
use crate::home::Home;
use crate::install::{GUARD_BUDGET_S, HostConfig};
use crate::sandbox::{self, Grants, exposure};
use crate::time;
const LEGACY_TIOCSTI: &str = "/proc/sys/dev/tty/legacy_tiocsti";
const HANG_WINDOW_MS: i64 = 7 * 24 * 3_600_000;
pub(super) fn legacy_tiocsti() -> Option<String> {
cfg!(target_os = "linux").then(|| fs::read_to_string(LEGACY_TIOCSTI).unwrap_or_default())
}
pub(super) fn tiocsti(codex: bool, setting: Option<&str>) -> Option<String> {
let value = setting.map(str::trim).filter(|v| codex && *v != "0")?;
let name = Host::Codex.display_name();
Some(if value.is_empty() {
format!(
"{name:<16} this kernel (before Linux 6.2) always lets a command type into your \
terminal (TIOCSTI), and Codex's sandbox cannot stop it; a 6.2+ kernel with \
dev.tty.legacy_tiocsti=0 does"
)
} else {
format!(
"{name:<16} dev.tty.legacy_tiocsti is {value}: Codex's sandbox cannot stop a command \
typing into your terminal (TIOCSTI); `sudo sysctl dev.tty.legacy_tiocsti=0` turns that off"
)
})
}
pub(super) fn state_in_grant(home: &Home, policy: &Policy, linux: bool) -> Option<String> {
if !linux {
return None;
}
let ctx = crate::context::eval_context(home, None, None).ok()?;
let rules = sandbox::landlock_rules(policy, &ctx, Grants::default()).ok()?;
let tmpdir = sandbox::session_tmpdir();
let why = exposure::refusal(home.root(), &rules.rules, tmpdir.as_deref(), false)?;
Some(format!("openmoat run {why}"))
}
pub(super) const RENAMED: &str = "command `moat` is the old name of `openmoat`; it still \
works for now and will be removed in a later release";
pub(super) fn alias_lock(recorded: &str, binary: &Path) -> Option<String> {
let alias = crate::install::alias_of(binary)?;
(alias.to_string_lossy() == recorded).then(|| {
format!(
"binary the lock records {recorded}, the old `moat` name of {}; it still \
works, and `openmoat init` moves the hooks and the lock to openmoat",
binary.display()
)
})
}
pub(super) fn alias_hooks(home: &Home, binary: &Path) -> Vec<String> {
Host::ALL
.into_iter()
.filter_map(|host| HostConfig::for_host(home, host).ok())
.filter(|config| config.runs_alias(binary))
.map(|config| {
format!(
"{:<16} hook runs the old `moat` name; it still works, and `openmoat init` \
switches it to openmoat",
config.host.display_name()
)
})
.collect()
}
pub(super) fn old_macos(version: Option<&str>) -> Option<String> {
let reason = crate::os_version::run_floor(version)?;
Some(format!(
"openmoat run {reason}; the agent hooks still apply the policy"
))
}
pub(super) fn hangs(home: &Home, now_ms: i64) -> Option<String> {
let path = home.hangs_path();
let found = hangs::since(&path, now_ms - HANG_WINDOW_MS)?;
let phase = found.common_phase.map_or_else(String::new, |p| {
format!(", most while {} (phase {})", p.doing(), p.name())
});
Some(format!(
"guard {} call(s) denied with no decision within {GUARD_BUDGET_S} s \
in the last 7 days, the latest at {}{phase}; not in the audit log, listed in {}",
found.count,
time::timestamp(found.newest_ms),
path.display()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tiocsti_warns_only_for_codex_on_a_kernel_that_allows_it() {
let warning = tiocsti(true, Some("1\n")).unwrap();
assert!(warning.contains("legacy_tiocsti is 1:"), "{warning}");
assert!(
warning.contains("sysctl dev.tty.legacy_tiocsti=0"),
"{warning}"
);
assert_eq!(tiocsti(true, Some("0\n")), None);
assert_eq!(tiocsti(true, None), None, "not Linux");
let old_kernel = tiocsti(true, Some("")).unwrap();
assert!(old_kernel.contains("before Linux 6.2"), "{old_kernel}");
assert_eq!(tiocsti(false, Some("1\n")), None);
}
#[cfg(unix)]
#[test]
fn a_state_directory_inside_a_grant_is_a_warning_on_linux_only() {
let dir = tempfile::tempdir().unwrap();
let home = Home::at(dir.path().join("state"));
fs::create_dir_all(home.root()).unwrap();
let policy = Policy::parse(openmoat_core::DEFAULT_POLICY).unwrap();
let row = state_in_grant(&home, &policy, true).unwrap();
assert!(row.starts_with("openmoat run "), "{row}");
assert!(row.contains("Move MOAT_HOME out of /"), "{row}");
assert_eq!(state_in_grant(&home, &policy, false), None);
}
#[test]
fn an_old_macos_is_a_moat_run_warning() {
let row = old_macos(Some("12.7.6")).unwrap();
assert!(row.starts_with("openmoat run needs macOS 13"), "{row}");
assert!(row.contains("hooks still apply the policy"), "{row}");
assert_eq!(old_macos(Some("13.0")), None);
assert_eq!(old_macos(None), None, "not macOS, or unreadable");
}
#[test]
fn hangs_older_than_a_week_are_not_reported() {
let dir = tempfile::tempdir().unwrap();
let home = Home::at(dir.path().to_path_buf());
let now = HANG_WINDOW_MS * 2;
assert_eq!(hangs(&home, now), None);
fs::write(home.hangs_path(), format!("1 codex\n{now} cursor\n")).unwrap();
let row = hangs(&home, now).unwrap();
assert!(row.contains("1 call(s) denied"), "{row}");
assert!(!row.contains("most while"), "{row}");
fs::write(
home.hangs_path(),
format!("{now} codex taint 30010\n{now} cursor\n"),
)
.unwrap();
let row = hangs(&home, now).unwrap();
assert!(row.contains("2 call(s) denied"), "{row}");
assert!(
row.contains("most while reading the session's earlier calls (phase taint)"),
"{row}"
);
}
}