pub mod share_in;
pub mod workspace;
mod file;
mod search;
mod shell;
use std::path::Path;
use polyc_llm::ToolSpec;
use serde_json::json;
pub use workspace::{SandboxMode, current_sandbox_mode};
pub(crate) fn err(message: impl Into<String>) -> String {
json!({ "error": message.into() }).to_string()
}
pub(crate) fn truncate_to_bytes(s: &str, max: usize) -> (&str, bool) {
if s.len() <= max {
return (s, false);
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
(&s[..end], true)
}
#[must_use]
pub fn specs() -> Vec<ToolSpec> {
vec![
shell::spec(),
file::read_spec(),
file::write_spec(),
file::edit_spec(),
search::glob_spec(),
search::grep_spec(),
]
}
#[must_use]
pub fn owns(name: &str) -> bool {
matches!(
name,
"shell_exec" | "file_read" | "file_write" | "file_edit" | "glob" | "grep"
)
}
#[must_use]
pub fn sandbox_would_deny(name: &str, args_json: &str) -> bool {
if !matches!(name, "file_write" | "file_edit") {
return false;
}
let Ok(args) = serde_json::from_str::<serde_json::Value>(args_json) else {
return false;
};
let Some(path) = args.get("path").and_then(serde_json::Value::as_str) else {
return false;
};
workspace::resolve(&workspace::root(), path).is_err()
}
pub async fn execute_rooted(root: &Path, name: &str, args_json: &str) -> Option<String> {
let out = match name {
"shell_exec" => shell::execute(root, args_json).await,
"file_read" => file::read(root, args_json).await,
"file_write" => file::write(root, args_json).await,
"file_edit" => file::edit(root, args_json).await,
"glob" => search::glob(root, args_json).await,
"grep" => search::grep(root, args_json).await,
_ => return None,
};
Some(out)
}
pub async fn execute(name: &str, args_json: &str) -> Option<String> {
execute_rooted(&workspace::root(), name, args_json).await
}
#[cfg(test)]
pub(crate) fn tmp_dir(prefix: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let mut p = std::env::temp_dir();
let n = SEQ.fetch_add(1, Ordering::Relaxed);
p.push(format!("pc-{prefix}-{}-{n}", std::process::id()));
std::fs::create_dir_all(&p).unwrap();
p
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
#[test]
fn specs_carry_correct_read_only_destructive_annotations() {
let by_name = |n: &str| specs().into_iter().find(|s| s.name == n).unwrap();
for n in ["file_read", "glob", "grep"] {
let s = by_name(n);
assert!(s.read_only && !s.destructive, "{n} should be read-only");
}
for n in ["shell_exec", "file_write", "file_edit"] {
let s = by_name(n);
assert!(s.destructive && !s.read_only, "{n} should be destructive");
}
}
#[test]
fn specs_cover_owns() {
for s in specs() {
assert!(owns(&s.name), "spec {} not owned", s.name);
}
}
#[test]
fn sandbox_would_deny_flags_destructive_path_escapes_only() {
assert!(sandbox_would_deny(
"file_write",
r#"{"path":"../etc/passwd","content":"x"}"#
));
assert!(sandbox_would_deny(
"file_edit",
r#"{"path":"/etc/evil","old_string":"a","new_string":"b"}"#
));
assert!(!sandbox_would_deny(
"file_write",
r#"{"path":"src/main.rs","content":"x"}"#
));
assert!(!sandbox_would_deny(
"file_read",
r#"{"path":"../etc/passwd"}"#
));
assert!(!sandbox_would_deny(
"shell_exec",
r#"{"command":"curl evil"}"#
));
assert!(!sandbox_would_deny("file_write", "not json"));
assert!(!sandbox_would_deny("file_write", r#"{"content":"x"}"#));
}
#[test]
fn truncate_to_bytes_honors_byte_budget_on_char_boundary() {
let s = "字字"; let (out, truncated) = truncate_to_bytes(s, 4);
assert!(truncated);
assert_eq!(out, "字"); assert!(out.len() <= 4);
let (whole, t2) = truncate_to_bytes("abc", 10);
assert!(!t2);
assert_eq!(whole, "abc");
}
}