vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::{Context, Result};
use serde_json::Value;
use vtcode_core::config::loader::ConfigManager;
use vtcode_core::tools::registry::ToolRegistry;

use super::permission_prompt::{
    extract_shell_approval_command_prefix_words, extract_shell_approval_scope_signature,
    render_shell_persistent_approval_prefix_entry,
};

const SHELL_APPROVAL_SCOPE_MARKER: &str = "|sandbox_permissions=";
const DEFAULT_SHELL_APPROVAL_SCOPE_SIGNATURE: &str =
    "sandbox_permissions=\"use_default\"|additional_permissions=null";

fn shell_command_words_match_prefix(command_words: &[String], prefix_words: &[String]) -> bool {
    command_words.len() >= prefix_words.len()
        && prefix_words
            .iter()
            .zip(command_words.iter())
            .all(|(prefix, command)| prefix == command)
}

fn split_persisted_shell_approval_prefix(entry: &str) -> (&str, Option<&str>) {
    if let Some(index) = entry.find(SHELL_APPROVAL_SCOPE_MARKER) {
        let (prefix, scoped) = entry.split_at(index);
        (prefix, Some(&scoped[1..]))
    } else {
        (entry, None)
    }
}

pub(super) fn shell_command_has_persisted_approval_prefix(
    tool_registry: &ToolRegistry,
    command_words: &[String],
    scope_signature: &str,
) -> bool {
    if command_words.is_empty() {
        return false;
    }

    tool_registry
        .commands_config()
        .approval_prefixes
        .iter()
        .map(|entry| entry.trim())
        .filter(|entry| !entry.is_empty())
        .any(|entry| {
            let (prefix_text, entry_scope_signature) = split_persisted_shell_approval_prefix(entry);
            let prefix_words = shell_words::split(prefix_text).ok();
            let scope_matches = entry_scope_signature
                .unwrap_or(DEFAULT_SHELL_APPROVAL_SCOPE_SIGNATURE)
                == scope_signature;

            scope_matches
                && prefix_words
                    .as_deref()
                    .is_some_and(|prefix| shell_command_words_match_prefix(command_words, prefix))
        })
}

pub(super) async fn persisted_shell_approval(
    tool_registry: &ToolRegistry,
    normalized_tool_name: &str,
    tool_args: Option<&Value>,
) -> Option<(Vec<String>, String)> {
    let (command_words, scope_signature) =
        extract_shell_approval_command_prefix_words(normalized_tool_name, tool_args).zip(
            extract_shell_approval_scope_signature(normalized_tool_name, tool_args),
        )?;

    if tool_registry
        .find_persisted_shell_approval_prefix(&command_words, &scope_signature)
        .await
        .is_some()
        || shell_command_has_persisted_approval_prefix(
            tool_registry,
            &command_words,
            &scope_signature,
        )
    {
        Some((command_words, scope_signature))
    } else {
        None
    }
}

pub(super) async fn persist_shell_approval_prefix_rule(
    tool_registry: &ToolRegistry,
    tool_name: &str,
    tool_args: Option<&Value>,
    prefix_rule: &[String],
) -> Result<String> {
    let rendered_rule =
        render_shell_persistent_approval_prefix_entry(tool_name, tool_args, prefix_rule)
            .context("Failed to render shell approval prefix entry")?;
    tool_registry
        .persist_approval_cache_prefix(&rendered_rule)
        .await
        .context("Failed to persist shell approval prefix to tool policy")?;
    let workspace_root = tool_registry.workspace_root().clone();
    let mut manager = ConfigManager::load_from_workspace(&workspace_root).with_context(|| {
        format!(
            "Failed to load configuration for workspace {}",
            workspace_root.display()
        )
    })?;
    let mut config = manager.config().clone();

    if !config
        .commands
        .approval_prefixes
        .iter()
        .any(|existing| existing == &rendered_rule)
    {
        config
            .commands
            .approval_prefixes
            .push(rendered_rule.clone());
        manager
            .save_config(&config)
            .context("Failed to persist shell approval prefix")?;
    }

    tool_registry.apply_commands_config(&config.commands);
    Ok(rendered_rule)
}