1use crate::mcp::RequestId;
2use crate::models::AdditionalPermissionProfile;
3use crate::models::PermissionProfile;
4use crate::parse_command::ParsedCommand;
5use crate::protocol::FileChange;
6use crate::protocol::ReviewDecision;
7use crate::request_permissions::RequestPermissionProfile;
8use codex_utils_absolute_path::AbsolutePathBuf;
9use schemars::JsonSchema;
10use serde::Deserialize;
11use serde::Serialize;
12use serde_json::Value as JsonValue;
13use std::collections::HashMap;
14use std::path::PathBuf;
15use ts_rs::TS;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ResolvedPermissionProfile {
20 pub permission_profile: PermissionProfile,
21}
22
23#[allow(clippy::large_enum_variant)]
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum EscalationPermissions {
26 AdditionalPermissionProfile(AdditionalPermissionProfile),
28 ResolvedPermissionProfile(ResolvedPermissionProfile),
30}
31
32#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
38#[serde(transparent)]
39#[ts(type = "Array<string>")]
40pub struct ExecPolicyAmendment {
41 pub command: Vec<String>,
42}
43
44impl ExecPolicyAmendment {
45 pub fn new(command: Vec<String>) -> Self {
46 Self { command }
47 }
48
49 pub fn command(&self) -> &[String] {
50 &self.command
51 }
52}
53
54impl From<Vec<String>> for ExecPolicyAmendment {
55 fn from(command: Vec<String>) -> Self {
56 Self { command }
57 }
58}
59
60#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
61#[serde(rename_all = "snake_case")]
62pub enum NetworkApprovalProtocol {
63 Http,
66 #[serde(alias = "https_connect", alias = "http-connect")]
67 Https,
68 Socks5Tcp,
69 Socks5Udp,
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
73pub struct NetworkApprovalContext {
74 pub host: String,
75 pub protocol: NetworkApprovalProtocol,
76}
77
78#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
79#[serde(rename_all = "snake_case")]
80pub enum NetworkPolicyRuleAction {
81 Allow,
82 Deny,
83}
84
85#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
86#[serde(rename_all = "lowercase")]
87pub enum GuardianRiskLevel {
88 Low,
89 Medium,
90 High,
91 Critical,
92}
93
94#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
95#[serde(rename_all = "lowercase")]
96pub enum GuardianUserAuthorization {
97 Unknown,
98 Low,
99 Medium,
100 High,
101}
102
103#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
105#[serde(rename_all = "lowercase")]
106pub enum GuardianAssessmentOutcome {
107 Allow,
108 Deny,
109}
110
111#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
112#[serde(rename_all = "snake_case")]
113pub enum GuardianAssessmentStatus {
114 InProgress,
115 Approved,
116 Denied,
117 TimedOut,
118 Aborted,
119}
120
121#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
122#[serde(rename_all = "snake_case")]
123pub enum GuardianAssessmentDecisionSource {
124 Agent,
125}
126
127#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
128#[serde(rename_all = "snake_case")]
129pub enum GuardianCommandSource {
130 Shell,
131 UnifiedExec,
132}
133
134#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
135#[serde(tag = "type", rename_all = "snake_case")]
136#[ts(tag = "type", rename_all = "snake_case")]
137pub enum GuardianAssessmentAction {
138 Command {
139 source: GuardianCommandSource,
140 command: String,
141 cwd: AbsolutePathBuf,
142 },
143 Execve {
144 source: GuardianCommandSource,
145 program: String,
146 argv: Vec<String>,
147 cwd: AbsolutePathBuf,
148 },
149 ApplyPatch {
150 cwd: AbsolutePathBuf,
151 files: Vec<AbsolutePathBuf>,
152 },
153 NetworkAccess {
154 target: String,
155 host: String,
156 protocol: NetworkApprovalProtocol,
157 port: u16,
158 },
159 McpToolCall {
160 server: String,
161 tool_name: String,
162 connector_id: Option<String>,
163 connector_name: Option<String>,
164 tool_title: Option<String>,
165 },
166 RequestPermissions {
167 reason: Option<String>,
168 permissions: RequestPermissionProfile,
169 },
170}
171
172#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
173pub struct NetworkPolicyAmendment {
174 pub host: String,
175 pub action: NetworkPolicyRuleAction,
176}
177
178#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
179pub struct GuardianAssessmentEvent {
180 pub id: String,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 #[ts(optional)]
185 pub target_item_id: Option<String>,
186 #[serde(default)]
189 pub turn_id: String,
190 #[serde(default)]
191 #[ts(type = "number")]
192 pub started_at_ms: i64,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 #[ts(optional, type = "number")]
195 pub completed_at_ms: Option<i64>,
196 pub status: GuardianAssessmentStatus,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 #[ts(optional)]
200 pub risk_level: Option<GuardianRiskLevel>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 #[ts(optional)]
204 pub user_authorization: Option<GuardianUserAuthorization>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 #[ts(optional)]
208 pub rationale: Option<String>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 #[ts(optional)]
212 pub decision_source: Option<GuardianAssessmentDecisionSource>,
213 pub action: GuardianAssessmentAction,
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
218pub struct ExecApprovalRequestEvent {
219 pub call_id: String,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
226 #[ts(optional)]
227 pub approval_id: Option<String>,
228 #[serde(default)]
231 pub turn_id: String,
232 #[serde(
234 default,
235 rename = "environmentId",
236 alias = "environment_id",
237 skip_serializing_if = "Option::is_none"
238 )]
239 #[ts(optional)]
240 #[ts(rename = "environmentId")]
241 pub environment_id: Option<String>,
242 #[ts(type = "number")]
243 pub started_at_ms: i64,
244 pub command: Vec<String>,
246 pub cwd: AbsolutePathBuf,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub reason: Option<String>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 #[ts(optional)]
254 pub network_approval_context: Option<NetworkApprovalContext>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 #[ts(optional)]
258 pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 #[ts(optional)]
262 pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 #[ts(optional)]
266 pub additional_permissions: Option<AdditionalPermissionProfile>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
272 #[ts(optional)]
273 pub available_decisions: Option<Vec<ReviewDecision>>,
274 pub parsed_cmd: Vec<ParsedCommand>,
275}
276
277impl ExecApprovalRequestEvent {
278 pub fn effective_approval_id(&self) -> String {
279 self.approval_id
280 .clone()
281 .unwrap_or_else(|| self.call_id.clone())
282 }
283
284 pub fn effective_available_decisions(&self) -> Vec<ReviewDecision> {
285 match &self.available_decisions {
288 Some(decisions) => decisions.clone(),
289 None => Self::default_available_decisions(
290 self.network_approval_context.as_ref(),
291 self.proposed_execpolicy_amendment.as_ref(),
292 self.proposed_network_policy_amendments.as_deref(),
293 self.additional_permissions.as_ref(),
294 ),
295 }
296 }
297
298 pub fn default_available_decisions(
299 network_approval_context: Option<&NetworkApprovalContext>,
300 proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>,
301 proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>,
302 additional_permissions: Option<&AdditionalPermissionProfile>,
303 ) -> Vec<ReviewDecision> {
304 if network_approval_context.is_some() {
305 let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession];
306 if let Some(amendment) = proposed_network_policy_amendments.and_then(|amendments| {
307 amendments
308 .iter()
309 .find(|amendment| amendment.action == NetworkPolicyRuleAction::Allow)
310 }) {
311 decisions.push(ReviewDecision::NetworkPolicyAmendment {
312 network_policy_amendment: amendment.clone(),
313 });
314 }
315 decisions.push(ReviewDecision::Abort);
316 return decisions;
317 }
318
319 if additional_permissions.is_some() {
320 return vec![ReviewDecision::Approved, ReviewDecision::Abort];
321 }
322
323 let mut decisions = vec![ReviewDecision::Approved];
324 if let Some(prefix) = proposed_execpolicy_amendment {
325 decisions.push(ReviewDecision::ApprovedExecpolicyAmendment {
326 proposed_execpolicy_amendment: prefix.clone(),
327 });
328 }
329 decisions.push(ReviewDecision::Abort);
330 decisions
331 }
332}
333
334#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
335#[serde(tag = "mode", rename_all = "snake_case")]
336#[ts(tag = "mode")]
337pub enum ElicitationRequest {
338 Form {
339 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
340 #[ts(optional, rename = "_meta")]
341 meta: Option<JsonValue>,
342 message: String,
343 requested_schema: JsonValue,
344 },
345 #[serde(rename = "openai/form")]
346 #[ts(rename = "openai/form")]
347 OpenAiForm {
348 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
349 #[ts(optional, rename = "_meta")]
350 meta: Option<JsonValue>,
351 message: String,
352 requested_schema: JsonValue,
353 },
354 Url {
355 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
356 #[ts(optional, rename = "_meta")]
357 meta: Option<JsonValue>,
358 message: String,
359 url: String,
360 elicitation_id: String,
361 },
362}
363
364#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
365pub struct ElicitationRequestEvent {
366 #[serde(default, skip_serializing_if = "Option::is_none")]
368 #[ts(optional)]
369 pub turn_id: Option<String>,
370 pub server_name: String,
371 #[ts(type = "string | number")]
372 pub id: RequestId,
373 pub request: ElicitationRequest,
374}
375
376#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
377#[serde(rename_all = "lowercase")]
378pub enum ElicitationAction {
379 Accept,
380 Decline,
381 Cancel,
382}
383
384#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
385pub struct ApplyPatchApprovalRequestEvent {
386 pub call_id: String,
388 #[serde(default)]
391 pub turn_id: String,
392 #[ts(type = "number")]
393 pub started_at_ms: i64,
394 pub changes: HashMap<PathBuf, FileChange>,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub reason: Option<String>,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub grant_root: Option<PathBuf>,
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use codex_utils_absolute_path::test_support::PathBufExt;
407 use codex_utils_absolute_path::test_support::test_path_buf;
408 use pretty_assertions::assert_eq;
409
410 #[test]
411 fn guardian_assessment_action_deserializes_command_shape() {
412 let action: GuardianAssessmentAction = serde_json::from_value(serde_json::json!({
413 "type": "command",
414 "source": "shell",
415 "command": "rm -rf /tmp/guardian",
416 "cwd": test_path_buf("/tmp"),
417 }))
418 .expect("guardian action");
419
420 assert_eq!(
421 action,
422 GuardianAssessmentAction::Command {
423 source: GuardianCommandSource::Shell,
424 command: "rm -rf /tmp/guardian".to_string(),
425 cwd: test_path_buf("/tmp").abs(),
426 }
427 );
428 }
429
430 #[cfg(unix)]
431 #[test]
432 fn guardian_assessment_action_round_trips_execve_shape() {
433 let value = serde_json::json!({
434 "type": "execve",
435 "source": "shell",
436 "program": "/bin/rm",
437 "argv": ["/usr/bin/rm", "-f", "/tmp/file.sqlite"],
438 "cwd": "/tmp",
439 });
440 let action: GuardianAssessmentAction =
441 serde_json::from_value(value.clone()).expect("guardian action");
442
443 assert_eq!(
444 serde_json::to_value(&action).expect("serialize guardian action"),
445 value
446 );
447
448 assert_eq!(
449 action,
450 GuardianAssessmentAction::Execve {
451 source: GuardianCommandSource::Shell,
452 program: "/bin/rm".to_string(),
453 argv: vec![
454 "/usr/bin/rm".to_string(),
455 "-f".to_string(),
456 "/tmp/file.sqlite".to_string(),
457 ],
458 cwd: test_path_buf("/tmp").abs(),
459 }
460 );
461 }
462}