use std::path::{Path, PathBuf};
use crate::{agent::CursorTool, permission::PermissionMode};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct AllowedTools {
tools: Vec<CursorTool>,
}
impl AllowedTools {
pub(crate) fn tools(&self) -> &[CursorTool] {
&self.tools
}
}
#[derive(Clone, Debug)]
pub(crate) struct CursorSpawnRequest {
pub(crate) model: Option<String>,
pub(crate) cwd: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CursorSpawnPlan {
pub(crate) args: Vec<String>,
pub(crate) cwd: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum CursorSpawnError {
#[error(
"cursor agents run only in Plan or Bypass, not {0}: Rho cannot ask a human to approve Cursor tool calls"
)]
ApprovalUnsupported(PermissionMode),
#[error(
"cursor agents require a nonempty tools list: it is the source of Cursor's tool fence"
)]
NoToolsAllowed,
}
pub(crate) fn map_permission_mode(
mode: PermissionMode,
tools: &[CursorTool],
) -> Result<AllowedTools, CursorSpawnError> {
let tools: Vec<CursorTool> = match mode {
PermissionMode::Plan => tools
.iter()
.copied()
.filter(|tool| tool.is_read_only())
.collect(),
PermissionMode::Bypass => tools.to_vec(),
PermissionMode::Auto | PermissionMode::AllowEdits | PermissionMode::Supervised => {
return Err(CursorSpawnError::ApprovalUnsupported(mode));
}
};
if tools.is_empty() {
return Err(CursorSpawnError::NoToolsAllowed);
}
Ok(AllowedTools { tools })
}
const FROZEN_IDENTITY_FLAGS: &[&str] = &["--model"];
const ACP_SUBCOMMAND: &str = "acp";
pub(crate) fn apply_frozen_identity_args(
mut plan: CursorSpawnPlan,
frozen: &[String],
) -> CursorSpawnPlan {
let subcommand = plan.args.pop();
debug_assert_eq!(subcommand.as_deref(), Some(ACP_SUBCOMMAND));
plan.args =
crate::cli_runtime::overlay_identity_flags(plan.args, frozen, FROZEN_IDENTITY_FLAGS);
plan.args.push(ACP_SUBCOMMAND.into());
plan
}
pub(crate) fn build_spawn_plan(request: &CursorSpawnRequest) -> CursorSpawnPlan {
let mut args = vec!["--trust".to_string()];
if let Some(model) = &request.model {
args.push("--model".into());
args.push(model.clone());
}
args.push(ACP_SUBCOMMAND.into());
CursorSpawnPlan {
args,
cwd: request.cwd.clone(),
}
}
pub(crate) fn log_path(output_file: &Path) -> PathBuf {
output_file.with_file_name(crate::subagent::LOG_FILE_NAME)
}
#[cfg(test)]
#[path = "spawn_tests.rs"]
mod tests;