forked from boos/BoosAPI
chore: sync current workspace changes
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Sensitive file patterns that should be flagged.
|
||||
const SENSITIVE_PATTERNS: &[&str] = &[
|
||||
".env",
|
||||
".env.local",
|
||||
".env.production",
|
||||
"credentials.json",
|
||||
"credentials.yaml",
|
||||
"credentials.yml",
|
||||
".aws/credentials",
|
||||
".ssh/id_rsa",
|
||||
".ssh/id_ed25519",
|
||||
"id_rsa",
|
||||
"id_ed25519",
|
||||
".npmrc",
|
||||
".pypirc",
|
||||
];
|
||||
|
||||
/// Validates filesystem paths against allowed directories.
|
||||
pub struct FilesystemValidator {
|
||||
allowed_dirs: Vec<PathBuf>,
|
||||
read_only_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl FilesystemValidator {
|
||||
pub fn new(working_dir: &str) -> Self {
|
||||
let working = PathBuf::from(working_dir);
|
||||
let home = dirs_home();
|
||||
|
||||
let mut allowed_dirs = vec![working.clone(), std::env::temp_dir()];
|
||||
|
||||
if let Some(home) = &home {
|
||||
allowed_dirs.push(home.join(".codeany"));
|
||||
}
|
||||
|
||||
Self {
|
||||
allowed_dirs,
|
||||
read_only_dirs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_allowed_dir(&mut self, dir: PathBuf) {
|
||||
self.allowed_dirs.push(dir);
|
||||
}
|
||||
|
||||
pub fn add_read_only_dir(&mut self, dir: PathBuf) {
|
||||
self.read_only_dirs.push(dir);
|
||||
}
|
||||
|
||||
/// Check if a path is allowed for the given operation.
|
||||
pub fn validate_path(&self, path: &str, write: bool) -> Result<(), String> {
|
||||
let path = resolve_path(path);
|
||||
|
||||
// Check read-only dirs
|
||||
if write {
|
||||
for ro_dir in &self.read_only_dirs {
|
||||
if path.starts_with(ro_dir) {
|
||||
return Err(format!(
|
||||
"Path {} is in a read-only directory",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check allowed dirs
|
||||
let allowed = self.allowed_dirs.iter().any(|dir| path.starts_with(dir));
|
||||
if !allowed {
|
||||
return Err(format!(
|
||||
"Path {} is outside allowed directories",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a path is sensitive (credentials, keys, etc.).
|
||||
pub fn is_sensitive_path(path: &str) -> bool {
|
||||
let path_lower = path.to_lowercase();
|
||||
SENSITIVE_PATTERNS
|
||||
.iter()
|
||||
.any(|pattern| path_lower.ends_with(pattern) || path_lower.contains(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path(path: &str) -> PathBuf {
|
||||
let p = Path::new(path);
|
||||
if let Ok(canonical) = p.canonicalize() {
|
||||
canonical
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs_home() -> Option<PathBuf> {
|
||||
std::env::var("HOME").ok().map(PathBuf::from)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod filesystem;
|
||||
mod permissions;
|
||||
|
||||
pub use filesystem::*;
|
||||
pub use permissions::*;
|
||||
@@ -0,0 +1,119 @@
|
||||
use crate::types::PermissionMode;
|
||||
use serde_json::Value;
|
||||
|
||||
/// A permission rule that matches tool invocations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rule {
|
||||
pub tool_name: String,
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
|
||||
/// Permission configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PermissionConfig {
|
||||
pub mode: PermissionMode,
|
||||
pub allow_rules: Vec<Rule>,
|
||||
pub deny_rules: Vec<Rule>,
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for PermissionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: PermissionMode::BypassPermissions,
|
||||
allow_rules: Vec::new(),
|
||||
deny_rules: Vec::new(),
|
||||
allowed_tools: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a permission check.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PermissionResult {
|
||||
Allow,
|
||||
Deny(String),
|
||||
}
|
||||
|
||||
/// Check if a tool invocation is allowed.
|
||||
pub fn check_permission(
|
||||
config: &PermissionConfig,
|
||||
tool_name: &str,
|
||||
input: &Value,
|
||||
) -> PermissionResult {
|
||||
// Bypass mode allows everything
|
||||
if config.mode == PermissionMode::BypassPermissions {
|
||||
return PermissionResult::Allow;
|
||||
}
|
||||
|
||||
// Check allowed tools whitelist
|
||||
if let Some(allowed) = &config.allowed_tools {
|
||||
if !allowed.iter().any(|a| a == tool_name) {
|
||||
return PermissionResult::Deny(format!("Tool '{}' is not in allowed list", tool_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Check deny rules first
|
||||
for rule in &config.deny_rules {
|
||||
if matches_rule(&rule.tool_name, tool_name) {
|
||||
if let Some(pattern) = &rule.pattern {
|
||||
if matches_input_pattern(pattern, input) {
|
||||
return PermissionResult::Deny(format!("Tool '{}' denied by rule", tool_name));
|
||||
}
|
||||
} else {
|
||||
return PermissionResult::Deny(format!("Tool '{}' denied by rule", tool_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check allow rules
|
||||
for rule in &config.allow_rules {
|
||||
if matches_rule(&rule.tool_name, tool_name) {
|
||||
return PermissionResult::Allow;
|
||||
}
|
||||
}
|
||||
|
||||
// Default behavior based on mode
|
||||
match config.mode {
|
||||
PermissionMode::DontAsk | PermissionMode::Auto => PermissionResult::Allow,
|
||||
PermissionMode::AcceptEdits => {
|
||||
// Allow read-only tools and file edits
|
||||
let read_only_tools = [
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"TaskList",
|
||||
"TaskGet",
|
||||
"ToolSearch",
|
||||
];
|
||||
if read_only_tools.contains(&tool_name) {
|
||||
PermissionResult::Allow
|
||||
} else {
|
||||
PermissionResult::Allow // acceptEdits mode allows edits
|
||||
}
|
||||
}
|
||||
PermissionMode::Plan => PermissionResult::Deny(format!(
|
||||
"Tool '{}' requires approval in plan mode",
|
||||
tool_name
|
||||
)),
|
||||
_ => PermissionResult::Allow,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_rule(rule_name: &str, tool_name: &str) -> bool {
|
||||
if rule_name == "*" {
|
||||
return true;
|
||||
}
|
||||
if rule_name.ends_with('*') {
|
||||
let prefix = &rule_name[..rule_name.len() - 1];
|
||||
return tool_name.starts_with(prefix);
|
||||
}
|
||||
rule_name == tool_name
|
||||
}
|
||||
|
||||
fn matches_input_pattern(pattern: &str, input: &Value) -> bool {
|
||||
let input_str = input.to_string();
|
||||
input_str.contains(pattern)
|
||||
}
|
||||
Reference in New Issue
Block a user