use std::{
collections::BTreeMap,
ffi::OsString,
path::PathBuf,
process::{ExitStatus, Stdio},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::{process::Command, time};
use super::{
apply_cli_overrides, resolve_cli_overrides, spawn_with_retry, tee_stream, CliOverridesPatch,
CodexClient, CodexError, ConfigOverride, ConsoleTarget, FlagState,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecPolicyDecision {
Allow,
Prompt,
Forbidden,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecPolicyRuleMatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decision: Option<ExecPolicyDecision>,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecPolicyMatch {
pub decision: ExecPolicyDecision,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<ExecPolicyRuleMatch>,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecPolicyNoMatch {
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecPolicyEvaluation {
#[serde(rename = "match", default, skip_serializing_if = "Option::is_none")]
pub match_result: Option<ExecPolicyMatch>,
#[serde(rename = "noMatch", default, skip_serializing_if = "Option::is_none")]
pub no_match: Option<ExecPolicyNoMatch>,
}
impl ExecPolicyEvaluation {
pub fn decision(&self) -> Option<ExecPolicyDecision> {
self.match_result.as_ref().map(|result| result.decision)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecPolicyCheckResult {
pub status: ExitStatus,
pub stdout: String,
pub stderr: String,
pub evaluation: ExecPolicyEvaluation,
}
impl ExecPolicyCheckResult {
pub fn decision(&self) -> Option<ExecPolicyDecision> {
self.evaluation.decision()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecPolicyCheckRequest {
pub policies: Vec<PathBuf>,
pub pretty: bool,
pub command: Vec<OsString>,
pub overrides: CliOverridesPatch,
}
impl ExecPolicyCheckRequest {
pub fn new<I, S>(command: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
Self {
policies: Vec::new(),
pretty: false,
command: command.into_iter().map(Into::into).collect(),
overrides: CliOverridesPatch::default(),
}
}
pub fn policy(mut self, policy: impl Into<PathBuf>) -> Self {
self.policies.push(policy.into());
self
}
pub fn policies<I, P>(mut self, policies: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
self.policies
.extend(policies.into_iter().map(|policy| policy.into()));
self
}
pub fn pretty(mut self, enable: bool) -> Self {
self.pretty = enable;
self
}
pub fn with_overrides(mut self, overrides: CliOverridesPatch) -> Self {
self.overrides = overrides;
self
}
pub fn config_override(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.overrides
.config_overrides
.push(ConfigOverride::new(key, value));
self
}
pub fn config_override_raw(mut self, raw: impl Into<String>) -> Self {
self.overrides
.config_overrides
.push(ConfigOverride::from_raw(raw));
self
}
pub fn profile(mut self, profile: impl Into<String>) -> Self {
let profile = profile.into();
self.overrides.profile = (!profile.trim().is_empty()).then_some(profile);
self
}
pub fn oss(mut self, enable: bool) -> Self {
self.overrides.oss = if enable {
FlagState::Enable
} else {
FlagState::Disable
};
self
}
pub fn enable_feature(mut self, name: impl Into<String>) -> Self {
self.overrides.feature_toggles.enable.push(name.into());
self
}
pub fn disable_feature(mut self, name: impl Into<String>) -> Self {
self.overrides.feature_toggles.disable.push(name.into());
self
}
pub fn search(mut self, enable: bool) -> Self {
self.overrides.search = if enable {
FlagState::Enable
} else {
FlagState::Disable
};
self
}
}
impl CodexClient {
pub async fn check_execpolicy(
&self,
request: ExecPolicyCheckRequest,
) -> Result<ExecPolicyCheckResult, CodexError> {
if request.command.is_empty() {
return Err(CodexError::EmptyExecPolicyCommand);
}
let ExecPolicyCheckRequest {
policies,
pretty,
command,
overrides,
} = request;
let dir_ctx = self.directory_context()?;
let resolved_overrides =
resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
let mut process = Command::new(self.command_env.binary_path());
process
.arg("execpolicy")
.arg("check")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.current_dir(dir_ctx.path());
for policy in policies {
process.arg("--policy").arg(policy);
}
if pretty {
process.arg("--pretty");
}
apply_cli_overrides(&mut process, &resolved_overrides, true);
process.arg("--");
process.args(&command);
self.command_env.apply(&mut process)?;
let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
let stdout_task = tokio::spawn(tee_stream(
stdout,
ConsoleTarget::Stdout,
self.mirror_stdout,
));
let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
let wait_task = async move {
let status = child
.wait()
.await
.map_err(|source| CodexError::Wait { source })?;
let stdout_bytes = stdout_task
.await
.map_err(CodexError::Join)?
.map_err(CodexError::CaptureIo)?;
let stderr_bytes = stderr_task
.await
.map_err(CodexError::Join)?
.map_err(CodexError::CaptureIo)?;
Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
};
let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
wait_task.await?
} else {
match time::timeout(self.timeout, wait_task).await {
Ok(result) => result?,
Err(_) => {
return Err(CodexError::Timeout {
timeout: self.timeout,
});
}
}
};
let stdout_string = String::from_utf8(stdout_bytes)?;
let stderr_string = String::from_utf8(stderr_bytes)?;
if !status.success() {
return Err(CodexError::NonZeroExit {
status,
stderr: stderr_string,
});
}
let evaluation: ExecPolicyEvaluation =
serde_json::from_str(&stdout_string).map_err(|source| CodexError::ExecPolicyParse {
stdout: stdout_string.clone(),
source,
})?;
Ok(ExecPolicyCheckResult {
status,
stdout: stdout_string,
stderr: stderr_string,
evaluation,
})
}
}