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:
asepharyana
2026-07-12 03:56:43 +07:00
parent 4bfbe1d1b9
commit a974118b5a
39 changed files with 79 additions and 2175 deletions
-170
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,3 @@
pub mod catastrophic;
pub mod harness;
pub mod mode;
pub mod runtime;
-2
View File
@@ -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,
-13
View File
@@ -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);
}
}
+1 -1
View File
@@ -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\
+61 -16
View File
@@ -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)?;
-228
View File
@@ -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
View File
@@ -1 +0,0 @@
pub mod daemon;
-6
View File
@@ -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,
-1
View File
@@ -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(),
}
-1
View File
@@ -46,7 +46,6 @@ pub enum Overlay {
Editor,
Effort,
Mcp,
Security,
Todo,
Rewind,
Learning,
-4
View File
@@ -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)]
}
-1
View File
@@ -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,
-1
View File
@@ -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),
-13
View File
@@ -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)]
-30
View File
@@ -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(&current_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
View File
@@ -1 +0,0 @@
pub mod install;
+1 -5
View File
@@ -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)
-78
View File
@@ -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))
}
}
-78
View File
@@ -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)
}
-3
View File
@@ -1,3 +0,0 @@
pub mod download;
pub mod fetch;
pub mod search;
-242
View File
@@ -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())
}
-8
View File
@@ -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
View File
@@ -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)
-42
View File
@@ -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();