feat(token): add refresh token verification to TokenService

feat(bootstrap): create temporary settings and config files to prevent data loss

refactor(edit_log): switch from Vec to VecDeque for efficient memory management

fix(gateway): ensure store directories are created before starting the API server

refactor(bgbash): implement a global singleton for BashControl

feat(auth): enhance session authentication middleware to use SessionRepository

fix(edit_log_repo): update to use VecDeque for in-memory edit log storage

fix(memory_repo): add newline escaping for frontmatter fields

fix(session_lock_repo): improve error handling for lock file operations

fix(bash_tools): prevent path traversal in job_id argument

refactor(delete): enforce empty directory deletion in file system tools

fix(edit): optimize string replacement to only replace the first occurrence

fix(git_cred): improve credential management with piped input to git commands

feat(git_operator): add safety filter to block destructive git operations

fix(shell): register background jobs in Bash control

feat(spawn): add access tier specification for pipeline stages

refactor(hive_mind): run directives concurrently for improved performance

fix(auth): update refresh token verification in the refresh handler

fix(chat): optimize LLM client usage based on model matching

fix(conversations): enhance message deletion to target specific indices

feat(api): add JWT authentication middleware for all API routes

fix(state): implement refresh token verification in JwtTokenService

fix(daemon): improve usage tracking with saturating addition

fix(tui): handle compacted messages in the TUI state management
This commit is contained in:
asepharyana
2026-07-20 12:26:10 +07:00
parent 600ea041ef
commit a04651905f
26 changed files with 497 additions and 453 deletions
+9 -16
View File
@@ -37,6 +37,11 @@ impl Tool for BashOutput {
let job_id = arg_str(args, "job_id")?;
info!("Getting output for job: {job_id}");
// Prevent path traversal
if job_id.contains('/') || job_id.contains('\\') || job_id.contains("..") {
anyhow::bail!("invalid job_id '{job_id}': must not contain path separators");
}
// Read from the session's bash output directory
let output_dir = ctx.session_dir.join("bash-outputs");
let output_file = output_dir.join(&job_id);
@@ -80,23 +85,11 @@ impl Tool for BashKill {
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let job_id = crate::tools::arg_str(args, "job_id")?;
info!("bash_kill called for job: {job_id}");
// Try to kill by PID (if job_id is numeric) or by process name
if let Ok(pid) = job_id.parse::<u32>() {
use std::process::Command;
match Command::new("kill").arg(pid.to_string()).output() {
Ok(output) if output.status.success() => {
Ok(format!("Killed background job '{job_id}' (PID {pid})"))
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(format!("Failed to kill job '{job_id}': {stderr}"))
}
Err(e) => {
Ok(format!("Failed to kill job '{job_id}': {e}"))
}
}
if crate::bgbash::control::bash_control().cancel(&job_id) {
Ok(format!("Killed background job '{job_id}'"))
} else {
Ok(format!("Invalid job ID '{job_id}' — expected numeric PID"))
anyhow::bail!("no active background job found with ID '{job_id}'")
}
}
}
+4 -2
View File
@@ -45,8 +45,10 @@ impl Tool for Delete {
fs::remove_file(&path)?;
Ok(format!("Deleted file '{rel}'"))
} else if path.is_dir() {
fs::remove_dir_all(&path)?;
Ok(format!("Deleted directory '{rel}' and all contents"))
fs::remove_dir(&path).map_err(|e| {
anyhow::anyhow!("failed to delete directory '{rel}': {e} (directory must be empty)")
})?;
Ok(format!("Deleted empty directory '{rel}'"))
} else {
anyhow::bail!("'{rel}' is neither a file nor a directory")
}
+1 -1
View File
@@ -56,7 +56,7 @@ impl Tool for Edit {
anyhow::bail!("old text not found in '{}'", rel);
}
let new_content = content.replace(&old, &new);
let new_content = content.replacen(&old, &new, 1);
fs::write(&path, &new_content)?;
Ok(format!(
+20 -5
View File
@@ -3,6 +3,8 @@
use crate::tools::{execute_cmd, Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use std::io::Write;
use std::process::{Command, Stdio};
pub struct GitCred;
@@ -49,10 +51,15 @@ impl Tool for GitCred {
let url = crate::tools::arg_str(args, "url")?;
let username = crate::tools::arg_str(args, "username")?;
let password = crate::tools::arg_str(args, "password")?;
let _input = format!("url={url}\nusername={username}\npassword={password}\n");
let _output = execute_cmd(
std::process::Command::new("git").args(["credential", "approve"]),
)?;
let input = format!("url={url}\nusername={username}\npassword={password}\n");
let mut child = Command::new("git")
.args(["credential", "approve"])
.stdin(Stdio::piped())
.spawn()?;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(input.as_bytes())?;
}
child.wait()?;
Ok(format!("Credential stored for {url}"))
}
"list" => {
@@ -63,7 +70,15 @@ impl Tool for GitCred {
}
"erase" => {
let url = crate::tools::arg_str(args, "url")?;
let _input = format!("url={url}\n");
let input = format!("url={url}\n");
let mut child = Command::new("git")
.args(["credential", "reject"])
.stdin(Stdio::piped())
.spawn()?;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(input.as_bytes())?;
}
child.wait()?;
Ok(format!("Credential erased for {url}"))
}
_ => anyhow::bail!("unknown action: {}", action),
@@ -1,5 +1,6 @@
//! Git operator tool — commit, push, pull, branch operations.
use crate::tools::shell_filter::git::check_git_destructive;
use crate::tools::{execute_cmd, Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
@@ -46,6 +47,12 @@ impl Tool for GitOperator {
})
.unwrap_or_default();
// Safety filter: block destructive git operations
let cmd_str = format!("git {} {}", operation, extra_args.join(" "));
if let Err(e) = check_git_destructive(&cmd_str) {
anyhow::bail!("blocked: {e}");
}
let mut cmd = std::process::Command::new("git");
cmd.arg(&operation);
for arg in &extra_args {
+1
View File
@@ -61,6 +61,7 @@ impl Tool for Bash {
if run_in_background {
let job = crate::bgbash::job::spawn_bash_job(cmd);
crate::bgbash::control::bash_control().register(job.clone());
return Ok(format!("Background job: {}", job.id));
}
+15 -3
View File
@@ -151,7 +151,8 @@ impl Tool for SpawnPipeline {
"items": {
"type": "object",
"properties": {
"directive": {"type": "string", "description": "Directive for this pipeline stage"}
"directive": {"type": "string", "description": "Directive for this pipeline stage"},
"access": {"type": "string", "enum": ["read", "write", "full"], "description": "Access tier for this stage"}
},
"required": ["directive"]
},
@@ -200,17 +201,28 @@ impl Tool for SpawnPipeline {
.unwrap_or("")
.to_string();
let access_str = stage
.get("access")
.and_then(|v| v.as_str())
.unwrap_or("full");
let access = match access_str {
"read" => AccessTier::Read,
"write" => AccessTier::Write,
_ => AccessTier::Full,
};
let subagent_ctx = SubagentContext::new(
directive.clone(),
ctx.clone(),
"full".to_string(),
access_str.to_string(),
base_url.clone(),
api_key.clone(),
model.clone(),
);
let result = rt.block_on(async {
run_agent(subagent_ctx, &directive, AccessTier::Full, ctx.clone()).await
run_agent(subagent_ctx, &directive, access, ctx.clone()).await
})?;
pipeline_result.push_str(&format!("Stage {}: {}\n", i, result));