Refactor: Remove security module and related functionality
- Deleted the `security` module and its associated files, including `daemon.rs` and `install.rs`. - Removed references to security features in various modules, including `mod.rs`, `mode/mod.rs`, and `input.rs`. - Updated the `MiscState` struct to eliminate security-related fields. - Adjusted the `apply_action` function to remove security action handling. - Increased the maximum limits for tool-only turns and agent steps in `actions/mod.rs`. - Modified the review prompt to exclude security checks. - Cleaned up the `git_operator` and `shell` tools to remove catastrophic guard checks. - Removed internet-related tools and their references from the tool module.
This commit is contained in:
@@ -12,7 +12,7 @@ impl Tool for GitOperator {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Execute git operations with catastrophic guard protection"
|
||||
"Execute git operations"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
@@ -46,10 +46,6 @@ impl Tool for GitOperator {
|
||||
.collect()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("missing required argument: args"))?;
|
||||
let full_cmd_str = format!("git {} {}", operation, arg_list.join(" "));
|
||||
let workspace_roots: Vec<&std::path::Path> = _ctx.workspaces.iter().map(|p| p.as_path()).collect();
|
||||
crate::app::catastrophic::CatastrophicGuard::check_all(&full_cmd_str, &workspace_roots)
|
||||
.map_err(|e| anyhow!("catastrophic guard blocked: {}", e))?;
|
||||
let output = Command::new("git")
|
||||
.arg(&operation)
|
||||
.args(&arg_list)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use std::fs;
|
||||
use std::io::copy;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
|
||||
pub struct Download;
|
||||
|
||||
impl Tool for Download {
|
||||
fn name(&self) -> &'static str {
|
||||
"download"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Download a file from a URL to a local path"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL to download from"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Local path to save the file (relative to workspace root)"
|
||||
}
|
||||
},
|
||||
"required": ["url", "path"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
if !ctx.internet_mode.can_download() {
|
||||
anyhow::bail!("download requires internet mode Full, current mode: {:?}", ctx.internet_mode);
|
||||
}
|
||||
let url = args.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: url"))?
|
||||
.to_string();
|
||||
let rel = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: path"))?
|
||||
.to_string();
|
||||
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
|
||||
crate::app::catastrophic::CatastrophicGuard::check_download_path(&path)
|
||||
.map_err(|e| anyhow!("download blocked: {}", e))?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| anyhow!("failed to create parent directories: {}", e))?;
|
||||
}
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(120))
|
||||
.user_agent("ZedSex/1.0")
|
||||
.build()
|
||||
.map_err(|e| anyhow!("failed to create HTTP client: {}", e))?;
|
||||
let response = client.get(&url)
|
||||
.send()
|
||||
.map_err(|e| anyhow!("failed to download '{}': {}", url, e))?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("download '{}' returned HTTP {}", url, status.as_u16());
|
||||
}
|
||||
let total: u64 = response.content_length().unwrap_or(0);
|
||||
let mut file = fs::File::create(&path)
|
||||
.map_err(|e| anyhow!("failed to create file '{}': {}", rel, e))?;
|
||||
let mut content = response;
|
||||
let written = copy(&mut content, &mut file)
|
||||
.map_err(|e| anyhow!("failed to write to '{}': {}", rel, e))?;
|
||||
Ok(format!("downloaded {} of {} bytes to {}", written, total, rel))
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
use std::time::Duration;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
pub struct Fetch;
|
||||
|
||||
impl Tool for Fetch {
|
||||
fn name(&self) -> &'static str {
|
||||
"fetch"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Fetch a URL and convert the HTML content to markdown"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL to fetch"
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
if !ctx.internet_mode.can_fetch() {
|
||||
anyhow::bail!("fetch requires internet mode Full, current mode: {:?}", ctx.internet_mode);
|
||||
}
|
||||
let url = args.get("url")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: url"))?
|
||||
.to_string();
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.user_agent("ZedSex/1.0")
|
||||
.build()
|
||||
.map_err(|e| anyhow!("failed to create HTTP client: {}", e))?;
|
||||
let response = client.get(&url)
|
||||
.send()
|
||||
.map_err(|e| anyhow!("failed to fetch '{}': {}", url, e))?;
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("fetch '{}' returned HTTP {}", url, status.as_u16());
|
||||
}
|
||||
let content_type = response.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let body = response.text()
|
||||
.map_err(|e| anyhow!("failed to read response body: {}", e))?;
|
||||
if content_type.contains("text/html") || content_type.contains("application/xhtml") || content_type.is_empty() {
|
||||
let markdown = html_to_markdown(&body)?;
|
||||
Ok(markdown)
|
||||
} else {
|
||||
let preview = body.chars().take(2000).collect::<String>();
|
||||
Ok(format!("Content-Type: {}\n\n{}", content_type, preview))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn html_to_markdown(html: &str) -> Result<String> {
|
||||
let frag = scraper::Html::parse_document(html);
|
||||
let sel = scraper::Selector::parse("body")
|
||||
.map_err(|e| anyhow!("failed to parse selector: {}", e))?;
|
||||
let body = frag.select(&sel).next()
|
||||
.map(|e| e.inner_html())
|
||||
.unwrap_or_else(|| html.to_string());
|
||||
let text = scraper::Html::parse_fragment(&body);
|
||||
let result: String = text.root_element().text().collect::<Vec<_>>().join("\n");
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod download;
|
||||
pub mod fetch;
|
||||
pub mod search;
|
||||
@@ -1,242 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SearchProvider {
|
||||
Tavily,
|
||||
Brave,
|
||||
SerpApi,
|
||||
Google,
|
||||
}
|
||||
|
||||
impl SearchProvider {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"tavily" => Some(SearchProvider::Tavily),
|
||||
"brave" => Some(SearchProvider::Brave),
|
||||
"serpapi" | "serp_api" => Some(SearchProvider::SerpApi),
|
||||
"google" => Some(SearchProvider::Google),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchResult {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub snippet: String,
|
||||
}
|
||||
|
||||
pub struct Search;
|
||||
|
||||
impl Tool for Search {
|
||||
fn name(&self) -> &'static str {
|
||||
"web_search"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Search the web for information using a configured search provider (Tavily, Brave, SerpAPI, or Google)."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Number of results to return (default: 5)",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
if !ctx.internet_mode.can_search() {
|
||||
anyhow::bail!("web_search requires internet mode Full, current mode: {:?}", ctx.internet_mode);
|
||||
}
|
||||
let query = args.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: query"))?
|
||||
.to_string();
|
||||
let num_results = args.get("num_results")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(5) as usize;
|
||||
|
||||
let provider = detect_search_provider();
|
||||
match provider {
|
||||
Some(p) => search_with_provider(&p, &query, num_results),
|
||||
None => Ok(format!(
|
||||
"No search provider configured for query '{}'.\n\
|
||||
Set ZESDEX_SEARCH_PROVIDER and corresponding API key env vars.\n\
|
||||
Supported: tavily (ZESDEX_TAVILY_API_KEY), \
|
||||
brave (ZESDEX_BRAVE_API_KEY), \
|
||||
serpapi (ZESDEX_SERPAPI_KEY), \
|
||||
google (ZESDEX_GOOGLE_API_KEY).",
|
||||
query
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_search_provider() -> Option<SearchProvider> {
|
||||
if std::env::var("ZESDEX_SEARCH_PROVIDER").ok().is_some() {
|
||||
let provider_str = std::env::var("ZESDEX_SEARCH_PROVIDER").unwrap_or_default();
|
||||
if let Some(p) = SearchProvider::from_str(&provider_str) {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
if std::env::var("ZESDEX_TAVILY_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Tavily);
|
||||
}
|
||||
if std::env::var("ZESDEX_BRAVE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Brave);
|
||||
}
|
||||
if std::env::var("ZESDEX_SERPAPI_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::SerpApi);
|
||||
}
|
||||
if std::env::var("ZESDEX_GOOGLE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Google);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn search_with_provider(provider: &SearchProvider, query: &str, num_results: usize) -> Result<String> {
|
||||
let results = match provider {
|
||||
SearchProvider::Tavily => search_tavily(query, num_results)?,
|
||||
SearchProvider::Brave => search_brave(query, num_results)?,
|
||||
SearchProvider::SerpApi => search_serpapi(query, num_results)?,
|
||||
SearchProvider::Google => search_google(query, num_results)?,
|
||||
};
|
||||
if results.is_empty() {
|
||||
return Ok(format!("No results found for '{}'.", query));
|
||||
}
|
||||
let mut output = format!("Search results for '{}':\n\n", query);
|
||||
for (i, r) in results.iter().enumerate() {
|
||||
output.push_str(&format!("{}. {}\n {}\n {}\n\n", i + 1, r.title, r.url, r.snippet));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn search_tavily(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_TAVILY_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_TAVILY_API_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let body = json!({
|
||||
"api_key": api_key,
|
||||
"query": query,
|
||||
"max_results": num_results,
|
||||
"include_answer": false,
|
||||
"search_depth": "basic",
|
||||
});
|
||||
let resp = client.post("https://api.tavily.com/search")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Tavily API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
Some(SearchResult {
|
||||
title: r["title"].as_str()?.to_string(),
|
||||
url: r["url"].as_str()?.to_string(),
|
||||
snippet: r["content"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_brave(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_BRAVE_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_BRAVE_API_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://api.search.brave.com/res/v1/web/search")
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.header("X-Subscription-Token", &api_key)
|
||||
.query(&[("q", query), ("count", &num_results.to_string())])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Brave API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["web"]["results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
Some(SearchResult {
|
||||
title: r["title"].as_str()?.to_string(),
|
||||
url: r["url"].as_str()?.to_string(),
|
||||
snippet: r["description"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_serpapi(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_SERPAPI_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_SERPAPI_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://serpapi.com/search.json")
|
||||
.query(&[
|
||||
("q", query),
|
||||
("api_key", &api_key),
|
||||
("engine", "google"),
|
||||
("num", &num_results.to_string()),
|
||||
])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("SerpAPI error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["organic_results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
let title = r.get("title")?.as_str()?.to_string();
|
||||
let url = r.get("link")?.as_str()?.to_string();
|
||||
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
Some(SearchResult { title, url, snippet })
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_google(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_GOOGLE_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_GOOGLE_API_KEY not set"))?;
|
||||
let cx = std::env::var("ZESDEX_GOOGLE_CX")
|
||||
.map_err(|_| anyhow!("ZESDEX_GOOGLE_CX (Custom Search Engine ID) not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://www.googleapis.com/customsearch/v1")
|
||||
.query(&[
|
||||
("q", query),
|
||||
("key", &api_key),
|
||||
("cx", &cx),
|
||||
("num", &num_results.min(10).to_string()),
|
||||
])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Google Custom Search API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["items"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
let title = r.get("title")?.as_str()?.to_string();
|
||||
let url = r.get("link")?.as_str()?.to_string();
|
||||
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
Some(SearchResult { title, url, snippet })
|
||||
}).collect())
|
||||
}
|
||||
@@ -7,7 +7,6 @@ pub mod fs;
|
||||
pub mod git_cred;
|
||||
pub mod git_operator;
|
||||
pub mod git_worktree;
|
||||
pub mod internet;
|
||||
pub mod memory;
|
||||
pub mod plan;
|
||||
pub mod search;
|
||||
@@ -39,7 +38,6 @@ pub struct ToolCtx {
|
||||
pub _download_dir: PathBuf,
|
||||
pub worktrees_dir: PathBuf,
|
||||
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
|
||||
pub internet_mode: super::model::settings::InternetMode,
|
||||
pub origin: crate::app::state::types::Origin,
|
||||
pub graduated_checks: Vec<GraduatedCheck>,
|
||||
}
|
||||
@@ -67,7 +65,6 @@ pub struct ToolCtxBuilder {
|
||||
pub download_dir: PathBuf,
|
||||
pub worktrees_dir: PathBuf,
|
||||
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
|
||||
pub internet_mode: super::model::settings::InternetMode,
|
||||
pub origin: crate::app::state::types::Origin,
|
||||
pub graduated_checks: Vec<GraduatedCheck>,
|
||||
}
|
||||
@@ -81,7 +78,6 @@ impl Default for ToolCtxBuilder {
|
||||
download_dir: PathBuf::new(),
|
||||
worktrees_dir: PathBuf::new(),
|
||||
dir_cache: std::sync::Arc::new(tokio::sync::RwLock::new(super::app::state::misc::DirCache::new())),
|
||||
internet_mode: super::model::settings::InternetMode::Off,
|
||||
origin: crate::app::state::types::Origin::Main,
|
||||
graduated_checks: Vec::new(),
|
||||
}
|
||||
@@ -99,7 +95,6 @@ impl ToolCtxBuilder {
|
||||
_download_dir: self.download_dir,
|
||||
worktrees_dir: self.worktrees_dir,
|
||||
dir_cache: self.dir_cache,
|
||||
internet_mode: self.internet_mode,
|
||||
origin: self.origin,
|
||||
graduated_checks: self.graduated_checks,
|
||||
}
|
||||
@@ -125,9 +120,6 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
||||
Box::new(super::tool::plan::PlanReady),
|
||||
Box::new(super::tool::workflow::WorkflowRun),
|
||||
Box::new(super::tool::workflow::NoteFinding),
|
||||
Box::new(super::tool::internet::fetch::Fetch),
|
||||
Box::new(super::tool::internet::download::Download),
|
||||
Box::new(super::tool::internet::search::Search),
|
||||
Box::new(super::tool::memory::remember::Remember),
|
||||
Box::new(super::tool::memory::forget::Forget),
|
||||
Box::new(super::tool::memory::recall::Recall),
|
||||
|
||||
+2
-5
@@ -13,7 +13,7 @@ impl Tool for Bash {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Execute a shell command via bash -c with catastrophic guard protection"
|
||||
"Execute a shell command via bash -c"
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
@@ -41,16 +41,13 @@ impl Tool for Bash {
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let cmd = args.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: command"))?
|
||||
.to_string();
|
||||
let _description = args.get("description").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120000).min(600000);
|
||||
let workspace_roots: Vec<&std::path::Path> = ctx.workspaces.iter().map(|p| p.as_path()).collect();
|
||||
crate::app::catastrophic::CatastrophicGuard::check_all(&cmd, &workspace_roots)
|
||||
.map_err(|e| anyhow!("catastrophic guard blocked: {}", e))?;
|
||||
super::shell_filter::credentials::check_credential_read(&cmd)
|
||||
.map_err(|e| anyhow!("blocked: {}", e))?;
|
||||
super::shell_filter::git::check_git_destructive(&cmd)
|
||||
|
||||
Reference in New Issue
Block a user