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,
|
||||
|
||||
Reference in New Issue
Block a user