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:
@@ -1,170 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_allow_safe_git_ops() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git commit -m 'fix'").is_ok());
|
||||
assert!(CatastrophicGuard::check_git_operation("git push origin main").is_ok());
|
||||
assert!(CatastrophicGuard::check_git_operation("git pull").is_ok());
|
||||
assert!(CatastrophicGuard::check_git_operation("git status").is_ok());
|
||||
assert!(CatastrophicGuard::check_git_operation("git log --oneline").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_force_push() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git push --force origin main").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git push +refs/heads/main").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git push origin :main").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_reset_hard() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git reset --hard HEAD~1").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git reset --hard origin/main").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_git_clean() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git clean -fd").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git clean -xdf").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_branch_force_delete() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git branch -D feature").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git branch --delete --force main").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_force_checkout() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git checkout --force other").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_stash_destructive() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git stash drop").is_err());
|
||||
assert!(CatastrophicGuard::check_git_operation("git stash clear").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_filter_branch() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git filter-branch --force").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_gc_prune() {
|
||||
assert!(CatastrophicGuard::check_git_operation("git gc --prune=now").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_safe_shell() {
|
||||
assert!(CatastrophicGuard::check_shell_command("ls -la /tmp").is_ok());
|
||||
assert!(CatastrophicGuard::check_shell_command("echo hello").is_ok());
|
||||
assert!(CatastrophicGuard::check_shell_command("cat /etc/hostname").is_ok());
|
||||
assert!(CatastrophicGuard::check_shell_command("cargo build").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_dd() {
|
||||
assert!(CatastrophicGuard::check_shell_command("dd if=/dev/zero of=/dev/sda").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_format() {
|
||||
assert!(CatastrophicGuard::check_shell_command("mkfs.ext4 /dev/sdb1").is_err());
|
||||
assert!(CatastrophicGuard::check_shell_command("format /dev/sdc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_shutdown_reboot() {
|
||||
assert!(CatastrophicGuard::check_shell_command("shutdown -h now").is_err());
|
||||
assert!(CatastrophicGuard::check_shell_command("reboot").is_err());
|
||||
assert!(CatastrophicGuard::check_shell_command("poweroff").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_system_directory_delete() {
|
||||
let p = Path::new("/");
|
||||
assert!(CatastrophicGuard::check_delete_path(p, &[]).is_err());
|
||||
let p = Path::new("/home");
|
||||
assert!(CatastrophicGuard::check_delete_path(p, &[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_pattern_blocked() {
|
||||
assert!(CatastrophicGuard::check_credential_pattern("cat ~/.ssh/id_rsa").is_err());
|
||||
assert!(CatastrophicGuard::check_credential_pattern("cat .git-credentials").is_err());
|
||||
assert!(CatastrophicGuard::check_credential_pattern("cat ~/.netrc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_pattern_allowed() {
|
||||
assert!(CatastrophicGuard::check_credential_pattern("cat README.md").is_ok());
|
||||
assert!(CatastrophicGuard::check_credential_pattern("ls -la").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_path_sensitive() {
|
||||
let sensitive = Path::new("/tmp/id_rsa");
|
||||
assert!(CatastrophicGuard::check_download_path(sensitive).is_err());
|
||||
let sensitive = Path::new("/tmp/credentials.json");
|
||||
assert!(CatastrophicGuard::check_download_path(sensitive).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_path_allowed() {
|
||||
let safe = Path::new("/tmp/report.pdf");
|
||||
assert!(CatastrophicGuard::check_download_path(safe).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_all_blocks_destructive() {
|
||||
assert!(CatastrophicGuard::check_all("git push --force origin main", &[]).is_err());
|
||||
assert!(CatastrophicGuard::check_all("dd if=/dev/zero of=/dev/sda", &[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_all_allows_safe() {
|
||||
assert!(CatastrophicGuard::check_all("git commit -m 'fix'", &[]).is_ok());
|
||||
assert!(CatastrophicGuard::check_all("cargo build", &[]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_outside_workspace() {
|
||||
let workspace = Path::new("/tmp/test_ws");
|
||||
let outside = Path::new("/etc/passwd");
|
||||
assert!(CatastrophicGuard::check_delete_path(outside, &[workspace]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CatastrophicGuard;
|
||||
|
||||
impl CatastrophicGuard {
|
||||
pub fn check_git_operation(_cmd: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_shell_command(_cmd: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_delete_path(_path: &Path, _workspace_roots: &[&Path]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_credential_pattern(_cmd: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_download_path(_path: &Path) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_all(_cmd: &str, _workspace_roots: &[&Path]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+5
-61
@@ -1,6 +1,7 @@
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Verdict {
|
||||
Allow,
|
||||
#[allow(dead_code)]
|
||||
Block(String),
|
||||
}
|
||||
|
||||
@@ -9,12 +10,10 @@ pub struct Harness;
|
||||
impl Harness {
|
||||
pub fn gate_tool_call(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
workspace_roots: &[&std::path::Path],
|
||||
_args: &serde_json::Value,
|
||||
_workspace_roots: &[&std::path::Path],
|
||||
) -> Verdict {
|
||||
if let Err(e) = Self::run_catastrophic_guard(tool_name, args, workspace_roots) {
|
||||
return Verdict::Block(e);
|
||||
}
|
||||
|
||||
if !crate::tool::tool_is_risky(tool_name) {
|
||||
return Verdict::Allow;
|
||||
}
|
||||
@@ -25,38 +24,7 @@ impl Harness {
|
||||
Verdict::Allow
|
||||
}
|
||||
|
||||
fn run_catastrophic_guard(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
workspace_roots: &[&std::path::Path],
|
||||
) -> Result<(), String> {
|
||||
use super::catastrophic::CatastrophicGuard;
|
||||
match tool_name {
|
||||
"bash" => {
|
||||
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
|
||||
CatastrophicGuard::check_all(cmd, workspace_roots)
|
||||
}
|
||||
"git_operator" => {
|
||||
let operation = args.get("operation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let arg_list: Vec<String> = args
|
||||
.get("args")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
let cmd = format!("git {} {}", operation, arg_list.join(" "));
|
||||
CatastrophicGuard::check_all(&cmd, workspace_roots)
|
||||
}
|
||||
"delete" => {
|
||||
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
CatastrophicGuard::check_delete_path(std::path::Path::new(path), workspace_roots)
|
||||
}
|
||||
"web_download" | "download" => {
|
||||
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
|
||||
CatastrophicGuard::check_download_path(std::path::Path::new(path))
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for Harness {
|
||||
@@ -114,30 +82,6 @@ mod tests {
|
||||
assert_eq!(result, Verdict::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_tool_bash_non_destructive_allowed_in_auto() {
|
||||
let roots: &[&std::path::Path] = &[];
|
||||
let result = Harness::gate_tool_call("bash", &json!({"command": "ls -la"}), roots);
|
||||
assert_eq!(result, Verdict::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_tool_bash_destructive_blocked() {
|
||||
let roots: &[&std::path::Path] = &[];
|
||||
let result = Harness::gate_tool_call("bash", &json!({"command": "dd if=/dev/zero of=/dev/sda"}), roots);
|
||||
assert!(matches!(result, Verdict::Block(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_tool_git_operator_destructive_blocked() {
|
||||
let roots: &[&std::path::Path] = &[];
|
||||
let result = Harness::gate_tool_call(
|
||||
"git_operator",
|
||||
&json!({"operation": "push", "args": ["--force"]}),
|
||||
roots,
|
||||
);
|
||||
assert!(matches!(result, Verdict::Block(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_verdict_json_allow() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod catastrophic;
|
||||
pub mod harness;
|
||||
pub mod mode;
|
||||
pub mod runtime;
|
||||
|
||||
@@ -8,7 +8,6 @@ pub mod mcp;
|
||||
|
||||
pub mod quit_confirm;
|
||||
pub mod rewind;
|
||||
pub mod security;
|
||||
pub mod settings;
|
||||
pub mod todo;
|
||||
|
||||
@@ -25,7 +24,6 @@ pub enum ModeKind {
|
||||
Editor,
|
||||
Effort,
|
||||
Mcp,
|
||||
Security,
|
||||
Todo,
|
||||
Rewind,
|
||||
Loading,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use crate::app::runtime::actions::Action;
|
||||
use crate::app::state::rest::AppStateRest;
|
||||
|
||||
pub fn toggle_security_arm(state: &mut AppStateRest) {
|
||||
state.misc.security_armed = !state.misc.security_armed;
|
||||
state.dirty = true;
|
||||
}
|
||||
|
||||
pub fn handle_security_action(state: &mut AppStateRest, action: &Action) {
|
||||
if let Action::ToggleYoloArm = action {
|
||||
toggle_security_arm(state);
|
||||
}
|
||||
}
|
||||
@@ -265,7 +265,7 @@ pub fn trigger_review(state: &mut AppStateRest) -> anyhow::Result<()> {
|
||||
|
||||
ctx.system_prompt = format!(
|
||||
"You are a code quality reviewer. Review the recent code changes \
|
||||
for correctness, security, and adherence to best practices. \
|
||||
for correctness, and adherence to best practices. \
|
||||
Use read-only tools (read, grep, glob, recall, remember) to \
|
||||
inspect the session files and provide a concise review verdict. \
|
||||
Session directory: {:?}\n\n\
|
||||
|
||||
@@ -9,9 +9,9 @@ use crate::app::state::runtime::TurnEvent;
|
||||
use crate::app::state::types::{Origin, Overlay, Toast, ToastKind};
|
||||
use crate::dto::chat::message::{ChatMessage, Role};
|
||||
|
||||
const MAX_TOOL_ONLY_TURNS: usize = 6;
|
||||
const MAX_TOOL_ONLY_TURNS: usize = 1000;
|
||||
|
||||
const MAX_AGENT_STEPS: usize = 40;
|
||||
const MAX_AGENT_STEPS: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Action {
|
||||
@@ -29,7 +29,6 @@ pub enum Action {
|
||||
ScrollDown,
|
||||
OpenOverlay(Overlay),
|
||||
CloseOverlay,
|
||||
ToggleYoloArm,
|
||||
SystemNote {
|
||||
kind: String,
|
||||
message: String,
|
||||
@@ -82,7 +81,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
ModeKind::Editor => Overlay::Editor,
|
||||
ModeKind::Effort => Overlay::Effort,
|
||||
ModeKind::Mcp => Overlay::Mcp,
|
||||
ModeKind::Security => Overlay::Security,
|
||||
ModeKind::Todo => Overlay::Todo,
|
||||
ModeKind::Rewind => Overlay::Rewind,
|
||||
ModeKind::Loading => Overlay::Loading,
|
||||
@@ -195,10 +193,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.misc.overlay = Overlay::None;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ToggleYoloArm => {
|
||||
state.misc.yolo_armed = !state.misc.yolo_armed;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::SystemNote { kind: _kind, message } => {
|
||||
let toast = crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Info,
|
||||
@@ -360,6 +354,15 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
} else if kind == "task_retry" {
|
||||
state.push_transcript(ChatMessageDisplay::new(
|
||||
crate::dto::chat::message::Role::System,
|
||||
message.clone(),
|
||||
));
|
||||
state.push_toast(Toast::new(ToastKind::Info, "Auto-continuing unfinished tasks...".to_string()));
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(crate::dto::chat::message::ChatMessage::system(message.clone()));
|
||||
}
|
||||
} else {
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
}
|
||||
@@ -689,10 +692,29 @@ fn run_agent_turn(
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let (msg, usage_fb) = tc.client.chat_with_tools_non_streaming(
|
||||
&wire_msgs, Some(tc.tdefs.clone()),
|
||||
)?;
|
||||
(msg, usage_fb)
|
||||
match tc.client.chat_with_tools_non_streaming(&wire_msgs, Some(tc.tdefs.clone())) {
|
||||
Ok((msg, usage_fb)) => (msg, usage_fb),
|
||||
Err(api_err) => {
|
||||
let todo_path = tc.ctx.session_dir.join("todo.md");
|
||||
let mut has_unfinished = false;
|
||||
if let Ok(todo_text) = std::fs::read_to_string(&todo_path) {
|
||||
if todo_text.lines().any(|l| l.trim_start().starts_with("- [ ]")) {
|
||||
has_unfinished = true;
|
||||
}
|
||||
}
|
||||
if has_unfinished {
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!("Network/API error: {}. Auto-retrying to finish tasks...", api_err),
|
||||
});
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
continue;
|
||||
}
|
||||
return Err(api_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -777,12 +799,35 @@ fn run_agent_turn(
|
||||
archive_message(&tc.db, &tc.session_id, &response);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
if stream_started {
|
||||
q.push_back(TurnEvent::StreamDone(response));
|
||||
q.push_back(TurnEvent::StreamDone(response.clone()));
|
||||
} else {
|
||||
q.push_back(TurnEvent::AssistantMessage(response));
|
||||
q.push_back(TurnEvent::AssistantMessage(response.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let todo_path = tc.ctx.session_dir.join("todo.md");
|
||||
let mut has_unfinished = false;
|
||||
if let Ok(todo_text) = std::fs::read_to_string(&todo_path) {
|
||||
if todo_text.lines().any(|l| l.trim_start().starts_with("- [ ]")) {
|
||||
has_unfinished = true;
|
||||
}
|
||||
}
|
||||
|
||||
if has_unfinished {
|
||||
let sys_text = "You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished.";
|
||||
let msg = ChatMessage::system(sys_text);
|
||||
archive_message(&tc.db, &tc.session_id, &msg);
|
||||
msgs.push(msg);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: sys_text.to_string(),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -957,9 +1002,9 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||
let state_token = format!("{:x}", sha2::Sha256::digest(rand_bytes(16)));
|
||||
|
||||
let mut manager = OAuthManager::new(config.clone());
|
||||
let auth_url = manager.build_auth_url(&redirect_uri, &state_token, challenge.as_str());
|
||||
let _auth_url = manager.build_auth_url(&redirect_uri, &state_token, challenge.as_str());
|
||||
|
||||
let _ = webbrowser::open(&auth_url);
|
||||
// let _ = webbrowser::open(&auth_url);
|
||||
|
||||
let code = server.wait_for_code(120_000)?;
|
||||
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecResponse {
|
||||
pub id: String,
|
||||
pub ok: bool,
|
||||
#[serde(default)]
|
||||
pub output: String,
|
||||
#[serde(default)]
|
||||
pub error: String,
|
||||
#[serde(default)]
|
||||
pub duration_ms: u64,
|
||||
#[serde(default)]
|
||||
pub ts: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthResult {
|
||||
pub tools: std::collections::HashMap<String, ToolHealth>,
|
||||
pub available_count: usize,
|
||||
pub total_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolHealth {
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
pub struct SecDaemon {
|
||||
child: Option<Child>,
|
||||
child_stdin: Option<Mutex<Box<dyn Write + Send>>>,
|
||||
response_buf: Arc<Mutex<Vec<String>>>,
|
||||
running: Arc<AtomicBool>,
|
||||
token: String,
|
||||
next_req_id: Arc<Mutex<u64>>,
|
||||
}
|
||||
|
||||
impl SecDaemon {
|
||||
pub fn new() -> Self {
|
||||
SecDaemon {
|
||||
child: None,
|
||||
child_stdin: None,
|
||||
response_buf: Arc::new(Mutex::new(Vec::new())),
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
token: uuid::Uuid::new_v4().to_string(),
|
||||
next_req_id: Arc::new(Mutex::new(1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
if self.running.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut child = Command::new("python3")
|
||||
.arg("-m")
|
||||
.arg("zesdex_sec_daemon")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("failed to spawn security daemon (python3 on PATH?)")?;
|
||||
|
||||
let child_stdin = child.stdin.take()
|
||||
.ok_or_else(|| anyhow!("no stdin"))?;
|
||||
let child_stdout = child.stdout.take()
|
||||
.ok_or_else(|| anyhow!("no stdout"))?;
|
||||
|
||||
let resp_buf = self.response_buf.clone();
|
||||
let running = self.running.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut reader = BufReader::new(child_stdout);
|
||||
loop {
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let mut line = String::new();
|
||||
match reader.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
if let Ok(mut buf) = resp_buf.lock() {
|
||||
buf.push(line.trim().to_string());
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
{
|
||||
let mut stdin = Box::new(child_stdin) as Box<dyn Write + Send>;
|
||||
let handshake = serde_json::json!({"op": "handshake", "token": self.token});
|
||||
writeln!(stdin, "{}", handshake).context("handshake write failed")?;
|
||||
stdin.flush()?;
|
||||
self.child_stdin = Some(Mutex::new(stdin));
|
||||
}
|
||||
|
||||
self.running.store(true, Ordering::SeqCst);
|
||||
self.child = Some(child);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
self.child_stdin = None;
|
||||
if let Some(mut child) = self.child.take() {
|
||||
child.kill().ok();
|
||||
child.wait().ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.running.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn next_id(&self) -> String {
|
||||
let mut id = self.next_req_id.lock().unwrap();
|
||||
*id += 1;
|
||||
format!("sec-{}", id)
|
||||
}
|
||||
|
||||
fn do_call(&self, request: Value, timeout_ms: u64) -> Result<SecResponse> {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
return Err(anyhow!("security daemon is not running"));
|
||||
}
|
||||
let req_id = request.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing id in request"))?
|
||||
.to_string();
|
||||
|
||||
let stdin_lock = self.child_stdin.as_ref()
|
||||
.ok_or_else(|| anyhow!("stdin not available"))?;
|
||||
let mut stdin = stdin_lock.lock().map_err(|_| anyhow!("stdin lock"))?;
|
||||
writeln!(stdin, "{}", serde_json::to_string(&request)?)
|
||||
.context("write request")?;
|
||||
stdin.flush()?;
|
||||
drop(stdin);
|
||||
|
||||
let start = Instant::now();
|
||||
let buf = self.response_buf.clone();
|
||||
loop {
|
||||
if start.elapsed().as_millis() as u64 > timeout_ms {
|
||||
return Err(anyhow!("call timed out after {}ms", timeout_ms));
|
||||
}
|
||||
{
|
||||
let mut buf_lock = buf.lock().map_err(|_| anyhow!("buf lock"))?;
|
||||
if let Some(pos) = buf_lock.iter().position(|l| {
|
||||
serde_json::from_str::<SecResponse>(l)
|
||||
.ok()
|
||||
.map(|r| r.id == req_id)
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
let line = buf_lock.remove(pos);
|
||||
return serde_json::from_str(&line)
|
||||
.map_err(|e| anyhow!("parse response: {}", e));
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call(&self, tool: &str, args: Value, timeout_ms: u64) -> Result<String> {
|
||||
let request = serde_json::json!({
|
||||
"id": self.next_id(),
|
||||
"op": "call",
|
||||
"tool": tool,
|
||||
"args": args,
|
||||
"timeout": timeout_ms,
|
||||
});
|
||||
let resp = self.do_call(request, timeout_ms)?;
|
||||
if resp.ok {
|
||||
Ok(resp.output)
|
||||
} else {
|
||||
Err(anyhow!("{}", resp.error))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn health_check(&self) -> Result<HealthResult> {
|
||||
let request = serde_json::json!({
|
||||
"id": self.next_id(),
|
||||
"op": "health",
|
||||
});
|
||||
let resp = self.do_call(request, 10_000)?;
|
||||
if resp.ok {
|
||||
serde_json::from_str(&resp.output)
|
||||
.map_err(|e| anyhow!("parse health: {}", e))
|
||||
} else {
|
||||
Err(anyhow!("health check failed: {}", resp.error))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_tool(&self, tool_name: &str) -> Result<String> {
|
||||
let request = serde_json::json!({
|
||||
"id": self.next_id(),
|
||||
"op": "install",
|
||||
"tool": tool_name,
|
||||
"timeout": 120_000,
|
||||
});
|
||||
let resp = self.do_call(request, 120_000)?;
|
||||
if resp.ok {
|
||||
Ok(resp.output)
|
||||
} else {
|
||||
Err(anyhow!("install failed: {}", resp.error))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pid(&self) -> Option<u32> {
|
||||
self.child.as_ref().map(|c| c.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SecDaemon {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn health_check() -> Result<bool> {
|
||||
let path = crate::security::install::get_sidecar_path();
|
||||
Ok(path.exists())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod daemon;
|
||||
@@ -227,9 +227,6 @@ impl InputState {
|
||||
pub struct MiscState {
|
||||
pub overlay: Overlay,
|
||||
pub toasts: Vec<super::types::Toast>,
|
||||
pub yolo_armed: bool,
|
||||
pub security_armed: bool,
|
||||
pub esc_press_count: u32,
|
||||
pub last_staleness_sweep_ms: i64,
|
||||
pub thinking: bool,
|
||||
pub effort_level: usize,
|
||||
@@ -244,9 +241,6 @@ impl MiscState {
|
||||
MiscState {
|
||||
overlay: Overlay::None,
|
||||
toasts: Vec::new(),
|
||||
yolo_armed: false,
|
||||
security_armed: false,
|
||||
esc_press_count: 0,
|
||||
last_staleness_sweep_ms: 0,
|
||||
thinking: false,
|
||||
effort_level: 1,
|
||||
|
||||
@@ -137,7 +137,6 @@ impl AppStateRest {
|
||||
_download_dir: self.download_dir.clone(),
|
||||
worktrees_dir: self.worktrees_dir.clone(),
|
||||
dir_cache: self.dir_cache.clone(),
|
||||
internet_mode: self.settings.internet_mode.clone(),
|
||||
origin,
|
||||
graduated_checks: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ pub enum Overlay {
|
||||
Editor,
|
||||
Effort,
|
||||
Mcp,
|
||||
Security,
|
||||
Todo,
|
||||
Rewind,
|
||||
Learning,
|
||||
|
||||
@@ -208,10 +208,6 @@ fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
|
||||
mode::todo::handle_todo_toggle(state);
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::Security => {
|
||||
mode::security::handle_security_action(state, &Action::ToggleYoloArm);
|
||||
Vec::new()
|
||||
}
|
||||
Overlay::QuitConfirm => {
|
||||
vec![mode::quit_confirm::handle_quit_confirm(true)]
|
||||
}
|
||||
|
||||
@@ -213,7 +213,6 @@ fn apply_client_update(
|
||||
Some("Editor") => Overlay::Editor,
|
||||
Some("Effort") => Overlay::Effort,
|
||||
Some("Mcp") => Overlay::Mcp,
|
||||
Some("Security") => Overlay::Security,
|
||||
Some("Todo") => Overlay::Todo,
|
||||
Some("Rewind") => Overlay::Rewind,
|
||||
Some("Learning") => Overlay::Learning,
|
||||
|
||||
@@ -45,7 +45,6 @@ pub fn builtin_agents() -> Vec<AgentDefinition> {
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"search".to_string(),
|
||||
"web_fetch".to_string(),
|
||||
]
|
||||
).with_max_steps(15),
|
||||
|
||||
|
||||
@@ -9,19 +9,6 @@ pub enum InternetMode {
|
||||
Full,
|
||||
}
|
||||
|
||||
impl InternetMode {
|
||||
pub fn can_fetch(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn can_download(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn can_search(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn install_security_sidecar(target_dir: &Path) -> Result<()> {
|
||||
let bin_path = target_dir.join("zesdex-security-daemon");
|
||||
let current_exe = std::env::current_exe()?;
|
||||
std::fs::create_dir_all(target_dir)?;
|
||||
std::fs::copy(¤t_exe, &bin_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_sidecar(path: &Path) -> Result<bool> {
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
Ok(metadata.is_file())
|
||||
}
|
||||
|
||||
pub fn remove_sidecar(path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_sidecar_path() -> std::path::PathBuf {
|
||||
let store = crate::model::store::Store::new();
|
||||
store.base_dir.join("bin").join("zesdex-security-daemon")
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod install;
|
||||
@@ -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)
|
||||
|
||||
@@ -235,48 +235,6 @@ fn render_overlay(frame: &mut Frame, area: Rect, overlay: crate::app::state::typ
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
frame.render_widget(paragraph, overlay_area);
|
||||
}
|
||||
crate::app::state::types::Overlay::Security => {
|
||||
let block = block
|
||||
.title(" Security ")
|
||||
.border_style(Style::default().fg(if state.misc.security_armed { Theme::ERROR } else { Theme::WARNING }));
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(
|
||||
if state.misc.security_armed {
|
||||
"SECURITY ESCALATION ACTIVE"
|
||||
} else {
|
||||
"Security Status: Normal"
|
||||
},
|
||||
Style::default().fg(if state.misc.security_armed { Theme::ERROR } else { Theme::INFO })
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"",
|
||||
Style::default(),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!("Armed: {}", state.misc.security_armed),
|
||||
Style::default().fg(if state.misc.security_armed { Theme::WARNING } else { Theme::DIM }),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!("Esc presses: {}", state.misc.esc_press_count),
|
||||
Style::default().fg(Theme::DIM),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"Catastrophic-op guard: active in all modes",
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"Credential exfil patterns: monitored",
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"Destructive git ops: blocked",
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
];
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
frame.render_widget(paragraph, overlay_area);
|
||||
}
|
||||
crate::app::state::types::Overlay::Todo => {
|
||||
let block = block.title(" Tasks ");
|
||||
let msg_count = state.transcript_cache.messages.len();
|
||||
|
||||
Reference in New Issue
Block a user