From 3c0071432dc6c9d7c2552bfcf5027af9410508b7 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Thu, 20 Nov 2025 17:10:03 -0500 Subject: [PATCH] Add allow-prefix amendment helper to execpolicy --- codex-rs/Cargo.lock | 1 + codex-rs/execpolicy/Cargo.toml | 1 + codex-rs/execpolicy/src/amend.rs | 143 +++++++++++++++++++++++++++++++ codex-rs/execpolicy/src/lib.rs | 3 + 4 files changed, 148 insertions(+) create mode 100644 codex-rs/execpolicy/src/amend.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b8175ee36b..b465e9187a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "serde_json", "shlex", "starlark", + "tempfile", "thiserror 2.0.17", ] diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index bececed4b2..1af1bd1b8e 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -27,3 +27,4 @@ thiserror = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } +tempfile = { workspace = true } diff --git a/codex-rs/execpolicy/src/amend.rs b/codex-rs/execpolicy/src/amend.rs new file mode 100644 index 0000000000..9c415e8c46 --- /dev/null +++ b/codex-rs/execpolicy/src/amend.rs @@ -0,0 +1,143 @@ +use std::fs::OpenOptions; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + +use serde_json; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AmendError { + #[error("prefix rule requires at least one token")] + EmptyPrefix, + #[error("policy path has no parent: {path}")] + MissingParent { path: PathBuf }, + #[error("failed to create policy directory {dir}: {source}")] + CreatePolicyDir { + dir: PathBuf, + source: std::io::Error, + }, + #[error("failed to format prefix token {token}: {source}")] + SerializeToken { + token: String, + source: serde_json::Error, + }, + #[error("failed to open policy file {path}: {source}")] + OpenPolicyFile { + path: PathBuf, + source: std::io::Error, + }, + #[error("failed to write to policy file {path}: {source}")] + WritePolicyFile { + path: PathBuf, + source: std::io::Error, + }, + #[error("failed to read metadata for policy file {path}: {source}")] + PolicyMetadata { + path: PathBuf, + source: std::io::Error, + }, +} + +pub fn append_allow_prefix_rule(policy_path: &Path, prefix: &[String]) -> Result<(), AmendError> { + if prefix.is_empty() { + return Err(AmendError::EmptyPrefix); + } + + let tokens: Vec = prefix + .iter() + .map(|token| { + serde_json::to_string(token).map_err(|source| AmendError::SerializeToken { + token: token.clone(), + source, + }) + }) + .collect::>()?; + let pattern = tokens.join(", "); + let rule = format!("prefix_rule(pattern=[{pattern}], decision=\"allow\")\n"); + + let dir = policy_path + .parent() + .ok_or_else(|| AmendError::MissingParent { + path: policy_path.to_path_buf(), + })?; + match std::fs::create_dir(dir) { + Ok(()) => {} + Err(ref source) if source.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(source) => { + return Err(AmendError::CreatePolicyDir { + dir: dir.to_path_buf(), + source, + }); + } + } + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(policy_path) + .map_err(|source| AmendError::OpenPolicyFile { + path: policy_path.to_path_buf(), + source, + })?; + let needs_newline = file + .metadata() + .map(|metadata| metadata.len() > 0) + .map_err(|source| AmendError::PolicyMetadata { + path: policy_path.to_path_buf(), + source, + })?; + let final_rule = if needs_newline { + format!("\n{rule}") + } else { + rule + }; + + file.write_all(final_rule.as_bytes()) + .map_err(|source| AmendError::WritePolicyFile { + path: policy_path.to_path_buf(), + source, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use tempfile::tempdir; + + #[test] + fn appends_rule_and_creates_directories() { + let tmp = tempdir().expect("create temp dir"); + let policy_path = tmp.path().join("policy").join("default.codexpolicy"); + + append_allow_prefix_rule(&policy_path, &[String::from("bash"), String::from("-lc")]) + .expect("append rule"); + + let contents = + std::fs::read_to_string(&policy_path).expect("default.codexpolicy should exist"); + assert_eq!( + contents, + "prefix_rule(pattern=[\"bash\", \"-lc\"], decision=\"allow\")\n" + ); + } + + #[test] + fn separates_rules_with_newlines_when_appending() { + let tmp = tempdir().expect("create temp dir"); + let policy_path = tmp.path().join("policy").join("default.codexpolicy"); + std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir"); + std::fs::write( + &policy_path, + "prefix_rule(pattern=[\"ls\"], decision=\"allow\")\n", + ) + .expect("write seed rule"); + + append_allow_prefix_rule(&policy_path, &[String::from("git")]).expect("append rule"); + + let contents = std::fs::read_to_string(&policy_path).expect("read policy"); + assert_eq!( + contents, + "prefix_rule(pattern=[\"ls\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\"], decision=\"allow\")\n" + ); + } +} diff --git a/codex-rs/execpolicy/src/lib.rs b/codex-rs/execpolicy/src/lib.rs index 1b789fd862..a95d678b5d 100644 --- a/codex-rs/execpolicy/src/lib.rs +++ b/codex-rs/execpolicy/src/lib.rs @@ -1,9 +1,12 @@ +pub mod amend; pub mod decision; pub mod error; pub mod parser; pub mod policy; pub mod rule; +pub use amend::AmendError; +pub use amend::append_allow_prefix_rule; pub use decision::Decision; pub use error::Error; pub use error::Result;