From d8a1d1d14c3d19611421c7a4897845d2ff710a07 Mon Sep 17 00:00:00 2001 From: Chummy Date: Thu, 26 Feb 2026 00:03:55 +0800 Subject: [PATCH] fix: reconcile non-cli approval governance with current dev APIs --- src/agent/loop_.rs | 8 ++ src/channels/mod.rs | 172 +++++++++++++++++++++++++++++++-------- src/channels/telegram.rs | 2 +- src/config/mod.rs | 2 +- src/config/schema.rs | 3 + src/tools/browser.rs | 57 ++++++++++++- 6 files changed, 206 insertions(+), 38 deletions(-) diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index ea0ab8767..033e94b48 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -982,6 +982,14 @@ pub(crate) async fn run_tool_call_loop( anyhow::bail!("Agent exceeded maximum tool iterations ({max_iterations})") } +/// Build the tool instruction block for the system prompt from concrete tool +/// specs so the LLM knows how to invoke tools. +pub(crate) fn build_tool_instructions(tools_registry: &[Box]) -> String { + let specs: Vec = + tools_registry.iter().map(|tool| tool.spec()).collect(); + build_tool_instructions_from_specs(&specs) +} + /// Build the tool instruction block for the system prompt from concrete tool /// specs so the LLM knows how to invoke tools. pub(crate) fn build_tool_instructions_from_specs(tool_specs: &[crate::tools::ToolSpec]) -> String { diff --git a/src/channels/mod.rs b/src/channels/mod.rs index fdd35fc36..d322730e7 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -68,10 +68,11 @@ pub use whatsapp::WhatsAppChannel; pub use whatsapp_web::WhatsAppWebChannel; use crate::agent::loop_::{ - build_shell_policy_instructions, build_tool_instructions, run_tool_call_loop, scrub_credentials, + build_shell_policy_instructions, build_tool_instructions_from_specs, run_tool_call_loop, + scrub_credentials, }; -use crate::approval::ApprovalManager; -use crate::config::Config; +use crate::approval::{ApprovalManager, PendingApprovalError}; +use crate::config::{Config, NonCliNaturalLanguageApprovalMode}; use crate::identity; use crate::memory::{self, Memory}; use crate::observability::{self, runtime_trace, Observer}; @@ -81,7 +82,6 @@ use crate::security::SecurityPolicy; use crate::tools::{self, Tool}; use crate::util::truncate_with_ellipsis; use anyhow::{Context, Result}; -use regex::Regex; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -244,7 +244,7 @@ struct ChannelRuntimeContext { interrupt_on_new_message: bool, multimodal: crate::config::MultimodalConfig, hooks: Option>, - non_cli_excluded_tools: Arc>, + non_cli_excluded_tools: Arc>>, query_classification: crate::config::QueryClassificationConfig, model_routes: Vec, approval_manager: Arc, @@ -691,13 +691,21 @@ fn parse_runtime_command(channel_name: &str, content: &str) -> Option = parts.collect(); + let tail = args.join(" ").trim().to_string(); match base_command.as_str() { // History reset commands are safe for all channels. "/new" | "/clear" => Some(ChannelRuntimeCommand::NewSession), + "/approve-request" => Some(ChannelRuntimeCommand::RequestToolApproval(tail)), + "/approve-confirm" => Some(ChannelRuntimeCommand::ConfirmToolApproval(tail)), + "/approve-pending" => Some(ChannelRuntimeCommand::ListPendingApprovals), + "/approve" => Some(ChannelRuntimeCommand::ApproveTool(tail)), + "/unapprove" => Some(ChannelRuntimeCommand::UnapproveTool(tail)), + "/approvals" => Some(ChannelRuntimeCommand::ListApprovals), // Provider/model switching remains limited to channels with session routing. "/models" if supports_runtime_model_switch(channel_name) => { - if let Some(provider) = parts.next() { + if let Some(provider) = args.first() { Some(ChannelRuntimeCommand::SetProvider( provider.trim().to_string(), )) @@ -706,7 +714,7 @@ fn parse_runtime_command(channel_name: &str, content: &str) -> Option { - let model = parts.collect::>().join(" ").trim().to_string(); + let model = tail; if model.is_empty() { Some(ChannelRuntimeCommand::ShowModel) } else { @@ -717,6 +725,76 @@ fn parse_runtime_command(channel_name: &str, content: &str) -> Option bool { + let token = value.trim(); + !token.is_empty() + && token + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ':')) +} + +fn extract_runtime_tail_token(text: &str, prefixes: &[&str]) -> Option { + prefixes.iter().find_map(|prefix| { + text.strip_prefix(prefix).and_then(|rest| { + let token = rest.trim(); + if is_runtime_token(token) { + Some(token.to_string()) + } else { + None + } + }) + }) +} + +fn parse_natural_language_runtime_command(content: &str) -> Option { + let trimmed = content.trim(); + if trimmed.is_empty() { + return None; + } + + let lower = trimmed.to_ascii_lowercase(); + if matches!( + lower.as_str(), + "show pending approvals" | "list pending approvals" | "pending approvals" + ) { + return Some(ChannelRuntimeCommand::ListPendingApprovals); + } + if trimmed == "查看授权" + || matches!( + lower.as_str(), + "show approvals" | "list approvals" | "approvals" + ) + { + return Some(ChannelRuntimeCommand::ListApprovals); + } + + if let Some(request_id) = extract_runtime_tail_token(&lower, &["confirm "]) { + return Some(ChannelRuntimeCommand::ConfirmToolApproval(request_id)); + } + if let Some(request_id) = extract_runtime_tail_token(trimmed, &["确认授权 "]) { + return Some(ChannelRuntimeCommand::ConfirmToolApproval(request_id)); + } + + if let Some(tool) = + extract_runtime_tail_token(&lower, &["revoke tool ", "unapprove ", "revoke "]) + { + return Some(ChannelRuntimeCommand::UnapproveTool(tool)); + } + if let Some(tool) = extract_runtime_tail_token(trimmed, &["撤销工具 ", "取消授权 "]) { + return Some(ChannelRuntimeCommand::UnapproveTool(tool)); + } + + if let Some(tool) = extract_runtime_tail_token(&lower, &["approve tool ", "approve "]) { + return Some(ChannelRuntimeCommand::RequestToolApproval(tool)); + } + if let Some(tool) = extract_runtime_tail_token(trimmed, &["授权工具 ", "请放开 ", "放开 "]) + { + return Some(ChannelRuntimeCommand::RequestToolApproval(tool)); + } + + None +} + fn is_approval_management_command(command: &ChannelRuntimeCommand) -> bool { matches!( command, @@ -4464,7 +4542,9 @@ pub async fn start_channels(config: Config) -> Result<()> { } else { None }, - non_cli_excluded_tools: Arc::new(config.autonomy.non_cli_excluded_tools.clone()), + non_cli_excluded_tools: Arc::new(Mutex::new( + config.autonomy.non_cli_excluded_tools.clone(), + )), query_classification: config.query_classification.clone(), model_routes: config.model_routes.clone(), approval_manager: Arc::new(ApprovalManager::from_config(&config.autonomy)), @@ -4768,7 +4848,7 @@ mod tests { provider_runtime_options: providers::ProviderRuntimeOptions::default(), workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -4822,7 +4902,7 @@ mod tests { provider_runtime_options: providers::ProviderRuntimeOptions::default(), workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -4879,7 +4959,7 @@ mod tests { provider_runtime_options: providers::ProviderRuntimeOptions::default(), workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -5478,6 +5558,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(vec!["mock_price".to_string()])), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( &crate::config::AutonomyConfig::default(), )), @@ -5548,14 +5630,14 @@ BTC is currently around $65,000 based on latest tool output."# workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, interrupt_on_new_message: false, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( &crate::config::AutonomyConfig::default(), )), multimodal: crate::config::MultimodalConfig::default(), hooks: None, - query_classification: crate::config::QueryClassificationConfig::default(), - model_routes: Vec::new(), }); process_channel_message( @@ -5612,14 +5694,14 @@ BTC is currently around $65,000 based on latest tool output."# workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, interrupt_on_new_message: false, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( &crate::config::AutonomyConfig::default(), )), multimodal: crate::config::MultimodalConfig::default(), hooks: None, - query_classification: crate::config::QueryClassificationConfig::default(), - model_routes: Vec::new(), }); process_channel_message( @@ -5838,7 +5920,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -5902,7 +5984,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -5975,7 +6057,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -6079,6 +6161,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); assert_eq!( @@ -6212,6 +6296,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); assert_eq!( @@ -6320,6 +6406,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager, }); @@ -6422,6 +6510,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(vec!["shell".to_string()])), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager, }); @@ -6514,6 +6604,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); @@ -6647,6 +6739,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); @@ -6760,6 +6854,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); @@ -6853,6 +6949,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); @@ -6965,6 +7063,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config(&autonomy_cfg)), }); @@ -7077,7 +7177,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7153,7 +7253,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7244,7 +7344,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7385,6 +7485,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( &crate::config::AutonomyConfig::default(), )), @@ -7479,7 +7581,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7544,7 +7646,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7720,7 +7822,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7805,7 +7907,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: true, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7902,7 +8004,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: true, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -7981,7 +8083,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -8045,7 +8147,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -8566,7 +8668,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -8656,7 +8758,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -8746,7 +8848,7 @@ BTC is currently around $65,000 based on latest tool output."# interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -9377,7 +9479,7 @@ BTC is currently around $65,000 based on latest tool output."#; interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( @@ -9448,7 +9550,7 @@ BTC is currently around $65,000 based on latest tool output."#; interrupt_on_new_message: false, multimodal: crate::config::MultimodalConfig::default(), hooks: None, - non_cli_excluded_tools: Arc::new(Vec::new()), + non_cli_excluded_tools: Arc::new(Mutex::new(Vec::new())), query_classification: crate::config::QueryClassificationConfig::default(), model_routes: Vec::new(), approval_manager: Arc::new(ApprovalManager::from_config( diff --git a/src/channels/telegram.rs b/src/channels/telegram.rs index f7aba48c4..85f0d3500 100644 --- a/src/channels/telegram.rs +++ b/src/channels/telegram.rs @@ -7,7 +7,7 @@ use directories::UserDirs; use parking_lot::Mutex; use reqwest::multipart::{Form, Part}; use std::fmt::Write as _; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::Duration; use tokio::fs; diff --git a/src/config/mod.rs b/src/config/mod.rs index d6b0c76ab..46aefc9be 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,7 +11,7 @@ pub use schema::{ DockerRuntimeConfig, EmbeddingRouteConfig, EstopConfig, FeishuConfig, GatewayConfig, GroupReplyConfig, GroupReplyMode, HardwareConfig, HardwareTransport, HeartbeatConfig, HooksConfig, HttpRequestConfig, IMessageConfig, IdentityConfig, LarkConfig, MatrixConfig, - MemoryConfig, ModelRouteConfig, MultimodalConfig, NextcloudTalkConfig, ObservabilityConfig, + MultimodalConfig, NextcloudTalkConfig, NonCliNaturalLanguageApprovalMode, ObservabilityConfig, OtpConfig, OtpMethod, PeripheralBoardConfig, PeripheralsConfig, ProviderConfig, ProxyConfig, ProxyScope, QdrantConfig, QueryClassificationConfig, ReliabilityConfig, ResearchPhaseConfig, ResearchTrigger, ResourceLimitsConfig, RuntimeConfig, SandboxBackend, SandboxConfig, diff --git a/src/config/schema.rs b/src/config/schema.rs index b8a360558..c05e62c36 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -2450,6 +2450,9 @@ impl Default for AutonomyConfig { always_ask: default_always_ask(), allowed_roots: Vec::new(), non_cli_excluded_tools: default_non_cli_excluded_tools(), + non_cli_approval_approvers: Vec::new(), + non_cli_natural_language_approval_mode: NonCliNaturalLanguageApprovalMode::default(), + non_cli_natural_language_approval_mode_by_channel: HashMap::new(), } } } diff --git a/src/tools/browser.rs b/src/tools/browser.rs index 95369ca12..036bbab28 100644 --- a/src/tools/browser.rs +++ b/src/tools/browser.rs @@ -720,6 +720,61 @@ impl BrowserTool { Ok(()) } + async fn resolve_output_path_for_write( + &self, + key: &str, + path: &str, + ) -> anyhow::Result { + let trimmed = path.trim(); + self.validate_output_path(key, trimmed)?; + + tokio::fs::create_dir_all(&self.security.workspace_dir).await?; + let workspace_root = tokio::fs::canonicalize(&self.security.workspace_dir) + .await + .unwrap_or_else(|_| self.security.workspace_dir.clone()); + + let raw_path = Path::new(trimmed); + let output_path = if raw_path.is_absolute() { + raw_path.to_path_buf() + } else { + workspace_root.join(raw_path) + }; + + let parent = output_path + .parent() + .ok_or_else(|| anyhow::anyhow!("'{key}' path has no parent directory"))?; + tokio::fs::create_dir_all(parent).await?; + let resolved_parent = tokio::fs::canonicalize(parent).await?; + if !self.security.is_resolved_path_allowed(&resolved_parent) { + anyhow::bail!( + "{}", + self.security + .resolved_path_violation_message(&resolved_parent) + ); + } + + match tokio::fs::symlink_metadata(&output_path).await { + Ok(meta) => { + if meta.file_type().is_symlink() { + anyhow::bail!( + "Refusing to write browser output through symlink: {}", + output_path.display() + ); + } + if !meta.is_file() { + anyhow::bail!( + "Browser output path is not a regular file: {}", + output_path.display() + ); + } + } + Err(err) if err.kind() == ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + + Ok(output_path) + } + fn validate_computer_use_action( &self, action: &str, @@ -1127,7 +1182,7 @@ impl Tool for BrowserTool { }); } - let mut action = match parse_browser_action(action_str, &args) { + let action = match parse_browser_action(action_str, &args) { Ok(a) => a, Err(e) => { return Ok(ToolResult {