#!/usr/bin/env rust-script
use anyhow::{Context, Result};
use assert_fs::TempDir;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use vtcode_core::config::constants::tools;
use vtcode_core::utils::colors::style;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ToolPolicy {
Allow,
#[default]
Prompt,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolPolicyConfig {
pub version: u32,
pub available_tools: Vec<String>,
pub policies: IndexMap<String, ToolPolicy>,
}
impl Default for ToolPolicyConfig {
fn default() -> Self {
Self {
version: 1,
available_tools: Vec::new(),
policies: IndexMap::new(),
}
}
}
pub struct ToolPolicyManager {
config_path: PathBuf,
config: ToolPolicyConfig,
}
impl ToolPolicyManager {
pub fn new_with_path(config_path: PathBuf) -> Result<Self> {
let config = Self::load_or_create_config(&config_path)?;
Ok(Self {
config_path,
config,
})
}
fn load_or_create_config(config_path: &PathBuf) -> Result<ToolPolicyConfig> {
if config_path.exists() {
let content =
fs::read_to_string(config_path).context("Failed to read tool policy config")?;
serde_json::from_str(&content).context("Failed to parse tool policy config")
} else {
let config = ToolPolicyConfig::default();
Ok(config)
}
}
pub fn update_available_tools(&mut self, tools: Vec<String>) -> Result<()> {
let current_tools: hashbrown::HashSet<String> =
self.config.available_tools.iter().cloned().collect();
let new_tools: hashbrown::HashSet<String> = tools.iter().cloned().collect();
for tool in &tools {
if !current_tools.contains(tool) {
self.config
.policies
.insert(tool.clone(), ToolPolicy::Prompt);
}
}
let tools_to_remove: Vec<_> = self
.config
.policies
.keys()
.filter(|tool| !new_tools.contains(tool.as_str()))
.cloned()
.collect();
for tool in tools_to_remove {
self.config.policies.shift_remove(&tool);
}
self.config.available_tools = tools;
self.save_config()
}
pub fn get_policy(&self, tool_name: &str) -> ToolPolicy {
self.config
.policies
.get(tool_name)
.cloned()
.unwrap_or(ToolPolicy::Prompt)
}
pub fn set_policy(&mut self, tool_name: &str, policy: ToolPolicy) -> Result<()> {
self.config.policies.insert(tool_name.to_string(), policy);
self.save_config()
}
fn save_config(&self) -> Result<()> {
let content = serde_json::to_string_pretty(&self.config)
.context("Failed to serialize tool policy config")?;
fs::write(&self.config_path, content).context("Failed to write tool policy config")?;
Ok(())
}
pub fn print_status(&self) {
println!("{}", style("Tool Policy Status").cyan().bold());
println!("Config file: {}", self.config_path.display());
println!();
if self.config.policies.is_empty() {
println!("No tools configured yet.");
return;
}
let mut allow_count = 0;
let mut prompt_count = 0;
let mut deny_count = 0;
for (tool, policy) in &self.config.policies {
let (status, color_name) = match policy {
ToolPolicy::Allow => {
allow_count += 1;
("ALLOW", "green")
}
ToolPolicy::Prompt => {
prompt_count += 1;
("PROMPT", "yellow")
}
ToolPolicy::Deny => {
deny_count += 1;
("DENY", "red")
}
};
let status_styled = match color_name {
"green" => style(status).green(),
"yellow" => style(status).cyan(),
"red" => style(status).red(),
_ => style(status),
};
println!(
" {} {}",
style(format!("{:15}", tool)).cyan(),
status_styled
);
}
println!();
println!(
"Summary: {} allowed, {} prompt, {} denied",
style(allow_count).green(),
style(prompt_count).cyan(),
style(deny_count).red()
);
}
}
fn main() -> Result<()> {
println!("{}", style("Tool Policy System Test").bold().cyan());
println!();
let temp_dir = TempDir::new()?;
let config_path = temp_dir.path().join("tool-policy.json");
let mut policy_manager = ToolPolicyManager::new_with_path(config_path)?;
println!("{}", style("Test 1: Adding initial tools").cyan());
let initial_tools = vec![
"read_file".to_string(),
"write_file".to_string(),
tools::LIST_FILES.to_string(),
];
policy_manager.update_available_tools(initial_tools)?;
policy_manager.print_status();
println!();
println!("{}", style("Test 2: Setting specific policies").cyan());
policy_manager.set_policy("read_file", ToolPolicy::Allow)?;
policy_manager.set_policy("write_file", ToolPolicy::Deny)?;
policy_manager.print_status();
println!();
println!("{}", style("Test 3: Adding new tools").cyan());
let updated_tools = vec![
"read_file".to_string(),
"write_file".to_string(),
tools::LIST_FILES.to_string(),
"run_pty_cmd".to_string(),
tools::GREP_FILE.to_string(),
];
policy_manager.update_available_tools(updated_tools)?;
policy_manager.print_status();
println!();
println!("{}", style("Test 4: Removing tools").cyan());
let final_tools = vec![
"read_file".to_string(),
tools::LIST_FILES.to_string(),
tools::GREP_FILE.to_string(),
];
policy_manager.update_available_tools(final_tools)?;
policy_manager.print_status();
println!();
println!("{}", style("Test 5: Policy retrieval").cyan());
println!(
"read_file policy: {:?}",
policy_manager.get_policy("read_file")
);
println!(
"list_files policy: {:?}",
policy_manager.get_policy(tools::LIST_FILES)
);
println!(
"nonexistent_tool policy: {:?}",
policy_manager.get_policy("nonexistent_tool")
);
println!();
println!(
"{}",
style("✓ All tests completed successfully!").green().bold()
);
println!("The tool policy system is working correctly.");
println!();
println!("Key features demonstrated:");
println!("• Persistent storage in JSON format");
println!("• Automatic addition of new tools as 'prompt'");
println!("• Removal of deleted tools from configuration");
println!("• Policy setting and retrieval");
println!("• Status display with color coding");
Ok(())
}