use std::path::{Path, PathBuf};
use scv_core::ToolError;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct AgentArgs {
#[serde(default, deserialize_with = "blank_as_none")]
pub(crate) agent: Option<String>,
pub(crate) prompt: String,
pub(crate) timeout_seconds: Option<u64>,
#[serde(default, deserialize_with = "blank_as_none")]
pub(crate) session: Option<String>,
#[serde(default, deserialize_with = "blank_as_none")]
pub(crate) cwd: Option<String>,
#[serde(default, deserialize_with = "blank_as_none")]
pub(crate) model: Option<String>,
#[serde(default, deserialize_with = "blank_as_none")]
pub(crate) effort: Option<String>,
}
fn blank_as_none<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<String>, D::Error> {
let value = Option::<String>::deserialize(deserializer)?;
Ok(value.filter(|value| !value.trim().is_empty()))
}
pub(crate) const MAX_AGENT_CWD_BYTES: usize = 4096;
pub(crate) fn validate_agent_cwd(cwd: &str) -> Result<(), ToolError> {
if cwd.trim().is_empty() || cwd.len() > MAX_AGENT_CWD_BYTES || cwd.contains('\0') {
return Err(ToolError::invalid_arguments(format!(
"cwd must be a non-empty directory path of at most {MAX_AGENT_CWD_BYTES} bytes"
)));
}
Ok(())
}
pub(crate) fn resolve_agent_cwd(workspace: &Path, cwd: Option<&str>) -> Result<PathBuf, ToolError> {
let root = std::fs::canonicalize(workspace)
.map_err(|error| ToolError::failed(format!("resolve workspace: {error}")))?;
let Some(cwd) = cwd else {
return Ok(root);
};
validate_agent_cwd(cwd)?;
let resolved = std::fs::canonicalize(root.join(cwd))
.map_err(|error| ToolError::invalid_arguments(format!("cwd {cwd:?}: {error}")))?;
if !resolved.starts_with(&root) {
return Err(ToolError::invalid_arguments(format!(
"cwd {cwd:?} is outside the workspace"
)));
}
if !resolved.is_dir() {
return Err(ToolError::invalid_arguments(format!(
"cwd {cwd:?} is not a directory"
)));
}
Ok(resolved)
}
pub const AGENT_EFFORTS: [&str; 5] = ["low", "medium", "high", "xhigh", "max"];
pub fn valid_model_name(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& !value.starts_with(['-', '@'])
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || "._:/@[]-".contains(c))
}
pub fn valid_effort(value: &str) -> bool {
AGENT_EFFORTS.contains(&value)
}