vtcode 0.175.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use serde_json::Value;

use crate::agent::runloop::unified::tool_summary::{describe_tool_action, humanize_tool_name};

mod patterns;
#[cfg(test)]
mod tests;

use super::approval_cache::web_fetch_domain;
use super::permission_prompt::{
    extract_shell_approval_command_words, extract_shell_command_text, extract_shell_permission_scope_signature,
    extract_shell_persistent_approval_prefix_rule, extract_shell_raw_command_text, render_shell_approval_command_words,
    render_shell_persistent_approval_prefix_entry,
};
use patterns::{learned_shell_pattern, segmented_shell_learning_target};

/// Secondary learning key for shell-command "families" (e.g. all safe
/// `find <subdir> ...` invocations share one key) so the auto-approve
/// classifier promotes equivalent-pattern calls after the user has approved a
/// few variants. Only attached for command shapes that are demonstrably safe
/// regardless of remaining flags — see [`learned_shell_pattern`].
#[derive(Debug, Clone)]
pub(super) struct LearnedPattern {
    pub key: String,
    pub label: String,
}

#[derive(Debug, Clone)]
pub(super) struct ApprovalLearningTarget {
    pub approval_key: String,
    pub display_label: String,
    pub pattern: Option<LearnedPattern>,
}

impl ApprovalLearningTarget {
    pub fn new(approval_key: String, display_label: String) -> Self {
        Self { approval_key, display_label, pattern: None }
    }

    pub fn with_pattern(mut self, pattern: Option<LearnedPattern>) -> Self {
        self.pattern = pattern;
        self
    }

    /// Iterate over every (key, label) pair this target contributes to
    /// learning: the exact invocation first, then the optional family pattern.
    ///
    /// Skips the pattern key if it matches the approval key to avoid
    /// double-counting approvals in [`record_approval_blocking`] (the
    /// auto-approve classifier) and duplicate session cache entries.
    pub fn iter_keys(&self) -> impl Iterator<Item = (&str, &str)> {
        let approval_key = self.approval_key.as_str();
        let display_label = self.display_label.as_str();
        std::iter::once((approval_key, display_label)).chain(
            self.pattern
                .iter()
                .filter(move |p| p.key != approval_key)
                .map(|p| (p.key.as_str(), p.label.as_str())),
        )
    }
}

#[derive(Debug, Clone)]
pub(super) struct ToolDisplayLabels {
    pub prompt_label: String,
    pub learning_label: String,
}

#[derive(Debug, Clone)]
pub(super) enum PersistentApprovalTarget {
    ToolLevel,
    ExactInvocation {
        display_label: String,
    },
    /// A safe, learned command family (e.g. all write-free `awk` reads under
    /// one workspace path). Generated by [`learned_shell_pattern`] for
    /// invocations the authoritative read-only classifier proved safe, so the
    /// family key can never promote a mutating sibling. Offered so a single
    /// "Always approve" click groups equivalent safe invocations instead of
    /// persisting a key that changes with every argument (the `awk 'NR>=a && NR<=b'`
    /// repeat-prompt shape).
    LearnedPattern {
        key: String,
        display_label: String,
    },
    PrefixRule {
        prefix_rule: Vec<String>,
        display_label: String,
    },
}

fn exact_shell_learning_target(
    tool_name: &str,
    tool_args: Option<&Value>,
    default_learning_label: &str,
) -> Option<ApprovalLearningTarget> {
    let scope_signature = extract_shell_permission_scope_signature(tool_name, tool_args)?;

    if let Some(command_words) = extract_shell_approval_command_words(tool_name, tool_args) {
        let raw_command_text = extract_shell_raw_command_text(tool_name, tool_args);
        if let Some(target) =
            segmented_shell_learning_target(&command_words, &scope_signature, raw_command_text.as_deref())
        {
            return Some(target);
        }

        let rendered_command = render_shell_approval_command_words(&command_words);
        return Some(ApprovalLearningTarget::new(
            format!("{rendered_command}|{scope_signature}"),
            format!("command `{rendered_command}`"),
        ));
    }

    if let Some(command_text) = extract_shell_command_text(tool_name, tool_args) {
        return Some(ApprovalLearningTarget::new(
            format!("{command_text}|{scope_signature}"),
            format!("command `{command_text}`"),
        ));
    }

    let fallback_key = tool_args.map(Value::to_string).unwrap_or_else(|| tool_name.to_string());
    Some(ApprovalLearningTarget::new(
        format!("{fallback_key}|{scope_signature}"),
        default_learning_label.to_string(),
    ))
}

