use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Instant;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::{Value as JsonValue, json};
use toml_edit::{DocumentMut, Item};
use crate::core::degraded_aggregation::{DegradationAggregationInput, aggregate_degraded_entries};
use crate::core::tripwire::glob_match;
use crate::db::StoredMemory;
use crate::models::{DomainError, RecoveryKind, RepairActionRiskClass, repair_action_safety};
pub const PREFLIGHT_GUARD_SCHEMA_V1: &str = "ee.preflight.guard.v1";
pub const NO_RISK_MEMORIES_CODE: &str = "no_risk_memories";
pub const PREFLIGHT_PATTERNS_UNAVAILABLE_CODE: &str = "preflight_patterns_unavailable";
pub const PREFLIGHT_RULES_RELATIVE_PATH: &str = ".ee/preflight_rules.toml";
pub const PREFLIGHT_RULES_MAX_BYTES: u64 = 4 * 1024 * 1024;
const TRAUMA_GUARD_PREFLIGHT_SURFACE: &str = "trauma_guard_preflight";
fn elapsed_ms_since(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn preflight_trace_workspace_id(workspace: &Path) -> String {
let path = workspace.to_string_lossy();
let digest = blake3::hash(path.as_bytes()).to_hex().to_string();
format!("wsp_{}", &digest[..16])
}
fn trace_trauma_guard_preflight(
workspace: &Path,
phase: &'static str,
elapsed_ms: u64,
degraded_codes: &[&str],
) {
tracing::info!(
workspace_id = %preflight_trace_workspace_id(workspace),
request_id = "preflight_guard_request",
bead_id = option_env!("EE_TRACE_BEAD_ID").unwrap_or("bd-3usjw.6"),
surface = TRAUMA_GUARD_PREFLIGHT_SURFACE,
phase,
elapsed_ms,
degraded_codes = ?degraded_codes,
"trauma guard preflight checkpoint"
);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardAction {
Warn,
#[serde(rename = "high_risk", alias = "halt")]
Halt,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepairCommandNextAction {
RunDirectly,
RunPreflightFirst,
CoordinateFirst,
AskHuman,
ManualOnly,
}
impl RepairCommandNextAction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RunDirectly => "run_directly",
Self::RunPreflightFirst => "run_preflight_first",
Self::CoordinateFirst => "coordinate_first",
Self::AskHuman => "ask_human",
Self::ManualOnly => "manual_only",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepairCommandPreflightAssessment {
pub command: Option<String>,
pub risk_class: &'static str,
pub preflight_command: Option<String>,
pub requires_human_approval: bool,
pub mutates_external_state: bool,
pub mutates_tracker_state: bool,
pub privacy_class: &'static str,
pub next_action: RepairCommandNextAction,
pub rule_id: &'static str,
pub source: &'static str,
pub reason_code: &'static str,
pub evidence: Vec<&'static str>,
pub preconditions: Vec<&'static str>,
}
impl GuardAction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Warn => "warn",
Self::Halt => "high_risk",
}
}
#[must_use]
pub const fn stops_execution(self) -> bool {
false
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuleSource {
Builtin { name: String },
WorkspaceFile { path: String },
ProceduralRule { rule_id: String },
Tripwire { tripwire_id: String },
}
impl RuleSource {
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
Self::Builtin { .. } => "builtin",
Self::WorkspaceFile { .. } => "workspace_file",
Self::ProceduralRule { .. } => "procedural_rule",
Self::Tripwire { .. } => "tripwire",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PreflightGuardRule {
pub id: String,
pub pattern: String,
pub action: GuardAction,
pub message: String,
pub source: RuleSource,
}
#[derive(Clone, Debug, Default)]
pub struct PreflightGuardRegistry {
rules: Vec<PreflightGuardRule>,
load_degraded: Vec<PreflightGuardDegradation>,
}
impl PreflightGuardRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_builtins() -> Self {
Self {
rules: builtin_rules(),
load_degraded: Vec::new(),
}
}
pub fn load(workspace: &Path) -> Result<Self, DomainError> {
let mut registry = Self::with_builtins();
let resolved_workspace = workspace
.canonicalize()
.unwrap_or_else(|_| workspace.to_path_buf());
let rules_path = resolved_workspace.join(PREFLIGHT_RULES_RELATIVE_PATH);
let source_label = rules_path.to_string_lossy().into_owned();
if let Err(error) = validate_preflight_rules_path(&rules_path) {
registry
.load_degraded
.push(workspace_rules_load_degradation(&source_label, &error));
return Ok(registry);
}
let body = match read_preflight_rules_file_no_follow(&rules_path) {
Ok(body) => body,
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) =>
{
return Ok(registry);
}
Err(error) => {
let domain_error = DomainError::Storage {
message: format!("Failed to read {source_label}: {error}"),
repair: Some(format!(
"Check filesystem permissions on {} or remove the file to fall back to builtins.",
source_label
)),
};
registry
.load_degraded
.push(workspace_rules_load_degradation(
&source_label,
&domain_error,
));
return Ok(registry);
}
};
let workspace_rules = match parse_workspace_rules(&body, &source_label) {
Ok(rules) => rules,
Err(error) => {
registry
.load_degraded
.push(workspace_rules_load_degradation(&source_label, &error));
return Ok(registry);
}
};
registry.rules.extend(workspace_rules);
Ok(registry)
}
pub fn from_toml(body: &str, source_label: &str) -> Result<Self, DomainError> {
Ok(Self {
rules: parse_workspace_rules(body, source_label)?,
load_degraded: Vec::new(),
})
}
#[must_use]
pub fn rules(&self) -> &[PreflightGuardRule] {
&self.rules
}
#[must_use]
pub fn load_degradations(&self) -> &[PreflightGuardDegradation] {
&self.load_degraded
}
pub fn set_rules(&mut self, rules: Vec<PreflightGuardRule>) {
self.rules = rules;
self.load_degraded.clear();
}
pub fn extend_from_links<I>(&mut self, linked: I)
where
I: IntoIterator<Item = PreflightGuardRule>,
{
for rule in linked {
if !self.rules.iter().any(|existing| existing.id == rule.id) {
self.rules.push(rule);
}
}
}
#[must_use]
pub fn match_command(&self, command: &str) -> Vec<&PreflightGuardRule> {
self.rules
.iter()
.filter(|rule| rule_matches_command(rule, command))
.collect()
}
}
fn read_preflight_rules_file_no_follow(path: &Path) -> std::io::Result<String> {
let file = open_preflight_rules_file_for_read(path)?;
let limit = PREFLIGHT_RULES_MAX_BYTES.saturating_add(1);
let mut bytes = Vec::new();
file.take(limit).read_to_end(&mut bytes)?;
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > PREFLIGHT_RULES_MAX_BYTES {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"preflight rule file at {} grew past the {PREFLIGHT_RULES_MAX_BYTES}-byte cap after the metadata check (TOCTOU)",
path.display()
),
));
}
String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
fn open_preflight_rules_file_for_read(path: &Path) -> std::io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.read(true);
configure_preflight_rules_open_no_follow(&mut options);
options.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn configure_preflight_rules_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn configure_preflight_rules_open_no_follow(_options: &mut fs::OpenOptions) {}
fn validate_preflight_rules_path(path: &Path) -> Result<(), DomainError> {
if let Some(symlink_path) =
first_existing_symlink_component(path).map_err(|error| DomainError::Storage {
message: format!(
"Failed to inspect preflight rule path component {}: {}",
error.path.display(),
error.source
),
repair: Some("Fix or remove .ee/preflight_rules.toml.".to_owned()),
})?
{
return Err(DomainError::Configuration {
message: format!(
"Refusing to read preflight rule file {} through symlinked path component {}.",
path.display(),
symlink_path.display()
),
repair: Some(
"Replace .ee/preflight_rules.toml with a regular file inside the workspace."
.to_owned(),
),
});
}
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) =>
{
return Ok(());
}
Err(error) => {
return Err(DomainError::Storage {
message: format!(
"Failed to inspect preflight rule file {}: {error}",
path.display()
),
repair: Some("Fix or remove .ee/preflight_rules.toml.".to_owned()),
});
}
};
if !metadata.is_file() {
return Err(DomainError::Configuration {
message: format!(
"Preflight rule path is not a regular file: {}",
path.display()
),
repair: Some("Replace .ee/preflight_rules.toml with a regular TOML file.".to_owned()),
});
}
if metadata.len() > PREFLIGHT_RULES_MAX_BYTES {
return Err(DomainError::Configuration {
message: format!(
"Refusing to load preflight rule file {}: {} bytes exceeds the {} byte ceiling.",
path.display(),
metadata.len(),
PREFLIGHT_RULES_MAX_BYTES
),
repair: Some(format!(
"Truncate or rewrite {} (typical files are a few KB); the bundled builtins still apply.",
path.display()
)),
});
}
Ok(())
}
fn workspace_rules_load_degradation(
source_label: &str,
error: &DomainError,
) -> PreflightGuardDegradation {
preflight_patterns_unavailable_degradation(format!(
"Destructive pattern workspace rules unavailable from {source_label}: {}. Built-in destructive patterns still apply.",
error.message()
))
}
#[derive(Debug)]
struct SymlinkComponentInspectionError {
path: PathBuf,
source: std::io::Error,
}
fn first_existing_symlink_component(
path: &Path,
) -> Result<Option<PathBuf>, SymlinkComponentInspectionError> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component.as_os_str());
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => return Ok(Some(current)),
Ok(_) => {}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) =>
{
return Ok(None);
}
Err(source) => {
return Err(SymlinkComponentInspectionError {
path: current,
source,
});
}
}
}
Ok(None)
}
fn rule_matches_command(rule: &PreflightGuardRule, command: &str) -> bool {
if let RuleSource::Builtin { name } = &rule.source {
match name.as_str() {
"rm_rf_root" => return matches_rm_rf_target(command, RmTargetClass::Absolute),
"rm_rf_home" => return matches_rm_rf_target(command, RmTargetClass::Home),
"file_deletion" => return matches_file_deletion(command),
"git_reset_hard" => return matches_git_reset_hard(command),
"git_worktree_add" => return matches_git_worktree_add(command),
"git_stash" => return matches_git_subcommand(command, "stash"),
"git_rebase" => return matches_git_subcommand(command, "rebase"),
"git_checkout_off_main" => return matches_git_checkout_off_main(command),
"git_clean_fd" => return matches_git_clean_destructive(command),
"script_code_rewrite" => return matches_script_code_rewrite(command),
"unsafe_cleanup" => return matches_unsafe_cleanup(command),
"kubectl_mass_delete" => return matches_kubectl_mass_delete(command),
"drop_table_sql" => return matches_drop_table_sql(command),
"terraform_destroy" => return matches_terraform_destroy(command),
"raw_block_device_write" => return matches_raw_block_device_write(command),
"filesystem_create" => return matches_filesystem_create(command),
"git_push_force" => return matches_git_push_force(command),
_ => {}
}
}
glob_match(&rule.pattern, command)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RmTargetClass {
Absolute,
Home,
}
fn matches_rm_rf_target(command: &str, target_class: RmTargetClass) -> bool {
if command_contains_active_command_substitution(command, |body| {
matches_rm_rf_target(body, target_class)
}) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| rm_segment_matches_target(segment, target_class))
}
fn rm_segment_matches_target(segment: &[String], target_class: RmTargetClass) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_rm_rf_target(shell_body, target_class);
}
if inline_interpreter_body(segment, command_index)
.is_some_and(|body| script_body_mentions_rm_rf_target(body, target_class))
{
return true;
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "rm" && !is_variable_command(word))
{
return false;
}
let mut has_recursive = false;
let mut has_force = false;
let mut saw_option_end = false;
let mut targets = Vec::new();
for word in segment.iter().skip(command_index + 1) {
if !saw_option_end && word == "--" {
saw_option_end = true;
continue;
}
if !saw_option_end && word.starts_with('-') && word != "-" {
if rm_option_has_recursive(word) {
has_recursive = true;
}
if rm_option_has_force(word) {
has_force = true;
}
continue;
}
targets.push(word.as_str());
}
has_recursive
&& has_force
&& targets
.iter()
.any(|target| rm_target_matches_class(target, target_class))
}
fn matches_file_deletion(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_file_deletion) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| file_deletion_segment_matches(segment))
}
fn file_deletion_segment_matches(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_file_deletion(shell_body);
}
if inline_interpreter_body(segment, command_index)
.is_some_and(script_body_mentions_file_deletion)
{
return true;
}
let Some(command_name) = segment
.get(command_index)
.map(|word| command_basename(word))
else {
return false;
};
if is_variable_command(command_name) {
return rm_has_deletion_target(segment, command_index);
}
match command_name {
"rm" => rm_has_deletion_target(segment, command_index),
"unlink" | "rmdir" => rm_has_deletion_target(segment, command_index),
_ => false,
}
}
fn rm_has_deletion_target(segment: &[String], command_index: usize) -> bool {
let mut saw_option_end = false;
for word in segment.iter().skip(command_index + 1) {
if !saw_option_end && word == "--" {
saw_option_end = true;
continue;
}
if !saw_option_end && word.starts_with('-') && word != "-" {
continue;
}
return true;
}
false
}
fn shell_segment_command_index(segment: &[String]) -> Option<usize> {
let mut index = 0;
while index < segment.len() {
let word = &segment[index];
if command_basename(word) == "sudo" {
index = sudo_wrapped_command_index(segment, index + 1)?;
continue;
}
if word == "command" || word == "builtin" {
index += 1;
continue;
}
if command_basename(word) == "env" {
index = env_wrapped_command_index(segment, index + 1)?;
continue;
}
if looks_like_env_assignment(word) {
index += 1;
continue;
}
return Some(index);
}
None
}
fn sudo_wrapped_command_index(segment: &[String], mut index: usize) -> Option<usize> {
while index < segment.len() {
let word = segment[index].as_str();
if word == "--" {
return segment.get(index + 1).map(|_| index + 1);
}
if sudo_option_takes_value(word) {
index += 2;
continue;
}
if sudo_option_is_value_form(word) || sudo_option_is_flag(word) {
index += 1;
continue;
}
return Some(index);
}
None
}
fn sudo_option_takes_value(word: &str) -> bool {
matches!(
word,
"-C" | "-D"
| "-g"
| "-h"
| "-p"
| "-r"
| "-t"
| "-T"
| "-U"
| "-u"
| "--chdir"
| "--close-from"
| "--command-timeout"
| "--group"
| "--host"
| "--login-class"
| "--prompt"
| "--role"
| "--type"
| "--user"
)
}
fn sudo_option_is_value_form(word: &str) -> bool {
[
"--chdir=",
"--close-from=",
"--command-timeout=",
"--group=",
"--host=",
"--login-class=",
"--prompt=",
"--preserve-env=",
"--role=",
"--type=",
"--user=",
]
.iter()
.any(|prefix| word.starts_with(prefix))
|| sudo_short_option_has_attached_value(word)
}
fn sudo_short_option_has_attached_value(word: &str) -> bool {
word.starts_with('-')
&& !word.starts_with("--")
&& word.len() > 2
&& matches!(
word.as_bytes().get(1).copied(),
Some(b'C' | b'D' | b'g' | b'h' | b'p' | b'r' | b't' | b'T' | b'U' | b'u')
)
}
fn sudo_option_is_flag(word: &str) -> bool {
matches!(
word,
"-" | "-A"
| "-B"
| "-b"
| "-E"
| "-e"
| "-H"
| "-i"
| "-K"
| "-k"
| "-l"
| "-n"
| "-P"
| "-S"
| "-s"
| "-V"
| "-v"
| "--askpass"
| "--background"
| "--bell"
| "--edit"
| "--help"
| "--list"
| "--login"
| "--non-interactive"
| "--preserve-env"
| "--reset-timestamp"
| "--remove-timestamp"
| "--set-home"
| "--stdin"
| "--validate"
| "--version"
) || sudo_short_flag_group(word)
}
fn sudo_short_flag_group(word: &str) -> bool {
word.starts_with('-')
&& !word.starts_with("--")
&& word.len() > 2
&& word.chars().skip(1).all(|ch| {
matches!(
ch,
'A' | 'B'
| 'b'
| 'E'
| 'e'
| 'H'
| 'i'
| 'K'
| 'k'
| 'l'
| 'n'
| 'P'
| 'S'
| 's'
| 'V'
| 'v'
)
})
}
fn env_wrapped_command_index(segment: &[String], mut index: usize) -> Option<usize> {
while index < segment.len() {
let word = segment[index].as_str();
if word == "--" {
return segment.get(index + 1).map(|_| index + 1);
}
if looks_like_env_assignment(word) {
index += 1;
continue;
}
if env_option_takes_value(word) {
index += 2;
continue;
}
if env_option_is_value_form(word) || env_option_is_flag(word) {
index += 1;
continue;
}
return Some(index);
}
None
}
fn env_option_takes_value(word: &str) -> bool {
matches!(
word,
"-u" | "--unset"
| "-C"
| "--chdir"
| "-S"
| "--split-string"
| "--block-signal"
| "--default-signal"
| "--ignore-signal"
)
}
fn env_option_is_value_form(word: &str) -> bool {
[
"--unset=",
"--chdir=",
"--split-string=",
"--block-signal=",
"--default-signal=",
"--ignore-signal=",
]
.iter()
.any(|prefix| word.starts_with(prefix))
}
fn env_option_is_flag(word: &str) -> bool {
matches!(
word,
"-" | "-i" | "--ignore-environment" | "-0" | "--null" | "-v" | "--debug"
) || env_short_flag_group(word)
}
fn env_short_flag_group(word: &str) -> bool {
word.starts_with('-')
&& !word.starts_with("--")
&& word.len() > 2
&& word.chars().skip(1).all(|ch| matches!(ch, 'i' | '0' | 'v'))
}
fn command_contains_active_command_substitution(
command: &str,
mut body_matches: impl FnMut(&str) -> bool,
) -> bool {
let chars = command.chars().collect::<Vec<_>>();
let mut index = 0;
let mut quote: Option<char> = None;
let mut escaped = false;
while index < chars.len() {
let ch = chars[index];
if quote == Some('\'') {
if ch == '\'' {
quote = None;
}
index += 1;
continue;
}
if escaped {
escaped = false;
index += 1;
continue;
}
if ch == '\\' {
escaped = true;
index += 1;
continue;
}
if ch == '\'' && quote.is_none() {
quote = Some('\'');
index += 1;
continue;
}
if ch == '"' {
quote = if quote == Some('"') {
None
} else if quote.is_none() {
Some('"')
} else {
quote
};
index += 1;
continue;
}
if ch == '`' {
if let Some((body, close_index)) = backtick_command_substitution_body(&chars, index + 1)
{
if body_matches(&body) {
return true;
}
index = close_index + 1;
continue;
}
}
if ch == '$' && chars.get(index + 1) == Some(&'(') {
if let Some((body, close_index)) =
dollar_paren_command_substitution_body(&chars, index + 2)
{
if body_matches(&body) {
return true;
}
index = close_index + 1;
continue;
}
}
index += 1;
}
false
}
fn backtick_command_substitution_body(chars: &[char], start: usize) -> Option<(String, usize)> {
let mut body = String::new();
let mut index = start;
let mut escaped = false;
while index < chars.len() {
let ch = chars[index];
if escaped {
body.push(ch);
escaped = false;
index += 1;
continue;
}
if ch == '\\' {
escaped = true;
index += 1;
continue;
}
if ch == '`' {
return Some((body, index));
}
body.push(ch);
index += 1;
}
None
}
fn dollar_paren_command_substitution_body(chars: &[char], start: usize) -> Option<(String, usize)> {
let mut body = String::new();
let mut depth = 1usize;
let mut index = start;
let mut quote: Option<char> = None;
let mut escaped = false;
while index < chars.len() {
let ch = chars[index];
if quote == Some('\'') {
body.push(ch);
if ch == '\'' {
quote = None;
}
index += 1;
continue;
}
if escaped {
body.push(ch);
escaped = false;
index += 1;
continue;
}
if ch == '\\' {
body.push(ch);
escaped = true;
index += 1;
continue;
}
if ch == '\'' && quote.is_none() {
quote = Some('\'');
body.push(ch);
index += 1;
continue;
}
if ch == '"' {
quote = if quote == Some('"') {
None
} else if quote.is_none() {
Some('"')
} else {
quote
};
body.push(ch);
index += 1;
continue;
}
if ch == '$' && chars.get(index + 1) == Some(&'(') {
depth += 1;
body.push(ch);
body.push('(');
index += 2;
continue;
}
if ch == '(' {
depth += 1;
body.push(ch);
index += 1;
continue;
}
if ch == ')' && quote.is_none() {
depth -= 1;
if depth == 0 {
return Some((body, index));
}
}
body.push(ch);
index += 1;
}
None
}
fn shell_c_argument(segment: &[String], command_index: usize) -> Option<&str> {
let command = segment.get(command_index)?;
if command_basename(command) == "eval" {
return segment.get(command_index + 1).map(String::as_str);
}
if !is_shell_command(command) {
return None;
}
let mut index = command_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if word == "-c" {
return segment.get(index + 1).map(String::as_str);
}
if word.starts_with('-') && !word.starts_with("--") && word.contains('c') {
return segment.get(index + 1).map(String::as_str);
}
index += 1;
}
None
}
fn is_shell_command(word: &str) -> bool {
matches!(
command_basename(word),
"bash"
| "csh"
| "dash"
| "elvish"
| "fish"
| "ksh"
| "nu"
| "sh"
| "tcsh"
| "xonsh"
| "zsh"
)
}
fn command_basename(word: &str) -> &str {
word.rsplit('/').next().unwrap_or(word)
}
fn is_variable_command(word: &str) -> bool {
word.starts_with('$')
}
fn matches_git_reset_hard(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_git_reset_hard) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_reset_segment_is_hard(segment))
}
fn git_reset_segment_is_hard(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_reset_hard(shell_body);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "git")
{
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
if segment
.get(subcommand_index)
.is_none_or(|subcommand| subcommand != "reset")
{
return false;
}
segment
.iter()
.skip(subcommand_index + 1)
.any(|word| git_reset_option_is_hard(word))
}
fn git_reset_option_is_hard(word: &str) -> bool {
word == "--hard" || word.starts_with("--hard=")
}
fn matches_git_worktree_add(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_git_worktree_add) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_worktree_segment_is_add(segment))
}
fn git_worktree_segment_is_add(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_worktree_add(shell_body);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "git")
{
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
if segment
.get(subcommand_index)
.is_none_or(|subcommand| subcommand != "worktree")
{
return false;
}
segment
.iter()
.skip(subcommand_index + 1)
.find(|word| !word.starts_with('-') || word.as_str() == "-")
.is_some_and(|subcommand| subcommand == "add")
}
fn matches_git_subcommand(command: &str, expected_subcommand: &str) -> bool {
if command_contains_active_command_substitution(command, |body| {
matches_git_subcommand(body, expected_subcommand)
}) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_segment_has_subcommand(segment, expected_subcommand))
}
fn git_segment_has_subcommand(segment: &[String], expected_subcommand: &str) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_subcommand(shell_body, expected_subcommand);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "git")
{
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
segment
.get(subcommand_index)
.is_some_and(|subcommand| subcommand == expected_subcommand)
}
fn matches_git_checkout_off_main(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_git_checkout_off_main) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_checkout_segment_is_off_main(segment))
}
fn git_checkout_segment_is_off_main(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_checkout_off_main(shell_body);
}
let Some(command_name) = segment.get(command_index) else {
return false;
};
if command_basename(command_name) != "git" {
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
match segment.get(subcommand_index).map(String::as_str) {
Some("checkout") => git_checkout_segment_is_forbidden(segment, subcommand_index),
Some("switch") => git_switch_segment_is_off_main(segment, subcommand_index),
_ => false,
}
}
fn git_subcommand_index(segment: &[String], command_index: usize) -> Option<usize> {
let mut index = command_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if git_global_option_takes_value(word) {
index += 2;
continue;
}
if word.starts_with("--") && word.contains('=') {
index += 1;
continue;
}
if word.starts_with('-') {
index += 1;
continue;
}
return Some(index);
}
None
}
fn git_global_option_takes_value(word: &str) -> bool {
matches!(
word,
"-C" | "-c" | "--config-env" | "--exec-path" | "--git-dir" | "--namespace" | "--work-tree"
)
}
fn git_checkout_segment_is_forbidden(segment: &[String], checkout_index: usize) -> bool {
let mut index = checkout_index + 1;
let mut target = None;
while index < segment.len() {
let word = segment[index].as_str();
if word == "-" {
return true;
}
if word == "--" {
return segment.get(index + 1).is_some();
}
if git_checkout_option_creates_detaches_or_forces(word)
|| git_checkout_option_overwrites_worktree(word)
|| git_checkout_pathspec_option(word)
{
return true;
}
if git_checkout_option_takes_value(word) {
index += 2;
continue;
}
if word.starts_with("--") && word.contains('=') {
index += 1;
continue;
}
if word.starts_with('-') {
index += 1;
continue;
}
if target.is_some() {
return true;
}
target = Some(word);
index += 1;
}
target.is_some_and(|target| target != "main")
}
fn git_checkout_option_creates_detaches_or_forces(word: &str) -> bool {
matches!(
word,
"-b" | "-B" | "-d" | "-f" | "--branch" | "--orphan" | "--detach" | "--force"
) || word.starts_with("-b")
|| word.starts_with("-B")
|| word.starts_with("-d")
|| word.starts_with("--branch=")
|| word.starts_with("--orphan=")
|| word.starts_with("--detach=")
|| word.starts_with("--force=")
|| (word.starts_with('-')
&& !word.starts_with("--")
&& word.chars().skip(1).any(|ch| ch == 'f'))
}
fn git_checkout_option_overwrites_worktree(word: &str) -> bool {
matches!(word, "-p" | "--patch")
|| word.starts_with("--patch=")
|| (word.starts_with('-')
&& !word.starts_with("--")
&& word.chars().skip(1).any(|ch| ch == 'p'))
}
fn git_checkout_pathspec_option(word: &str) -> bool {
matches!(word, "--pathspec-from-file" | "--pathspec-file-nul")
|| word.starts_with("--pathspec-from-file=")
}
fn git_checkout_option_takes_value(word: &str) -> bool {
matches!(word, "--conflict")
}
fn git_switch_segment_is_off_main(segment: &[String], switch_index: usize) -> bool {
let mut index = switch_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if word == "-" {
return true;
}
if word == "--" {
return segment
.get(index + 1)
.is_some_and(|target| target != "main");
}
if git_switch_option_creates_or_detaches(word) {
return true;
}
if git_switch_option_discards_changes(word) {
return true;
}
if git_switch_option_takes_value(word) {
index += 2;
continue;
}
if word.starts_with("--") && word.contains('=') {
index += 1;
continue;
}
if word.starts_with('-') {
index += 1;
continue;
}
return word != "main";
}
false
}
fn git_switch_option_creates_or_detaches(word: &str) -> bool {
matches!(
word,
"-c" | "-C" | "-d" | "--create" | "--force-create" | "--detach" | "--orphan"
) || word.starts_with("-c")
|| word.starts_with("-C")
|| word.starts_with("-d")
|| word.starts_with("--create=")
|| word.starts_with("--detach=")
|| word.starts_with("--force-create=")
|| word.starts_with("--orphan=")
}
fn git_switch_option_discards_changes(word: &str) -> bool {
matches!(word, "-f" | "--force" | "--discard-changes")
|| word.starts_with("--force=")
|| word.starts_with("--discard-changes=")
|| (word.starts_with('-')
&& !word.starts_with("--")
&& word.chars().skip(1).any(|ch| ch == 'f'))
}
fn git_switch_option_takes_value(word: &str) -> bool {
matches!(word, "--conflict")
}
fn matches_git_clean_destructive(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_git_clean_destructive) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_clean_segment_is_destructive(segment))
}
fn git_clean_segment_is_destructive(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_clean_destructive(shell_body);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "git")
{
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
if segment
.get(subcommand_index)
.is_none_or(|subcommand| subcommand != "clean")
{
return false;
}
git_clean_args_are_destructive(&segment[subcommand_index + 1..])
}
fn git_clean_args_are_destructive(args: &[String]) -> bool {
let mut has_force = false;
let mut dry_run = false;
let mut index = 0;
while index < args.len() {
let word = args[index].as_str();
if word == "--" {
break;
}
if git_clean_option_takes_value(word) {
index += 2;
continue;
}
if git_clean_option_is_value_form(word) {
index += 1;
continue;
}
if git_clean_option_has_dry_run(word) {
dry_run = true;
}
if git_clean_option_has_force(word) {
has_force = true;
}
index += 1;
}
has_force && !dry_run
}
fn git_clean_option_takes_value(word: &str) -> bool {
matches!(word, "-e" | "--exclude")
}
fn git_clean_option_is_value_form(word: &str) -> bool {
word.starts_with("--exclude=") || (word.starts_with("-e") && word.len() > 2)
}
fn git_clean_option_has_dry_run(word: &str) -> bool {
if word == "--dry-run" || word.starts_with("--dry-run=") {
return true;
}
word.starts_with('-') && !word.starts_with("--") && word.chars().skip(1).any(|ch| ch == 'n')
}
fn git_clean_option_has_force(word: &str) -> bool {
if word == "--force" || word.starts_with("--force=") {
return true;
}
word.starts_with('-') && !word.starts_with("--") && word.chars().skip(1).any(|ch| ch == 'f')
}
fn matches_git_push_force(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_git_push_force) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| git_push_segment_has_force(segment))
}
fn git_push_segment_has_force(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_git_push_force(shell_body);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "git")
{
return false;
}
let Some(subcommand_index) = git_subcommand_index(segment, command_index) else {
return false;
};
if segment
.get(subcommand_index)
.is_none_or(|subcommand| subcommand != "push")
{
return false;
}
segment
.iter()
.skip(subcommand_index + 1)
.any(|word| git_push_option_is_force(word))
}
fn git_push_option_is_force(word: &str) -> bool {
if word.starts_with('+') && word.len() > 1 {
return true;
}
if word == "--force"
|| word.starts_with("--force=")
|| word == "--force-with-lease"
|| word.starts_with("--force-with-lease=")
{
return true;
}
word.starts_with('-') && !word.starts_with("--") && word.chars().skip(1).any(|ch| ch == 'f')
}
fn matches_script_code_rewrite(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_script_code_rewrite) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| script_rewrite_segment_matches(segment))
}
fn script_rewrite_segment_matches(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_script_code_rewrite(shell_body);
}
let Some(command_name) = segment
.get(command_index)
.map(|word| command_basename(word))
else {
return false;
};
match command_name {
"sed" => segment.iter().skip(command_index + 1).any(|word| {
word == "--in-place" || word.starts_with("--in-place=") || word.starts_with("-i")
}),
"perl" | "ruby" => segment.iter().skip(command_index + 1).any(|word| {
word.starts_with('-')
&& !word.starts_with("--")
&& word.chars().skip(1).any(|ch| ch == 'i')
}),
"python" | "python3" | "node" | "bun" | "deno" => {
inline_script_body(segment, command_index, command_name)
.is_some_and(inline_script_rewrites_code)
}
_ => false,
}
}
fn inline_interpreter_body(segment: &[String], command_index: usize) -> Option<&str> {
let command_name = segment
.get(command_index)
.map(|word| command_basename(word))?;
if !is_inline_interpreter_command(command_name) {
return None;
}
inline_script_body(segment, command_index, command_name)
}
fn is_inline_interpreter_command(command_name: &str) -> bool {
matches!(
command_name,
"bun" | "deno" | "lua" | "node" | "perl" | "php" | "python" | "python3" | "ruby" | "tcl"
)
}
fn inline_script_body<'a>(
segment: &'a [String],
command_index: usize,
command_name: &str,
) -> Option<&'a str> {
let mut index = command_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if matches!(word, "-c" | "-e" | "--eval") {
return segment.get(index + 1).map(String::as_str);
}
if command_name == "deno" && word == "eval" {
return segment.get(index + 1).map(String::as_str);
}
index += 1;
}
None
}
fn inline_script_rewrites_code(body: &str) -> bool {
let has_write = [
"write_text",
".write(",
"open(",
"writeFile",
"writeFileSync",
"createWriteStream",
]
.iter()
.any(|needle| body.contains(needle));
has_write && inline_script_mentions_repo_code(body)
}
fn inline_script_mentions_repo_code(body: &str) -> bool {
[
"src/",
"tests/",
".rs",
"Cargo.toml",
"AGENTS.md",
"README.md",
]
.iter()
.any(|needle| body.contains(needle))
}
fn script_body_mentions_file_deletion(body: &str) -> bool {
matches_file_deletion(body)
|| inline_script_mentions_file_deletion_api(body)
|| quoted_string_literals(body)
.iter()
.any(|literal| matches_file_deletion(literal))
}
fn inline_script_mentions_file_deletion_api(body: &str) -> bool {
let lower = body.to_ascii_lowercase();
let compact = lower
.chars()
.filter(|ch| !ch.is_ascii_whitespace())
.collect::<String>();
[
".unlink(",
"os.remove(",
"os.unlink(",
"shutil.rmtree(",
"fs.unlink(",
".unlinksync(",
"fs.unlinksync(",
"fs.rm(",
".rmsync(",
"fs.rmsync(",
"fs.rmdir(",
".rmdirsync(",
"fs.rmdirsync(",
"deno.remove(",
"deno.removesync(",
"file.delete(",
"fileutils.rm_rf(",
"fileutils.rm_r(",
"remove_file(",
"remove_dir_all(",
"unlink(",
]
.iter()
.any(|needle| compact.contains(needle))
|| lower.contains("unlink \"")
|| lower.contains("unlink '")
}
fn script_body_mentions_rm_rf_target(body: &str, target_class: RmTargetClass) -> bool {
matches_rm_rf_target(body, target_class)
|| quoted_string_literals(body)
.iter()
.any(|literal| matches_rm_rf_target(literal, target_class))
}
fn quoted_string_literals(body: &str) -> Vec<String> {
let mut literals = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
let mut escaped = false;
for ch in body.chars() {
if escaped {
if quote.is_some() {
current.push(ch);
}
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
match quote {
Some(quote_ch) if ch == quote_ch => {
literals.push(std::mem::take(&mut current));
quote = None;
}
Some(_) => current.push(ch),
None if ch == '\'' || ch == '"' || ch == '`' => quote = Some(ch),
None => {}
}
}
literals
}
fn matches_unsafe_cleanup(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_unsafe_cleanup) {
return true;
}
shell_command_segments(command)
.iter()
.any(|segment| unsafe_cleanup_segment_matches(segment))
}
fn unsafe_cleanup_segment_matches(segment: &[String]) -> bool {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_unsafe_cleanup(shell_body);
}
let Some(command_name) = segment
.get(command_index)
.map(|word| command_basename(word))
else {
return false;
};
match command_name {
"find" => find_segment_deletes(segment, command_index),
"xargs" => segment_tail_invokes_file_deletion(segment, command_index + 1),
_ => false,
}
}
fn find_segment_deletes(segment: &[String], command_index: usize) -> bool {
let mut index = command_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if word == "-delete" {
return true;
}
if word == "-exec" || word == "-execdir" {
return segment_tail_invokes_file_deletion(segment, index + 1);
}
index += 1;
}
false
}
fn segment_tail_invokes_file_deletion(segment: &[String], start_index: usize) -> bool {
for index in start_index..segment.len() {
let Some(word) = segment.get(index) else {
continue;
};
if command_basename(word) == "rm" {
return true;
}
if let Some(shell_body) = shell_c_argument(segment, index) {
if matches_file_deletion(shell_body) || matches_unsafe_cleanup(shell_body) {
return true;
}
}
if inline_interpreter_body(segment, index).is_some_and(script_body_mentions_file_deletion) {
return true;
}
}
false
}
fn looks_like_env_assignment(word: &str) -> bool {
let Some((name, _)) = word.split_once('=') else {
return false;
};
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
fn rm_option_has_recursive(option: &str) -> bool {
if option.starts_with("--") {
option == "--recursive"
} else {
option
.get(1..)
.unwrap_or("")
.chars()
.any(|ch| matches!(ch, 'r' | 'R'))
}
}
fn rm_option_has_force(option: &str) -> bool {
if option.starts_with("--") {
option == "--force"
} else {
option.get(1..).unwrap_or("").chars().any(|ch| ch == 'f')
}
}
fn rm_target_matches_class(target: &str, target_class: RmTargetClass) -> bool {
match target_class {
RmTargetClass::Absolute => target.starts_with('/'),
RmTargetClass::Home => target.starts_with('~'),
}
}
fn matches_kubectl_mass_delete(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_kubectl_mass_delete) {
return true;
}
shell_command_segments(command).iter().any(|segment| {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_kubectl_mass_delete(shell_body);
}
if segment
.get(command_index)
.is_none_or(|word| command_basename(word) != "kubectl")
{
return false;
}
let args = &segment[command_index + 1..];
args.iter().any(|arg| arg == "delete")
&& args.iter().any(|arg| kubectl_flag_is_truthy(arg, "--all"))
&& args
.iter()
.any(|arg| arg == "-A" || kubectl_flag_is_truthy(arg, "--all-namespaces"))
})
}
fn kubectl_flag_is_truthy(arg: &str, flag: &str) -> bool {
if arg == flag {
return true;
}
if let Some(value) = arg
.strip_prefix(flag)
.and_then(|rest| rest.strip_prefix('='))
{
return matches!(
value.to_ascii_lowercase().as_str(),
"true" | "1" | "yes" | "y" | "on" | "t"
);
}
false
}
pub(crate) fn matches_drop_table_sql(command: &str) -> bool {
let lowered = command.to_ascii_lowercase();
let bytes = lowered.as_bytes();
let mut index = 0;
while index < bytes.len() {
if sql_guard_keyword_at(bytes, index, b"drop")
&& skip_sql_guard_keyword_gap(bytes, index + b"drop".len())
.is_some_and(|table_index| sql_guard_keyword_at(bytes, table_index, b"table"))
{
return true;
}
index += 1;
}
false
}
fn sql_guard_keyword_at(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
bytes
.get(index..index.saturating_add(keyword.len()))
.is_some_and(|candidate| candidate == keyword)
&& index
.checked_sub(1)
.and_then(|previous| bytes.get(previous))
.is_none_or(|byte| !sql_guard_word_byte(*byte))
&& bytes
.get(index + keyword.len())
.is_none_or(|byte| !sql_guard_word_byte(*byte))
}
fn sql_guard_word_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
fn skip_sql_guard_keyword_gap(bytes: &[u8], mut index: usize) -> Option<usize> {
let mut consumed = false;
loop {
let before = index;
while bytes.get(index).is_some_and(u8::is_ascii_whitespace) {
index += 1;
consumed = true;
}
if bytes.get(index) == Some(&b'/') && bytes.get(index + 1) == Some(&b'*') {
index += 2;
consumed = true;
let mut closed = false;
while index + 1 < bytes.len() {
if bytes[index] == b'*' && bytes[index + 1] == b'/' {
index += 2;
closed = true;
break;
}
index += 1;
}
if !closed {
return None;
}
continue;
}
if bytes.get(index) == Some(&b'-') && bytes.get(index + 1) == Some(&b'-') {
index += 2;
consumed = true;
while bytes
.get(index)
.is_some_and(|byte| !matches!(*byte, b'\n' | b'\r'))
{
index += 1;
}
continue;
}
if index == before {
break;
}
}
consumed.then_some(index)
}
fn matches_terraform_destroy(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_terraform_destroy) {
return true;
}
shell_command_segments(command).iter().any(|segment| {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_terraform_destroy(shell_body);
}
segment
.get(command_index)
.is_some_and(|word| command_basename(word) == "terraform")
&& segment
.iter()
.skip(command_index + 1)
.any(|arg| arg == "destroy")
})
}
fn matches_raw_block_device_write(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_raw_block_device_write) {
return true;
}
shell_command_segments(command).iter().any(|segment| {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_raw_block_device_write(shell_body);
}
segment
.get(command_index)
.is_some_and(|word| command_basename(word) == "dd")
&& segment
.iter()
.skip(command_index + 1)
.filter_map(|arg| arg.strip_prefix("of="))
.any(is_block_device_path)
})
}
fn matches_filesystem_create(command: &str) -> bool {
if command_contains_active_command_substitution(command, matches_filesystem_create) {
return true;
}
shell_command_segments(command).iter().any(|segment| {
let Some(command_index) = shell_segment_command_index(segment) else {
return false;
};
if let Some(shell_body) = shell_c_argument(segment, command_index) {
return matches_filesystem_create(shell_body);
}
let Some(command_name) = segment.get(command_index) else {
return false;
};
let command_name = command_basename(command_name);
let mkfs_command = command_name == "mkfs"
|| command_name.starts_with("mkfs.")
|| matches!(command_name, "mke2fs" | "mkfs_ext4");
mkfs_command
&& segment
.iter()
.skip(command_index + 1)
.any(|arg| is_block_device_path(arg))
})
}
fn is_block_device_path(path: &str) -> bool {
path.starts_with("/dev/sd")
|| path.starts_with("/dev/xvd")
|| path.starts_with("/dev/vd")
|| path.starts_with("/dev/nvme")
|| path.starts_with("/dev/disk")
|| path.starts_with("/dev/rdisk")
}
fn shell_command_segments(command: &str) -> Vec<Vec<String>> {
expand_env_split_string_segments(parse_shell_command_segments(command), 0)
}
fn parse_shell_command_segments(command: &str) -> Vec<Vec<String>> {
let mut segments = Vec::new();
let mut current_segment = Vec::new();
let mut current_word = String::new();
let mut quote: Option<char> = None;
let mut escaped = false;
for ch in command.chars() {
if escaped {
current_word.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if let Some(quote_ch) = quote {
if ch == quote_ch {
quote = None;
} else {
current_word.push(ch);
}
continue;
}
match ch {
'\'' | '"' => quote = Some(ch),
';' | '|' | '&' | '(' | ')' => {
finish_shell_word(&mut current_word, &mut current_segment);
finish_shell_segment(&mut current_segment, &mut segments);
}
ch if ch.is_whitespace() => finish_shell_word(&mut current_word, &mut current_segment),
_ => current_word.push(ch),
}
}
if escaped {
current_word.push('\\');
}
finish_shell_word(&mut current_word, &mut current_segment);
finish_shell_segment(&mut current_segment, &mut segments);
segments
}
const MAX_ENV_SPLIT_STRING_DEPTH: usize = 4;
fn expand_env_split_string_segments(segments: Vec<Vec<String>>, depth: usize) -> Vec<Vec<String>> {
let mut expanded = Vec::new();
for segment in segments {
let split_bodies = env_split_string_bodies(&segment);
expanded.push(segment);
if depth >= MAX_ENV_SPLIT_STRING_DEPTH {
continue;
}
for body in split_bodies {
expanded.extend(expand_env_split_string_segments(
parse_shell_command_segments(&body),
depth + 1,
));
}
}
expanded
}
fn env_split_string_bodies(segment: &[String]) -> Vec<String> {
let Some(env_index) = env_command_index(segment) else {
return Vec::new();
};
let mut bodies = Vec::new();
let mut index = env_index + 1;
while index < segment.len() {
let word = segment[index].as_str();
if word == "--" {
break;
}
if looks_like_env_assignment(word) {
index += 1;
continue;
}
if word == "-S" || word == "--split-string" {
if let Some(body) = segment.get(index + 1) {
bodies.push(env_split_body_with_trailing(
body,
segment.get(index + 2..).unwrap_or_default(),
));
}
index += 2;
continue;
}
if let Some(body) = word.strip_prefix("--split-string=") {
bodies.push(env_split_body_with_trailing(
body,
segment.get(index + 1..).unwrap_or_default(),
));
index += 1;
continue;
}
if let Some(body) = word.strip_prefix("-S") {
if !body.is_empty() {
bodies.push(env_split_body_with_trailing(
body,
segment.get(index + 1..).unwrap_or_default(),
));
index += 1;
continue;
}
}
if env_option_takes_value(word) {
index += 2;
continue;
}
if env_option_is_value_form(word) || env_option_is_flag(word) {
index += 1;
continue;
}
break;
}
bodies
}
fn env_split_body_with_trailing(body: &str, trailing: &[String]) -> String {
if trailing.is_empty() {
return body.to_owned();
}
let mut combined = body.to_owned();
for word in trailing {
combined.push(' ');
combined.push_str(word);
}
combined
}
fn env_command_index(segment: &[String]) -> Option<usize> {
let mut index = 0;
while index < segment.len() {
let word = &segment[index];
if command_basename(word) == "sudo" {
index = sudo_wrapped_command_index(segment, index + 1)?;
continue;
}
if word == "command" || word == "builtin" {
index += 1;
continue;
}
if looks_like_env_assignment(word) {
index += 1;
continue;
}
if command_basename(word) == "env" {
return Some(index);
}
return None;
}
None
}
fn finish_shell_word(current_word: &mut String, current_segment: &mut Vec<String>) {
if !current_word.is_empty() {
current_segment.push(std::mem::take(current_word));
}
}
fn finish_shell_segment(current_segment: &mut Vec<String>, segments: &mut Vec<Vec<String>>) {
if !current_segment.is_empty() {
segments.push(std::mem::take(current_segment));
}
}
fn parse_workspace_rules(
body: &str,
source_label: &str,
) -> Result<Vec<PreflightGuardRule>, DomainError> {
let document = body
.parse::<DocumentMut>()
.map_err(|error| DomainError::Usage {
message: format!("Failed to parse {source_label}: {error}"),
repair: Some(format!(
"Fix the TOML syntax in {source_label} or delete the file."
)),
})?;
let Some(rules_item) = document.get("rules") else {
return Ok(Vec::new());
};
let array = rules_item
.as_array_of_tables()
.ok_or_else(|| DomainError::Usage {
message: format!(
"{source_label}: expected `[[rules]]` array of tables, got {kind}",
kind = describe_item(rules_item)
),
repair: Some(
"Use TOML array-of-tables syntax: each rule starts with `[[rules]]`.".to_owned(),
),
})?;
let mut rules = Vec::with_capacity(array.len());
for (index, table) in array.iter().enumerate() {
let id = table
.get("id")
.and_then(Item::as_str)
.ok_or_else(|| DomainError::Usage {
message: format!("{source_label}: rule[{index}] missing string `id`"),
repair: Some("Add an `id = \"...\"` field to each [[rules]] entry.".to_owned()),
})?;
let pattern =
table
.get("pattern")
.and_then(Item::as_str)
.ok_or_else(|| DomainError::Usage {
message: format!("{source_label}: rule[{index}] missing string `pattern`"),
repair: Some(
"Add a `pattern = \"...\"` glob field to each [[rules]] entry.".to_owned(),
),
})?;
let action_str = table.get("action").and_then(Item::as_str).unwrap_or("warn");
let action = match action_str {
"warn" => GuardAction::Warn,
"halt" => GuardAction::Halt,
other => {
return Err(DomainError::Usage {
message: format!("{source_label}: rule[{index}] has invalid action `{other}`"),
repair: Some("Use `action = \"warn\"` or `action = \"halt\"`.".to_owned()),
});
}
};
let message = table
.get("message")
.and_then(Item::as_str)
.unwrap_or(pattern)
.to_owned();
rules.push(PreflightGuardRule {
id: id.to_owned(),
pattern: pattern.to_owned(),
action,
message,
source: RuleSource::WorkspaceFile {
path: source_label.to_owned(),
},
});
}
Ok(rules)
}
fn describe_item(item: &Item) -> &'static str {
if item.is_table() {
"table"
} else if item.is_value() {
"value"
} else if item.is_array_of_tables() {
"array_of_tables"
} else if item.is_none() {
"none"
} else {
"other"
}
}
fn builtin_rules() -> Vec<PreflightGuardRule> {
vec![
PreflightGuardRule {
id: "builtin:rm_rf_root".to_owned(),
pattern: "*rm -rf /*".to_owned(),
action: GuardAction::Halt,
message: "rm -rf targeting filesystem root is forbidden by AGENTS.md (\"Irreversible Git & Filesystem Actions\").".to_owned(),
source: RuleSource::Builtin { name: "rm_rf_root".to_owned() },
},
PreflightGuardRule {
id: "builtin:rm_rf_home".to_owned(),
pattern: "*rm -rf ~*".to_owned(),
action: GuardAction::Halt,
message: "rm -rf targeting $HOME is forbidden by AGENTS.md.".to_owned(),
source: RuleSource::Builtin { name: "rm_rf_home".to_owned() },
},
PreflightGuardRule {
id: "builtin:file_deletion".to_owned(),
pattern: "*".to_owned(),
action: GuardAction::Halt,
message: "Deleting files or folders requires explicit written user permission under AGENTS.md RULE NUMBER 1.".to_owned(),
source: RuleSource::Builtin { name: "file_deletion".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_reset_hard".to_owned(),
pattern: "*git reset --hard*".to_owned(),
action: GuardAction::Halt,
message: "git reset --hard is on the AGENTS.md absolutely-forbidden list. Use git diff and ask the user before any rollback.".to_owned(),
source: RuleSource::Builtin { name: "git_reset_hard".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_clean_fd".to_owned(),
pattern: "*git clean*".to_owned(),
action: GuardAction::Halt,
message: "git clean with force deletes untracked work and is forbidden by AGENTS.md.".to_owned(),
source: RuleSource::Builtin { name: "git_clean_fd".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_worktree_add".to_owned(),
pattern: "*git worktree add*".to_owned(),
action: GuardAction::Halt,
message: "git worktree add is forbidden by AGENTS.md RULE 2 (\"NO WORKTREES. EVER.\").".to_owned(),
source: RuleSource::Builtin { name: "git_worktree_add".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_stash".to_owned(),
pattern: "*git stash*".to_owned(),
action: GuardAction::Halt,
message: "git stash is forbidden by AGENTS.md because stashed work is easily lost in multi-agent sessions.".to_owned(),
source: RuleSource::Builtin { name: "git_stash".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_rebase".to_owned(),
pattern: "*git rebase*".to_owned(),
action: GuardAction::Halt,
message: "git rebase is forbidden by AGENTS.md; keep work on main without rewriting history.".to_owned(),
source: RuleSource::Builtin { name: "git_rebase".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_checkout_off_main".to_owned(),
pattern: "*git checkout*".to_owned(),
action: GuardAction::Halt,
message: "git checkout away from main or checkout-based file overwrite is forbidden by AGENTS.md.".to_owned(),
source: RuleSource::Builtin { name: "git_checkout_off_main".to_owned() },
},
PreflightGuardRule {
id: "builtin:script_code_rewrite".to_owned(),
pattern: "*".to_owned(),
action: GuardAction::Halt,
message: "Script-based in-place code rewrites are forbidden by AGENTS.md; edit source files manually.".to_owned(),
source: RuleSource::Builtin { name: "script_code_rewrite".to_owned() },
},
PreflightGuardRule {
id: "builtin:unsafe_cleanup".to_owned(),
pattern: "*".to_owned(),
action: GuardAction::Halt,
message: "Unsafe cleanup commands that delete selected files require explicit human approval before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "unsafe_cleanup".to_owned() },
},
PreflightGuardRule {
id: "builtin:git_push_force".to_owned(),
pattern: "*git push*--force*".to_owned(),
action: GuardAction::Warn,
message: "git push --force overwrites upstream history; ensure you have explicit user authorization (AGENTS.md \"Executing actions with care\").".to_owned(),
source: RuleSource::Builtin { name: "git_push_force".to_owned() },
},
PreflightGuardRule {
id: "builtin:kubectl_mass_delete".to_owned(),
pattern: "*kubectl delete*--all*".to_owned(),
action: GuardAction::Halt,
message: "kubectl mass deletion across namespaces can remove live workloads; require explicit approval before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "kubectl_mass_delete".to_owned() },
},
PreflightGuardRule {
id: "builtin:drop_table_sql".to_owned(),
pattern: "*DROP TABLE*".to_owned(),
action: GuardAction::Halt,
message: "DROP TABLE is destructive database DDL; require explicit approval and backup evidence before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "drop_table_sql".to_owned() },
},
PreflightGuardRule {
id: "builtin:terraform_destroy".to_owned(),
pattern: "*terraform destroy*".to_owned(),
action: GuardAction::Halt,
message: "terraform destroy tears down infrastructure and requires explicit approval before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "terraform_destroy".to_owned() },
},
PreflightGuardRule {
id: "builtin:raw_block_device_write".to_owned(),
pattern: "*dd *of=/dev/*".to_owned(),
action: GuardAction::Halt,
message: "Writing raw bytes to a block device is destructive and requires explicit approval before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "raw_block_device_write".to_owned() },
},
PreflightGuardRule {
id: "builtin:filesystem_create".to_owned(),
pattern: "*mkfs* /dev/*".to_owned(),
action: GuardAction::Halt,
message: "Creating a filesystem on a block device destroys existing data and requires explicit approval before proceeding.".to_owned(),
source: RuleSource::Builtin { name: "filesystem_create".to_owned() },
},
]
}
#[derive(Clone, Debug)]
pub struct PreflightGuardOptions {
pub command: String,
pub workspace: PathBuf,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct GuardMatch {
pub rule_id: String,
pub pattern: String,
pub action: GuardAction,
pub message: String,
pub source: RuleSource,
pub resolution: MatchResolution,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreflightMemoryMatch {
pub memory_id: String,
pub kind: String,
pub content: String,
pub provenance_uri: Option<String>,
pub severity: &'static str,
pub severity_source: &'static str,
pub score: f64,
pub matched_terms: Vec<String>,
}
impl Serialize for PreflightMemoryMatch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let content = crate::policy::redact_secret_like_content(&self.content).content;
let provenance_uri = self
.provenance_uri
.as_ref()
.map(|uri| crate::policy::redact_secret_like_content(uri).content);
let mut state = serializer.serialize_struct("PreflightMemoryMatch", 8)?;
state.serialize_field("memoryId", &self.memory_id)?;
state.serialize_field("kind", &self.kind)?;
state.serialize_field("content", &content)?;
state.serialize_field("provenanceUri", &provenance_uri)?;
state.serialize_field("severity", &self.severity)?;
state.serialize_field("severitySource", &self.severity_source)?;
state.serialize_field("score", &self.score)?;
state.serialize_field("matchedTerms", &self.matched_terms)?;
state.end()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PreflightGuardDegradation {
pub code: &'static str,
pub severity: &'static str,
pub message: String,
pub repair: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchResolution {
Matched,
}
impl MatchResolution {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Matched => "matched",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct PreflightGuardReport {
pub schema: String,
pub command: String,
pub matches: Vec<GuardMatch>,
pub matched_memories: Vec<PreflightMemoryMatch>,
pub degraded: Vec<PreflightGuardDegradation>,
pub exit_code: u32,
pub checked_at: String,
}
impl PreflightGuardReport {
#[must_use]
pub fn to_json(&self) -> JsonValue {
json!({
"schema": self.schema,
"command": self.command,
"exitCode": self.exit_code,
"checkedAt": self.checked_at,
"matches": self.matches.iter().map(|m| json!({
"ruleId": m.rule_id,
"pattern": m.pattern,
"action": m.action.as_str(),
"message": m.message,
"recovery": recovery_guidance_for_match(m),
"source": m.source,
"resolution": m.resolution.as_str(),
})).collect::<Vec<_>>(),
"matchedMemories": self.matched_memories,
"degraded": preflight_guard_degraded_json(&self.degraded),
})
}
#[must_use]
pub fn human_summary(&self) -> String {
let mut out = String::with_capacity(256);
out.push_str("preflight: advisory command-risk context (execution is never blocked).\n");
out.push_str(&format!(" command: {}\n", self.command));
for m in &self.matches {
out.push_str(&format!(
" - [{action} | {resolution}] {id}: {message}\n",
action = m.action.as_str(),
resolution = m.resolution.as_str(),
id = m.rule_id,
message = m.message,
));
}
if !self.matched_memories.is_empty() {
out.push_str("\nMatched memories:\n");
for memory in &self.matched_memories {
out.push_str(&format!(
" - [{} | {} | {:.4}] {}\n",
memory.kind, memory.severity, memory.score, memory.memory_id
));
}
}
for degraded in &self.degraded {
out.push_str(&format!(
"\nDegraded: {} ({})\nNext: {}\n",
degraded.message, degraded.code, degraded.repair
));
}
out
}
}
fn recovery_guidance_for_match(matched: &GuardMatch) -> &'static str {
match matched.rule_id.as_str() {
"builtin:script_code_rewrite" => {
"Make source edits manually in the affected files; do not use script-based rewrites."
}
"builtin:file_deletion"
| "builtin:rm_rf_root"
| "builtin:rm_rf_home"
| "builtin:git_clean_fd"
| "builtin:unsafe_cleanup" => {
"Stop and obtain explicit written user approval for the exact deletion or cleanup command before proceeding."
}
"builtin:git_worktree_add"
| "builtin:git_checkout_off_main"
| "builtin:git_stash"
| "builtin:git_rebase"
| "builtin:git_reset_hard" => {
"Stay on main, inspect with git status or git diff, and coordinate instead of rewriting or hiding work."
}
"builtin:git_push_force" => {
"Proceed only with explicit user authorization for the exact history rewrite."
}
_ if matched.action == GuardAction::Halt => {
"Stop and obtain explicit human approval for the exact command before proceeding."
}
_ => "Review the warning and confirm the command is intentional before proceeding.",
}
}
#[must_use]
pub fn classify_repair_command_for_preflight(command: &str) -> RepairCommandPreflightAssessment {
classify_repair_action_for_preflight(RecoveryKind::Command, Some(command))
}
#[must_use]
pub fn classify_repair_action_for_preflight(
kind: RecoveryKind,
command: Option<&str>,
) -> RepairCommandPreflightAssessment {
let safety = repair_action_safety(kind, command);
let (next_action, rule_id, reason_code) = repair_command_preflight_policy(
safety.risk_class,
safety.preflight_command.is_some(),
safety.requires_human_approval,
);
RepairCommandPreflightAssessment {
command: command.map(str::to_owned),
risk_class: safety.risk_class.as_str(),
preflight_command: safety.preflight_command,
requires_human_approval: safety.requires_human_approval,
mutates_external_state: safety.mutates_external_state,
mutates_tracker_state: safety.mutates_tracker_state,
privacy_class: safety.privacy_class,
next_action,
rule_id,
source: "repair_action_safety",
reason_code,
evidence: safety.evidence,
preconditions: safety.preconditions,
}
}
fn repair_command_preflight_policy(
risk_class: RepairActionRiskClass,
has_preflight_command: bool,
requires_human_approval: bool,
) -> (RepairCommandNextAction, &'static str, &'static str) {
match risk_class {
RepairActionRiskClass::ReadOnlyProbe => (
RepairCommandNextAction::RunDirectly,
"repair_safety:read_only_probe",
"read_only_probe_command",
),
RepairActionRiskClass::IdempotentRefresh => (
if requires_human_approval {
RepairCommandNextAction::AskHuman
} else if has_preflight_command {
RepairCommandNextAction::RunPreflightFirst
} else {
RepairCommandNextAction::RunDirectly
},
"repair_safety:idempotent_refresh",
"idempotent_refresh_command",
),
RepairActionRiskClass::MutatingLocalRepair => (
if requires_human_approval {
RepairCommandNextAction::AskHuman
} else if has_preflight_command {
RepairCommandNextAction::RunPreflightFirst
} else {
RepairCommandNextAction::RunDirectly
},
"repair_safety:mutating_local_repair",
"mutating_local_repair_command",
),
RepairActionRiskClass::MutatingExternalCoordinationRepair => (
if requires_human_approval {
RepairCommandNextAction::AskHuman
} else {
RepairCommandNextAction::CoordinateFirst
},
"repair_safety:mutating_external_coordination_repair",
"external_coordination_repair_command",
),
RepairActionRiskClass::ApprovalRequiredRepair => (
RepairCommandNextAction::AskHuman,
"repair_safety:approval_required_repair",
"approval_required_repair_command",
),
RepairActionRiskClass::DestructiveOrIrreversibleRepair => (
RepairCommandNextAction::AskHuman,
"repair_safety:destructive_or_irreversible_repair",
"destructive_or_irreversible_repair_command",
),
RepairActionRiskClass::UnavailableOrManualOnly => (
RepairCommandNextAction::ManualOnly,
"repair_safety:unavailable_or_manual_only",
"manual_only_repair",
),
}
}
#[must_use]
pub fn run_preflight_guard(
registry: &PreflightGuardRegistry,
options: &PreflightGuardOptions,
) -> PreflightGuardReport {
let started = Instant::now();
trace_trauma_guard_preflight(&options.workspace, "input", 0, &[]);
let checked_at = chrono::Utc::now().to_rfc3339();
let matches = registry.match_command(&options.command);
let mut report_matches = Vec::with_capacity(matches.len());
for matched in matches {
report_matches.push(GuardMatch {
rule_id: matched.id.clone(),
pattern: matched.pattern.clone(),
action: matched.action,
message: matched.message.clone(),
source: matched.source.clone(),
resolution: MatchResolution::Matched,
});
}
let report = PreflightGuardReport {
schema: PREFLIGHT_GUARD_SCHEMA_V1.to_owned(),
command: options.command.clone(),
exit_code: 0,
checked_at,
matches: report_matches,
matched_memories: Vec::new(),
degraded: registry.load_degraded.clone(),
};
let degraded_codes = report
.degraded
.iter()
.map(|degraded| degraded.code)
.collect::<Vec<_>>();
trace_trauma_guard_preflight(
&options.workspace,
"response",
elapsed_ms_since(started),
°raded_codes,
);
report
}
fn preflight_guard_degraded_json(degraded: &[PreflightGuardDegradation]) -> Vec<JsonValue> {
aggregate_degraded_entries(degraded.iter().map(|entry| {
DegradationAggregationInput::new(
"preflight_guard",
entry.code,
entry.severity,
entry.message.clone(),
entry.repair.clone(),
)
}))
.into_iter()
.map(|entry| {
json!({
"code": entry.code,
"severity": entry.severity,
"message": entry.message,
"repair": entry.repair,
"sources": entry.sources,
})
})
.collect()
}
pub fn match_trauma_guard_memories(
command: &str,
memories: &[StoredMemory],
) -> Vec<PreflightMemoryMatch> {
let command_terms = trauma_guard_command_terms(command);
if command_terms.is_empty() {
return Vec::new();
}
let mut matches = memories
.iter()
.filter(|memory| trauma_guard_memory_kind(memory.kind.as_str()))
.filter_map(|memory| trauma_guard_memory_match(memory, &command_terms))
.collect::<Vec<_>>();
matches.sort_by(|left, right| {
right
.score
.total_cmp(&left.score)
.then_with(|| left.memory_id.cmp(&right.memory_id))
});
matches
}
#[must_use]
pub fn no_risk_memories_degradation() -> PreflightGuardDegradation {
PreflightGuardDegradation {
code: NO_RISK_MEMORIES_CODE,
severity: "info",
message: "Destructive command was recognized, but no matching risk, anti-pattern, or failure memories were available.".to_owned(),
repair: "ee remember --workspace . --kind risk --severity high \"Document this destructive-command risk.\" --json".to_owned(),
}
}
#[must_use]
pub fn preflight_patterns_unavailable_degradation(
message: impl Into<String>,
) -> PreflightGuardDegradation {
PreflightGuardDegradation {
code: PREFLIGHT_PATTERNS_UNAVAILABLE_CODE,
severity: "medium",
message: message.into(),
repair: "Restore the destructive pattern catalog, then rerun ee preflight check --cmd <command> --json."
.to_owned(),
}
}
fn trauma_guard_memory_match(
memory: &StoredMemory,
command_terms: &std::collections::BTreeSet<String>,
) -> Option<PreflightMemoryMatch> {
let memory_terms = trauma_guard_text_terms(&memory.content);
let matched_terms = command_terms
.intersection(&memory_terms)
.cloned()
.collect::<Vec<_>>();
if matched_terms.is_empty() {
return None;
}
let score = matched_terms.len() as f64 / command_terms.len() as f64;
Some(PreflightMemoryMatch {
memory_id: memory.id.clone(),
kind: memory.kind.clone(),
content: memory.content.clone(),
provenance_uri: memory.provenance_uri.clone(),
severity: inferred_trauma_guard_severity(memory.kind.as_str()),
severity_source: "inferred_from_memory_kind",
score,
matched_terms,
})
}
fn trauma_guard_memory_kind(kind: &str) -> bool {
matches!(kind, "risk" | "anti-pattern" | "failure")
}
fn inferred_trauma_guard_severity(kind: &str) -> &'static str {
match kind {
"risk" | "anti-pattern" => "high",
"failure" => "medium",
_ => "info",
}
}
fn trauma_guard_command_terms(command: &str) -> std::collections::BTreeSet<String> {
let mut terms = trauma_guard_text_terms(command);
let lower = command.to_ascii_lowercase();
if lower.contains("rm") {
terms.extend(
["delete", "remove", "recursive"]
.into_iter()
.map(str::to_owned),
);
}
if lower.contains("git reset") {
terms.extend(["reset", "hard"].into_iter().map(str::to_owned));
}
if lower.contains("git clean") {
terms.extend(["clean", "delete"].into_iter().map(str::to_owned));
}
if lower.contains("push") && lower.contains("force") {
terms.extend(["push", "force", "history"].into_iter().map(str::to_owned));
}
terms
}
fn trauma_guard_text_terms(text: &str) -> std::collections::BTreeSet<String> {
text.split(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '-')
.map(str::trim)
.filter(|term| term.len() >= 2)
.map(str::to_ascii_lowercase)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn registry_with_only(rules: Vec<PreflightGuardRule>) -> PreflightGuardRegistry {
let mut registry = PreflightGuardRegistry::new();
registry.set_rules(rules);
registry
}
fn rule(id: &str, pattern: &str, action: GuardAction) -> PreflightGuardRule {
PreflightGuardRule {
id: id.to_owned(),
pattern: pattern.to_owned(),
action,
message: format!("test rule {id}"),
source: RuleSource::Builtin {
name: id.to_owned(),
},
}
}
fn opts(command: &str) -> PreflightGuardOptions {
PreflightGuardOptions {
command: command.to_owned(),
workspace: PathBuf::from("."),
}
}
#[test]
fn no_match_yields_exit_zero() {
let registry = registry_with_only(vec![rule("r1", "*rm -rf /*", GuardAction::Halt)]);
let report = run_preflight_guard(®istry, &opts("ls -la"));
assert_eq!(report.exit_code, 0);
assert!(report.matches.is_empty());
}
#[test]
fn legacy_halt_match_is_advisory() {
let registry = registry_with_only(vec![rule("r1", "*rm -rf /*", GuardAction::Halt)]);
let report = run_preflight_guard(®istry, &opts("rm -rf /tmp/foo"));
assert_eq!(report.exit_code, 0);
assert_eq!(report.matches.len(), 1);
assert_eq!(report.matches[0].rule_id, "r1");
assert_eq!(report.matches[0].action, GuardAction::Halt);
assert_eq!(report.matches[0].resolution, MatchResolution::Matched);
}
#[test]
fn multiple_rules_are_all_cited_without_blocking() {
let registry = registry_with_only(vec![
rule("r1", "*rm -rf*", GuardAction::Halt),
rule("r2", "*--no-verify*", GuardAction::Warn),
]);
let report =
run_preflight_guard(®istry, &opts("git commit --no-verify -m 'rm -rf old'"));
let ids: Vec<_> = report.matches.iter().map(|m| m.rule_id.as_str()).collect();
assert_eq!(ids, vec!["r1", "r2"]);
assert_eq!(report.exit_code, 0);
}
#[test]
fn warn_only_match_does_not_halt() {
let registry = registry_with_only(vec![rule("warn1", "*--no-verify*", GuardAction::Warn)]);
let report = run_preflight_guard(®istry, &opts("git commit --no-verify"));
assert_eq!(report.exit_code, 0);
assert_eq!(report.matches.len(), 1);
assert_eq!(report.matches[0].action, GuardAction::Warn);
}
#[test]
fn repair_preflight_classifier_marks_read_only_probes_runnable() {
let assessment = classify_repair_command_for_preflight("br sync --status");
assert_eq!(assessment.risk_class, "read_only_probe");
assert_eq!(assessment.next_action, RepairCommandNextAction::RunDirectly);
assert_eq!(assessment.rule_id, "repair_safety:read_only_probe");
assert_eq!(assessment.source, "repair_action_safety");
assert_eq!(assessment.reason_code, "read_only_probe_command");
assert!(!assessment.requires_human_approval);
assert!(!assessment.mutates_external_state);
assert!(!assessment.mutates_tracker_state);
assert!(assessment.preflight_command.is_none());
}
#[test]
fn repair_preflight_classifier_coordinates_tracker_mutations() {
let assessment = classify_repair_command_for_preflight("br sync --flush-only");
assert_eq!(
assessment.risk_class,
"mutating_external_coordination_repair"
);
assert_eq!(
assessment.next_action,
RepairCommandNextAction::CoordinateFirst
);
assert_eq!(
assessment.rule_id,
"repair_safety:mutating_external_coordination_repair"
);
assert!(assessment.mutates_external_state);
assert!(assessment.mutates_tracker_state);
assert!(assessment.preflight_command.is_some());
}
#[test]
fn repair_preflight_classifier_requires_human_for_agent_mail_repair() {
let assessment = classify_repair_command_for_preflight("am doctor repair --yes");
assert_eq!(
assessment.risk_class,
"mutating_external_coordination_repair"
);
assert_eq!(assessment.next_action, RepairCommandNextAction::AskHuman);
assert!(assessment.requires_human_approval);
assert!(assessment.mutates_external_state);
assert!(assessment.preflight_command.is_some());
assert_eq!(assessment.privacy_class, "bounded_command_no_raw_state");
}
#[test]
fn repair_preflight_classifier_advises_human_review_for_destructive_repairs() {
let removal_command = format!("{} {}", "rm", "-rf target");
let assessment = classify_repair_command_for_preflight(&removal_command);
assert_eq!(assessment.risk_class, "destructive_or_irreversible_repair");
assert_eq!(assessment.next_action, RepairCommandNextAction::AskHuman);
assert_eq!(
assessment.rule_id,
"repair_safety:destructive_or_irreversible_repair"
);
assert!(assessment.requires_human_approval);
assert!(assessment.preflight_command.is_some());
}
#[test]
fn repair_preflight_classifier_marks_manual_only_repairs() {
let assessment = classify_repair_action_for_preflight(RecoveryKind::None, None);
assert_eq!(assessment.risk_class, "unavailable_or_manual_only");
assert_eq!(assessment.next_action, RepairCommandNextAction::ManualOnly);
assert_eq!(
assessment.rule_id,
"repair_safety:unavailable_or_manual_only"
);
assert!(assessment.requires_human_approval);
assert!(assessment.command.is_none());
assert!(assessment.preflight_command.is_none());
}
#[test]
fn workspace_toml_layered_after_builtins() {
let toml = r#"
[[rules]]
id = "ws_curl_pipe"
pattern = "*curl*|*sh*"
action = "halt"
message = "Reject curl|sh installers per workspace policy."
"#;
let registry_result = PreflightGuardRegistry::from_toml(toml, "test.toml");
assert!(
registry_result.is_ok(),
"parse should succeed: {registry_result:?}"
);
let registry = if let Ok(registry) = registry_result {
registry
} else {
PreflightGuardRegistry::new()
};
let report = run_preflight_guard(
®istry,
&opts("curl https://example.com/install.sh | sh -"),
);
assert_eq!(report.exit_code, 0);
assert_eq!(report.matches[0].rule_id, "ws_curl_pipe");
assert_eq!(
&report.matches[0].source,
&RuleSource::WorkspaceFile {
path: "test.toml".to_owned()
}
);
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
#[test]
fn workspace_rules_load_through_symlinked_workspace_root() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
let real_workspace = tempdir.path().join("real-workspace");
let ee_dir = real_workspace.join(".ee");
std::fs::create_dir_all(&ee_dir).map_err(|error| error.to_string())?;
std::fs::write(
ee_dir.join("preflight_rules.toml"),
r#"
[[rules]]
id = "workspace_alias_rule"
pattern = "*alias-risk*"
action = "warn"
message = "Workspace alias risk context."
"#,
)
.map_err(|error| error.to_string())?;
let workspace_alias = tempdir.path().join("workspace-alias");
std::os::unix::fs::symlink(&real_workspace, &workspace_alias)
.map_err(|error| error.to_string())?;
let registry =
PreflightGuardRegistry::load(&workspace_alias).map_err(|error| error.to_string())?;
assert!(
registry
.rules()
.iter()
.any(|rule| rule.id == "workspace_alias_rule"),
"an explicitly selected workspace alias should load its in-workspace rules: {:?}",
registry.load_degradations(),
);
Ok(())
}
#[test]
fn workspace_rules_not_directory_path_is_treated_as_absent() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
std::fs::write(tempdir.path().join(".ee"), "not a metadata directory\n")
.map_err(|error| error.to_string())?;
let registry =
PreflightGuardRegistry::load(tempdir.path()).map_err(|error| error.to_string())?;
let mut options = opts("echo ok");
options.workspace = tempdir.path().to_path_buf();
let report = run_preflight_guard(®istry, &options);
assert_eq!(report.exit_code, 0);
assert!(report.matches.is_empty());
Ok(())
}
#[test]
fn malformed_workspace_rules_preserve_builtin_advisories() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
let ee_dir = tempdir.path().join(".ee");
std::fs::create_dir(&ee_dir).map_err(|error| error.to_string())?;
std::fs::write(
ee_dir.join("preflight_rules.toml"),
r#"
[[rules]]
id = "bad_action"
pattern = "*rm -rf*"
action = "explode"
"#,
)
.map_err(|error| error.to_string())?;
let registry =
PreflightGuardRegistry::load(tempdir.path()).map_err(|error| error.to_string())?;
assert_eq!(
registry.rules().len(),
PreflightGuardRegistry::with_builtins().rules().len(),
"malformed workspace rules must fall back to built-ins only",
);
assert_eq!(
registry
.load_degradations()
.first()
.map(|degradation| degradation.code),
Some(PREFLIGHT_PATTERNS_UNAVAILABLE_CODE),
);
let mut options = opts("rm -rf /tmp/preflight-guard-fixture");
options.workspace = tempdir.path().to_path_buf();
let report = run_preflight_guard(®istry, &options);
assert_eq!(report.exit_code, 0);
assert!(
report
.degraded
.iter()
.any(|degradation| degradation.code == PREFLIGHT_PATTERNS_UNAVAILABLE_CODE),
"report should carry workspace-rule load degradation: {:?}",
report.degraded,
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:file_deletion"),
"built-in deletion risk context should still be reported: {:?}",
report.matches,
);
Ok(())
}
#[test]
fn preflight_rules_oversize_load_preserves_builtins_with_degradation() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
let ee_dir = tempdir.path().join(".ee");
std::fs::create_dir(&ee_dir).map_err(|error| error.to_string())?;
let rules_path = ee_dir.join("preflight_rules.toml");
let cap = usize::try_from(PREFLIGHT_RULES_MAX_BYTES).map_err(|error| error.to_string())?;
let mut payload = String::with_capacity(cap + 1);
payload.push_str("# preflight rules size-cap fixture\n");
while payload.len() <= cap {
payload.push('#');
}
assert!(
payload.len() > cap,
"payload must exceed PREFLIGHT_RULES_MAX_BYTES; got {}",
payload.len(),
);
std::fs::write(&rules_path, &payload).map_err(|error| error.to_string())?;
let registry =
PreflightGuardRegistry::load(tempdir.path()).map_err(|error| error.to_string())?;
assert_eq!(
registry.rules().len(),
PreflightGuardRegistry::with_builtins().rules().len(),
"oversize workspace rules must not disable built-in destructive patterns",
);
let degradation = registry
.load_degradations()
.first()
.ok_or_else(|| "oversize workspace rules should record a degradation".to_owned())?;
assert_eq!(degradation.code, PREFLIGHT_PATTERNS_UNAVAILABLE_CODE);
assert!(
degradation.message.contains("ceiling")
|| degradation
.message
.contains(&PREFLIGHT_RULES_MAX_BYTES.to_string()),
"degradation must name the size ceiling; got {}",
degradation.message,
);
let mut options = opts("rm -rf /tmp/preflight-guard-fixture");
options.workspace = tempdir.path().to_path_buf();
let report = run_preflight_guard(®istry, &options);
assert_eq!(report.exit_code, 0);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:file_deletion"),
"built-in deletion guard must remain active: {:?}",
report.matches,
);
assert!(
report
.degraded
.iter()
.any(|entry| entry.code == PREFLIGHT_PATTERNS_UNAVAILABLE_CODE),
"guard report should surface pattern-load degradation: {:?}",
report.degraded,
);
Ok(())
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
#[test]
fn preflight_rules_final_read_open_rejects_symlinked_file() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
let ee_dir = tempdir.path().join(".ee");
std::fs::create_dir(&ee_dir).map_err(|error| error.to_string())?;
let rules_path = ee_dir.join("preflight_rules.toml");
let original_path = ee_dir.join("preflight_rules.toml.original");
let outside_path = tempdir.path().join("outside_rules.toml");
std::fs::write(
&rules_path,
r#"
[[rules]]
id = "safe"
pattern = "*safe*"
action = "warn"
message = "safe fixture"
"#,
)
.map_err(|error| error.to_string())?;
std::fs::write(
&outside_path,
r#"
[[rules]]
id = "outside"
pattern = "*rm -rf*"
action = "halt"
message = "outside fixture"
"#,
)
.map_err(|error| error.to_string())?;
validate_preflight_rules_path(&rules_path).map_err(|error| error.to_string())?;
std::fs::rename(&rules_path, &original_path).map_err(|error| error.to_string())?;
std::os::unix::fs::symlink(&outside_path, &rules_path)
.map_err(|error| error.to_string())?;
let error = read_preflight_rules_file_no_follow(&rules_path)
.expect_err("final-path symlink should be rejected by O_NOFOLLOW");
assert_ne!(
error.kind(),
std::io::ErrorKind::NotFound,
"final symlink read should fail because the path is a symlink"
);
assert!(
std::fs::symlink_metadata(&rules_path)
.map_err(|error| error.to_string())?
.file_type()
.is_symlink(),
"rejected preflight rules symlink should remain available for inspection"
);
assert_eq!(
std::fs::read_to_string(&outside_path).map_err(|error| error.to_string())?,
r#"
[[rules]]
id = "outside"
pattern = "*rm -rf*"
action = "halt"
message = "outside fixture"
"#
);
Ok(())
}
#[test]
fn workspace_toml_missing_id_is_usage_error() {
let toml = r#"
[[rules]]
pattern = "*foo*"
"#;
let registry_result = PreflightGuardRegistry::from_toml(toml, "bad.toml");
assert!(registry_result.is_err(), "should reject missing id");
let message = if let Err(err) = registry_result {
err.message()
} else {
String::new()
};
assert!(message.contains("missing string `id`"), "{message}");
}
#[test]
fn workspace_toml_invalid_action_is_usage_error() {
let toml = r#"
[[rules]]
id = "x"
pattern = "*foo*"
action = "explode"
"#;
let registry_result = PreflightGuardRegistry::from_toml(toml, "bad.toml");
assert!(registry_result.is_err(), "should reject unknown action");
let message = if let Err(err) = registry_result {
err.message()
} else {
String::new()
};
assert!(message.contains("invalid action `explode`"), "{message}");
}
#[test]
fn builtins_report_agents_md_forbidden_actions_without_blocking() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"rm -rf /",
"rm -rf /tmp/work",
"rm -rf ~/projects",
"rm src/lib.rs",
"unlink src/lib.rs",
"rmdir scratch",
"bash -lc 'rm test_clamp.rs'",
"git reset --hard HEAD~3",
"git -C . reset --hard HEAD~3",
"git clean -fd",
"git clean -xdf",
"git clean -f untracked.rs",
"git clean --exclude '-n' --force",
"git worktree add ../parallel main",
"git -C . worktree add ../parallel main",
"git stash push -m savepoint",
"git -C . stash push -m savepoint",
"git rebase -i origin/main",
"git -C . rebase origin/main",
"git checkout feature/other",
"git -C . checkout HEAD~1",
"bash -lc 'git checkout old-branch'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory",
);
assert!(
!report.matches.is_empty(),
"command `{command}` produced no match",
);
assert!(
report
.matches
.iter()
.any(|m| matches!(m.source, RuleSource::Builtin { .. })),
"command `{command}` did not cite a builtin rule",
);
}
}
#[test]
fn builtin_file_deletion_ignores_help_and_text_mentions() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"rm --help",
"echo rm src/lib.rs",
"rg 'rm src/lib.rs' docs src",
"bash -lc 'echo rm src/lib.rs'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != "builtin:file_deletion")
);
}
}
#[test]
fn builtin_git_clean_allows_dry_run_only() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"git clean -nd",
"git clean --dry-run -d",
"git clean -nfd",
"git clean -fdn",
"git clean --dry-run --force -d",
"git clean --force --dry-run -d",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != "builtin:git_clean_fd")
);
}
}
#[test]
fn builtin_git_checkout_allows_explicit_main_checkout_only() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"git checkout main",
"git checkout --quiet main",
"git switch main",
"git switch --quiet main",
"git pull --rebase origin main",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(report.matches.iter().all(|matched| {
matched.rule_id != "builtin:git_checkout_off_main"
&& matched.rule_id != "builtin:git_rebase"
}));
}
for command in [
"git checkout -- src/lib.rs",
"git checkout -- main",
"git checkout main -- src/lib.rs",
"git checkout main src/lib.rs",
"git checkout -",
"git checkout -b main",
"git checkout -B main",
"git checkout -b experiment",
"git checkout --detach main",
"git checkout -f main",
"git checkout -p",
"git checkout -p main",
"git checkout -pq main",
"git checkout --patch main",
"git checkout --patch -- src/lib.rs",
"git checkout --pathspec-from-file=paths.txt",
"git switch -",
"git switch feature/other",
"git switch -c experiment",
"git switch -C main",
"git switch --force main",
"git switch --discard-changes main",
"git -C . switch --detach main",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:git_checkout_off_main")
);
}
}
#[test]
fn builtin_script_rewrite_and_cleanup_rules_report_known_risky_shapes() {
let registry = PreflightGuardRegistry::with_builtins();
for (command, rule_id) in [
(
"sed -i '' 's/old/new/' src/lib.rs",
"builtin:script_code_rewrite",
),
(
"perl -pi -e 's/old/new/' src/lib.rs",
"builtin:script_code_rewrite",
),
(
"python -c 'from pathlib import Path; Path(\"src/lib.rs\").write_text(\"x\")'",
"builtin:script_code_rewrite",
),
("find src -name '*.rs' -delete", "builtin:unsafe_cleanup"),
(
"find . -name '*.tmp' -exec rm {} ;",
"builtin:unsafe_cleanup",
),
(
"find . -name '*.tmp' -exec sh -c 'rm -f \"$1\"' sh {} \\;",
"builtin:unsafe_cleanup",
),
(
"find . -name '*.tmp' -execdir bash -lc 'rm -rf \"$1\"' bash {} +",
"builtin:unsafe_cleanup",
),
(
"find . -type d -exec sh -c 'find \"$1\" -name stale -delete' sh {} \\;",
"builtin:unsafe_cleanup",
),
("rg TODO src | xargs rm", "builtin:unsafe_cleanup"),
(
"rg TODO src | xargs sh -c 'rm -f \"$@\"' sh",
"builtin:unsafe_cleanup",
),
(
"rg stale src | xargs python -c 'import os, sys; os.remove(sys.argv[1])'",
"builtin:unsafe_cleanup",
),
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == rule_id),
"command `{command}` did not cite {rule_id}: {:?}",
report.matches,
);
}
}
#[test]
fn builtin_file_deletion_rule_reports_inline_interpreter_delete_apis() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"python -c 'from pathlib import Path; Path(\"src/lib.rs\").unlink()'",
"python -c 'import os; os.remove(\"src/lib.rs\")'",
"python -c 'import shutil; shutil.rmtree(\"target/tmp\")'",
"node -e 'require(\"fs\").unlinkSync(\"src/lib.rs\")'",
"node -e 'require(\"fs\").rmSync(\"target/tmp\", {recursive: true})'",
"ruby -e 'File.delete(\"src/lib.rs\")'",
"perl -e 'unlink \"src/lib.rs\"'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:file_deletion"),
"command `{command}` did not cite file deletion guard: {:?}",
report.matches,
);
}
}
#[test]
fn destructive_builtins_recurse_through_active_command_substitution() {
let registry = PreflightGuardRegistry::with_builtins();
for (command, rule_id) in [
(
"echo \"$(sed -i '' 's/old/new/' src/lib.rs)\"",
"builtin:script_code_rewrite",
),
(
"printf '%s\\n' \"$(find src -name '*.rs' -delete)\"",
"builtin:unsafe_cleanup",
),
(
"echo \"$( (find src -name '*.rs' -delete) )\"",
"builtin:unsafe_cleanup",
),
(
"echo 'literal \\' $(find src -name '*.rs' -delete)",
"builtin:unsafe_cleanup",
),
(
"echo $(kubectl delete pods --all=true --all-namespaces=true)",
"builtin:kubectl_mass_delete",
),
(
"echo $(terraform destroy -auto-approve)",
"builtin:terraform_destroy",
),
(
"echo \"$(printf ')' ; terraform destroy -auto-approve)\"",
"builtin:terraform_destroy",
),
(
"echo \"$( (printf hi) ; terraform destroy -auto-approve)\"",
"builtin:terraform_destroy",
),
(
"echo $(dd if=/tmp/disk.img of=/dev/disk2)",
"builtin:raw_block_device_write",
),
(
"echo $(mkfs.ext4 /dev/nvme0n1)",
"builtin:filesystem_create",
),
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == rule_id),
"command `{command}` did not cite {rule_id}: {:?}",
report.matches,
);
}
}
#[test]
fn builtin_script_rewrite_and_cleanup_rules_allow_read_only_shapes() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"sed -n '1,20p' src/lib.rs",
"python -c 'print(\"src/lib.rs\")'",
"find src -name '*.rs' -print",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(report.matches.iter().all(|matched| {
matched.rule_id != "builtin:script_code_rewrite"
&& matched.rule_id != "builtin:unsafe_cleanup"
}));
}
}
#[test]
fn builtin_rm_rf_rules_require_command_position() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"git log --grep=\"rm -rf /\"",
"echo do not rm -rf / blindly",
"confirm -rf /var/cache",
"rm --force --preserve-root /var/cache",
] {
let report = run_preflight_guard(®istry, &opts(command));
if command.starts_with("rm ") {
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:file_deletion")
);
continue;
}
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(report.matches.iter().all(|matched| {
matched.rule_id != "builtin:rm_rf_root" && matched.rule_id != "builtin:rm_rf_home"
}));
}
for command in [
"cd /tmp && rm -rf /var/cache",
"sudo rm -fr /var/cache",
"sudo -n rm -rf /var/cache",
"sudo -u root rm -rf /var/cache",
"sudo -E -u root -g wheel rm -rf /var/cache",
"sudo --user root --group wheel rm -rf /var/cache",
"sudo --user=root --group=wheel rm -rf /var/cache",
"sudo --preserve-env=PATH rm -rf /var/cache",
"/usr/bin/sudo rm -rf /var/cache",
"env FOO=bar rm -r -f ~/scratch",
"env -i rm -rf /var/cache",
"env --ignore-environment rm -rf /var/cache",
"env -u PATH rm -rf /var/cache",
"env --unset=PATH rm -rf /var/cache",
"env --chdir /tmp rm -rf /var/cache",
"env FOO=bar sudo -u root rm -rf /var/cache",
"env -i sudo --user root --group wheel rm -rf /var/cache",
"env --unset=PATH sudo --preserve-env=PATH rm -rf /var/cache",
"env -- sudo -E -u root rm -rf /var/cache",
"/usr/bin/env rm -rf /var/cache",
"sudo /usr/bin/env -i rm -rf /var/cache",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(report.matches.iter().any(|matched| {
matched.rule_id == "builtin:rm_rf_root" || matched.rule_id == "builtin:rm_rf_home"
}));
}
}
#[test]
fn builtin_deletion_rules_recurse_through_indirect_execution_bypasses() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"python -c \"import os; os.system('rm -rf /')\"",
"node -e \"require('child_process').execSync('rm -rf /')\"",
"perl -e \"system('rm -rf /')\"",
"ruby -e \"system('rm -rf /')\"",
"ksh -c 'rm -rf /'",
"tcsh -c 'rm -rf /'",
"RM=rm $RM -rf /",
"env RM=rm bash -c \"$RM -rf /\"",
"env -S 'rm -rf /'",
"env --split-string='rm -rf /'",
"env -S rm -rf /",
"env --split-string rm -rf /",
"/usr/bin/env -i -S 'rm -rf /'",
"sudo /usr/bin/env --split-string='bash -c \"rm -rf /\"'",
"$(rm -rf /)",
"`rm -rf /`",
"eval 'rm -rf /'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(
report.exit_code, 0,
"command `{command}` must remain advisory"
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:rm_rf_root"),
"command `{command}` did not cite rm_rf_root: {:?}",
report.matches,
);
assert!(
report
.matches
.iter()
.any(|matched| matched.rule_id == "builtin:file_deletion"),
"command `{command}` did not cite file_deletion: {:?}",
report.matches,
);
}
}
#[test]
fn builtins_do_not_classify_cargo_as_destructive() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"cargo check --lib",
"cargo +nightly clippy --all-targets -- -D warnings",
"cargo -Z unstable-options check --all-targets",
"cargo fix --allow-dirty",
"cargo install --path .",
"cargo rustc --lib",
"env TMPDIR=/tmp cargo test --workspace --no-run",
"env -i TMPDIR=/tmp cargo test --workspace --no-run",
"env -iv cargo test --workspace --no-run",
"env --ignore-environment cargo clippy --all-targets -- -D warnings",
"env -u CARGO_HOME cargo check --all-targets",
"CARGO_TARGET_DIR=/tmp/cargo_target cargo test --workspace --no-run",
"cargo-clippy clippy --all-targets -- -D warnings",
"bash -lc 'cargo check --lib --message-format=short 2>&1 | tail -20'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != "builtin:local_cargo_heavy_verification"),
"command `{command}` was incorrectly classified as destructive: {:?}",
report.matches,
);
}
}
#[test]
fn builtins_do_not_classify_env_wrapped_cargo_as_destructive() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"env -S 'cargo test --lib preflight_guard'",
"env -Scargo test --lib preflight_guard",
"env --split-string='cargo check --all-targets'",
"env -i --split-string 'cargo clippy --all-targets -- -D warnings'",
"sudo /usr/bin/env -S 'CARGO_TARGET_DIR=/tmp/cargo_target cargo test --workspace --no-run'",
"sudo /usr/bin/env -SCARGO_TARGET_DIR=/tmp/cargo_target cargo test --workspace --no-run",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != "builtin:local_cargo_heavy_verification"),
"command `{command}` was incorrectly classified as destructive: {:?}",
report.matches,
);
}
}
#[test]
fn builtins_do_not_classify_rust_compilers_as_destructive() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"rustc src/main.rs",
"rustdoc --test src/lib.rs",
"env RCH_REQUIRE_REMOTE=1 rustc --crate-type lib src/lib.rs",
"env -i RCH_REQUIRE_REMOTE=1 rustdoc --test src/lib.rs",
"bash -lc 'rustdoc --test src/lib.rs'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != "builtin:local_rust_compiler_verification"),
"command `{command}` was incorrectly classified as destructive: {:?}",
report.matches,
);
}
}
#[test]
fn local_cargo_guard_allows_rch_wrapped_cargo_and_lightweight_commands() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"RCH_REQUIRE_REMOTE=1 rch exec -- env TMPDIR=/tmp CARGO_TARGET_DIR=/tmp/ee-rch-target cargo test --lib foo",
"RCH_REQUIRE_REMOTE=1 rch exec -- rustc src/main.rs",
"scripts/rch_verify.sh -- rustdoc --test src/lib.rs",
"./scripts/rch_verify.sh -- cargo check --all-targets",
"scripts/rch_verify.sh --bead-id bd-123 -- cargo check --all-targets",
"bash scripts/rch_verify.sh --summary -- cargo clippy --all-targets -- -D warnings",
"RCH_REQUIRE_REMOTE=1 rch exec -- cargo --target-dir /tmp/ee-rch-target test --lib foo",
"cargo metadata --no-deps --format-version 1",
"cargo fmt --check",
"rustfmt +nightly --edition 2024 --check src/core/preflight_guard.rs",
"rg 'cargo check' src docs",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(
report.matches.iter().all(|matched| {
matched.rule_id != "builtin:local_cargo_heavy_verification"
&& matched.rule_id != "builtin:local_cargo_target_dir_override"
&& matched.rule_id != "builtin:local_rust_compiler_verification"
}),
"command `{command}` unexpectedly matched local Rust verification rules: {:?}",
report.matches,
);
}
}
#[test]
fn builtins_do_not_gate_rch_verify_wrapper_payloads() {
let registry = PreflightGuardRegistry::with_builtins();
for (command, rule_id) in [
(
"rch_verify.sh -- cargo test --lib foo",
"builtin:local_cargo_heavy_verification",
),
(
"bash rch_verify.sh -- cargo check --all-targets",
"builtin:local_cargo_heavy_verification",
),
(
"/tmp/scripts/rch_verify.sh -- cargo --target-dir /tmp/ee-rch-target test --lib foo",
"builtin:local_cargo_heavy_verification",
),
(
"../scripts/rch_verify.sh -- rustdoc --test src/lib.rs",
"builtin:local_rust_compiler_verification",
),
(
"bash /tmp/scripts/rch_verify.sh -- rustc src/main.rs",
"builtin:local_rust_compiler_verification",
),
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != rule_id),
"command `{command}` was incorrectly gated by {rule_id}: {:?}",
report.matches,
);
}
let report = run_preflight_guard(
®istry,
&opts(
"/tmp/scripts/rch_verify.sh -- cargo --target-dir /tmp/ee-rch-target test --lib foo",
),
);
let ids = report
.matches
.iter()
.map(|matched| matched.rule_id.as_str())
.collect::<Vec<_>>();
assert!(!ids.contains(&"builtin:local_cargo_target_dir_override"));
}
#[test]
fn builtins_do_not_gate_bare_rch_exec_payloads() {
let registry = PreflightGuardRegistry::with_builtins();
for (command, rule_id) in [
(
"rch exec -- env TMPDIR=/tmp cargo test --lib foo",
"builtin:local_cargo_heavy_verification",
),
(
"rch --json exec -- cargo check --all-targets",
"builtin:local_cargo_heavy_verification",
),
(
"rch exec -- cargo --target-dir /tmp/ee-rch-target test --lib foo",
"builtin:local_cargo_heavy_verification",
),
(
"rch exec -- rustc src/main.rs",
"builtin:local_rust_compiler_verification",
),
(
"/Users/jemanuel/projects/remote_compilation_helper/target-local/release/rch exec -- rustdoc --test src/lib.rs",
"builtin:local_rust_compiler_verification",
),
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report
.matches
.iter()
.all(|matched| matched.rule_id != rule_id),
"command `{command}` was incorrectly gated by {rule_id}: {:?}",
report.matches,
);
}
}
#[test]
fn cargo_target_dir_is_not_a_destructive_command_rule() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"env -i CARGO_TARGET_DIR=/tmp/cargo_target cargo test --workspace --no-run",
"cargo --target-dir /tmp/cargo_target test --workspace --no-run",
"cargo +nightly --target-dir=/tmp/cargo_target test --workspace --no-run",
] {
let report = run_preflight_guard(®istry, &opts(command));
let ids = report
.matches
.iter()
.map(|matched| matched.rule_id.as_str())
.collect::<Vec<_>>();
assert!(!ids.contains(&"builtin:local_cargo_heavy_verification"));
assert!(!ids.contains(&"builtin:local_cargo_target_dir_override"));
assert_eq!(report.exit_code, 0);
}
}
#[test]
fn rust_verifier_command_substitution_is_not_a_destructive_rule() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"br comment bd-123 --message \"$(cargo test --lib foo)\"",
"br comment bd-123 --message `cargo check --lib`",
"am send --body \"$(scripts/rch_verify.sh -- cargo test --lib foo)\"",
"bash -lc 'br comment bd-123 --message \"$(rustdoc src/lib.rs)\"'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` must be advisory");
assert!(
report.matches.iter().all(|matched| {
matched.rule_id != "builtin:rust_verifier_command_substitution"
}),
"command `{command}` was incorrectly gated: {:?}",
report.matches,
);
}
}
#[test]
fn rust_verifier_command_substitution_allows_rch_wrapper_and_literal_prose() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"scripts/rch_verify.sh --bead-id bd-123 -- cargo test --lib foo",
"br comment bd-123 --message 'RCH command: `cargo test --lib foo`'",
"rg '$(cargo test --lib foo)' docs/rch_runbook.md",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should pass");
assert!(report.matches.iter().all(|matched| {
matched.rule_id != "builtin:rust_verifier_command_substitution"
}));
}
}
#[test]
fn builtin_force_push_warns_but_does_not_halt() {
let registry = PreflightGuardRegistry::with_builtins();
for command in [
"git push --force origin main",
"git -C . push -f origin main",
"bash -lc 'git push --force-with-lease origin main'",
] {
let report = run_preflight_guard(®istry, &opts(command));
assert_eq!(report.exit_code, 0, "command `{command}` should warn only");
assert_eq!(report.matches.len(), 1, "command `{command}` match count");
assert_eq!(report.matches[0].action, GuardAction::Warn);
assert_eq!(report.matches[0].rule_id, "builtin:git_push_force");
}
}
#[test]
fn json_output_uses_stable_schema() {
let registry = registry_with_only(vec![rule("r1", "*rm -rf*", GuardAction::Halt)]);
let report = run_preflight_guard(®istry, &opts("rm -rf /tmp"));
let json = report.to_json();
assert_eq!(json["schema"].as_str(), Some(PREFLIGHT_GUARD_SCHEMA_V1));
assert_eq!(json["exitCode"].as_i64(), Some(0));
let m0 = &json["matches"][0];
assert_eq!(m0["ruleId"].as_str(), Some("r1"));
assert_eq!(m0["action"].as_str(), Some("high_risk"));
assert_eq!(m0["resolution"].as_str(), Some("matched"));
assert_eq!(
m0["recovery"].as_str(),
Some(
"Stop and obtain explicit human approval for the exact command before proceeding."
)
);
}
#[test]
fn json_output_aggregates_duplicate_degraded_codes() {
let mut report =
run_preflight_guard(&PreflightGuardRegistry::with_builtins(), &opts("echo ok"));
report.degraded = vec![
PreflightGuardDegradation {
code: PREFLIGHT_PATTERNS_UNAVAILABLE_CODE,
severity: "info",
message: "First pattern catalog warning.".to_owned(),
repair: "ee preflight check --cmd \"echo ok\" --json".to_owned(),
},
PreflightGuardDegradation {
code: PREFLIGHT_PATTERNS_UNAVAILABLE_CODE,
severity: "medium",
message: "Second pattern catalog warning.".to_owned(),
repair: "Check preflight rule sources.".to_owned(),
},
];
let json = report.to_json();
let degraded = json["degraded"]
.as_array()
.expect("degraded array should be present");
assert_eq!(
degraded.len(),
1,
"expected one aggregated degradation, got {degraded:?}",
);
assert_eq!(
degraded[0]["code"].as_str(),
Some(PREFLIGHT_PATTERNS_UNAVAILABLE_CODE)
);
assert_eq!(degraded[0]["severity"].as_str(), Some("medium"));
assert_eq!(
degraded[0]["repair"].as_str(),
Some("Check preflight rule sources.")
);
assert_eq!(degraded[0]["sources"][0].as_str(), Some("preflight_guard"));
}
#[test]
fn preflight_rules_bounded_read_rejects_toctou_growth() -> Result<(), String> {
let tempdir = tempfile::tempdir().map_err(|error| error.to_string())?;
let ee_dir = tempdir.path().join(".ee");
std::fs::create_dir(&ee_dir).map_err(|error| error.to_string())?;
let rules_path = ee_dir.join("preflight_rules.toml");
let cap = usize::try_from(PREFLIGHT_RULES_MAX_BYTES).map_err(|error| error.to_string())?;
let mut payload = String::with_capacity(cap + 1);
while payload.len() <= cap {
payload.push('#');
}
std::fs::write(&rules_path, &payload).map_err(|error| error.to_string())?;
let error = read_preflight_rules_file_no_follow(&rules_path)
.expect_err("bounded read must reject CAP+1 bytes even if metadata check is bypassed");
assert_eq!(
error.kind(),
std::io::ErrorKind::InvalidData,
"bounded read TOCTOU rejection must surface InvalidData; got {error}"
);
let message = error.to_string();
assert!(
message.contains("TOCTOU"),
"rejection message must name the TOCTOU defense; got {message:?}"
);
assert!(
message.contains(&PREFLIGHT_RULES_MAX_BYTES.to_string()),
"rejection message must cite the cap constant; got {message:?}"
);
Ok(())
}
}