mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 19:21:51 +08:00
Enforce tool permissions before execution
The Rust CLI/runtime now models permissions as ordered access levels, derives tool requirements from the shared tool specs, and prompts REPL users before one-off danger-full-access escalations from workspace-write sessions. This also wires explicit --permission-mode parsing and makes /permissions operate on the live session state instead of an implicit env-derived default. Constraint: Must preserve the existing three user-facing modes read-only, workspace-write, and danger-full-access Constraint: Must avoid new dependencies and keep enforcement inside the existing runtime/tool plumbing Rejected: Keep the old Allow/Deny/Prompt policy model | could not represent ordered tool requirements across the CLI surface Rejected: Continue sourcing live session mode solely from RUSTY_CLAUDE_PERMISSION_MODE | /permissions would not reliably reflect the current session state Confidence: high Scope-risk: moderate Reversibility: clean Directive: Add required_permission entries for new tools before exposing them to the runtime Tested: cargo fmt; cargo clippy --workspace --all-targets -- -D warnings; cargo test -q Not-tested: Manual interactive REPL approval flow in a live Anthropic session
This commit is contained in:
@@ -408,7 +408,8 @@ mod tests {
|
|||||||
.sum::<i32>();
|
.sum::<i32>();
|
||||||
Ok(total.to_string())
|
Ok(total.to_string())
|
||||||
});
|
});
|
||||||
let permission_policy = PermissionPolicy::new(PermissionMode::Prompt);
|
let permission_policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite)
|
||||||
|
.with_tool_requirement("add", PermissionMode::DangerFullAccess);
|
||||||
let system_prompt = SystemPromptBuilder::new()
|
let system_prompt = SystemPromptBuilder::new()
|
||||||
.with_project_context(ProjectContext {
|
.with_project_context(ProjectContext {
|
||||||
cwd: PathBuf::from("/tmp/project"),
|
cwd: PathBuf::from("/tmp/project"),
|
||||||
@@ -487,7 +488,8 @@ mod tests {
|
|||||||
Session::new(),
|
Session::new(),
|
||||||
SingleCallApiClient,
|
SingleCallApiClient,
|
||||||
StaticToolExecutor::new(),
|
StaticToolExecutor::new(),
|
||||||
PermissionPolicy::new(PermissionMode::Prompt),
|
PermissionPolicy::new(PermissionMode::WorkspaceWrite)
|
||||||
|
.with_tool_requirement("blocked", PermissionMode::DangerFullAccess),
|
||||||
vec!["system".to_string()],
|
vec!["system".to_string()],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -536,7 +538,7 @@ mod tests {
|
|||||||
session,
|
session,
|
||||||
SimpleApi,
|
SimpleApi,
|
||||||
StaticToolExecutor::new(),
|
StaticToolExecutor::new(),
|
||||||
PermissionPolicy::new(PermissionMode::Allow),
|
PermissionPolicy::new(PermissionMode::ReadOnly),
|
||||||
vec!["system".to_string()],
|
vec!["system".to_string()],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -563,7 +565,7 @@ mod tests {
|
|||||||
Session::new(),
|
Session::new(),
|
||||||
SimpleApi,
|
SimpleApi,
|
||||||
StaticToolExecutor::new(),
|
StaticToolExecutor::new(),
|
||||||
PermissionPolicy::new(PermissionMode::Allow),
|
PermissionPolicy::new(PermissionMode::ReadOnly),
|
||||||
vec!["system".to_string()],
|
vec!["system".to_string()],
|
||||||
);
|
);
|
||||||
runtime.run_turn("a", None).expect("turn a");
|
runtime.run_turn("a", None).expect("turn a");
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum PermissionMode {
|
pub enum PermissionMode {
|
||||||
Allow,
|
ReadOnly,
|
||||||
Deny,
|
WorkspaceWrite,
|
||||||
Prompt,
|
DangerFullAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionMode {
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::ReadOnly => "read-only",
|
||||||
|
Self::WorkspaceWrite => "workspace-write",
|
||||||
|
Self::DangerFullAccess => "danger-full-access",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct PermissionRequest {
|
pub struct PermissionRequest {
|
||||||
pub tool_name: String,
|
pub tool_name: String,
|
||||||
pub input: String,
|
pub input: String,
|
||||||
|
pub current_mode: PermissionMode,
|
||||||
|
pub required_mode: PermissionMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -31,31 +44,41 @@ pub enum PermissionOutcome {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct PermissionPolicy {
|
pub struct PermissionPolicy {
|
||||||
default_mode: PermissionMode,
|
active_mode: PermissionMode,
|
||||||
tool_modes: BTreeMap<String, PermissionMode>,
|
tool_requirements: BTreeMap<String, PermissionMode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PermissionPolicy {
|
impl PermissionPolicy {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(default_mode: PermissionMode) -> Self {
|
pub fn new(active_mode: PermissionMode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
default_mode,
|
active_mode,
|
||||||
tool_modes: BTreeMap::new(),
|
tool_requirements: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_tool_mode(mut self, tool_name: impl Into<String>, mode: PermissionMode) -> Self {
|
pub fn with_tool_requirement(
|
||||||
self.tool_modes.insert(tool_name.into(), mode);
|
mut self,
|
||||||
|
tool_name: impl Into<String>,
|
||||||
|
required_mode: PermissionMode,
|
||||||
|
) -> Self {
|
||||||
|
self.tool_requirements
|
||||||
|
.insert(tool_name.into(), required_mode);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn mode_for(&self, tool_name: &str) -> PermissionMode {
|
pub fn active_mode(&self) -> PermissionMode {
|
||||||
self.tool_modes
|
self.active_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn required_mode_for(&self, tool_name: &str) -> PermissionMode {
|
||||||
|
self.tool_requirements
|
||||||
.get(tool_name)
|
.get(tool_name)
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(self.default_mode)
|
.unwrap_or(PermissionMode::DangerFullAccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -65,23 +88,43 @@ impl PermissionPolicy {
|
|||||||
input: &str,
|
input: &str,
|
||||||
mut prompter: Option<&mut dyn PermissionPrompter>,
|
mut prompter: Option<&mut dyn PermissionPrompter>,
|
||||||
) -> PermissionOutcome {
|
) -> PermissionOutcome {
|
||||||
match self.mode_for(tool_name) {
|
let current_mode = self.active_mode();
|
||||||
PermissionMode::Allow => PermissionOutcome::Allow,
|
let required_mode = self.required_mode_for(tool_name);
|
||||||
PermissionMode::Deny => PermissionOutcome::Deny {
|
if current_mode >= required_mode {
|
||||||
reason: format!("tool '{tool_name}' denied by permission policy"),
|
return PermissionOutcome::Allow;
|
||||||
},
|
}
|
||||||
PermissionMode::Prompt => match prompter.as_mut() {
|
|
||||||
Some(prompter) => match prompter.decide(&PermissionRequest {
|
let request = PermissionRequest {
|
||||||
tool_name: tool_name.to_string(),
|
tool_name: tool_name.to_string(),
|
||||||
input: input.to_string(),
|
input: input.to_string(),
|
||||||
}) {
|
current_mode,
|
||||||
|
required_mode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if current_mode == PermissionMode::WorkspaceWrite
|
||||||
|
&& required_mode == PermissionMode::DangerFullAccess
|
||||||
|
{
|
||||||
|
return match prompter.as_mut() {
|
||||||
|
Some(prompter) => match prompter.decide(&request) {
|
||||||
PermissionPromptDecision::Allow => PermissionOutcome::Allow,
|
PermissionPromptDecision::Allow => PermissionOutcome::Allow,
|
||||||
PermissionPromptDecision::Deny { reason } => PermissionOutcome::Deny { reason },
|
PermissionPromptDecision::Deny { reason } => PermissionOutcome::Deny { reason },
|
||||||
},
|
},
|
||||||
None => PermissionOutcome::Deny {
|
None => PermissionOutcome::Deny {
|
||||||
reason: format!("tool '{tool_name}' requires interactive approval"),
|
reason: format!(
|
||||||
|
"tool '{tool_name}' requires approval to escalate from {} to {}",
|
||||||
|
current_mode.as_str(),
|
||||||
|
required_mode.as_str()
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionOutcome::Deny {
|
||||||
|
reason: format!(
|
||||||
|
"tool '{tool_name}' requires {} permission; current mode is {}",
|
||||||
|
required_mode.as_str(),
|
||||||
|
current_mode.as_str()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,25 +136,92 @@ mod tests {
|
|||||||
PermissionPrompter, PermissionRequest,
|
PermissionPrompter, PermissionRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AllowPrompter;
|
struct RecordingPrompter {
|
||||||
|
seen: Vec<PermissionRequest>,
|
||||||
|
allow: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl PermissionPrompter for AllowPrompter {
|
impl PermissionPrompter for RecordingPrompter {
|
||||||
fn decide(&mut self, request: &PermissionRequest) -> PermissionPromptDecision {
|
fn decide(&mut self, request: &PermissionRequest) -> PermissionPromptDecision {
|
||||||
assert_eq!(request.tool_name, "bash");
|
self.seen.push(request.clone());
|
||||||
PermissionPromptDecision::Allow
|
if self.allow {
|
||||||
|
PermissionPromptDecision::Allow
|
||||||
|
} else {
|
||||||
|
PermissionPromptDecision::Deny {
|
||||||
|
reason: "not now".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn uses_tool_specific_overrides() {
|
fn allows_tools_when_active_mode_meets_requirement() {
|
||||||
let policy = PermissionPolicy::new(PermissionMode::Deny)
|
let policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite)
|
||||||
.with_tool_mode("bash", PermissionMode::Prompt);
|
.with_tool_requirement("read_file", PermissionMode::ReadOnly)
|
||||||
|
.with_tool_requirement("write_file", PermissionMode::WorkspaceWrite);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
policy.authorize("read_file", "{}", None),
|
||||||
|
PermissionOutcome::Allow
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
policy.authorize("write_file", "{}", None),
|
||||||
|
PermissionOutcome::Allow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn denies_read_only_escalations_without_prompt() {
|
||||||
|
let policy = PermissionPolicy::new(PermissionMode::ReadOnly)
|
||||||
|
.with_tool_requirement("write_file", PermissionMode::WorkspaceWrite)
|
||||||
|
.with_tool_requirement("bash", PermissionMode::DangerFullAccess);
|
||||||
|
|
||||||
let outcome = policy.authorize("bash", "echo hi", Some(&mut AllowPrompter));
|
|
||||||
assert_eq!(outcome, PermissionOutcome::Allow);
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
policy.authorize("edit", "x", None),
|
policy.authorize("write_file", "{}", None),
|
||||||
PermissionOutcome::Deny { .. }
|
PermissionOutcome::Deny { reason } if reason.contains("requires workspace-write permission")
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
policy.authorize("bash", "{}", None),
|
||||||
|
PermissionOutcome::Deny { reason } if reason.contains("requires danger-full-access permission")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prompts_for_workspace_write_to_danger_full_access_escalation() {
|
||||||
|
let policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite)
|
||||||
|
.with_tool_requirement("bash", PermissionMode::DangerFullAccess);
|
||||||
|
let mut prompter = RecordingPrompter {
|
||||||
|
seen: Vec::new(),
|
||||||
|
allow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = policy.authorize("bash", "echo hi", Some(&mut prompter));
|
||||||
|
|
||||||
|
assert_eq!(outcome, PermissionOutcome::Allow);
|
||||||
|
assert_eq!(prompter.seen.len(), 1);
|
||||||
|
assert_eq!(prompter.seen[0].tool_name, "bash");
|
||||||
|
assert_eq!(
|
||||||
|
prompter.seen[0].current_mode,
|
||||||
|
PermissionMode::WorkspaceWrite
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
prompter.seen[0].required_mode,
|
||||||
|
PermissionMode::DangerFullAccess
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn honors_prompt_rejection_reason() {
|
||||||
|
let policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite)
|
||||||
|
.with_tool_requirement("bash", PermissionMode::DangerFullAccess);
|
||||||
|
let mut prompter = RecordingPrompter {
|
||||||
|
seen: Vec::new(),
|
||||||
|
allow: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
policy.authorize("bash", "echo hi", Some(&mut prompter)),
|
||||||
|
PermissionOutcome::Deny { reason } if reason == "not now"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use runtime::{
|
|||||||
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tools::{execute_tool, mvp_tool_specs};
|
use tools::{execute_tool, mvp_tool_specs, ToolSpec};
|
||||||
|
|
||||||
const DEFAULT_MODEL: &str = "claude-sonnet-4-20250514";
|
const DEFAULT_MODEL: &str = "claude-sonnet-4-20250514";
|
||||||
const DEFAULT_MAX_TOKENS: u32 = 32;
|
const DEFAULT_MAX_TOKENS: u32 = 32;
|
||||||
@@ -67,14 +67,16 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
} => LiveCli::new(model, false, allowed_tools)?
|
permission_mode,
|
||||||
|
} => LiveCli::new(model, false, allowed_tools, permission_mode)?
|
||||||
.run_turn_with_output(&prompt, output_format)?,
|
.run_turn_with_output(&prompt, output_format)?,
|
||||||
CliAction::Login => run_login()?,
|
CliAction::Login => run_login()?,
|
||||||
CliAction::Logout => run_logout()?,
|
CliAction::Logout => run_logout()?,
|
||||||
CliAction::Repl {
|
CliAction::Repl {
|
||||||
model,
|
model,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
} => run_repl(model, allowed_tools)?,
|
permission_mode,
|
||||||
|
} => run_repl(model, allowed_tools, permission_mode)?,
|
||||||
CliAction::Help => print_help(),
|
CliAction::Help => print_help(),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -98,12 +100,14 @@ enum CliAction {
|
|||||||
model: String,
|
model: String,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
},
|
},
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
Repl {
|
Repl {
|
||||||
model: String,
|
model: String,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
},
|
},
|
||||||
// prompt-mode formatting is only supported for non-interactive runs
|
// prompt-mode formatting is only supported for non-interactive runs
|
||||||
Help,
|
Help,
|
||||||
@@ -127,9 +131,11 @@ impl CliOutputFormat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
let mut model = DEFAULT_MODEL.to_string();
|
let mut model = DEFAULT_MODEL.to_string();
|
||||||
let mut output_format = CliOutputFormat::Text;
|
let mut output_format = CliOutputFormat::Text;
|
||||||
|
let mut permission_mode = default_permission_mode();
|
||||||
let mut wants_version = false;
|
let mut wants_version = false;
|
||||||
let mut allowed_tool_values = Vec::new();
|
let mut allowed_tool_values = Vec::new();
|
||||||
let mut rest = Vec::new();
|
let mut rest = Vec::new();
|
||||||
@@ -159,10 +165,21 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
output_format = CliOutputFormat::parse(value)?;
|
output_format = CliOutputFormat::parse(value)?;
|
||||||
index += 2;
|
index += 2;
|
||||||
}
|
}
|
||||||
|
"--permission-mode" => {
|
||||||
|
let value = args
|
||||||
|
.get(index + 1)
|
||||||
|
.ok_or_else(|| "missing value for --permission-mode".to_string())?;
|
||||||
|
permission_mode = parse_permission_mode_arg(value)?;
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
flag if flag.starts_with("--output-format=") => {
|
flag if flag.starts_with("--output-format=") => {
|
||||||
output_format = CliOutputFormat::parse(&flag[16..])?;
|
output_format = CliOutputFormat::parse(&flag[16..])?;
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
flag if flag.starts_with("--permission-mode=") => {
|
||||||
|
permission_mode = parse_permission_mode_arg(&flag[18..])?;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
"--allowedTools" | "--allowed-tools" => {
|
"--allowedTools" | "--allowed-tools" => {
|
||||||
let value = args
|
let value = args
|
||||||
.get(index + 1)
|
.get(index + 1)
|
||||||
@@ -195,6 +212,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
return Ok(CliAction::Repl {
|
return Ok(CliAction::Repl {
|
||||||
model,
|
model,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
|
if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
|
||||||
@@ -220,6 +238,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if !other.starts_with('/') => Ok(CliAction::Prompt {
|
other if !other.starts_with('/') => Ok(CliAction::Prompt {
|
||||||
@@ -227,6 +246,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
model,
|
model,
|
||||||
output_format,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
}),
|
}),
|
||||||
other => Err(format!("unknown subcommand: {other}")),
|
other => Err(format!("unknown subcommand: {other}")),
|
||||||
}
|
}
|
||||||
@@ -280,6 +300,33 @@ fn normalize_tool_name(value: &str) -> String {
|
|||||||
value.trim().replace('-', "_").to_ascii_lowercase()
|
value.trim().replace('-', "_").to_ascii_lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_permission_mode() -> PermissionMode {
|
||||||
|
env::var("RUSTY_CLAUDE_PERMISSION_MODE")
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
.and_then(normalize_permission_mode)
|
||||||
|
.map_or(PermissionMode::WorkspaceWrite, permission_mode_from_label)
|
||||||
|
}
|
||||||
|
|
||||||
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
|
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
|
||||||
mvp_tool_specs()
|
mvp_tool_specs()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -786,7 +833,7 @@ fn run_resume_command(
|
|||||||
cumulative: usage,
|
cumulative: usage,
|
||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
},
|
},
|
||||||
permission_mode_label(),
|
default_permission_mode().as_str(),
|
||||||
&status_context(Some(session_path))?,
|
&status_context(Some(session_path))?,
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
@@ -841,8 +888,9 @@ fn run_resume_command(
|
|||||||
fn run_repl(
|
fn run_repl(
|
||||||
model: String,
|
model: String,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut cli = LiveCli::new(model, true, allowed_tools)?;
|
let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
|
||||||
let editor = input::LineEditor::new("› ");
|
let editor = input::LineEditor::new("› ");
|
||||||
println!("{}", cli.startup_banner());
|
println!("{}", cli.startup_banner());
|
||||||
|
|
||||||
@@ -881,6 +929,7 @@ struct ManagedSessionSummary {
|
|||||||
struct LiveCli {
|
struct LiveCli {
|
||||||
model: String,
|
model: String,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
|
||||||
session: SessionHandle,
|
session: SessionHandle,
|
||||||
@@ -891,6 +940,7 @@ impl LiveCli {
|
|||||||
model: String,
|
model: String,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let system_prompt = build_system_prompt()?;
|
let system_prompt = build_system_prompt()?;
|
||||||
let session = create_managed_session_handle()?;
|
let session = create_managed_session_handle()?;
|
||||||
@@ -900,10 +950,12 @@ impl LiveCli {
|
|||||||
system_prompt.clone(),
|
system_prompt.clone(),
|
||||||
enable_tools,
|
enable_tools,
|
||||||
allowed_tools.clone(),
|
allowed_tools.clone(),
|
||||||
|
permission_mode,
|
||||||
)?;
|
)?;
|
||||||
let cli = Self {
|
let cli = Self {
|
||||||
model,
|
model,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
runtime,
|
runtime,
|
||||||
session,
|
session,
|
||||||
@@ -914,8 +966,9 @@ impl LiveCli {
|
|||||||
|
|
||||||
fn startup_banner(&self) -> String {
|
fn startup_banner(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Rusty Claude CLI\n Model {}\n Working directory {}\n Session {}\n\nType /help for commands. Shift+Enter or Ctrl+J inserts a newline.",
|
"Rusty Claude CLI\n Model {}\n Permission mode {}\n Working directory {}\n Session {}\n\nType /help for commands. Shift+Enter or Ctrl+J inserts a newline.",
|
||||||
self.model,
|
self.model,
|
||||||
|
self.permission_mode.as_str(),
|
||||||
env::current_dir().map_or_else(
|
env::current_dir().map_or_else(
|
||||||
|_| "<unknown>".to_string(),
|
|_| "<unknown>".to_string(),
|
||||||
|path| path.display().to_string(),
|
|path| path.display().to_string(),
|
||||||
@@ -932,7 +985,8 @@ impl LiveCli {
|
|||||||
TerminalRenderer::new().color_theme(),
|
TerminalRenderer::new().color_theme(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
)?;
|
)?;
|
||||||
let result = self.runtime.run_turn(input, None);
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||||
|
let result = self.runtime.run_turn(input, Some(&mut permission_prompter));
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
spinner.finish(
|
spinner.finish(
|
||||||
@@ -1055,7 +1109,7 @@ impl LiveCli {
|
|||||||
cumulative,
|
cumulative,
|
||||||
estimated_tokens: self.runtime.estimated_tokens(),
|
estimated_tokens: self.runtime.estimated_tokens(),
|
||||||
},
|
},
|
||||||
permission_mode_label(),
|
self.permission_mode.as_str(),
|
||||||
&status_context(Some(&self.session.path)).expect("status context should load"),
|
&status_context(Some(&self.session.path)).expect("status context should load"),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -1095,6 +1149,7 @@ impl LiveCli {
|
|||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.model.clone_from(&model);
|
self.model.clone_from(&model);
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -1107,7 +1162,10 @@ impl LiveCli {
|
|||||||
|
|
||||||
fn set_permissions(&mut self, mode: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
|
fn set_permissions(&mut self, mode: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let Some(mode) = mode else {
|
let Some(mode) = mode else {
|
||||||
println!("{}", format_permissions_report(permission_mode_label()));
|
println!(
|
||||||
|
"{}",
|
||||||
|
format_permissions_report(self.permission_mode.as_str())
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1117,20 +1175,21 @@ impl LiveCli {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if normalized == permission_mode_label() {
|
if normalized == self.permission_mode.as_str() {
|
||||||
println!("{}", format_permissions_report(normalized));
|
println!("{}", format_permissions_report(normalized));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let previous = permission_mode_label().to_string();
|
let previous = self.permission_mode.as_str().to_string();
|
||||||
let session = self.runtime.session().clone();
|
let session = self.runtime.session().clone();
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.permission_mode = permission_mode_from_label(normalized);
|
||||||
|
self.runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
normalized,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!(
|
println!(
|
||||||
@@ -1149,19 +1208,19 @@ impl LiveCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.session = create_managed_session_handle()?;
|
self.session = create_managed_session_handle()?;
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.runtime = build_runtime(
|
||||||
Session::new(),
|
Session::new(),
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!(
|
println!(
|
||||||
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
||||||
self.model,
|
self.model,
|
||||||
permission_mode_label(),
|
self.permission_mode.as_str(),
|
||||||
self.session.id,
|
self.session.id,
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1184,13 +1243,13 @@ impl LiveCli {
|
|||||||
let handle = resolve_session_reference(&session_ref)?;
|
let handle = resolve_session_reference(&session_ref)?;
|
||||||
let session = Session::load_from_path(&handle.path)?;
|
let session = Session::load_from_path(&handle.path)?;
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.session = handle;
|
self.session = handle;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -1261,13 +1320,13 @@ impl LiveCli {
|
|||||||
let handle = resolve_session_reference(target)?;
|
let handle = resolve_session_reference(target)?;
|
||||||
let session = Session::load_from_path(&handle.path)?;
|
let session = Session::load_from_path(&handle.path)?;
|
||||||
let message_count = session.messages.len();
|
let message_count = session.messages.len();
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.runtime = build_runtime(
|
||||||
session,
|
session,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.session = handle;
|
self.session = handle;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
@@ -1291,13 +1350,13 @@ impl LiveCli {
|
|||||||
let removed = result.removed_message_count;
|
let removed = result.removed_message_count;
|
||||||
let kept = result.compacted_session.messages.len();
|
let kept = result.compacted_session.messages.len();
|
||||||
let skipped = removed == 0;
|
let skipped = removed == 0;
|
||||||
self.runtime = build_runtime_with_permission_mode(
|
self.runtime = build_runtime(
|
||||||
result.compacted_session,
|
result.compacted_session,
|
||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
permission_mode_label(),
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
println!("{}", format_compact_report(removed, kept, skipped));
|
println!("{}", format_compact_report(removed, kept, skipped));
|
||||||
@@ -1686,14 +1745,6 @@ fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permission_mode_label() -> &'static str {
|
|
||||||
match env::var("RUSTY_CLAUDE_PERMISSION_MODE") {
|
|
||||||
Ok(value) if value == "read-only" => "read-only",
|
|
||||||
Ok(value) if value == "danger-full-access" => "danger-full-access",
|
|
||||||
_ => "workspace-write",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_diff_report() -> Result<String, Box<dyn std::error::Error>> {
|
fn render_diff_report() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let output = std::process::Command::new("git")
|
let output = std::process::Command::new("git")
|
||||||
.args(["diff", "--", ":(exclude).omx"])
|
.args(["diff", "--", ":(exclude).omx"])
|
||||||
@@ -1823,25 +1874,7 @@ fn build_runtime(
|
|||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
permission_mode: PermissionMode,
|
||||||
{
|
|
||||||
build_runtime_with_permission_mode(
|
|
||||||
session,
|
|
||||||
model,
|
|
||||||
system_prompt,
|
|
||||||
enable_tools,
|
|
||||||
allowed_tools,
|
|
||||||
permission_mode_label(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_runtime_with_permission_mode(
|
|
||||||
session: Session,
|
|
||||||
model: String,
|
|
||||||
system_prompt: Vec<String>,
|
|
||||||
enable_tools: bool,
|
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
|
||||||
permission_mode: &str,
|
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
{
|
{
|
||||||
Ok(ConversationRuntime::new(
|
Ok(ConversationRuntime::new(
|
||||||
@@ -1853,6 +1886,52 @@ fn build_runtime_with_permission_mode(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CliPermissionPrompter {
|
||||||
|
current_mode: PermissionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliPermissionPrompter {
|
||||||
|
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());
|
||||||
|
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}"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AnthropicRuntimeClient {
|
struct AnthropicRuntimeClient {
|
||||||
runtime: tokio::runtime::Runtime,
|
runtime: tokio::runtime::Runtime,
|
||||||
client: AnthropicClient,
|
client: AnthropicClient,
|
||||||
@@ -2096,15 +2175,16 @@ impl ToolExecutor for CliToolExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permission_policy(mode: &str) -> PermissionPolicy {
|
fn permission_policy(mode: PermissionMode) -> PermissionPolicy {
|
||||||
if normalize_permission_mode(mode) == Some("read-only") {
|
tool_permission_specs()
|
||||||
PermissionPolicy::new(PermissionMode::Deny)
|
.into_iter()
|
||||||
.with_tool_mode("read_file", PermissionMode::Allow)
|
.fold(PermissionPolicy::new(mode), |policy, spec| {
|
||||||
.with_tool_mode("glob_search", PermissionMode::Allow)
|
policy.with_tool_requirement(spec.name, spec.required_permission)
|
||||||
.with_tool_mode("grep_search", PermissionMode::Allow)
|
})
|
||||||
} else {
|
}
|
||||||
PermissionPolicy::new(PermissionMode::Allow)
|
|
||||||
}
|
fn tool_permission_specs() -> Vec<ToolSpec> {
|
||||||
|
mvp_tool_specs()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
||||||
@@ -2169,6 +2249,7 @@ fn print_help() {
|
|||||||
println!("Flags:");
|
println!("Flags:");
|
||||||
println!(" --model MODEL Override the active model");
|
println!(" --model MODEL Override the active model");
|
||||||
println!(" --output-format FORMAT Non-interactive output format: text or json");
|
println!(" --output-format FORMAT Non-interactive output format: text or json");
|
||||||
|
println!(" --permission-mode MODE Set read-only, workspace-write, or danger-full-access");
|
||||||
println!(" --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)");
|
println!(" --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)");
|
||||||
println!(" --version, -V Print version and build information locally");
|
println!(" --version, -V Print version and build information locally");
|
||||||
println!();
|
println!();
|
||||||
@@ -2203,7 +2284,7 @@ mod tests {
|
|||||||
resume_supported_slash_commands, status_context, CliAction, CliOutputFormat, SlashCommand,
|
resume_supported_slash_commands, status_context, CliAction, CliOutputFormat, SlashCommand,
|
||||||
StatusUsage, DEFAULT_MODEL,
|
StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use runtime::{ContentBlock, ConversationMessage, MessageRole};
|
use runtime::{ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2213,6 +2294,7 @@ mod tests {
|
|||||||
CliAction::Repl {
|
CliAction::Repl {
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::WorkspaceWrite,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2231,6 +2313,7 @@ mod tests {
|
|||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::WorkspaceWrite,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2251,6 +2334,7 @@ mod tests {
|
|||||||
model: "claude-opus".to_string(),
|
model: "claude-opus".to_string(),
|
||||||
output_format: CliOutputFormat::Json,
|
output_format: CliOutputFormat::Json,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::WorkspaceWrite,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2267,6 +2351,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_permission_mode_flag() {
|
||||||
|
let args = vec!["--permission-mode=read-only".to_string()];
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&args).expect("args should parse"),
|
||||||
|
CliAction::Repl {
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::ReadOnly,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_allowed_tools_flags_with_aliases_and_lists() {
|
fn parses_allowed_tools_flags_with_aliases_and_lists() {
|
||||||
let args = vec![
|
let args = vec![
|
||||||
@@ -2284,6 +2381,7 @@ mod tests {
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
),
|
),
|
||||||
|
permission_mode: PermissionMode::WorkspaceWrite,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
|
|||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use runtime::{
|
use runtime::{
|
||||||
edit_file, execute_bash, glob_search, grep_search, read_file, write_file, BashCommandInput,
|
edit_file, execute_bash, glob_search, grep_search, read_file, write_file, BashCommandInput,
|
||||||
GrepSearchInput,
|
GrepSearchInput, PermissionMode,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -45,6 +45,7 @@ pub struct ToolSpec {
|
|||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub description: &'static str,
|
pub description: &'static str,
|
||||||
pub input_schema: Value,
|
pub input_schema: Value,
|
||||||
|
pub required_permission: PermissionMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -66,6 +67,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["command"],
|
"required": ["command"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
@@ -80,6 +82,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["path"],
|
"required": ["path"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "write_file",
|
name: "write_file",
|
||||||
@@ -93,6 +96,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["path", "content"],
|
"required": ["path", "content"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "edit_file",
|
name: "edit_file",
|
||||||
@@ -108,6 +112,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["path", "old_string", "new_string"],
|
"required": ["path", "old_string", "new_string"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "glob_search",
|
name: "glob_search",
|
||||||
@@ -121,6 +126,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["pattern"],
|
"required": ["pattern"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "grep_search",
|
name: "grep_search",
|
||||||
@@ -146,6 +152,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["pattern"],
|
"required": ["pattern"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "WebFetch",
|
name: "WebFetch",
|
||||||
@@ -160,6 +167,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["url", "prompt"],
|
"required": ["url", "prompt"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "WebSearch",
|
name: "WebSearch",
|
||||||
@@ -180,6 +188,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["query"],
|
"required": ["query"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "TodoWrite",
|
name: "TodoWrite",
|
||||||
@@ -207,6 +216,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["todos"],
|
"required": ["todos"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "Skill",
|
name: "Skill",
|
||||||
@@ -220,6 +230,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["skill"],
|
"required": ["skill"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "Agent",
|
name: "Agent",
|
||||||
@@ -236,6 +247,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["description", "prompt"],
|
"required": ["description", "prompt"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "ToolSearch",
|
name: "ToolSearch",
|
||||||
@@ -249,6 +261,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["query"],
|
"required": ["query"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "NotebookEdit",
|
name: "NotebookEdit",
|
||||||
@@ -265,6 +278,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["notebook_path"],
|
"required": ["notebook_path"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "Sleep",
|
name: "Sleep",
|
||||||
@@ -277,6 +291,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["duration_ms"],
|
"required": ["duration_ms"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "SendUserMessage",
|
name: "SendUserMessage",
|
||||||
@@ -297,6 +312,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["message", "status"],
|
"required": ["message", "status"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "Config",
|
name: "Config",
|
||||||
@@ -312,6 +328,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["setting"],
|
"required": ["setting"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "StructuredOutput",
|
name: "StructuredOutput",
|
||||||
@@ -320,6 +337,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::ReadOnly,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "REPL",
|
name: "REPL",
|
||||||
@@ -334,6 +352,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["code", "language"],
|
"required": ["code", "language"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
},
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "PowerShell",
|
name: "PowerShell",
|
||||||
@@ -349,6 +368,7 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
"required": ["command"],
|
"required": ["command"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}),
|
}),
|
||||||
|
required_permission: PermissionMode::DangerFullAccess,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user