1use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum PermissionDecision {
21 Permit,
22 Reject,
23 Prompt,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PermissionDecisionResult {
29 pub decision: PermissionDecision,
30 pub reason: String,
31 pub tool_name: String,
32 pub description: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub request_id: Option<String>,
35 #[serde(default)]
36 pub reviewable: bool,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct PermissionGateRequest {
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub request_id: Option<String>,
46 pub tool_call_id: String,
47 pub tool_name: String,
48 pub description: String,
49 pub input: Value,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct PermissionGateResponse {
55 pub allow: bool,
56 pub reason: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(transparent)]
61pub struct ExecPolicyAmendment {
62 pub command: Vec<String>,
63}
64impl ExecPolicyAmendment {
65 pub fn new(command: Vec<String>) -> Self {
66 Self { command }
67 }
68 pub fn command(&self) -> &[String] {
69 &self.command
70 }
71}
72impl From<Vec<String>> for ExecPolicyAmendment {
73 fn from(command: Vec<String>) -> Self {
74 Self { command }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum NetworkApprovalProtocol {
81 Http,
82 Https,
83 Socks5Tcp,
84 Socks5Udp,
85}
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct NetworkApprovalContext {
88 pub host: String,
89 pub protocol: NetworkApprovalProtocol,
90}
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum NetworkPolicyRuleAction {
94 Allow,
95 Deny,
96}
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct NetworkPolicyAmendment {
99 pub host: String,
100 pub action: NetworkPolicyRuleAction,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct PendingApprovalContext {
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub request_id: Option<String>,
108 pub tool_name: String,
109 #[serde(default)]
110 pub command: Option<String>,
111 #[serde(default)]
112 pub network: Option<NetworkApprovalContext>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub payload_digest: Option<String>,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "lowercase")]
121pub enum GuardianRiskLevel {
122 Low,
123 Medium,
124 High,
125 Critical,
126}
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "lowercase")]
129pub enum GuardianUserAuthorization {
130 Unknown,
131 Low,
132 Medium,
133 High,
134}
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "lowercase")]
137pub enum GuardianAssessmentOutcome {
138 Allow,
139 Deny,
140}
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum GuardianAssessmentStatus {
144 InProgress,
145 Approved,
146 Denied,
147 TimedOut,
148 Aborted,
149}
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum GuardianAssessmentDecisionSource {
153 Agent,
154}
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum GuardianCommandSource {
158 Shell,
159 UnifiedExec,
160}
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(tag = "type", rename_all = "snake_case")]
163pub enum GuardianAssessmentAction {
164 Command {
165 source: GuardianCommandSource,
166 command: String,
167 cwd: String,
168 },
169 Execve {
170 source: GuardianCommandSource,
171 program: String,
172 argv: Vec<String>,
173 cwd: String,
174 },
175 WriteStdin {
176 approval_id: String,
177 process_id: String,
178 stdin: String,
179 cwd: String,
180 },
181 ApplyPatch {
182 cwd: String,
183 files: Vec<String>,
184 },
185 NetworkAccess {
186 target: String,
187 host: String,
188 protocol: NetworkApprovalProtocol,
189 port: u16,
190 },
191 McpToolCall {
192 server: String,
193 tool_name: String,
194 connector_id: Option<String>,
195 connector_name: Option<String>,
196 tool_title: Option<String>,
197 },
198 RequestPermissions {
199 reason: Option<String>,
200 permissions: Value,
201 },
202}
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct GuardianAssessmentEvent {
205 pub id: String,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub target_item_id: Option<String>,
208 #[serde(default)]
209 pub turn_id: String,
210 #[serde(default)]
211 pub started_at_ms: i64,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub completed_at_ms: Option<i64>,
214 pub status: GuardianAssessmentStatus,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub risk_level: Option<GuardianRiskLevel>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub user_authorization: Option<GuardianUserAuthorization>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub rationale: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub decision_source: Option<GuardianAssessmentDecisionSource>,
223 pub action: GuardianAssessmentAction,
224}
225
226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "snake_case")]
228pub enum ExecApprovalKind {
229 #[default]
230 Command,
231 WriteStdin,
232}
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum ReviewDecision {
236 Approved,
237 ApprovedForSession,
238 ApprovedExecpolicyAmendment {
239 proposed_execpolicy_amendment: ExecPolicyAmendment,
240 },
241 NetworkPolicyAmendment {
242 network_policy_amendment: NetworkPolicyAmendment,
243 },
244 Abort,
245}
246impl ReviewDecision {
247 pub const fn is_approved(&self) -> bool {
248 matches!(
249 self,
250 Self::Approved
251 | Self::ApprovedForSession
252 | Self::ApprovedExecpolicyAmendment { .. }
253 | Self::NetworkPolicyAmendment { .. }
254 )
255 }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct ExecApprovalRequestEvent {
260 #[serde(default)]
261 pub kind: ExecApprovalKind,
262 pub call_id: String,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub approval_id: Option<String>,
265 #[serde(default)]
266 pub turn_id: String,
267 pub started_at_ms: i64,
268 pub command: Vec<String>,
269 pub cwd: String,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub reason: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub network_approval_context: Option<NetworkApprovalContext>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub available_decisions: Option<Vec<ReviewDecision>>,
280}
281impl ExecApprovalRequestEvent {
282 pub fn effective_approval_id(&self) -> String {
283 self.approval_id
284 .clone()
285 .unwrap_or_else(|| self.call_id.clone())
286 }
287 pub fn effective_available_decisions(&self) -> Vec<ReviewDecision> {
288 self.available_decisions.clone().unwrap_or_else(|| {
289 let mut out = vec![ReviewDecision::Approved];
290 if let Some(prefix) = &self.proposed_execpolicy_amendment {
291 out.push(ReviewDecision::ApprovedExecpolicyAmendment {
292 proposed_execpolicy_amendment: prefix.clone(),
293 });
294 }
295 out.push(ReviewDecision::Abort);
296 out
297 })
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302#[serde(
303 tag = "mode",
304 rename_all = "snake_case",
305 rename_all_fields = "camelCase"
306)]
307pub enum ElicitationRequest {
308 Form {
309 message: String,
310 requested_schema: Value,
311 },
312 Url {
313 message: String,
314 url: String,
315 elicitation_id: String,
316 },
317}
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct ElicitationRequestEvent {
321 pub turn_id: Option<String>,
322 pub server_name: String,
323 pub id: String,
324 pub request: ElicitationRequest,
325}
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "lowercase")]
328pub enum ElicitationAction {
329 Accept,
330 Decline,
331 Cancel,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 #[test]
338 fn exec_policy_amendment_is_transparent_array() {
339 let amendment = ExecPolicyAmendment::new(vec!["cargo".into(), "check".into()]);
340 assert_eq!(
341 serde_json::to_string(&amendment).unwrap(),
342 r#"["cargo","check"]"#
343 );
344 assert_eq!(
345 amendment.command(),
346 &["cargo".to_string(), "check".to_string()]
347 );
348 }
349 #[test]
350 fn approval_decisions_keep_codex_wire_tags() {
351 let value = serde_json::to_value(ReviewDecision::ApprovedForSession).unwrap();
352 assert_eq!(value, serde_json::json!("approved_for_session"));
353 let value = serde_json::to_value(NetworkApprovalProtocol::Https).unwrap();
354 assert_eq!(value, serde_json::json!("https"));
355 }
356 #[test]
357 fn approval_id_falls_back_to_call_id() {
358 let request = ExecApprovalRequestEvent {
359 kind: ExecApprovalKind::Command,
360 call_id: "call-1".into(),
361 approval_id: None,
362 turn_id: String::new(),
363 started_at_ms: 0,
364 command: vec!["echo".into()],
365 cwd: "/tmp".into(),
366 reason: None,
367 network_approval_context: None,
368 proposed_execpolicy_amendment: None,
369 proposed_network_policy_amendments: None,
370 available_decisions: None,
371 };
372 assert_eq!(request.effective_approval_id(), "call-1");
373 }
374 #[test]
375 fn san_permission_gate_contract_round_trips() {
376 let request = PermissionGateRequest {
377 request_id: Some("req-1".into()),
378 tool_call_id: "call-1".into(),
379 tool_name: "Bash".into(),
380 description: "run command".into(),
381 input: serde_json::json!({"command":"git status"}),
382 };
383 let value = serde_json::to_value(&request).unwrap();
384 assert_eq!(value["toolCallId"], "call-1");
385 assert_eq!(
386 serde_json::from_value::<PermissionDecision>(serde_json::json!("prompt")).unwrap(),
387 PermissionDecision::Prompt
388 );
389 }
390}