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
16pub type ApprovalFuture<'a> = Pin<Box<dyn Future<Output = ServerRequestReply> + Send + 'a>>;
22
23#[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
50pub trait ApprovalHandler: Send + Sync {
56 fn handle<'a>(&'a self, request: &'a ServerRequest) -> ApprovalFuture<'a>;
57}
58
59#[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 pub fn new(message: impl Into<String>) -> Self {
83 Self {
84 message: message.into(),
85 ..Self::default()
86 }
87 }
88}
89
90#[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(¶ms.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#[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
203pub struct FnApprovalHandler<F>(F);
209
210impl<F> FnApprovalHandler<F>
211where
212 F: Fn(&ServerRequest) -> ServerRequestReply + Send + Sync,
213{
214 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
229pub struct AsyncFnApprovalHandler<F>(F);
234
235impl<F> AsyncFnApprovalHandler<F>
236where
237 F: for<'a> Fn(&'a ServerRequest) -> ApprovalFuture<'a> + Send + Sync,
238{
239 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}