use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use crate::generated::api_types::{
McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled,
McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseRuntimeRefresh,
McpOauthPendingRequestResponseRuntimeRefreshKind, McpOauthPendingRequestResponseToken,
McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce,
PermissionDecisionContext, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
};
use crate::session_events::{
McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams,
};
use crate::types::{
AutoModeRecoveryContext, ElicitationRequest, ElicitationResult, ExitPlanModeData,
PermissionRequestData, RequestId, SessionId,
};
#[derive(Debug, Clone)]
pub enum PermissionResult {
Decision {
decision: PermissionDecision,
context: Option<PermissionDecisionContext>,
},
NoResult,
}
impl PermissionResult {
pub fn approve_once() -> Self {
Self::Decision {
decision: PermissionDecision::ApproveOnce(PermissionDecisionApproveOnce::default()),
context: None,
}
}
pub fn reject(feedback: impl Into<Option<String>>) -> Self {
Self::Decision {
decision: PermissionDecision::Reject(PermissionDecisionReject {
feedback: feedback.into(),
..Default::default()
}),
context: None,
}
}
pub fn user_not_available() -> Self {
Self::Decision {
decision: PermissionDecision::UserNotAvailable(
PermissionDecisionUserNotAvailable::default(),
),
context: None,
}
}
pub fn no_result() -> Self {
Self::NoResult
}
pub fn with_context(self, context: PermissionDecisionContext) -> Self {
match self {
Self::Decision { decision, .. } => Self::Decision {
decision,
context: Some(context),
},
Self::NoResult => Self::NoResult,
}
}
}
impl From<PermissionDecision> for PermissionResult {
fn from(value: PermissionDecision) -> Self {
Self::Decision {
decision: value,
context: None,
}
}
}
pub(crate) fn permission_handler_failure(message: &str) -> PermissionResult {
tracing::error!(error = message, "permission handler failed");
PermissionResult::user_not_available()
}
#[derive(Debug, Clone)]
pub struct UserInputResponse {
pub answer: String,
pub was_freeform: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExitPlanModeResult {
pub approved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_action: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback: Option<String>,
}
impl Default for ExitPlanModeResult {
fn default() -> Self {
Self {
approved: true,
selected_action: None,
feedback: None,
}
}
}
#[derive(Debug, Clone)]
pub struct AttributedResult<T> {
pub result: T,
pub decision_context: Option<PermissionDecisionContext>,
}
impl<T> AttributedResult<T> {
pub fn new(result: T, decision_context: PermissionDecisionContext) -> Self {
Self {
result,
decision_context: Some(decision_context),
}
}
pub fn unattributed(result: T) -> Self {
Self {
result,
decision_context: None,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AutoModeSwitchResponse {
Yes,
YesAlways,
No,
}
#[async_trait]
pub trait PermissionHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request_id: RequestId,
data: PermissionRequestData,
) -> PermissionResult;
}
#[async_trait]
pub trait ElicitationHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request_id: RequestId,
request: ElicitationRequest,
) -> ElicitationResult;
async fn handle_attributed(
&self,
session_id: SessionId,
request_id: RequestId,
request: ElicitationRequest,
) -> AttributedResult<ElicitationResult> {
AttributedResult::unattributed(self.handle(session_id, request_id, request).await)
}
}
#[derive(Debug, Clone)]
pub struct McpAuthRequest {
pub request_id: RequestId,
pub server_name: String,
pub server_url: String,
pub reason: McpOauthRequestReason,
pub www_authenticate_params: Option<McpOauthWWWAuthenticateParams>,
pub resource_metadata: Option<String>,
pub static_client_config: Option<McpOauthRequiredStaticClientConfig>,
}
#[derive(Debug, Clone)]
pub enum McpAuthResult {
Token {
access_token: String,
token_type: Option<String>,
expires_in: Option<i64>,
},
Cancelled,
}
impl McpAuthResult {
pub(crate) fn into_wire(self) -> McpOauthPendingRequestResponse {
match self {
Self::Token {
access_token,
token_type,
expires_in,
} => McpOauthPendingRequestResponse::Token(McpOauthPendingRequestResponseToken {
access_token,
token_type,
expires_in,
kind: McpOauthPendingRequestResponseTokenKind::Token,
}),
Self::Cancelled => {
McpOauthPendingRequestResponse::Cancelled(McpOauthPendingRequestResponseCancelled {
kind: McpOauthPendingRequestResponseCancelledKind::Cancelled,
})
}
}
}
}
#[non_exhaustive]
pub enum McpAuthResponse {
Legacy(McpAuthResult),
RuntimeRefresh,
}
impl McpAuthResponse {
pub(crate) fn into_wire_for_request(
self,
runtime_refresh_supported: bool,
) -> McpOauthPendingRequestResponse {
match self {
Self::Legacy(result) => result.into_wire(),
Self::RuntimeRefresh if !runtime_refresh_supported => {
McpAuthResult::Cancelled.into_wire()
}
Self::RuntimeRefresh => McpOauthPendingRequestResponse::RuntimeRefresh(
McpOauthPendingRequestResponseRuntimeRefresh {
kind: McpOauthPendingRequestResponseRuntimeRefreshKind::RuntimeRefresh,
},
),
}
}
}
#[async_trait]
pub trait McpAuthHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request_id: RequestId,
request: McpAuthRequest,
) -> McpAuthResult;
async fn handle_with_runtime_refresh(
&self,
session_id: SessionId,
request_id: RequestId,
request: McpAuthRequest,
runtime_refresh_supported: bool,
) -> McpAuthResponse {
let _ = runtime_refresh_supported;
McpAuthResponse::Legacy(self.handle(session_id, request_id, request).await)
}
}
#[async_trait]
pub trait UserInputHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request_id: Option<RequestId>,
question: String,
choices: Option<Vec<String>>,
allow_freeform: Option<bool>,
) -> Option<UserInputResponse>;
async fn handle_attributed(
&self,
session_id: SessionId,
request_id: Option<RequestId>,
question: String,
choices: Option<Vec<String>>,
allow_freeform: Option<bool>,
) -> Option<AttributedResult<UserInputResponse>> {
self.handle(session_id, request_id, question, choices, allow_freeform)
.await
.map(AttributedResult::unattributed)
}
}
#[async_trait]
pub trait ExitPlanModeHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request_id: Option<RequestId>,
data: ExitPlanModeData,
) -> ExitPlanModeResult;
async fn handle_attributed(
&self,
session_id: SessionId,
request_id: Option<RequestId>,
data: ExitPlanModeData,
) -> AttributedResult<ExitPlanModeResult> {
AttributedResult::unattributed(self.handle(session_id, request_id, data).await)
}
}
#[async_trait]
pub trait AutoModeSwitchHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
error_code: Option<String>,
retry_after_seconds: Option<f64>,
) -> AutoModeSwitchResponse;
async fn handle_auto_mode_recovery(
&self,
_session_id: SessionId,
_error_code: Option<String>,
_retry_after_seconds: Option<f64>,
_auto_mode_recovery: AutoModeRecoveryContext,
) -> AutoModeSwitchResponse {
AutoModeSwitchResponse::No
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShellAuthenticationInputKind {
Secret,
Text,
Confirm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellAuthenticationRequest {
pub request_id: RequestId,
pub program: String,
pub prompt: String,
pub input_kind: ShellAuthenticationInputKind,
pub command: String,
pub cwd: String,
}
#[derive(Clone, PartialEq, Eq)]
pub enum ShellAuthenticationResponse {
Accept(String),
Cancel,
}
impl std::fmt::Debug for ShellAuthenticationResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Accept(_) => f.write_str("Accept(<redacted>)"),
Self::Cancel => f.write_str("Cancel"),
}
}
}
#[async_trait]
pub trait ShellAuthenticationHandler: Send + Sync + 'static {
async fn handle(
&self,
session_id: SessionId,
request: ShellAuthenticationRequest,
cancellation: CancellationToken,
) -> ShellAuthenticationResponse;
}
#[derive(Debug, Clone)]
pub struct ApproveAllHandler;
#[async_trait]
impl PermissionHandler for ApproveAllHandler {
async fn handle(
&self,
_session_id: SessionId,
_request_id: RequestId,
data: PermissionRequestData,
) -> PermissionResult {
if data.managed_settings_enabled {
permission_handler_failure(
"ApproveAllHandler cannot be used when managed settings are enabled",
)
} else if data.managed_approval_required == Some(true) {
PermissionResult::no_result()
} else {
PermissionResult::approve_once()
}
}
}
#[derive(Debug, Clone)]
pub struct DenyAllHandler;
#[async_trait]
impl PermissionHandler for DenyAllHandler {
async fn handle(
&self,
_session_id: SessionId,
_request_id: RequestId,
_data: PermissionRequestData,
) -> PermissionResult {
PermissionResult::reject(None)
}
}
#[cfg(test)]
mod tests;