magi-code 0.63.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{config::TtsrSettings, instructions::InstructionFile, output::redact_sensitive_text};
use regex::Regex;

const STREAM_BUFFER_CHAR_LIMIT: usize = 8_192;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TtsrRuleSource {
    Builtin,
    Settings,
    AgentsCurated,
}

impl TtsrRuleSource {
    pub(super) const fn as_str(self) -> &'static str {
        match self {
            Self::Builtin => "builtin",
            Self::Settings => "settings",
            Self::AgentsCurated => "agents_curated",
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct TtsrRule {
    pub(super) source: TtsrRuleSource,
    pub(super) pattern: String,
    regex: Regex,
    pub(super) reminder: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct TtsrMatch {
    pub(super) rule_index: usize,
    pub(super) source: TtsrRuleSource,
    pub(super) pattern: String,
    pub(super) reminder: String,
    pub(super) matched_text_redacted: String,
}

#[derive(Debug, Clone)]
pub(crate) struct TtsrInterrupted {
    pub(super) ttsr_match: TtsrMatch,
}

impl TtsrInterrupted {
    #[cfg(test)]
    pub(crate) fn test_instance() -> Self {
        Self {
            ttsr_match: TtsrMatch {
                rule_index: 0,
                source: TtsrRuleSource::Builtin,
                pattern: "test".to_string(),
                reminder: "test reminder".to_string(),
                matched_text_redacted: "test".to_string(),
            },
        }
    }
}

impl std::fmt::Display for TtsrInterrupted {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("provider stream interrupted by TTSR rule")
    }
}

impl std::error::Error for TtsrInterrupted {}

#[derive(Debug, Clone)]
pub(super) struct TtsrRuleSet {
    rules: Vec<TtsrRule>,
    stream_buffer: String,
}

impl TtsrRuleSet {
    pub(super) fn new(
        settings: &TtsrSettings,
        instructions: &[InstructionFile],
    ) -> anyhow::Result<Option<Self>> {
        if !settings.enabled {
            return Ok(None);
        }
        let mut rules = builtin_rules()?;
        rules.extend(
            settings
                .rules
                .iter()
                .map(|rule| {
                    Ok(TtsrRule {
                        source: TtsrRuleSource::Settings,
                        pattern: rule.pattern.clone(),
                        regex: Regex::new(&rule.pattern)?,
                        reminder: rule.reminder.clone(),
                    })
                })
                .collect::<anyhow::Result<Vec<_>>>()?,
        );
        rules.extend(curated_agents_rules(instructions)?);
        Ok(Some(Self {
            rules,
            stream_buffer: String::new(),
        }))
    }

    pub(super) fn reset_stream_buffer(&mut self) {
        self.stream_buffer.clear();
    }

    pub(super) fn check_text(&mut self, candidate: &str) -> Option<TtsrMatch> {
        self.stream_buffer.push_str(candidate);
        truncate_to_last_chars(&mut self.stream_buffer, STREAM_BUFFER_CHAR_LIMIT);
        self.check_buffer()
    }

    pub(super) fn check_standalone(&self, candidate: &str) -> Option<TtsrMatch> {
        self.rules.iter().enumerate().find_map(|(index, rule)| {
            rule.regex.find(candidate).map(|matched| TtsrMatch {
                rule_index: index,
                source: rule.source,
                pattern: rule.pattern.clone(),
                reminder: rule.reminder.clone(),
                matched_text_redacted: redact_sensitive_text(matched.as_str()),
            })
        })
    }

    fn check_buffer(&self) -> Option<TtsrMatch> {
        self.rules.iter().enumerate().find_map(|(index, rule)| {
            rule.regex
                .find(&self.stream_buffer)
                .map(|matched| TtsrMatch {
                    rule_index: index,
                    source: rule.source,
                    pattern: rule.pattern.clone(),
                    reminder: rule.reminder.clone(),
                    matched_text_redacted: redact_sensitive_text(matched.as_str()),
                })
        })
    }

    #[cfg(test)]
    pub(super) fn rules(&self) -> &[TtsrRule] {
        &self.rules
    }
}

fn truncate_to_last_chars(text: &mut String, limit: usize) {
    let char_count = text.chars().count();
    if char_count <= limit {
        return;
    }
    let remove_chars = char_count - limit;
    if let Some((byte_index, _)) = text.char_indices().nth(remove_chars) {
        text.drain(..byte_index);
    }
}

fn builtin_rules() -> anyhow::Result<Vec<TtsrRule>> {
    [
        (
            TtsrRuleSource::Builtin,
            r"(?i)\b(rm\s+-rf\s+/(?:\s|$)|sudo\s+rm\s+-rf|trash\s+/(?:\s|$)|trash\s+~(?:/)?(?:\s|$)|trash\s+/\*)",
            "Safety reminder: do not emit destructive filesystem commands targeting root, home, or broad paths. Use non-destructive alternatives and ask for explicit confirmation when destructive action is required.",
        ),
        (
            TtsrRuleSource::Builtin,
            r#"(?i)\b(?:api[_-]?key|secret|token|password|refresh[_-]?token|bearer)\b\s*[:=]\s*['\"]?[A-Za-z0-9_\-\.]{12,}"#,
            "Security reminder: do not expose credentials or secret values. Redact secrets as [REDACTED] and describe safe handling steps instead.",
        ),
        (
            TtsrRuleSource::Builtin,
            r"(?i)\b(?:OPENAI_API_KEY|MC_API_KEY|--api-key)\b.*\b(?:openai-codex|anthropic|claude-code)\b|\b(?:openai-codex|anthropic|claude-code)\b.*\b(?:OPENAI_API_KEY|MC_API_KEY|--api-key)\b",
            "Credential routing reminder: do not route OPENAI_API_KEY, MC_API_KEY, or --api-key into openai-codex, anthropic, or claude-code providers.",
        ),
        (
            TtsrRuleSource::Builtin,
            r"(?i)\b(?:widen|bypass|disable|ignore)\b.{0,80}\b(?:cwd|root|sandbox|path access|root access)\b|\b(?:cwd|root|sandbox|path access|root access)\b.{0,80}\b(?:widen|bypass|disable|ignore)\b",
            "Safety boundary reminder: do not widen cwd/root access or bypass sandbox/path validation. Keep access scoped to the configured workspace.",
        ),
        (
            TtsrRuleSource::Builtin,
            r"(?i)\bgit\s+push\b[^\n]*(?:--force|-f)|\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-fd",
            "Git safety reminder: do not emit destructive git commands such as force-push, hard reset, or clean without explicit user instruction and safeguards.",
        ),
    ]
    .into_iter()
    .map(|(source, pattern, reminder)| Ok(TtsrRule {
        source,
        pattern: pattern.to_string(),
        regex: Regex::new(pattern)?,
        reminder: reminder.to_string(),
    }))
    .collect()
}

fn curated_agents_rules(instructions: &[InstructionFile]) -> anyhow::Result<Vec<TtsrRule>> {
    let combined = instructions
        .iter()
        .map(|file| file.content.as_str())
        .collect::<Vec<_>>()
        .join("\n");
    let lower = combined.to_ascii_lowercase();
    if lower.trim().is_empty() {
        return Ok(Vec::new());
    }
    // ponytail: AGENTS anti-pattern compilation is curated keyword mapping, not prose NLP. Upgrade path: explicit structured anti-pattern metadata in instruction files.
    let wanted = [
        lower.contains("rm") || lower.contains("destructive"),
        lower.contains("secret") || lower.contains("credential") || lower.contains("token"),
        lower.contains("openai_api_key")
            || lower.contains("mc_api_key")
            || lower.contains("--api-key"),
        lower.contains("cwd") || lower.contains("root access") || lower.contains("sandbox"),
        lower.contains("force-push")
            || lower.contains("reset --hard")
            || lower.contains("git clean"),
    ];
    let defaults = builtin_rules()?;
    Ok(defaults
        .into_iter()
        .zip(wanted)
        .filter_map(|(mut rule, include)| {
            include.then(|| {
                rule.source = TtsrRuleSource::AgentsCurated;
                rule
            })
        })
        .collect())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::TtsrSettings;

    fn enabled_ttsr_settings() -> TtsrSettings {
        TtsrSettings {
            enabled: true,
            rules: Vec::new(),
        }
    }

    #[test]
    fn ttsr_builtin_defaults_include_destructive_secret_credential_cwd_and_git_rules() {
        let rules = TtsrRuleSet::new(&enabled_ttsr_settings(), &[])
            .unwrap()
            .unwrap();
        assert_eq!(rules.rules().len(), 5);
        for sample in [
            "rm -rf / ".to_string(),
            format!("secret = {}", "x".repeat(16)),
            "OPENAI_API_KEY into openai-codex".to_string(),
            "bypass sandbox path access".to_string(),
            "git push origin main --force".to_string(),
        ] {
            assert!(
                rules.clone().check_text(&sample).is_some(),
                "sample missed: {sample}"
            );
        }
    }

    #[test]
    fn ttsr_destructive_rule_targets_root_home_glob_only_not_scoped_paths() {
        // The destructive trash/rm rule must catch genuinely dangerous targets
        // (root, home, glob wipes) while allowing scoped, legitimate cleanup
        // like `trash /Users/.../specific-file.txt`.
        let rules = TtsrRuleSet::new(&enabled_ttsr_settings(), &[])
            .unwrap()
            .unwrap();
        // Should still match dangerous targets.
        for sample in [
            "trash / ",
            "trash ~/ ",
            "trash ~",
            "trash /*",
            "rm -rf / ",
            "sudo rm -rf",
        ] {
            assert!(
                rules.clone().check_text(sample).is_some(),
                "dangerous target should match: {sample}"
            );
        }
        // Must NOT match scoped, legitimate file cleanup.
        for sample in [
            "trash /Users/magimetal/Dev/magi-code/.smoke_test_tmp.txt",
            "trash /Users/magimetal/Dev/magi-code/state/smoke_test_tmp.txt && echo \"cleanup OK\"",
            "trash /tmp/specific-file",
            "trash ~/file.txt",
            "trash ~/.cache/file",
            "trash ~/Dev/project/file.txt",
        ] {
            assert!(
                rules.clone().check_text(sample).is_none(),
                "scoped cleanup should not match: {sample}"
            );
        }
    }
}