165 lines
5.8 KiB
Rust
165 lines
5.8 KiB
Rust
use std::env;
|
|
use std::io::{self, Write};
|
|
use runtime::{PermissionMode, ResolvedPermissionMode, ConfigLoader, PermissionPolicy};
|
|
use tools::GlobalToolRegistry;
|
|
|
|
pub(crate) fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
|
|
normalize_permission_mode(value)
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"unsupported permission mode '{value}'. Use read-only, workspace-write, or danger-full-access."
|
|
)
|
|
})
|
|
.map(permission_mode_from_label)
|
|
}
|
|
|
|
pub(crate) fn permission_mode_from_label(mode: &str) -> PermissionMode {
|
|
match mode {
|
|
"read-only" => PermissionMode::ReadOnly,
|
|
"workspace-write" => PermissionMode::WorkspaceWrite,
|
|
"danger-full-access" => PermissionMode::DangerFullAccess,
|
|
other => panic!("unsupported permission mode label: {other}"),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn permission_mode_from_resolved(mode: ResolvedPermissionMode) -> PermissionMode {
|
|
match mode {
|
|
ResolvedPermissionMode::ReadOnly => PermissionMode::ReadOnly,
|
|
ResolvedPermissionMode::WorkspaceWrite => PermissionMode::WorkspaceWrite,
|
|
ResolvedPermissionMode::DangerFullAccess => PermissionMode::DangerFullAccess,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn default_permission_mode() -> PermissionMode {
|
|
env::var("RUSTY_CLAUDE_PERMISSION_MODE")
|
|
.ok()
|
|
.as_deref()
|
|
.and_then(normalize_permission_mode)
|
|
.map(permission_mode_from_label)
|
|
.or_else(config_permission_mode_for_current_dir)
|
|
.unwrap_or(PermissionMode::DangerFullAccess)
|
|
}
|
|
|
|
pub(crate) fn config_permission_mode_for_current_dir() -> Option<PermissionMode> {
|
|
let cwd = env::current_dir().ok()?;
|
|
let loader = ConfigLoader::default_for(&cwd);
|
|
loader
|
|
.load()
|
|
.ok()?
|
|
.permission_mode()
|
|
.map(permission_mode_from_resolved)
|
|
}
|
|
pub(crate) struct CliPermissionPrompter {
|
|
current_mode: PermissionMode,
|
|
}
|
|
|
|
impl CliPermissionPrompter {
|
|
pub(crate) fn new(current_mode: PermissionMode) -> Self {
|
|
Self { current_mode }
|
|
}
|
|
}
|
|
|
|
impl runtime::PermissionPrompter for CliPermissionPrompter {
|
|
fn decide(
|
|
&mut self,
|
|
request: &runtime::PermissionRequest,
|
|
) -> runtime::PermissionPromptDecision {
|
|
println!();
|
|
println!("Permission approval required");
|
|
println!(" Tool {}", request.tool_name);
|
|
println!(" Current mode {}", self.current_mode.as_str());
|
|
println!(" Required mode {}", request.required_mode.as_str());
|
|
if let Some(reason) = &request.reason {
|
|
println!(" Reason {reason}");
|
|
}
|
|
println!(" Input {}", request.input);
|
|
print!("Approve this tool call? [y/N]: ");
|
|
let _ = io::stdout().flush();
|
|
|
|
let mut response = String::new();
|
|
match io::stdin().read_line(&mut response) {
|
|
Ok(_) => {
|
|
let normalized = response.trim().to_ascii_lowercase();
|
|
if matches!(normalized.as_str(), "y" | "yes") {
|
|
runtime::PermissionPromptDecision::Allow
|
|
} else {
|
|
runtime::PermissionPromptDecision::Deny {
|
|
reason: format!(
|
|
"tool '{}' denied by user approval prompt",
|
|
request.tool_name
|
|
),
|
|
}
|
|
}
|
|
}
|
|
Err(error) => runtime::PermissionPromptDecision::Deny {
|
|
reason: format!("permission approval failed: {error}"),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
pub(crate) fn permission_policy(
|
|
mode: PermissionMode,
|
|
feature_config: &runtime::RuntimeFeatureConfig,
|
|
tool_registry: &GlobalToolRegistry,
|
|
) -> Result<PermissionPolicy, String> {
|
|
Ok(tool_registry.permission_specs(None)?.into_iter().fold(
|
|
PermissionPolicy::new(mode).with_permission_rules(feature_config.permission_rules()),
|
|
|policy, (name, required_permission)| {
|
|
policy.with_tool_requirement(name, required_permission)
|
|
},
|
|
))
|
|
}
|
|
pub(crate) fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
|
|
match mode.trim() {
|
|
"read-only" => Some("read-only"),
|
|
"workspace-write" => Some("workspace-write"),
|
|
"danger-full-access" => Some("danger-full-access"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
|
|
use serde_json::json;
|
|
use tools::GlobalToolRegistry;
|
|
use runtime::{PermissionMode, RuntimeFeatureConfig};
|
|
|
|
fn registry_with_plugin_tool() -> GlobalToolRegistry {
|
|
GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new(
|
|
"plugin-demo@external",
|
|
"plugin-demo",
|
|
PluginToolDefinition {
|
|
name: "plugin_echo".to_string(),
|
|
description: Some("Echo plugin payload".to_string()),
|
|
input_schema: json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"message": { "type": "string" }
|
|
},
|
|
"required": ["message"],
|
|
"additionalProperties": false
|
|
}),
|
|
},
|
|
"echo".to_string(),
|
|
Vec::new(),
|
|
PluginToolPermission::WorkspaceWrite,
|
|
None,
|
|
)]).expect("registry should build")
|
|
}
|
|
|
|
#[test]
|
|
fn permission_policy_uses_plugin_tool_permissions() {
|
|
let feature_config = runtime::RuntimeFeatureConfig::default();
|
|
let policy = permission_policy(
|
|
PermissionMode::ReadOnly,
|
|
&feature_config,
|
|
®istry_with_plugin_tool(),
|
|
)
|
|
.expect("permission policy should build");
|
|
let required = policy.required_mode_for("plugin_echo");
|
|
assert_eq!(required, PermissionMode::WorkspaceWrite);
|
|
}
|
|
}
|