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,