Skip to content

Commit 25922b9

Browse files
committed
adding support for reading execpolicy from /etc/codex and .codex of root git repo
1 parent 6c9c563 commit 25922b9

File tree

3 files changed

+114
-13
lines changed

3 files changed

+114
-13
lines changed

codex-rs/core/src/codex.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,10 @@ impl Codex {
176176

177177
let user_instructions = get_user_instructions(&config).await;
178178

179-
let exec_policy = load_exec_policy_for_features(&config.features, &config.codex_home)
180-
.await
181-
.map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?;
179+
let exec_policy =
180+
load_exec_policy_for_features(&config.features, &config.codex_home, &config.cwd)
181+
.await
182+
.map_err(|err| CodexErr::Fatal(format!("failed to load execpolicy: {err}")))?;
182183
let exec_policy = Arc::new(RwLock::new(exec_policy));
183184

184185
let config = Arc::new(config);

codex-rs/core/src/exec_policy.rs

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use tokio::task::spawn_blocking;
2323
use crate::bash::parse_shell_lc_plain_commands;
2424
use crate::features::Feature;
2525
use crate::features::Features;
26+
use crate::git_info::resolve_root_git_project_for_trust;
2627
use crate::sandboxing::SandboxPermissions;
2728
use crate::tools::sandboxing::ExecApprovalRequirement;
2829

@@ -76,17 +77,28 @@ pub enum ExecPolicyUpdateError {
7677
pub(crate) async fn load_exec_policy_for_features(
7778
features: &Features,
7879
codex_home: &Path,
80+
cwd: &Path,
7981
) -> Result<Policy, ExecPolicyError> {
8082
if !features.enabled(Feature::ExecPolicy) {
8183
Ok(Policy::empty())
8284
} else {
83-
load_exec_policy(codex_home).await
85+
load_exec_policy(codex_home, Some(cwd)).await
8486
}
8587
}
8688

87-
pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyError> {
88-
let policy_dir = codex_home.join(POLICY_DIR_NAME);
89-
let policy_paths = collect_policy_files(&policy_dir).await?;
89+
pub async fn load_exec_policy(
90+
codex_home: &Path,
91+
cwd: Option<&Path>,
92+
) -> Result<Policy, ExecPolicyError> {
93+
let cwd = cwd
94+
.map(PathBuf::from)
95+
.or_else(|| std::env::current_dir().ok());
96+
97+
let policy_dirs = execpolicy_directories(codex_home, cwd.as_deref());
98+
let mut policy_paths = Vec::new();
99+
for policy_dir in &policy_dirs {
100+
policy_paths.extend(collect_policy_files(policy_dir).await?);
101+
}
90102

91103
let mut parser = PolicyParser::new();
92104
for policy_path in &policy_paths {
@@ -108,9 +120,10 @@ pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyErr
108120

109121
let policy = parser.build();
110122
tracing::debug!(
111-
"loaded execpolicy from {} files in {}",
123+
policy_dirs = ?policy_dirs,
124+
"loaded execpolicy from {} files across {} policy directories",
112125
policy_paths.len(),
113-
policy_dir.display()
126+
policy_dirs.len()
114127
);
115128

116129
Ok(policy)
@@ -246,6 +259,31 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
246259
}
247260
}
248261

262+
fn execpolicy_directories(codex_home: &Path, cwd: Option<&Path>) -> Vec<PathBuf> {
263+
let mut policy_dirs = Vec::new();
264+
push_unique_policy_dir(&mut policy_dirs, codex_home.join(POLICY_DIR_NAME));
265+
266+
if let Some(repo_root) = cwd.and_then(resolve_root_git_project_for_trust) {
267+
push_unique_policy_dir(
268+
&mut policy_dirs,
269+
repo_root.join(".codex").join(POLICY_DIR_NAME),
270+
);
271+
}
272+
273+
push_unique_policy_dir(
274+
&mut policy_dirs,
275+
PathBuf::from("/etc/codex").join(POLICY_DIR_NAME),
276+
);
277+
278+
policy_dirs
279+
}
280+
281+
fn push_unique_policy_dir(policy_dirs: &mut Vec<PathBuf>, dir: PathBuf) {
282+
if !policy_dirs.iter().any(|existing| existing == &dir) {
283+
policy_dirs.push(dir);
284+
}
285+
}
286+
249287
async fn collect_policy_files(dir: &Path) -> Result<Vec<PathBuf>, ExecPolicyError> {
250288
let mut read_dir = match fs::read_dir(dir).await {
251289
Ok(read_dir) => read_dir,
@@ -301,6 +339,8 @@ mod tests {
301339
use codex_protocol::protocol::SandboxPolicy;
302340
use pretty_assertions::assert_eq;
303341
use std::fs;
342+
use std::path::PathBuf;
343+
use std::process::Command;
304344
use std::sync::Arc;
305345
use tempfile::tempdir;
306346

@@ -310,7 +350,7 @@ mod tests {
310350
features.disable(Feature::ExecPolicy);
311351
let temp_dir = tempdir().expect("create temp dir");
312352

313-
let policy = load_exec_policy_for_features(&features, temp_dir.path())
353+
let policy = load_exec_policy_for_features(&features, temp_dir.path(), temp_dir.path())
314354
.await
315355
.expect("policy result");
316356

@@ -340,6 +380,16 @@ mod tests {
340380
assert!(files.is_empty());
341381
}
342382

383+
#[test]
384+
fn execpolicy_directories_include_system_policy_dir() {
385+
let codex_home = PathBuf::from("/tmp/codex-home");
386+
387+
let dirs = execpolicy_directories(&codex_home, None);
388+
389+
assert!(dirs.contains(&codex_home.join(POLICY_DIR_NAME)));
390+
assert!(dirs.contains(&PathBuf::from("/etc/codex").join(POLICY_DIR_NAME)));
391+
}
392+
343393
#[tokio::test]
344394
async fn loads_policies_from_policy_subdirectory() {
345395
let temp_dir = tempdir().expect("create temp dir");
@@ -351,7 +401,7 @@ mod tests {
351401
)
352402
.expect("write policy file");
353403

354-
let policy = load_exec_policy(temp_dir.path())
404+
let policy = load_exec_policy(temp_dir.path(), Some(temp_dir.path()))
355405
.await
356406
.expect("policy result");
357407
let command = [vec!["rm".to_string()]];
@@ -376,7 +426,7 @@ mod tests {
376426
)
377427
.expect("write policy file");
378428

379-
let policy = load_exec_policy(temp_dir.path())
429+
let policy = load_exec_policy(temp_dir.path(), Some(temp_dir.path()))
380430
.await
381431
.expect("policy result");
382432
let command = [vec!["ls".to_string()]];
@@ -392,6 +442,54 @@ mod tests {
392442
);
393443
}
394444

445+
#[tokio::test]
446+
async fn loads_policies_from_git_repo_codex_dir() {
447+
let temp_dir = tempdir().expect("create temp dir");
448+
let codex_home = temp_dir.path().join("home");
449+
let repo_root = temp_dir.path().join("repo");
450+
fs::create_dir_all(&repo_root).expect("create repo dir");
451+
let git_init_status = Command::new("git")
452+
.env("GIT_CONFIG_GLOBAL", "/dev/null")
453+
.env("GIT_CONFIG_NOSYSTEM", "1")
454+
.arg("init")
455+
.current_dir(&repo_root)
456+
.status()
457+
.expect("initialize git repo");
458+
assert!(
459+
git_init_status.success(),
460+
"git init failed: {git_init_status:?}"
461+
);
462+
463+
let nested_cwd = repo_root.join("nested");
464+
fs::create_dir_all(&nested_cwd).expect("create nested cwd");
465+
466+
let repo_policy_dir = repo_root.join(".codex").join(POLICY_DIR_NAME);
467+
fs::create_dir_all(&repo_policy_dir).expect("create repo policy dir");
468+
fs::write(
469+
repo_policy_dir.join("deny.codexpolicy"),
470+
r#"prefix_rule(pattern=["git-policy"], decision="forbidden")"#,
471+
)
472+
.expect("write repo policy file");
473+
474+
let policy = load_exec_policy(&codex_home, Some(nested_cwd.as_path()))
475+
.await
476+
.expect("policy result");
477+
let command = [vec!["git-policy".to_string()]];
478+
let evaluation = policy.check_multiple(command.iter(), &|_| Decision::Allow);
479+
480+
assert_eq!(evaluation.decision, Decision::Forbidden);
481+
assert!(
482+
evaluation.matched_rules.iter().any(|rule_match| matches!(
483+
rule_match,
484+
RuleMatch::PrefixRuleMatch {
485+
matched_prefix,
486+
decision: Decision::Forbidden
487+
} if matched_prefix == &vec!["git-policy".to_string()]
488+
)),
489+
"expected git repo execpolicy rule to match: {evaluation:?}"
490+
);
491+
}
492+
395493
#[tokio::test]
396494
async fn evaluates_bash_lc_inner_commands() {
397495
let policy_src = r#"

codex-rs/exec-server/src/posix.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ fn format_program_name(path: &Path, preserve_program_paths: bool) -> Option<Stri
221221

222222
async fn load_exec_policy() -> anyhow::Result<Policy> {
223223
let codex_home = find_codex_home().context("failed to resolve codex_home for execpolicy")?;
224-
codex_core::load_exec_policy(&codex_home)
224+
let cwd = std::env::current_dir().ok();
225+
226+
codex_core::load_exec_policy(&codex_home, cwd.as_deref())
225227
.await
226228
.map_err(anyhow::Error::from)
227229
}

0 commit comments

Comments
 (0)