feat: enhance safety filters for shell commands by normalizing ANSI-C quoting

This commit is contained in:
asepharyana
2026-07-13 04:10:08 +07:00
parent a080957c26
commit d09e440e7e
14 changed files with 383 additions and 85 deletions
+2 -2
View File
@@ -46,9 +46,9 @@ impl Default for AppConfig {
});
providers.insert("router".to_string(), ProviderConfig {
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
api_key_env: None,
api_key_env: Some("ROUTER_API_KEY".to_string()),
default_model: Some("claude-opus-4-8".to_string()),
default_api_key: Some("sk-5281d60771dcd653-n01ipa-296e9a56".to_string()),
default_api_key: None,
});
let mut model_roles = HashMap::new();
model_roles.insert("default".to_string(), ModelRole {
+6 -2
View File
@@ -63,8 +63,12 @@ impl EditLog {
/// any filesystem operation fails.
pub fn append(&mut self, entry: EditLogEntry) -> std::io::Result<()> {
let line = serde_json::to_string(&entry)? + "\n";
let parent = self.path.parent().unwrap();
std::fs::create_dir_all(parent)?;
// Ensure parent directory exists; fall back to the current
// directory if path has no parent (should not happen in practice
// since EditLog::new always joins to a session dir).
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
+16 -2
View File
@@ -92,8 +92,22 @@ impl Memory {
self.content
);
let tmp = parent.join(format!(".{}.tmp", uuid::Uuid::new_v4()));
std::fs::write(&tmp, &content)?;
std::fs::rename(&tmp, path)?;
// Write to temp file with fsync for crash safety (prevents
// partial writes surviving a power loss).
{
let mut f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&tmp)?;
use std::io::Write;
f.write_all(content.as_bytes())?;
f.sync_all()?;
}
std::fs::rename(&tmp, &path)?;
// Sync the parent directory so the rename is durable.
if let Some(p) = path.parent() {
let _ = std::fs::File::open(p).and_then(|d| d.sync_all());
}
Ok(())
}
+50 -15
View File
@@ -3,6 +3,7 @@
use std::path::{Path, PathBuf};
use std::fs;
use std::io::Write;
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
/// auto-removed on drop.
@@ -21,29 +22,63 @@ impl SessionLock {
}
}
/// Attempt to acquire the session lock.
/// Attempt to acquire the session lock using an atomic file creation.
///
/// Flow: if `.lock` exists, read the PID inside it and check
/// `is_alive` — if that process is still running, fail to acquire →
/// otherwise (no lock file, unreadable PID, or dead owner) write our
/// own PID into `.lock` and succeed.
/// Flow: try `O_CREAT | O_EXCL` via `create_new(true)` → if that
/// succeeds, the lock is ours — write our PID and return ok. If the
/// file already exists, read the PID inside it and check `is_alive`:
/// if that process is still running, fail to acquire; otherwise the
/// lock is stale — overwrite it with our own PID and succeed.
///
/// Why: a stale lock file from a crashed process must not permanently
/// block new sessions, so liveness is re-checked via `kill(pid, 0)`
/// rather than trusting the file's mere existence.
/// Why: `create_new(true)` is atomic on POSIX (unlike the previous
/// read-then-write pattern which had a TOCTOU race between checking
/// `path.exists()` and writing). The stale-lock recovery path reads
/// the stale PID and verifies liveness via `kill(pid, 0)`.
///
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
/// process holds it, `Err` on I/O failure.
pub fn try_lock(&self) -> std::io::Result<bool> {
if self.path.exists() {
let content = fs::read_to_string(&self.path).unwrap_or_default();
if let Ok(pid) = content.trim().parse::<u32>() {
if self.is_alive(pid) {
return Ok(false);
}
// Phase 1: try atomic create. If it succeeds, the lock is ours.
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.path)
{
Ok(mut file) => {
write!(file, "{}", self.pid)?;
file.sync_all()?;
return Ok(true);
}
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
// Lock file exists — check if it's stale.
}
Err(e) => return Err(e),
}
// Phase 2: lock file exists — check liveness of the owning process.
let content = fs::read_to_string(&self.path).unwrap_or_default();
if let Ok(pid) = content.trim().parse::<u32>() {
if self.is_alive(pid) {
return Ok(false);
}
}
fs::write(&self.path, self.pid.to_string())?;
// Phase 3: stale lock — overwrite it atomically (best-effort).
// Use a temp file + rename to avoid partial writes corrupting the lock.
let tmp = self.path.with_extension("lock.tmp");
{
let mut tmp_file = fs::OpenOptions::new()
.create(true)
.write(true)
.open(&tmp)?;
write!(tmp_file, "{}", self.pid)?;
tmp_file.sync_all()?;
}
fs::rename(&tmp, &self.path)?;
// Sync the parent directory so the rename survives a crash.
if let Some(parent) = self.path.parent() {
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
}
Ok(true)
}