Codex Handbook
execpolicy-legacy/src/main.rs 169 lines
use anyhow::Result;use clap::Parser;use clap::Subcommand;use codex_execpolicy_legacy::ExecCall;use codex_execpolicy_legacy::MatchedExec;use codex_execpolicy_legacy::Policy;use codex_execpolicy_legacy::PolicyParser;use codex_execpolicy_legacy::ValidExec;use codex_execpolicy_legacy::get_default_policy;use serde::Deserialize;use serde::Serialize;use serde::de;use starlark::Error as StarlarkError;use std::path::PathBuf;use std::str::FromStr;const MATCHED_BUT_WRITES_FILES_EXIT_CODE: i32 = 12;const MIGHT_BE_SAFE_EXIT_CODE: i32 = 13;const FORBIDDEN_EXIT_CODE: i32 = 14;#[derive(Parser, Deserialize, Debug)]#[command(version, about, long_about = None)]pub struct Args {    /// If the command fails the policy, exit with 13, but print parseable JSON    /// to stdout.    #[clap(long)]    pub require_safe: bool,    /// Path to the policy file.    #[clap(long, short = 'p')]    pub policy: Option<PathBuf>,    #[command(subcommand)]    pub command: Command,}#[derive(Clone, Debug, Deserialize, Subcommand)]pub enum Command {    /// Checks the command as if the arguments were the inputs to execv(3).    Check {        #[arg(trailing_var_arg = true)]        command: Vec<String>,    },    /// Checks the command encoded as a JSON object.    #[clap(name = "check-json")]    CheckJson {        /// JSON object with "program" (str) and "args" (list[str]) fields.        #[serde(deserialize_with = "deserialize_from_json")]        exec: ExecArg,    },}#[derive(Clone, Debug, Deserialize)]pub struct ExecArg {    pub program: String,    #[serde(default)]    pub args: Vec<String>,}fn main() -> Result<()> {    env_logger::init();    let args = Args::parse();    let policy = match args.policy {        Some(policy) => {            let policy_source = policy.to_string_lossy().to_string();            let unparsed_policy = std::fs::read_to_string(policy)?;            let parser = PolicyParser::new(&policy_source, &unparsed_policy);            parser.parse()        }        None => get_default_policy(),    };    let policy = policy.map_err(StarlarkError::into_anyhow)?;    let exec = match args.command {        Command::Check { command } => match command.split_first() {            Some((first, rest)) => ExecArg {                program: first.to_string(),                args: rest.to_vec(),            },            None => {                eprintln!("no command provided");                std::process::exit(1);            }        },        Command::CheckJson { exec } => exec,    };    let (output, exit_code) = check_command(&policy, exec, args.require_safe);    let json = serde_json::to_string(&output)?;    println!("{json}");    std::process::exit(exit_code);}fn check_command(    policy: &Policy,    ExecArg { program, args }: ExecArg,    check: bool,) -> (Output, i32) {    let exec_call = ExecCall { program, args };    match policy.check(&exec_call) {        Ok(MatchedExec::Match { exec }) => {            if exec.might_write_files() {                let exit_code = if check {                    MATCHED_BUT_WRITES_FILES_EXIT_CODE                } else {                    0                };                (Output::Match { r#match: exec }, exit_code)            } else {                (Output::Safe { r#match: exec }, 0)            }        }        Ok(MatchedExec::Forbidden { reason, cause }) => {            let exit_code = if check { FORBIDDEN_EXIT_CODE } else { 0 };            (Output::Forbidden { reason, cause }, exit_code)        }        Err(err) => {            let exit_code = if check { MIGHT_BE_SAFE_EXIT_CODE } else { 0 };            (Output::Unverified { error: err }, exit_code)        }    }}#[derive(Debug, Serialize)]#[serde(tag = "result")]pub enum Output {    /// The command is verified as safe.    #[serde(rename = "safe")]    Safe { r#match: ValidExec },    /// The command has matched a rule in the policy, but the caller should    /// decide whether it is "safe" given the files it wants to write.    #[serde(rename = "match")]    Match { r#match: ValidExec },    /// The user is forbidden from running the command.    #[serde(rename = "forbidden")]    Forbidden {        reason: String,        cause: codex_execpolicy_legacy::Forbidden,    },    /// The safety of the command could not be verified.    #[serde(rename = "unverified")]    Unverified {        error: codex_execpolicy_legacy::Error,    },}fn deserialize_from_json<'de, D>(deserializer: D) -> Result<ExecArg, D::Error>where    D: de::Deserializer<'de>,{    let s = String::deserialize(deserializer)?;    let decoded = serde_json::from_str(&s)        .map_err(|e| serde::de::Error::custom(format!("JSON parse error: {e}")))?;    Ok(decoded)}impl FromStr for ExecArg {    type Err = anyhow::Error;    fn from_str(s: &str) -> Result<Self, Self::Err> {        serde_json::from_str(s).map_err(Into::into)    }}