fn web_fetch_approval_target(tool_name: &str, tool_args: Option<&Value>) -> Option<ApprovalLearningTarget> {
    use vtcode_core::config::constants::tools::{FETCH_URL, WEB_FETCH};

    if tool_name != WEB_FETCH && tool_name != FETCH_URL {
        return None;
    }
    let domain = web_fetch_domain(tool_args)?;
    Some(ApprovalLearningTarget::new(format!("{tool_name}:{domain}"), format!("fetch from {domain}")))
}

pub(super) fn approval_learning_target(
    tool_name: &str,
    tool_args: Option<&Value>,
    default_learning_label: &str,
) -> ApprovalLearningTarget {
    // For web_fetch / fetch_url, key by domain so that permanent approval is
    // scoped to the specific domain rather than the entire tool.
    if let Some(target) = web_fetch_approval_target(tool_name, tool_args) {
        return target;
    }

    let pattern = learned_shell_pattern(tool_name, tool_args);

    if let Some(scope_signature) = extract_shell_permission_scope_signature(tool_name, tool_args) {
        if let Some(prefix_rule) = extract_shell_persistent_approval_prefix_rule(tool_name, tool_args)
            && let Some(rendered_rule) =
                render_shell_persistent_approval_prefix_entry(tool_name, tool_args, &prefix_rule)
        {
            let rendered_prefix = render_shell_approval_command_words(&prefix_rule);
            return ApprovalLearningTarget::new(rendered_rule, format!("commands starting with `{rendered_prefix}`"))
                .with_pattern(pattern);
        }

        return exact_shell_learning_target(tool_name, tool_args, default_learning_label)
            .unwrap_or_else(|| {
                ApprovalLearningTarget::new(
                    format!("{tool_name}|{scope_signature}"),
                    default_learning_label.to_string(),
                )
            })
            .with_pattern(pattern);
    }

    ApprovalLearningTarget::new(
        vtcode_core::tools::names::canonical_tool_name(tool_name).to_owned(),
        default_learning_label.to_string(),
    )
}

pub(super) fn exact_shell_approval_target(
    tool_name: &str,
    tool_args: Option<&Value>,
    default_learning_label: &str,
) -> Option<ApprovalLearningTarget> {
    // For web_fetch / fetch_url, return the domain-scoped target so that
    // persisted approval lookups match the domain-specific key.
    if let Some(target) = web_fetch_approval_target(tool_name, tool_args) {
        return Some(target);
    }

    // Exact persistent cache entries intentionally omit any broader pattern:
    // "always approve this exact invocation" must not silently widen its scope.
    exact_shell_learning_target(tool_name, tool_args, default_learning_label)
}

pub(super) fn persistent_approval_target(
    tool_name: &str,
    tool_args: Option<&Value>,
    default_learning_label: &str,
) -> PersistentApprovalTarget {
    if let Some(prefix_rule) = extract_shell_persistent_approval_prefix_rule(tool_name, tool_args) {
        let rendered_prefix = render_shell_approval_command_words(&prefix_rule);
        return PersistentApprovalTarget::PrefixRule {
            prefix_rule,
            display_label: format!("commands starting with `{rendered_prefix}`"),
        };
    }

    // For web_fetch / fetch_url, always offer domain-scoped permanent approval.
    if let Some(target) = web_fetch_approval_target(tool_name, tool_args) {
        return PersistentApprovalTarget::ExactInvocation { display_label: target.display_label };
    }

    if extract_shell_permission_scope_signature(tool_name, tool_args).is_some() {
        let learning = approval_learning_target(tool_name, tool_args, default_learning_label);
        // Prefer the learned family when the invocation is a demonstrably safe,
        // single-command shape: persisting the exact key would re-prompt for
        // every new argument (e.g. a different `awk` line range). The family
        // key is only attached by `learned_shell_pattern` for read-only shapes,
        // and lookup re-derives it for the candidate, so a mutating sibling
        // stays exact-only and cannot inherit the family approval.
        if let Some(pattern) = &learning.pattern {
            return PersistentApprovalTarget::LearnedPattern {
                key: pattern.key.clone(),
                display_label: pattern.label.clone(),
            };
        }
        return PersistentApprovalTarget::ExactInvocation { display_label: learning.display_label };
    }

    PersistentApprovalTarget::ToolLevel
}

pub(super) fn tool_display_labels(tool_name: &str, tool_args: Option<&Value>) -> ToolDisplayLabels {
    let learning_label = humanize_tool_name(tool_name);
    let prompt_label = tool_args
        .map(|args| describe_tool_action(tool_name, args, None).0)
        .filter(|headline| !headline.is_empty())
        .unwrap_or_else(|| learning_label.clone());

    ToolDisplayLabels { prompt_label, learning_label }
}