Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
115 changes: 106 additions & 9 deletions codex-rs/core/src/exec_policy.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand All @@ -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");

Expand Down Expand Up @@ -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");
Expand All @@ -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()]];
Expand All @@ -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()]];
Expand All @@ -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#"
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/exec-server/src/posix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading