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};
#[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
}
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,
},
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 {
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> {
if let Some(target) = web_fetch_approval_target(tool_name, tool_args) {
return Some(target);
}
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}`"),
};
}
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);
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 }
}