Refactor and clean up code across multiple modules
- Simplified token type assignment in OAuth service. - Removed unused session_lock module and re-exported Session from zesdex_entities. - Cleaned up session entity by removing unnecessary comments and code. - Consolidated session handling in HTTP handlers for better readability. - Improved formatting and readability in OAuth repository tests. - Enhanced session lock repository with clearer match statements. - Streamlined session repository error handling. - Refined RNG tests for better clarity. - Adjusted module visibility and organization in lib.rs. - Updated IPC client and connection code for better error handling and clarity. - Improved frame handling in IPC for better readability. - Organized module imports and added test utilities for IPC. - Enhanced database connection error handling. - Simplified JWT token creation error handling. - Improved password verification error handling. - Cleaned up state management code for better readability. - Refactored middleware for session authentication and rate limiting. - Simplified clipboard utility for better error handling. - Enhanced logging initialization for better error reporting. - Improved pagination utility with clearer method annotations. - Cleaned up sanitization functions for filenames and paths. - Enhanced slug generation functions for better clarity and usability.
This commit is contained in:
@@ -7,14 +7,14 @@
|
||||
//! bash exfiltration and destructive-pattern detection) so that subagents
|
||||
//! are not a weaker link than the main agent.
|
||||
|
||||
use std::fmt::Write;
|
||||
use sha2::Digest;
|
||||
use tokio::sync::mpsc;
|
||||
use super::context::SubagentContext;
|
||||
use super::event::SubagentEvent;
|
||||
use crate::dto::chat::message::ChatMessage;
|
||||
use crate::dto::provider::request::ToolDef;
|
||||
use crate::tool::{all_tools, tool_defs, tool_is_risky};
|
||||
use super::context::SubagentContext;
|
||||
use super::event::SubagentEvent;
|
||||
use sha2::Digest;
|
||||
use std::fmt::Write;
|
||||
use tokio::sync::mpsc;
|
||||
use zesdex_cms::domain::repository::AppConfigRepository;
|
||||
use zesdex_cms::domain::repository::EditLogRepository;
|
||||
use zesdex_cms::domain::repository::SettingsRepository;
|
||||
@@ -29,7 +29,9 @@ use zesdex_cms::domain::repository::SettingsRepository;
|
||||
/// `build_subagent_context`'s default for non-reviewer roles).
|
||||
///
|
||||
/// Return: `(tool impls, schema defs)` for the subagent to use.
|
||||
fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::Tool>>, Vec<ToolDef>) {
|
||||
fn build_subagent_tools(
|
||||
allowed_tools: &[String],
|
||||
) -> (Vec<Box<dyn crate::tool::Tool>>, Vec<ToolDef>) {
|
||||
let all = all_tools();
|
||||
let filtered: Vec<Box<dyn crate::tool::Tool>> = if allowed_tools.is_empty() {
|
||||
all.into_iter()
|
||||
@@ -62,28 +64,44 @@ fn build_subagent_tools(allowed_tools: &[String]) -> (Vec<Box<dyn crate::tool::T
|
||||
/// for this before issuing requests (see `run_subagent`).
|
||||
fn resolve_provider_config() -> (String, String, Option<String>, String) {
|
||||
let store_base_dir = zesdex_entities::seaorm::common::store::Store::new().base_dir;
|
||||
let settings = zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository::new()
|
||||
.load(&store_base_dir)
|
||||
.unwrap_or_default();
|
||||
let app_config = zesdex_cms::infrastructure::persistence::app_config_repo::JsonAppConfigRepository::new()
|
||||
.load(&store_base_dir)
|
||||
.unwrap_or_default();
|
||||
let settings =
|
||||
zesdex_cms::infrastructure::persistence::settings_repo::JsonSettingsRepository::new()
|
||||
.load(&store_base_dir)
|
||||
.unwrap_or_default();
|
||||
let app_config =
|
||||
zesdex_cms::infrastructure::persistence::app_config_repo::JsonAppConfigRepository::new()
|
||||
.load(&store_base_dir)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut api_key = settings.api_keys.get(&settings.provider).cloned().unwrap_or_else(|| {
|
||||
tracing::warn!("[subagent] no API key for provider '{}' in settings, trying env/default", settings.provider);
|
||||
String::new()
|
||||
});
|
||||
let mut api_key = settings
|
||||
.api_keys
|
||||
.get(&settings.provider)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"[subagent] no API key for provider '{}' in settings, trying env/default",
|
||||
settings.provider
|
||||
);
|
||||
String::new()
|
||||
});
|
||||
let model = settings.model.clone();
|
||||
let base_url = app_config.providers.get(&settings.provider)
|
||||
let base_url = app_config
|
||||
.providers
|
||||
.get(&settings.provider)
|
||||
.map(|p| p.api_base.clone());
|
||||
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = app_config.providers.get(&settings.provider) {
|
||||
api_key = provider_cfg.api_key_env.as_ref()
|
||||
api_key = provider_cfg
|
||||
.api_key_env
|
||||
.as_ref()
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.or_else(|| provider_cfg.default_api_key.clone())
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!("[subagent] all API key resolution paths exhausted for '{}'", settings.provider);
|
||||
tracing::warn!(
|
||||
"[subagent] all API key resolution paths exhausted for '{}'",
|
||||
settings.provider
|
||||
);
|
||||
String::new()
|
||||
});
|
||||
}
|
||||
@@ -109,43 +127,82 @@ fn require_api_key(api_key: &str, provider: &str) -> anyhow::Result<()> {
|
||||
// ─── Subagent-level tool gating (mirrors Harness checks) ───
|
||||
|
||||
const STUB_PATTERNS: &[&str] = &[
|
||||
"todo!()", "todo!(",
|
||||
"unimplemented!()", "unimplemented!(",
|
||||
"FIXME", "fixme:", "XXX:", "PLACEHOLDER",
|
||||
"REPLACE_ME", "stub_value", "stub_function",
|
||||
"fake_response", "fake_data",
|
||||
"not implemented", "not yet implemented",
|
||||
"to be implemented", "to be done",
|
||||
"todo!()",
|
||||
"todo!(",
|
||||
"unimplemented!()",
|
||||
"unimplemented!(",
|
||||
"FIXME",
|
||||
"fixme:",
|
||||
"XXX:",
|
||||
"PLACEHOLDER",
|
||||
"REPLACE_ME",
|
||||
"stub_value",
|
||||
"stub_function",
|
||||
"fake_response",
|
||||
"fake_data",
|
||||
"not implemented",
|
||||
"not yet implemented",
|
||||
"to be implemented",
|
||||
"to be done",
|
||||
];
|
||||
|
||||
const DENIAL_PATTERNS: &[&str] = &[
|
||||
"// skip", "// skipping", "// skipping for now",
|
||||
"// for now just", "// punt", "// hack:",
|
||||
"// workaround:", "// cba", "// later",
|
||||
"// do later", "// ignore for now", "// disable",
|
||||
"// bypass", "// quick fix", "// temp fix",
|
||||
"// temporary fix", "// temp:", "// temporary:",
|
||||
"// skip",
|
||||
"// skipping",
|
||||
"// skipping for now",
|
||||
"// for now just",
|
||||
"// punt",
|
||||
"// hack:",
|
||||
"// workaround:",
|
||||
"// cba",
|
||||
"// later",
|
||||
"// do later",
|
||||
"// ignore for now",
|
||||
"// disable",
|
||||
"// bypass",
|
||||
"// quick fix",
|
||||
"// temp fix",
|
||||
"// temporary fix",
|
||||
"// temp:",
|
||||
"// temporary:",
|
||||
"// noop",
|
||||
];
|
||||
|
||||
const ASSUMPTION_PATTERNS: &[&str] = &[
|
||||
"// assume", "// probably", "// guess",
|
||||
"// should work", "// hopefully", "// i think",
|
||||
"// should be fine", "// likely",
|
||||
"// assume",
|
||||
"// probably",
|
||||
"// guess",
|
||||
"// should work",
|
||||
"// hopefully",
|
||||
"// i think",
|
||||
"// should be fine",
|
||||
"// likely",
|
||||
];
|
||||
|
||||
const EXFIL_PATTERNS: &[&str] = &[
|
||||
"curl ", "wget ", "nc -e ", "ncat ", "/dev/tcp/",
|
||||
"base64 -d |", "base64 --decode |",
|
||||
"openssl s_client", "ssh -R ",
|
||||
"scp /", "rsync /",
|
||||
"curl ",
|
||||
"wget ",
|
||||
"nc -e ",
|
||||
"ncat ",
|
||||
"/dev/tcp/",
|
||||
"base64 -d |",
|
||||
"base64 --decode |",
|
||||
"openssl s_client",
|
||||
"ssh -R ",
|
||||
"scp /",
|
||||
"rsync /",
|
||||
];
|
||||
|
||||
const SENSITIVE_PATH_PATTERNS: &[&str] = &[
|
||||
".ssh/id_rsa", ".ssh/id_ed25519",
|
||||
".aws/credentials", ".aws/config",
|
||||
".kube/config", ".docker/config.json",
|
||||
"/etc/shadow", "/etc/passwd", "/proc/self/environ",
|
||||
".ssh/id_rsa",
|
||||
".ssh/id_ed25519",
|
||||
".aws/credentials",
|
||||
".aws/config",
|
||||
".kube/config",
|
||||
".docker/config.json",
|
||||
"/etc/shadow",
|
||||
"/etc/passwd",
|
||||
"/proc/self/environ",
|
||||
];
|
||||
|
||||
const MIN_REASON_LEN: usize = 8;
|
||||
@@ -157,10 +214,7 @@ const MIN_REASON_LEN: usize = 8;
|
||||
/// assumption language, bash exfiltration, destructive commands, sensitive
|
||||
/// path reads — regardless of the allowed-tools list. Tools that are not
|
||||
/// risky only get the basic allowlist check.
|
||||
fn gate_subagent_tool_call(
|
||||
tool_name: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Option<String> {
|
||||
fn gate_subagent_tool_call(tool_name: &str, args: &serde_json::Value) -> Option<String> {
|
||||
// File-mutating tools: write / edit / delete
|
||||
if matches!(tool_name, "write" | "edit" | "delete") {
|
||||
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
|
||||
@@ -204,10 +258,16 @@ fn gate_subagent_tool_call(
|
||||
return Some("content contains stub/placeholder pattern; production code must be fully implemented".to_string());
|
||||
}
|
||||
if contains_any(content, DENIAL_PATTERNS) {
|
||||
return Some("content contains denial/punt pattern; implement properly instead of skipping".to_string());
|
||||
return Some(
|
||||
"content contains denial/punt pattern; implement properly instead of skipping"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if contains_any(content, ASSUMPTION_PATTERNS) {
|
||||
return Some("content contains assumption pattern; verify against data instead of guessing".to_string());
|
||||
return Some(
|
||||
"content contains assumption pattern; verify against data instead of guessing"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +291,9 @@ fn gate_subagent_tool_call(
|
||||
if !is_standard {
|
||||
for pat in EXFIL_PATTERNS {
|
||||
if cmd.contains(pat) {
|
||||
return Some(format!("potential data-exfiltration command blocked (matched '{pat}')"));
|
||||
return Some(format!(
|
||||
"potential data-exfiltration command blocked (matched '{pat}')"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,9 +302,21 @@ fn gate_subagent_tool_call(
|
||||
return Some(format!("refused to read/write sensitive path '{pat}'"));
|
||||
}
|
||||
}
|
||||
let dangerous = ["rm -rf /", "rm -rf --no-preserve-root", "rm -rf ~",
|
||||
"rm -fr /", "mkfs.", "dd if=", ":(){", "> /dev/sda",
|
||||
"chmod -R 000 /", "shutdown ", "poweroff ", "reboot ", "halt "];
|
||||
let dangerous = [
|
||||
"rm -rf /",
|
||||
"rm -rf --no-preserve-root",
|
||||
"rm -rf ~",
|
||||
"rm -fr /",
|
||||
"mkfs.",
|
||||
"dd if=",
|
||||
":(){",
|
||||
"> /dev/sda",
|
||||
"chmod -R 000 /",
|
||||
"shutdown ",
|
||||
"poweroff ",
|
||||
"reboot ",
|
||||
"halt ",
|
||||
];
|
||||
for pat in &dangerous {
|
||||
if cmd.contains(pat) {
|
||||
return Some(format!("destructive command pattern blocked: {pat}"));
|
||||
@@ -289,7 +363,9 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
|
||||
for entry in walker.flatten() {
|
||||
let path = entry.path();
|
||||
if let Ok(rel) = path.strip_prefix(root) {
|
||||
if rel.as_os_str().is_empty() { continue; }
|
||||
if rel.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
|
||||
let prefix = if is_dir { "[DIR] " } else { " " };
|
||||
writeln!(out, " {}{}", prefix, rel.display()).unwrap();
|
||||
@@ -331,8 +407,10 @@ fn format_subagent_progress(prefix: &str, text: &str) -> String {
|
||||
///
|
||||
/// Return: the concatenated text output, or an `anyhow::Error` if the LLM
|
||||
/// call fails at any step.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) -> anyhow::Result<String> {
|
||||
pub fn run_subagent(
|
||||
ctx: &SubagentContext,
|
||||
tx: &mpsc::Sender<SubagentEvent>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut output = String::new();
|
||||
let mut messages: Vec<ChatMessage> = Vec::new();
|
||||
|
||||
@@ -370,17 +448,23 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
// surfaces past a buried WARN log.
|
||||
if let Err(error) = require_api_key(&api_key, &provider) {
|
||||
let error = error.to_string();
|
||||
let _ = tx.blocking_send(SubagentEvent::StepFailed { step: 0, error: error.clone() });
|
||||
let _ = tx.blocking_send(SubagentEvent::StepFailed {
|
||||
step: 0,
|
||||
error: error.clone(),
|
||||
});
|
||||
anyhow::bail!(error);
|
||||
}
|
||||
|
||||
let client = crate::service::provider::LlmClient::new(api_key, model, base_url);
|
||||
|
||||
for step in 0..ctx.max_steps {
|
||||
|
||||
// Check abort flag before each LLM call so a stuck subagent can
|
||||
// be cancelled from the parent (mirrors main agent behaviour).
|
||||
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
|
||||
if ctx
|
||||
.abort_flag
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
|
||||
{
|
||||
let _ = tx.blocking_send(SubagentEvent::StepFailed {
|
||||
step,
|
||||
error: "subagent aborted by parent".to_string(),
|
||||
@@ -403,7 +487,11 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
Some(4096),
|
||||
|event| -> bool {
|
||||
// Check abort on every SSE event for responsive cancellation.
|
||||
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
|
||||
if ctx
|
||||
.abort_flag
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
|
||||
{
|
||||
return false; // signals provider to abort
|
||||
}
|
||||
match event {
|
||||
@@ -417,7 +505,11 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
let prog = format_subagent_progress("replying", ¤t_token);
|
||||
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
|
||||
}
|
||||
crate::app::runtime::stream::StreamEvent::Usage { prompt_tokens, completion_tokens, .. } => {
|
||||
crate::app::runtime::stream::StreamEvent::Usage {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
..
|
||||
} => {
|
||||
// Capture usage so the drain thread can route it
|
||||
// to the parent's `UsageStats::review_tokens`.
|
||||
// Last writer wins — providers send exactly one
|
||||
@@ -433,7 +525,10 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
let (response, returned_usage) = match stream_result {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let is_abort = ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
|
||||
let is_abort = ctx
|
||||
.abort_flag
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst))
|
||||
|| e.to_string().contains("aborted");
|
||||
let _ = tx.blocking_send(SubagentEvent::StepFailed {
|
||||
step,
|
||||
@@ -459,7 +554,8 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
// subagent never tells the parent about the tokens consumed.
|
||||
let (mut tok_in, mut tok_out) = returned_usage.unwrap_or((0, 0));
|
||||
if tok_in == 0 {
|
||||
let prompt_chars: usize = messages.iter()
|
||||
let prompt_chars: usize = messages
|
||||
.iter()
|
||||
.filter_map(|m| m.content.as_deref())
|
||||
.map(str::len)
|
||||
.sum();
|
||||
@@ -475,7 +571,10 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
});
|
||||
|
||||
let has_tool_calls = response.tool_calls.is_some()
|
||||
&& response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
|
||||
&& response
|
||||
.tool_calls
|
||||
.as_ref()
|
||||
.is_some_and(|tc| !tc.is_empty());
|
||||
|
||||
let content = response.content.clone().unwrap_or_default();
|
||||
|
||||
@@ -608,7 +707,8 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
|
||||
for (tool_call, result) in results_vec {
|
||||
let tool_name = &tool_call.function.name;
|
||||
let args = crate::dto::chat::tool::sanitize_tool_arguments(&tool_call.function.arguments);
|
||||
let args =
|
||||
crate::dto::chat::tool::sanitize_tool_arguments(&tool_call.function.arguments);
|
||||
|
||||
let _ = tx.blocking_send(SubagentEvent::ToolCall {
|
||||
tool: tool_name.clone(),
|
||||
@@ -617,24 +717,28 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
|
||||
|
||||
match result {
|
||||
Ok(output_text) => {
|
||||
messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone()));
|
||||
messages.push(ChatMessage::tool_result(
|
||||
tool_call.id.clone(),
|
||||
output_text.clone(),
|
||||
));
|
||||
let _ = tx.blocking_send(SubagentEvent::ToolResult {
|
||||
tool: tool_name.clone(),
|
||||
args: args.clone(),
|
||||
});
|
||||
|
||||
let is_readonly = tool_name == "read"
|
||||
|| tool_name == "view_file"
|
||||
|| tool_name == "grep"
|
||||
|| tool_name == "grep_search"
|
||||
|| tool_name == "glob"
|
||||
|| tool_name == "dir_list"
|
||||
|
||||
let is_readonly = tool_name == "read"
|
||||
|| tool_name == "view_file"
|
||||
|| tool_name == "grep"
|
||||
|| tool_name == "grep_search"
|
||||
|| tool_name == "glob"
|
||||
|| tool_name == "dir_list"
|
||||
|| tool_name == "list_dir";
|
||||
|
||||
|
||||
if is_readonly {
|
||||
if let Some(ref findings) = ctx.workflow_findings {
|
||||
if let Ok(mut f) = findings.lock() {
|
||||
let args_json = serde_json::to_string(&args).unwrap_or_default();
|
||||
let args_json =
|
||||
serde_json::to_string(&args).unwrap_or_default();
|
||||
let mut shared_text = output_text;
|
||||
if shared_text.len() > 50_000 {
|
||||
shared_text.truncate(50_000);
|
||||
|
||||
@@ -41,16 +41,9 @@ impl AgentDefinition {
|
||||
}
|
||||
|
||||
/// Builder method: set the maximum step count for this agent.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_max_steps(mut self, steps: usize) -> Self {
|
||||
self.max_steps = Some(steps);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method: set the temperature for this agent.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_temperature(mut self, temp: f32) -> Self {
|
||||
self.temperature = Some(temp);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user