Skip to main content

codex_app_server_client/
approvals.rs

1use std::{
2    future::{ready, Future},
3    pin::Pin,
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7use crate::protocol::{
8    ApplyPatchApprovalResponse, CommandExecutionApprovalDecision,
9    CommandExecutionRequestApprovalResponse, CurrentTimeReadResponse, ExecCommandApprovalResponse,
10    FileChangeApprovalDecision, FileChangeRequestApprovalResponse, GrantedPermissionProfile,
11    McpServerElicitationAction, McpServerElicitationRequestResponse, PermissionGrantScope,
12    PermissionsRequestApprovalResponse, RequestPermissionProfile, ReviewDecision, ServerRequest,
13};
14use crate::{PendingServerRequest, Result};
15
16/// Boxed future returned by [`ApprovalHandler`].
17///
18/// The future is lifetime-bound to the handler and request so custom handlers
19/// can borrow their own UI/channel state and inspect the typed request while
20/// waiting asynchronously for a decision.
21pub type ApprovalFuture<'a> = Pin<Box<dyn Future<Output = ServerRequestReply> + Send + 'a>>;
22
23/// A typed reply for a server-to-client app-server request.
24///
25/// `Result` is any JSON value that matches the request's generated response
26/// type. `Error` sends a JSON-RPC error reply to the app-server.
27#[derive(Clone, Debug, PartialEq)]
28pub enum ServerRequestReply {
29    Result(serde_json::Value),
30    Error {
31        code: i64,
32        message: String,
33        data: Option<serde_json::Value>,
34    },
35}
36
37impl ServerRequestReply {
38    pub fn send(self, request: PendingServerRequest) -> Result<()> {
39        match self {
40            Self::Result(result) => request.respond(result),
41            Self::Error {
42                code,
43                message,
44                data,
45            } => request.respond_error(code, message, data),
46        }
47    }
48}
49
50/// Policy hook for server-to-client app-server requests.
51///
52/// Implementations return a future so human-in-the-loop UI, channels, or other
53/// async policy engines can decide without blocking a Tokio worker while the
54/// session drains app-server events.
55pub trait ApprovalHandler: Send + Sync {
56    fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a>;
57}
58
59/// Approval handler that rejects every server request with a JSON-RPC error.
60///
61/// This is the safest first-mile default for examples and smoke tests: Codex
62/// can keep running, but no command, file-change, permission, elicitation, or
63/// dynamic-tool request is silently accepted.
64#[derive(Clone, Debug)]
65pub struct DenyAllApprovalHandler {
66    code: i64,
67    message: String,
68}
69
70impl Default for DenyAllApprovalHandler {
71    fn default() -> Self {
72        Self {
73            code: -32000,
74            message: "codex-app-server-client: no approval handler accepted this request"
75                .to_owned(),
76        }
77    }
78}
79
80impl DenyAllApprovalHandler {
81    /// Creates a deny-all handler with a custom error message.
82    pub fn new(message: impl Into<String>) -> Self {
83        Self {
84            message: message.into(),
85            ..Self::default()
86        }
87    }
88}
89
90/// Approval handler that approves Codex approval requests.
91///
92/// This answers command, file-change, legacy command, legacy patch, and
93/// permission-profile approval requests with the schema's positive response.
94/// Requests that require app-specific data, such as dynamic tool calls, user
95/// input, auth-token refreshes, and MCP elicitations, are rejected with a clear
96/// JSON-RPC error instead of guessing.
97#[derive(Clone, Debug, Default)]
98pub struct AllowAllApprovalHandler;
99
100impl ApprovalHandler for AllowAllApprovalHandler {
101    fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a> {
102        Box::pin(ready(match request {
103            ServerRequest::ItemCommandExecutionRequestApproval { .. } => {
104                serialized_result(CommandExecutionRequestApprovalResponse {
105                    decision: CommandExecutionApprovalDecision::Accept,
106                })
107            }
108            ServerRequest::ItemFileChangeRequestApproval { .. } => {
109                serialized_result(FileChangeRequestApprovalResponse {
110                    decision: FileChangeApprovalDecision::Accept,
111                })
112            }
113            ServerRequest::ItemPermissionsRequestApproval { params, .. } => {
114                serialized_result(PermissionsRequestApprovalResponse {
115                    permissions: grant_requested_permissions(&params.permissions),
116                    scope: PermissionGrantScope::Turn,
117                    strict_auto_review: None,
118                })
119            }
120            ServerRequest::ApplyPatchApproval { .. } => {
121                serialized_result(ApplyPatchApprovalResponse {
122                    decision: ReviewDecision::Approved,
123                })
124            }
125            ServerRequest::ExecCommandApproval { .. } => {
126                serialized_result(ExecCommandApprovalResponse {
127                    decision: ReviewDecision::Approved,
128                })
129            }
130            ServerRequest::CurrentTimeRead { .. } => current_time_reply(),
131            ServerRequest::McpServerElicitationRequest { .. } => {
132                serialized_result(McpServerElicitationRequestResponse {
133                    action: McpServerElicitationAction::Decline,
134                    content: None,
135                    meta: None,
136                })
137            }
138            _ => {
139                unsupported_request_error(request, "allow-all approval policy has no canned reply")
140            }
141        }))
142    }
143}
144
145/// Approval handler for read-only integrations.
146///
147/// This answers `currentTime/read` and declines command/file-change approval
148/// prompts without interrupting the turn. Permission escalations and other
149/// app-specific server requests receive JSON-RPC errors so callers can handle
150/// them explicitly if needed.
151#[derive(Clone, Debug, Default)]
152pub struct ReadOnlyApprovalHandler;
153
154impl ApprovalHandler for ReadOnlyApprovalHandler {
155    fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a> {
156        Box::pin(ready(match request {
157            ServerRequest::ItemCommandExecutionRequestApproval { .. } => {
158                serialized_result(CommandExecutionRequestApprovalResponse {
159                    decision: CommandExecutionApprovalDecision::Decline,
160                })
161            }
162            ServerRequest::ItemFileChangeRequestApproval { .. } => {
163                serialized_result(FileChangeRequestApprovalResponse {
164                    decision: FileChangeApprovalDecision::Decline,
165                })
166            }
167            ServerRequest::ApplyPatchApproval { .. } => {
168                serialized_result(ApplyPatchApprovalResponse {
169                    decision: ReviewDecision::Denied,
170                })
171            }
172            ServerRequest::ExecCommandApproval { .. } => {
173                serialized_result(ExecCommandApprovalResponse {
174                    decision: ReviewDecision::Denied,
175                })
176            }
177            ServerRequest::CurrentTimeRead { .. } => current_time_reply(),
178            ServerRequest::McpServerElicitationRequest { .. } => {
179                serialized_result(McpServerElicitationRequestResponse {
180                    action: McpServerElicitationAction::Decline,
181                    content: None,
182                    meta: None,
183                })
184            }
185            _ => unsupported_request_error(
186                request,
187                "read-only approval policy declined this request",
188            ),
189        }))
190    }
191}
192
193impl ApprovalHandler for DenyAllApprovalHandler {
194    fn handle<'a>(&'a self, _request: &'a ServerRequest) -> ApprovalFuture<'a> {
195        Box::pin(ready(ServerRequestReply::Error {
196            code: self.code,
197            message: self.message.clone(),
198            data: None,
199        }))
200    }
201}
202
203/// Approval handler backed by a closure.
204///
205/// Use this when the crate's canned policies are too broad or too narrow. The
206/// closure receives the typed [`ServerRequest`] and returns the exact reply to
207/// send back to the app-server.
208pub struct FnApprovalHandler<F>(F);
209
210impl<F> FnApprovalHandler<F>
211where
212    F: Fn(&ServerRequest) -> ServerRequestReply + Send + Sync,
213{
214    /// Wraps a closure as an [`ApprovalHandler`].
215    pub fn new(handler: F) -> Self {
216        Self(handler)
217    }
218}
219
220impl<F> ApprovalHandler for FnApprovalHandler<F>
221where
222    F: Fn(&ServerRequest) -> ServerRequestReply + Send + Sync,
223{
224    fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a> {
225        Box::pin(ready((self.0)(request)))
226    }
227}
228
229/// Approval handler backed by an async closure.
230///
231/// Use this for UI, channel, or service-backed approval policies that need to
232/// await a decision while a turn is being drained.
233pub struct AsyncFnApprovalHandler<F>(F);
234
235impl<F> AsyncFnApprovalHandler<F>
236where
237    F: for<'a> Fn(&'a ServerRequest) -> ApprovalFuture<'a> + Send + Sync,
238{
239    /// Wraps an async closure as an [`ApprovalHandler`].
240    pub fn new(handler: F) -> Self {
241        Self(handler)
242    }
243}
244
245impl<F> ApprovalHandler for AsyncFnApprovalHandler<F>
246where
247    F: for<'a> Fn(&'a ServerRequest) -> ApprovalFuture<'a> + Send + Sync,
248{
249    fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a> {
250        (self.0)(request)
251    }
252}
253
254fn grant_requested_permissions(requested: &RequestPermissionProfile) -> GrantedPermissionProfile {
255    GrantedPermissionProfile {
256        file_system: requested.file_system.clone(),
257        network: requested.network.clone(),
258    }
259}
260
261fn current_time_reply() -> ServerRequestReply {
262    let current_time_at = SystemTime::now()
263        .duration_since(UNIX_EPOCH)
264        .map(|duration| duration.as_secs() as i64)
265        .unwrap_or_default();
266    serialized_result(CurrentTimeReadResponse { current_time_at })
267}
268
269fn serialized_result<T>(response: T) -> ServerRequestReply
270where
271    T: serde::Serialize,
272{
273    ServerRequestReply::Result(
274        serde_json::to_value(response).expect("generated app-server response types serialize"),
275    )
276}
277
278fn unsupported_request_error(request: &ServerRequest, message: &'static str) -> ServerRequestReply {
279    ServerRequestReply::Error {
280        code: -32000,
281        message: format!(
282            "codex-app-server-client: {message} for {}",
283            request.method_name()
284        ),
285        data: Some(serde_json::json!({
286            "method": request.method_name(),
287            "expectedResponseType": request.expected_response_type_name(),
288        })),
289    }
290}