1use std::{
2 collections::BTreeMap,
3 ffi::OsString,
4 path::PathBuf,
5 process::{ExitStatus, Stdio},
6};
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use tokio::{process::Command, time};
11
12use super::{
13 apply_cli_overrides, resolve_cli_overrides, spawn_with_retry, tee_stream, CliOverridesPatch,
14 CodexClient, CodexError, ConfigOverride, ConsoleTarget, FlagState,
15};
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum ExecPolicyDecision {
21 Allow,
22 Prompt,
23 Forbidden,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct ExecPolicyRuleMatch {
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub name: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub description: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub decision: Option<ExecPolicyDecision>,
39 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
40 pub extra: BTreeMap<String, Value>,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct ExecPolicyMatch {
47 pub decision: ExecPolicyDecision,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub rules: Vec<ExecPolicyRuleMatch>,
50 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
51 pub extra: BTreeMap<String, Value>,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct ExecPolicyNoMatch {
58 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
59 pub extra: BTreeMap<String, Value>,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct ExecPolicyEvaluation {
66 #[serde(rename = "match", default, skip_serializing_if = "Option::is_none")]
67 pub match_result: Option<ExecPolicyMatch>,
68 #[serde(rename = "noMatch", default, skip_serializing_if = "Option::is_none")]
69 pub no_match: Option<ExecPolicyNoMatch>,
70}
71
72impl ExecPolicyEvaluation {
73 pub fn decision(&self) -> Option<ExecPolicyDecision> {
75 self.match_result.as_ref().map(|result| result.decision)
76 }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct ExecPolicyCheckResult {
82 pub status: ExitStatus,
84 pub stdout: String,
86 pub stderr: String,
88 pub evaluation: ExecPolicyEvaluation,
90}
91
92impl ExecPolicyCheckResult {
93 pub fn decision(&self) -> Option<ExecPolicyDecision> {
95 self.evaluation.decision()
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct ExecPolicyCheckRequest {
102 pub policies: Vec<PathBuf>,
104 pub pretty: bool,
106 pub command: Vec<OsString>,
108 pub overrides: CliOverridesPatch,
110}
111
112impl ExecPolicyCheckRequest {
113 pub fn new<I, S>(command: I) -> Self
114 where
115 I: IntoIterator<Item = S>,
116 S: Into<OsString>,
117 {
118 Self {
119 policies: Vec::new(),
120 pretty: false,
121 command: command.into_iter().map(Into::into).collect(),
122 overrides: CliOverridesPatch::default(),
123 }
124 }
125
126 pub fn policy(mut self, policy: impl Into<PathBuf>) -> Self {
128 self.policies.push(policy.into());
129 self
130 }
131
132 pub fn policies<I, P>(mut self, policies: I) -> Self
134 where
135 I: IntoIterator<Item = P>,
136 P: Into<PathBuf>,
137 {
138 self.policies
139 .extend(policies.into_iter().map(|policy| policy.into()));
140 self
141 }
142
143 pub fn pretty(mut self, enable: bool) -> Self {
145 self.pretty = enable;
146 self
147 }
148
149 pub fn with_overrides(mut self, overrides: CliOverridesPatch) -> Self {
151 self.overrides = overrides;
152 self
153 }
154
155 pub fn config_override(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
157 self.overrides
158 .config_overrides
159 .push(ConfigOverride::new(key, value));
160 self
161 }
162
163 pub fn config_override_raw(mut self, raw: impl Into<String>) -> Self {
165 self.overrides
166 .config_overrides
167 .push(ConfigOverride::from_raw(raw));
168 self
169 }
170
171 pub fn profile(mut self, profile: impl Into<String>) -> Self {
173 let profile = profile.into();
174 self.overrides.profile = (!profile.trim().is_empty()).then_some(profile);
175 self
176 }
177
178 pub fn oss(mut self, enable: bool) -> Self {
180 self.overrides.oss = if enable {
181 FlagState::Enable
182 } else {
183 FlagState::Disable
184 };
185 self
186 }
187
188 pub fn enable_feature(mut self, name: impl Into<String>) -> Self {
190 self.overrides.feature_toggles.enable.push(name.into());
191 self
192 }
193
194 pub fn disable_feature(mut self, name: impl Into<String>) -> Self {
196 self.overrides.feature_toggles.disable.push(name.into());
197 self
198 }
199
200 pub fn search(mut self, enable: bool) -> Self {
202 self.overrides.search = if enable {
203 FlagState::Enable
204 } else {
205 FlagState::Disable
206 };
207 self
208 }
209}
210
211impl CodexClient {
212 pub async fn check_execpolicy(
219 &self,
220 request: ExecPolicyCheckRequest,
221 ) -> Result<ExecPolicyCheckResult, CodexError> {
222 if request.command.is_empty() {
223 return Err(CodexError::EmptyExecPolicyCommand);
224 }
225
226 let ExecPolicyCheckRequest {
227 policies,
228 pretty,
229 command,
230 overrides,
231 } = request;
232
233 let dir_ctx = self.directory_context()?;
234 let resolved_overrides =
235 resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
236
237 let mut process = Command::new(self.command_env.binary_path());
238 process
239 .arg("execpolicy")
240 .arg("check")
241 .stdout(Stdio::piped())
242 .stderr(Stdio::piped())
243 .kill_on_drop(true)
244 .current_dir(dir_ctx.path());
245
246 for policy in policies {
247 process.arg("--policy").arg(policy);
248 }
249
250 if pretty {
251 process.arg("--pretty");
252 }
253
254 apply_cli_overrides(&mut process, &resolved_overrides, true);
255
256 process.arg("--");
257 process.args(&command);
258
259 self.command_env.apply(&mut process)?;
260
261 let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
262
263 let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
264 let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
265
266 let stdout_task = tokio::spawn(tee_stream(
267 stdout,
268 ConsoleTarget::Stdout,
269 self.mirror_stdout,
270 ));
271 let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
272
273 let wait_task = async move {
274 let status = child
275 .wait()
276 .await
277 .map_err(|source| CodexError::Wait { source })?;
278 let stdout_bytes = stdout_task
279 .await
280 .map_err(CodexError::Join)?
281 .map_err(CodexError::CaptureIo)?;
282 let stderr_bytes = stderr_task
283 .await
284 .map_err(CodexError::Join)?
285 .map_err(CodexError::CaptureIo)?;
286 Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
287 };
288
289 let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
290 wait_task.await?
291 } else {
292 match time::timeout(self.timeout, wait_task).await {
293 Ok(result) => result?,
294 Err(_) => {
295 return Err(CodexError::Timeout {
296 timeout: self.timeout,
297 });
298 }
299 }
300 };
301
302 let stdout_string = String::from_utf8(stdout_bytes)?;
303 let stderr_string = String::from_utf8(stderr_bytes)?;
304
305 if !status.success() {
306 return Err(CodexError::NonZeroExit {
307 status,
308 stderr: stderr_string,
309 });
310 }
311
312 let evaluation: ExecPolicyEvaluation =
313 serde_json::from_str(&stdout_string).map_err(|source| CodexError::ExecPolicyParse {
314 stdout: stdout_string.clone(),
315 source,
316 })?;
317
318 Ok(ExecPolicyCheckResult {
319 status,
320 stdout: stdout_string,
321 stderr: stderr_string,
322 evaluation,
323 })
324 }
325}