use std::sync::atomic::{AtomicU64, Ordering};
use agent_client_protocol::schema::v1::{
PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest,
SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption, SessionId,
SessionMode, SessionModeId, SetSessionConfigOptionRequest, ToolCallId, ToolCallUpdate,
ToolCallUpdateFields, ToolKind,
};
use agent_client_protocol::Error as AcpError;
use crate::permission::PermissionMode;
use rho_sdk::CapabilityKind;
pub(super) const MODE_CONFIG_ID: &str = "mode";
const OPTION_ALLOW_ONCE: &str = "allow_once";
const OPTION_ALLOW_ALWAYS: &str = "allow_always";
const OPTION_REJECT_ONCE: &str = "reject_once";
pub(super) fn mode_list() -> Vec<SessionMode> {
PermissionMode::ALL
.into_iter()
.map(|mode| SessionMode::new(mode_id(mode), mode.label()))
.collect()
}
pub(super) fn mode_id(mode: PermissionMode) -> SessionModeId {
SessionModeId::new(mode.as_str())
}
pub(super) fn parse_mode_id(id: &str) -> Option<PermissionMode> {
PermissionMode::ALL
.into_iter()
.find(|mode| mode.as_str() == id)
}
pub(super) fn mode_config_option(current: PermissionMode) -> SessionConfigOption {
SessionConfigOption::select(
MODE_CONFIG_ID,
"Mode",
current.as_str(),
PermissionMode::ALL
.into_iter()
.map(|mode| SessionConfigSelectOption::new(mode.as_str(), mode.label()))
.collect::<Vec<_>>(),
)
.description("Which tool calls run without asking.")
.category(SessionConfigOptionCategory::Mode)
}
pub(super) fn parse_mode_config_request(
request: &SetSessionConfigOptionRequest,
) -> Result<PermissionMode, AcpError> {
let Some(value) = request.value.as_value_id() else {
return Err(AcpError::invalid_params().data("mode requires a select value id"));
};
parse_mode_id(value.0.as_ref()).ok_or_else(|| {
AcpError::invalid_params().data(format!("unknown session mode '{}'", value.0.as_ref()))
})
}
pub(super) fn next_placeholder_tool_call_id(counter: &AtomicU64) -> String {
format!("pending-{}", counter.fetch_add(1, Ordering::Relaxed) + 1)
}
pub(super) fn permission_request(
session_id: &SessionId,
request: &rho_sdk::ApprovalRequest,
placeholder_tool_call_id: &str,
) -> RequestPermissionRequest {
let tool_call_id = request
.tool_call_id()
.map(|id| id.as_str().to_string())
.unwrap_or_else(|| placeholder_tool_call_id.to_string());
RequestPermissionRequest::new(
session_id.clone(),
ToolCallUpdate::new(
ToolCallId::new(tool_call_id),
ToolCallUpdateFields::new()
.title(request.reason())
.kind(tool_kind_for_capability(request.capability().kind())),
),
permission_options(),
)
}
pub(super) fn decision_for(outcome: &RequestPermissionOutcome) -> rho_sdk::ApprovalDecision {
match outcome {
RequestPermissionOutcome::Cancelled => rho_sdk::ApprovalDecision::Deny {
reason: "permission request cancelled".into(),
},
RequestPermissionOutcome::Selected(selected) => match selected.option_id.0.as_ref() {
OPTION_ALLOW_ONCE => rho_sdk::ApprovalDecision::AllowOnce,
OPTION_ALLOW_ALWAYS => rho_sdk::ApprovalDecision::AllowForSession,
OPTION_REJECT_ONCE => rho_sdk::ApprovalDecision::Deny {
reason: "permission rejected".into(),
},
_ => rho_sdk::ApprovalDecision::Deny {
reason: "unknown permission option".into(),
},
},
_ => rho_sdk::ApprovalDecision::Deny {
reason: "unknown permission outcome".into(),
},
}
}
fn permission_options() -> Vec<PermissionOption> {
vec![
PermissionOption::new(
OPTION_ALLOW_ONCE,
"Allow once",
PermissionOptionKind::AllowOnce,
),
PermissionOption::new(
OPTION_ALLOW_ALWAYS,
"Allow for this session",
PermissionOptionKind::AllowAlways,
),
PermissionOption::new(
OPTION_REJECT_ONCE,
"Reject once",
PermissionOptionKind::RejectOnce,
),
]
}
fn tool_kind_for_capability(kind: CapabilityKind) -> ToolKind {
match kind {
CapabilityKind::Read | CapabilityKind::InstructionDiscovery => ToolKind::Read,
CapabilityKind::Write => ToolKind::Edit,
CapabilityKind::Process => ToolKind::Execute,
CapabilityKind::Network => ToolKind::Fetch,
CapabilityKind::Skill => ToolKind::Other,
_ => ToolKind::Other,
}
}
#[cfg(test)]
#[path = "permission_tests.rs"]
mod tests;