codex_cli_sdk/
permissions.rs1use crate::types::events::{ApprovalRequestEvent, PatchApprovalRequestEvent};
2use serde::{Deserialize, Serialize};
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ApprovalDecision {
11 Approved,
13 ApprovedForSession,
15 #[default]
17 Denied,
18 Abort,
20}
21
22#[derive(Debug, Clone)]
24pub struct ApprovalOutcome {
25 pub decision: ApprovalDecision,
27
28 #[cfg(feature = "unstable")]
35 pub updated_command: Option<String>,
36}
37
38impl ApprovalOutcome {
39 pub fn new(decision: ApprovalDecision) -> Self {
41 Self {
42 decision,
43 #[cfg(feature = "unstable")]
44 updated_command: None,
45 }
46 }
47
48 #[cfg(feature = "unstable")]
53 pub fn with_updated_command(mut self, command: impl Into<String>) -> Self {
54 self.updated_command = Some(command.into());
55 self
56 }
57}
58
59impl From<ApprovalDecision> for ApprovalOutcome {
60 fn from(decision: ApprovalDecision) -> Self {
61 Self {
62 decision,
63 #[cfg(feature = "unstable")]
64 updated_command: None,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct ApprovalContext {
72 pub request: ApprovalRequestEvent,
74 pub thread_id: Option<String>,
76}
77
78pub type ApprovalCallback = Arc<
83 dyn Fn(ApprovalContext) -> Pin<Box<dyn Future<Output = ApprovalOutcome> + Send>> + Send + Sync,
84>;
85
86#[derive(Debug, Clone)]
88pub struct PatchApprovalContext {
89 pub request: PatchApprovalRequestEvent,
91 pub thread_id: Option<String>,
93}
94
95pub type PatchApprovalCallback = Arc<
100 dyn Fn(PatchApprovalContext) -> Pin<Box<dyn Future<Output = ApprovalOutcome> + Send>>
101 + Send
102 + Sync,
103>;
104
105#[derive(Debug, Serialize)]
107pub(crate) struct ApprovalResponse {
108 pub op: String,
109 pub id: String,
110 pub decision: ApprovalDecision,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub turn_id: Option<String>,
113}
114
115impl ApprovalResponse {
116 pub fn new(request_id: String, decision: ApprovalDecision) -> Self {
117 Self {
118 op: "ExecApproval".to_string(),
119 id: request_id,
120 decision,
121 turn_id: None,
122 }
123 }
124}
125
126#[derive(Debug, Serialize)]
128pub(crate) struct PatchApprovalResponse {
129 pub op: String,
130 pub id: String,
131 pub decision: ApprovalDecision,
132}
133
134impl PatchApprovalResponse {
135 pub fn new(request_id: String, decision: ApprovalDecision) -> Self {
136 Self {
137 op: "PatchApproval".to_string(),
138 id: request_id,
139 decision,
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn approval_response_serializes() {
150 let response = ApprovalResponse::new("req-1".into(), ApprovalDecision::Approved);
151 let json = serde_json::to_string(&response).unwrap();
152 assert!(json.contains("ExecApproval"));
153 assert!(json.contains("approved"));
154 assert!(!json.contains("turn_id")); }
156
157 #[test]
158 fn approval_decision_round_trip() {
159 let decision = ApprovalDecision::ApprovedForSession;
160 let json = serde_json::to_string(&decision).unwrap();
161 let parsed: ApprovalDecision = serde_json::from_str(&json).unwrap();
162 assert!(matches!(parsed, ApprovalDecision::ApprovedForSession));
163 }
164
165 #[test]
166 fn patch_approval_response_serializes() {
167 let response = PatchApprovalResponse::new("req-2".into(), ApprovalDecision::Denied);
168 let json = serde_json::to_string(&response).unwrap();
169 assert!(json.contains("PatchApproval"));
170 assert!(json.contains("denied"));
171 }
172
173 #[test]
174 fn default_decision_is_denied() {
175 assert!(matches!(
176 ApprovalDecision::default(),
177 ApprovalDecision::Denied
178 ));
179 }
180
181 #[test]
182 fn approval_outcome_from_decision() {
183 let outcome: ApprovalOutcome = ApprovalDecision::Approved.into();
184 assert!(matches!(outcome.decision, ApprovalDecision::Approved));
185 }
186
187 #[cfg(feature = "unstable")]
188 #[test]
189 fn approval_outcome_with_updated_command() {
190 let outcome = ApprovalOutcome::new(ApprovalDecision::Approved)
191 .with_updated_command("safe-command --flag");
192 assert!(matches!(outcome.decision, ApprovalDecision::Approved));
193 assert_eq!(
194 outcome.updated_command,
195 Some("safe-command --flag".to_string())
196 );
197 }
198
199 #[cfg(feature = "unstable")]
200 #[test]
201 fn approval_outcome_wire_compatible() {
202 let outcome =
204 ApprovalOutcome::new(ApprovalDecision::Approved).with_updated_command("overridden");
205 let response = ApprovalResponse::new("req-1".into(), outcome.decision);
206 let json = serde_json::to_string(&response).unwrap();
207 assert!(!json.contains("overridden"));
208 assert!(json.contains("approved"));
209 }
210}