mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
adding support for reading execpolicy from /etc/codex and .codex of root git repo
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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#"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user