Compare commits

...

1 Commits

Author SHA1 Message Date
kevin zhao
9df6dbcb0f adding support for reading execpolicy from /etc/codex and .codex of root git repo 2025-12-08 10:51:50 -08:00
3 changed files with 113 additions and 13 deletions

View File

@@ -176,9 +176,10 @@ impl Codex {
let user_instructions = get_user_instructions(&config).await;
let exec_policy = load_exec_policy_for_features(&config.features, &config.codex_home)
.await
.map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?;
let exec_policy =
load_exec_policy_for_features(&config.features, &config.codex_home, &config.cwd)
.await
.map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?;
let exec_policy = Arc::new(RwLock::new(exec_policy));
let config = Arc::new(config);

View File

@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
@@ -15,6 +16,7 @@ use codex_execpolicy::blocking_append_allow_prefix_rule;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use futures::future::try_join_all;
use thiserror::Error;
use tokio::fs;
use tokio::sync::RwLock;
@@ -23,6 +25,7 @@ use tokio::task::spawn_blocking;
use crate::bash::parse_shell_lc_plain_commands;
use crate::features::Feature;
use crate::features::Features;
use crate::git_info::resolve_root_git_project_for_trust;
use crate::sandboxing::SandboxPermissions;
use crate::tools::sandboxing::ExecApprovalRequirement;
@@ -76,17 +79,33 @@ pub enum ExecPolicyUpdateError {
pub(crate) async fn load_exec_policy_for_features(
features: &Features,
codex_home: &Path,
cwd: &Path,
) -> Result<Policy, ExecPolicyError> {
if !features.enabled(Feature::ExecPolicy) {
Ok(Policy::empty())
} else {
load_exec_policy(codex_home).await
load_exec_policy(codex_home, Some(cwd)).await
}
}
pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyError> {
let policy_dir = codex_home.join(POLICY_DIR_NAME);
let policy_paths = collect_policy_files(&policy_dir).await?;
pub async fn load_exec_policy(
codex_home: &Path,
cwd: Option<&Path>,
) -> Result<Policy, ExecPolicyError> {
let cwd = cwd
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok());
let policy_dirs = execpolicy_directories(codex_home, cwd.as_deref());
let policy_paths = try_join_all(
policy_dirs
.iter()
.map(|policy_dir| collect_policy_files(policy_dir.as_path())),
)
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let mut parser = PolicyParser::new();
for policy_path in &policy_paths {
@@ -108,9 +127,10 @@ pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyErr
let policy = parser.build();
tracing::debug!(
"loaded execpolicy from {} files in {}",
policy_dirs = ?policy_dirs,
"loaded execpolicy from {} files across {} policy directories",
policy_paths.len(),
policy_dir.display()
policy_dirs.len()
);
Ok(policy)
@@ -246,6 +266,23 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
}
}
fn execpolicy_directories(codex_home: &Path, cwd: Option<&Path>) -> Vec<PathBuf> {
let mut policy_dirs = HashSet::new();
// ~/.codex/policy
policy_dirs.insert(codex_home.join(POLICY_DIR_NAME));
// <repo_root>/.codex/policy
if let Some(repo_root) = cwd.and_then(resolve_root_git_project_for_trust) {
policy_dirs.insert(repo_root.join(".codex").join(POLICY_DIR_NAME));
}
// /etc/codex/policy
policy_dirs.insert(PathBuf::from("/etc/codex").join(POLICY_DIR_NAME));
policy_dirs.into_iter().collect()
}
async fn collect_policy_files(dir: &Path) -> Result<Vec<PathBuf>, ExecPolicyError> {
let mut read_dir = match fs::read_dir(dir).await {
Ok(read_dir) => read_dir,
@@ -301,6 +338,8 @@ mod tests {
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use tempfile::tempdir;
@@ -310,7 +349,7 @@ mod tests {
features.disable(Feature::ExecPolicy);
let temp_dir = tempdir().expect("create temp dir");
let policy = load_exec_policy_for_features(&features, temp_dir.path())
let policy = load_exec_policy_for_features(&features, temp_dir.path(), temp_dir.path())
.await
.expect("policy result");
@@ -340,6 +379,16 @@ mod tests {
assert!(files.is_empty());
}
#[test]
fn execpolicy_directories_include_system_policy_dir() {
let codex_home = PathBuf::from("/tmp/codex-home");
let dirs = execpolicy_directories(&codex_home, None);
assert!(dirs.contains(&codex_home.join(POLICY_DIR_NAME)));
assert!(dirs.contains(&PathBuf::from("/etc/codex").join(POLICY_DIR_NAME)));
}
#[tokio::test]
async fn loads_policies_from_policy_subdirectory() {
let temp_dir = tempdir().expect("create temp dir");
@@ -351,7 +400,7 @@ mod tests {
)
.expect("write policy file");
let policy = load_exec_policy(temp_dir.path())
let policy = load_exec_policy(temp_dir.path(), Some(temp_dir.path()))
.await
.expect("policy result");
let command = [vec!["rm".to_string()]];
@@ -376,7 +425,7 @@ mod tests {
)
.expect("write policy file");
let policy = load_exec_policy(temp_dir.path())
let policy = load_exec_policy(temp_dir.path(), Some(temp_dir.path()))
.await
.expect("policy result");
let command = [vec!["ls".to_string()]];
@@ -392,6 +441,54 @@ mod tests {
);
}
#[tokio::test]
async fn loads_policies_from_git_repo_codex_dir() {
let temp_dir = tempdir().expect("create temp dir");
let codex_home = temp_dir.path().join("home");
let repo_root = temp_dir.path().join("repo");
fs::create_dir_all(&repo_root).expect("create repo dir");
let git_init_status = Command::new("git")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.arg("init")
.current_dir(&repo_root)
.status()
.expect("initialize git repo");
assert!(
git_init_status.success(),
"git init failed: {git_init_status:?}"
);
let nested_cwd = repo_root.join("nested");
fs::create_dir_all(&nested_cwd).expect("create nested cwd");
let repo_policy_dir = repo_root.join(".codex").join(POLICY_DIR_NAME);
fs::create_dir_all(&repo_policy_dir).expect("create repo policy dir");
fs::write(
repo_policy_dir.join("deny.codexpolicy"),
r#"prefix_rule(pattern=["git-policy"], decision="forbidden")"#,
)
.expect("write repo policy file");
let policy = load_exec_policy(&codex_home, Some(nested_cwd.as_path()))
.await
.expect("policy result");
let command = [vec!["git-policy".to_string()]];
let evaluation = policy.check_multiple(command.iter(), &|_| Decision::Allow);
assert_eq!(evaluation.decision, Decision::Forbidden);
assert!(
evaluation.matched_rules.iter().any(|rule_match| matches!(
rule_match,
RuleMatch::PrefixRuleMatch {
matched_prefix,
decision: Decision::Forbidden
} if matched_prefix == &vec!["git-policy".to_string()]
)),
"expected git repo execpolicy rule to match: {evaluation:?}"
);
}
#[tokio::test]
async fn evaluates_bash_lc_inner_commands() {
let policy_src = r#"

View File

@@ -221,7 +221,9 @@ fn format_program_name(path: &Path, preserve_program_paths: bool) -> Option<Stri
async fn load_exec_policy() -> anyhow::Result<Policy> {
let codex_home = find_codex_home().context("failed to resolve codex_home for execpolicy")?;
codex_core::load_exec_policy(&codex_home)
let cwd = std::env::current_dir().ok();
codex_core::load_exec_policy(&codex_home, cwd.as_deref())
.await
.map_err(anyhow::Error::from)
}