use std::ffi::OsString;
use std::path::{Path, PathBuf};
use agent_client_protocol::schema::v1::{
AuthMethod, AuthMethodId, RequestPermissionRequest, SessionModeId, SessionUpdate,
};
use serde_json::Value;
use crate::{
acp_runtime::{permission::PermissionDecision, AcpAgentPolicy, AcpSpawnPlan, ExtensionAnswer},
agent::CursorTool,
cli_runtime::{status_sink::RuntimeLabel, CliExecutable},
permission::PermissionMode,
};
use super::{
acp_config::{self, CursorConfigPaths, CursorFence},
acp_extensions, acp_permissions, executable,
models::CURSOR_LABEL,
spawn,
};
const CURSOR_CONFIG_DIR_ENV: &str = "CURSOR_CONFIG_DIR";
pub(crate) struct CursorAcpPolicy {
model: Option<String>,
permission_mode: PermissionMode,
tools: Vec<CursorTool>,
cwd: PathBuf,
config_paths: CursorConfigPaths,
prepared: Option<PreparedFence>,
}
struct PreparedFence {
fence: CursorFence,
config_dir: PathBuf,
}
impl CursorAcpPolicy {
pub(crate) fn new(
model: Option<String>,
permission_mode: PermissionMode,
tools: Vec<CursorTool>,
cwd: PathBuf,
config_paths: CursorConfigPaths,
) -> Self {
Self {
model,
permission_mode,
tools,
cwd,
config_paths,
prepared: None,
}
}
}
impl AcpAgentPolicy for CursorAcpPolicy {
fn label(&self) -> RuntimeLabel {
CURSOR_LABEL
}
fn resolve_executable(&self) -> Result<CliExecutable, String> {
executable::resolve().map_err(|error| error.to_string())
}
fn spawn_plan(&self, frozen: Option<Vec<String>>) -> Result<AcpSpawnPlan, String> {
let config_dir = &self
.prepared
.as_ref()
.ok_or("cursor: internal error: spawn before the managed config dir was prepared")?
.config_dir;
let mut plan = spawn::build_spawn_plan(&spawn::CursorSpawnRequest {
model: self.model.clone(),
cwd: self.cwd.clone(),
});
if let Some(arguments) = frozen {
plan = spawn::apply_frozen_identity_args(plan, &arguments);
}
Ok(AcpSpawnPlan {
argv: plan.args.iter().map(OsString::from).collect(),
cwd: plan.cwd,
env: vec![(
OsString::from(CURSOR_CONFIG_DIR_ENV),
config_dir.clone().into_os_string(),
)],
})
}
fn prepare(&mut self, run_dir: &Path) -> Result<Vec<String>, String> {
let allowed = spawn::map_permission_mode(self.permission_mode, &self.tools)
.map_err(|error| error.to_string())?;
let fence = acp_config::fence(self.permission_mode, allowed.tools());
let mut notices = Vec::new();
if let Some(warning) = super::session::unknown_cursor_model_warning(self.model.as_deref()) {
tracing::warn!("{warning}");
notices.push(warning);
}
let managed = acp_config::write_run_config(&self.config_paths.user_dir(), run_dir, &fence)
.map_err(|error| format!("cursor: could not prepare managed config: {error:#}"))?;
notices.extend(managed.notices);
self.prepared = Some(PreparedFence {
fence,
config_dir: managed.dir,
});
Ok(notices)
}
fn log_path(&self, output_file: &Path) -> PathBuf {
spawn::log_path(output_file)
}
fn auth_method(&self, _advertised: &[AuthMethod]) -> Option<AuthMethodId> {
None
}
fn session_mode(&self) -> Option<SessionModeId> {
match self.permission_mode {
PermissionMode::Plan => Some(SessionModeId::new("plan")),
PermissionMode::Bypass
| PermissionMode::Auto
| PermissionMode::AllowEdits
| PermissionMode::Supervised => None,
}
}
fn decide_permission(&self, request: &RequestPermissionRequest) -> PermissionDecision {
match &self.prepared {
Some(prepared) => {
acp_permissions::decide(&prepared.fence, self.permission_mode, request)
}
None => PermissionDecision::Reject,
}
}
fn answer_extension(&self, method: &str, params: &Value) -> Option<ExtensionAnswer> {
acp_extensions::answer(self.permission_mode, method, params).map(|reply| ExtensionAnswer {
reply,
events: Vec::new(),
})
}
fn normalize_update(&self, update: SessionUpdate) -> SessionUpdate {
acp_extensions::normalize_update(update)
}
}
#[cfg(test)]
#[path = "acp_policy_tests.rs"]
mod tests;