Skip to main content

github_copilot_sdk/
handler.rs

1//! Optional session-callback traits.
2//!
3//! Each callback the CLI may dispatch (permission requests, elicitation
4//! prompts, user-input questions, exit-plan-mode prompts,
5//! auto-mode-switch prompts) has its own focused trait with a single
6//! `handle` method.
7//!
8//! Handlers are **optional**: install only the ones the application cares
9//! about. The SDK derives the corresponding wire flag on
10//! `session.create` / `session.resume` from the presence of each handler,
11//! so the runtime does not emit broadcasts this client would never
12//! respond to.
13//!
14//! Tool dispatch uses its own per-tool registry built from
15//! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to
16//! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools).
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21use crate::generated::api_types::{
22    McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled,
23    McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseToken,
24    McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce,
25    PermissionDecisionReject, PermissionDecisionUserNotAvailable,
26};
27use crate::session_events::{
28    McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams,
29};
30use crate::types::{
31    ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId,
32    SessionId,
33};
34
35/// Decision returned by a [`PermissionHandler`].
36///
37/// Either a concrete wire-level [`PermissionDecision`] (approve, reject,
38/// approve-for-session, approve-permanently, user-not-available, …) or
39/// [`PermissionResult::NoResult`], which tells the SDK to suppress its
40/// response so another connected client can answer instead.
41#[derive(Debug, Clone)]
42pub enum PermissionResult {
43    /// Send a permission decision on the wire.
44    Decision(PermissionDecision),
45    /// Decline to respond to this request, allowing another connected
46    /// client to answer instead. The SDK suppresses the response.
47    NoResult,
48}
49
50impl PermissionResult {
51    /// Approve this single request.
52    pub fn approve_once() -> Self {
53        Self::Decision(PermissionDecision::ApproveOnce(
54            PermissionDecisionApproveOnce::default(),
55        ))
56    }
57
58    /// Reject the request, optionally forwarding feedback to the LLM.
59    pub fn reject(feedback: impl Into<Option<String>>) -> Self {
60        Self::Decision(PermissionDecision::Reject(PermissionDecisionReject {
61            feedback: feedback.into(),
62            ..Default::default()
63        }))
64    }
65
66    /// Deny because no user is available to confirm.
67    pub fn user_not_available() -> Self {
68        Self::Decision(PermissionDecision::UserNotAvailable(
69            PermissionDecisionUserNotAvailable::default(),
70        ))
71    }
72
73    /// Decline to respond, allowing another connected client to answer
74    /// instead.
75    pub fn no_result() -> Self {
76        Self::NoResult
77    }
78}
79
80impl From<PermissionDecision> for PermissionResult {
81    fn from(value: PermissionDecision) -> Self {
82        Self::Decision(value)
83    }
84}
85
86/// Response to a user input request.
87#[derive(Debug, Clone)]
88pub struct UserInputResponse {
89    /// The user's answer text.
90    pub answer: String,
91    /// Whether the answer was free-form (not a preset choice).
92    pub was_freeform: bool,
93}
94
95/// Result of an exit-plan-mode request.
96#[derive(Debug, Clone, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct ExitPlanModeResult {
99    /// Whether the user approved exiting plan mode.
100    pub approved: bool,
101    /// The action the user selected (if any).
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub selected_action: Option<String>,
104    /// Optional feedback text from the user.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub feedback: Option<String>,
107}
108
109impl Default for ExitPlanModeResult {
110    fn default() -> Self {
111        Self {
112            approved: true,
113            selected_action: None,
114            feedback: None,
115        }
116    }
117}
118
119/// Response to an auto-mode-switch request.
120#[non_exhaustive]
121#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum AutoModeSwitchResponse {
124    /// Approve the auto-mode switch for this rate-limit cycle only.
125    Yes,
126    /// Approve and remember -- auto-accept future auto-mode switches in
127    /// this session without prompting.
128    YesAlways,
129    /// Decline the auto-mode switch. The session stays on the current
130    /// model and surfaces the rate-limit error.
131    No,
132}
133
134/// Handler for `permission.requested` broadcasts.
135///
136/// Install via
137/// [`SessionConfig::with_permission_handler`](crate::types::SessionConfig::with_permission_handler)
138/// (or the matching method on [`ResumeSessionConfig`](crate::types::ResumeSessionConfig)).
139/// When no permission handler is supplied, the SDK sends
140/// `requestPermission: false` on the wire and the runtime short-circuits
141/// permission prompts for this client.
142#[async_trait]
143pub trait PermissionHandler: Send + Sync + 'static {
144    /// Resolve a permission request.
145    async fn handle(
146        &self,
147        session_id: SessionId,
148        request_id: RequestId,
149        data: PermissionRequestData,
150    ) -> PermissionResult;
151}
152
153/// Handler for `elicitation.requested` broadcasts.
154///
155/// When unset, `requestElicitation: false` goes on the wire.
156#[async_trait]
157pub trait ElicitationHandler: Send + Sync + 'static {
158    /// Respond to an elicitation prompt (form, URL confirm, etc.).
159    async fn handle(
160        &self,
161        session_id: SessionId,
162        request_id: RequestId,
163        request: ElicitationRequest,
164    ) -> ElicitationResult;
165}
166
167/// MCP OAuth request that the SDK host can satisfy with a host-acquired token.
168#[derive(Debug, Clone)]
169pub struct McpAuthRequest {
170    /// Identifier for the pending MCP OAuth request.
171    pub request_id: RequestId,
172    /// Display name of the MCP server that requires OAuth.
173    pub server_name: String,
174    /// URL of the MCP server that requires OAuth.
175    pub server_url: String,
176    /// Why the runtime is requesting host-provided OAuth credentials.
177    pub reason: McpOauthRequestReason,
178    /// Parsed WWW-Authenticate parameters from the MCP server, if available.
179    pub www_authenticate_params: Option<McpOauthWWWAuthenticateParams>,
180    /// Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available.
181    pub resource_metadata: Option<String>,
182    /// Static OAuth client configuration, if the server specifies one.
183    pub static_client_config: Option<McpOauthRequiredStaticClientConfig>,
184}
185
186/// Result returned by an MCP auth request handler.
187#[derive(Debug, Clone)]
188pub enum McpAuthResult {
189    /// Supplies host-acquired OAuth token data.
190    Token {
191        /// Access token acquired by the SDK host.
192        access_token: String,
193        /// OAuth token type. Defaults to Bearer when omitted.
194        token_type: Option<String>,
195        /// Token lifetime in seconds, if known.
196        expires_in: Option<i64>,
197    },
198    /// Declines or cancels the pending OAuth request.
199    Cancelled,
200}
201
202impl McpAuthResult {
203    pub(crate) fn into_wire(self) -> McpOauthPendingRequestResponse {
204        match self {
205            Self::Token {
206                access_token,
207                token_type,
208                expires_in,
209            } => McpOauthPendingRequestResponse::Token(McpOauthPendingRequestResponseToken {
210                access_token,
211                token_type,
212                expires_in,
213                kind: McpOauthPendingRequestResponseTokenKind::Token,
214            }),
215            Self::Cancelled => {
216                McpOauthPendingRequestResponse::Cancelled(McpOauthPendingRequestResponseCancelled {
217                    kind: McpOauthPendingRequestResponseCancelledKind::Cancelled,
218                })
219            }
220        }
221    }
222}
223
224/// Handler for MCP server OAuth requests.
225#[async_trait]
226pub trait McpAuthHandler: Send + Sync + 'static {
227    /// Resolve an MCP OAuth request with host token data or cancellation.
228    async fn handle(
229        &self,
230        session_id: SessionId,
231        request_id: RequestId,
232        request: McpAuthRequest,
233    ) -> McpAuthResult;
234}
235
236/// Handler for `user_input.requested` events from the `ask_user` tool.
237///
238/// When unset, `requestUserInput: false` goes on the wire and the
239/// `ask_user` tool is disabled for the session.
240#[async_trait]
241pub trait UserInputHandler: Send + Sync + 'static {
242    /// Answer a question on behalf of the user. Return `None` to signal
243    /// "no answer available".
244    async fn handle(
245        &self,
246        session_id: SessionId,
247        question: String,
248        choices: Option<Vec<String>>,
249        allow_freeform: Option<bool>,
250    ) -> Option<UserInputResponse>;
251}
252
253/// Handler for `exit_plan_mode.requested` events. When unset,
254/// `requestExitPlanMode: false` goes on the wire.
255#[async_trait]
256pub trait ExitPlanModeHandler: Send + Sync + 'static {
257    /// Decide whether to leave plan mode.
258    async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult;
259}
260
261/// Handler for `auto_mode_switch.requested` events. When unset,
262/// `requestAutoModeSwitch: false` goes on the wire.
263#[async_trait]
264pub trait AutoModeSwitchHandler: Send + Sync + 'static {
265    /// Decide whether to fall back to the auto model after an eligible
266    /// rate-limit error. `retry_after_seconds`, when present, is the
267    /// number of seconds until the rate limit resets.
268    async fn handle(
269        &self,
270        session_id: SessionId,
271        error_code: Option<String>,
272        retry_after_seconds: Option<f64>,
273    ) -> AutoModeSwitchResponse;
274}
275
276/// A [`PermissionHandler`] that approves every request. Useful for CLI
277/// tools, scripts, and tests that don't need interactive permission
278/// prompts.
279#[derive(Debug, Clone)]
280pub struct ApproveAllHandler;
281
282#[async_trait]
283impl PermissionHandler for ApproveAllHandler {
284    async fn handle(
285        &self,
286        _session_id: SessionId,
287        _request_id: RequestId,
288        _data: PermissionRequestData,
289    ) -> PermissionResult {
290        PermissionResult::approve_once()
291    }
292}
293
294/// A [`PermissionHandler`] that denies every request.
295#[derive(Debug, Clone)]
296pub struct DenyAllHandler;
297
298#[async_trait]
299impl PermissionHandler for DenyAllHandler {
300    async fn handle(
301        &self,
302        _session_id: SessionId,
303        _request_id: RequestId,
304        _data: PermissionRequestData,
305    ) -> PermissionResult {
306        PermissionResult::reject(None)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[tokio::test]
315    async fn approve_all_handler_returns_approved() {
316        let result = ApproveAllHandler
317            .handle(
318                SessionId::from("s1"),
319                RequestId::new("1"),
320                PermissionRequestData::default(),
321            )
322            .await;
323        assert!(matches!(
324            result,
325            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
326        ));
327    }
328
329    #[tokio::test]
330    async fn deny_all_handler_returns_denied() {
331        let result = DenyAllHandler
332            .handle(
333                SessionId::from("s1"),
334                RequestId::new("1"),
335                PermissionRequestData::default(),
336            )
337            .await;
338        assert!(matches!(
339            result,
340            PermissionResult::Decision(PermissionDecision::Reject(_))
341        ));
342    }
343
344    #[test]
345    fn mcp_auth_result_token_converts_to_wire_response() {
346        let wire = McpAuthResult::Token {
347            access_token: "host-token".to_string(),
348            token_type: Some("Bearer".to_string()),
349            expires_in: Some(3600),
350        }
351        .into_wire();
352
353        match wire {
354            McpOauthPendingRequestResponse::Token(token) => {
355                assert_eq!(token.access_token, "host-token");
356                assert_eq!(token.token_type.as_deref(), Some("Bearer"));
357                assert_eq!(token.expires_in, Some(3600));
358            }
359            McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"),
360        }
361    }
362}