mirror of
https://github.com/openai/codex.git
synced 2026-03-05 21:45:28 +03:00
fix: policy/*.codexpolicy -> rules/*.rules (#7888)
We decided that `*.rules` is a more fitting (and concise) file extension than `*.codexpolicy`, so we are changing the file extension for the "execpolicy" effort. We are also changing the subfolder of `$CODEX_HOME` from `policy` to `rules` to match. This PR updates the in-repo docs and we will update the public docs once the next CLI release goes out. Locally, I created `~/.codex/rules/default.rules` with the following contents: ``` prefix_rule(pattern=["gh", "pr", "view"]) ``` And then I asked Codex to run: ``` gh pr view 7888 --json title,body,comments ``` and it was able to!
This commit is contained in:
@@ -8,7 +8,12 @@ use tempfile::TempDir;
|
||||
#[test]
|
||||
fn execpolicy_check_matches_expected_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let policy_path = codex_home.path().join("policy.codexpolicy");
|
||||
let policy_path = codex_home.path().join("rules").join("policy.rules");
|
||||
fs::create_dir_all(
|
||||
policy_path
|
||||
.parent()
|
||||
.expect("policy path should have a parent"),
|
||||
)?;
|
||||
fs::write(
|
||||
&policy_path,
|
||||
r#"
|
||||
@@ -24,7 +29,7 @@ prefix_rule(
|
||||
.args([
|
||||
"execpolicy",
|
||||
"check",
|
||||
"--policy",
|
||||
"--rules",
|
||||
policy_path
|
||||
.to_str()
|
||||
.expect("policy path should be valid UTF-8"),
|
||||
|
||||
@@ -30,9 +30,9 @@ const FORBIDDEN_REASON: &str = "execpolicy forbids this command";
|
||||
const PROMPT_CONFLICT_REASON: &str =
|
||||
"execpolicy requires approval for this command, but AskForApproval is set to Never";
|
||||
const PROMPT_REASON: &str = "execpolicy requires approval for this command";
|
||||
const POLICY_DIR_NAME: &str = "policy";
|
||||
const POLICY_EXTENSION: &str = "codexpolicy";
|
||||
const DEFAULT_POLICY_FILE: &str = "default.codexpolicy";
|
||||
const RULES_DIR_NAME: &str = "rules";
|
||||
const RULE_EXTENSION: &str = "rules";
|
||||
const DEFAULT_POLICY_FILE: &str = "default.rules";
|
||||
|
||||
fn is_policy_match(rule_match: &RuleMatch) -> bool {
|
||||
match rule_match {
|
||||
@@ -92,7 +92,7 @@ pub(crate) async fn load_exec_policy_for_features(
|
||||
}
|
||||
|
||||
pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyError> {
|
||||
let policy_dir = codex_home.join(POLICY_DIR_NAME);
|
||||
let policy_dir = codex_home.join(RULES_DIR_NAME);
|
||||
let policy_paths = collect_policy_files(&policy_dir).await?;
|
||||
|
||||
let mut parser = PolicyParser::new();
|
||||
@@ -124,7 +124,7 @@ pub async fn load_exec_policy(codex_home: &Path) -> Result<Policy, ExecPolicyErr
|
||||
}
|
||||
|
||||
pub(crate) fn default_policy_path(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(POLICY_DIR_NAME).join(DEFAULT_POLICY_FILE)
|
||||
codex_home.join(RULES_DIR_NAME).join(DEFAULT_POLICY_FILE)
|
||||
}
|
||||
|
||||
pub(crate) async fn append_execpolicy_amendment_and_update(
|
||||
@@ -304,7 +304,7 @@ async fn collect_policy_files(dir: &Path) -> Result<Vec<PathBuf>, ExecPolicyErro
|
||||
if path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext == POLICY_EXTENSION)
|
||||
.is_some_and(|ext| ext == RULE_EXTENSION)
|
||||
&& file_type.is_file()
|
||||
{
|
||||
policy_paths.push(path);
|
||||
@@ -349,14 +349,14 @@ mod tests {
|
||||
},
|
||||
policy.check_multiple(commands.iter(), &|_| Decision::Allow)
|
||||
);
|
||||
assert!(!temp_dir.path().join(POLICY_DIR_NAME).exists());
|
||||
assert!(!temp_dir.path().join(RULES_DIR_NAME).exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collect_policy_files_returns_empty_when_dir_missing() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
|
||||
let policy_dir = temp_dir.path().join(POLICY_DIR_NAME);
|
||||
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
|
||||
let files = collect_policy_files(&policy_dir)
|
||||
.await
|
||||
.expect("collect policy files");
|
||||
@@ -367,10 +367,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn loads_policies_from_policy_subdirectory() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
let policy_dir = temp_dir.path().join(POLICY_DIR_NAME);
|
||||
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
|
||||
fs::create_dir_all(&policy_dir).expect("create policy dir");
|
||||
fs::write(
|
||||
policy_dir.join("deny.codexpolicy"),
|
||||
policy_dir.join("deny.rules"),
|
||||
r#"prefix_rule(pattern=["rm"], decision="forbidden")"#,
|
||||
)
|
||||
.expect("write policy file");
|
||||
@@ -395,7 +395,7 @@ mod tests {
|
||||
async fn ignores_policies_outside_policy_dir() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
fs::write(
|
||||
temp_dir.path().join("root.codexpolicy"),
|
||||
temp_dir.path().join("root.rules"),
|
||||
r#"prefix_rule(pattern=["ls"], decision="prompt")"#,
|
||||
)
|
||||
.expect("write policy file");
|
||||
@@ -423,7 +423,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
|
||||
@@ -456,7 +456,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec!["rm".to_string()];
|
||||
@@ -485,7 +485,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec!["rm".to_string()];
|
||||
@@ -537,7 +537,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["apple"], decision="allow")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec![
|
||||
@@ -668,7 +668,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec!["rm".to_string()];
|
||||
@@ -726,7 +726,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["cat"], decision="allow")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
|
||||
@@ -783,7 +783,7 @@ prefix_rule(pattern=["rm"], decision="forbidden")
|
||||
let policy_src = r#"prefix_rule(pattern=["echo"], decision="allow")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.codexpolicy", policy_src)
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let policy = Arc::new(RwLock::new(parser.build()));
|
||||
let command = vec!["echo".to_string(), "safe".to_string()];
|
||||
|
||||
@@ -1633,7 +1633,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
|
||||
.await?;
|
||||
wait_for_completion(&test).await;
|
||||
|
||||
let policy_path = test.home.path().join("policy").join("default.codexpolicy");
|
||||
let policy_path = test.home.path().join("rules").join("default.rules");
|
||||
let policy_contents = fs::read_to_string(&policy_path)?;
|
||||
assert!(
|
||||
policy_contents
|
||||
|
||||
@@ -27,7 +27,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
|
||||
}
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
let policy_path = config.codex_home.join("policy").join("policy.codexpolicy");
|
||||
let policy_path = config.codex_home.join("rules").join("policy.rules");
|
||||
fs::create_dir_all(
|
||||
policy_path
|
||||
.parent()
|
||||
|
||||
@@ -72,9 +72,9 @@ pub async fn write_default_execpolicy<P>(policy: &str, codex_home: P) -> anyhow:
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let policy_dir = codex_home.as_ref().join("policy");
|
||||
let policy_dir = codex_home.as_ref().join("rules");
|
||||
tokio::fs::create_dir_all(&policy_dir).await?;
|
||||
tokio::fs::write(policy_dir.join("default.codexpolicy"), policy).await?;
|
||||
tokio::fs::write(policy_dir.join("default.rules"), policy).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ use tempfile::TempDir;
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn list_tools() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let policy_dir = codex_home.path().join("policy");
|
||||
let policy_dir = codex_home.path().join("rules");
|
||||
fs::create_dir_all(&policy_dir)?;
|
||||
fs::write(
|
||||
policy_dir.join("default.codexpolicy"),
|
||||
policy_dir.join("default.rules"),
|
||||
r#"prefix_rule(pattern=["ls"], decision="prompt")"#,
|
||||
)?;
|
||||
let dotslash_cache_temp_dir = TempDir::new()?;
|
||||
|
||||
@@ -20,14 +20,14 @@ prefix_rule(
|
||||
```
|
||||
|
||||
## CLI
|
||||
- From the Codex CLI, run `codex execpolicy check` subcommand with one or more policy files (for example `src/default.codexpolicy`) to check a command:
|
||||
- From the Codex CLI, run `codex execpolicy check` subcommand with one or more policy files (for example `src/default.rules`) to check a command:
|
||||
```bash
|
||||
codex execpolicy check --policy path/to/policy.codexpolicy git status
|
||||
codex execpolicy check --rules path/to/policy.rules git status
|
||||
```
|
||||
- Pass multiple `--policy` flags to merge rules, evaluated in the order provided, and use `--pretty` for formatted JSON.
|
||||
- Pass multiple `--rules` flags to merge rules, evaluated in the order provided, and use `--pretty` for formatted JSON.
|
||||
- You can also run the standalone dev binary directly during development:
|
||||
```bash
|
||||
cargo run -p codex-execpolicy -- check --policy path/to/policy.codexpolicy git status
|
||||
cargo run -p codex-execpolicy -- check --rules path/to/policy.rules git status
|
||||
```
|
||||
- Example outcomes:
|
||||
- Match: `{"matchedRules":[{...}],"decision":"allow"}`
|
||||
|
||||
@@ -154,7 +154,7 @@ mod tests {
|
||||
#[test]
|
||||
fn appends_rule_and_creates_directories() {
|
||||
let tmp = tempdir().expect("create temp dir");
|
||||
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
|
||||
let policy_path = tmp.path().join("rules").join("default.rules");
|
||||
|
||||
blocking_append_allow_prefix_rule(
|
||||
&policy_path,
|
||||
@@ -162,8 +162,7 @@ mod tests {
|
||||
)
|
||||
.expect("append rule");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(&policy_path).expect("default.codexpolicy should exist");
|
||||
let contents = std::fs::read_to_string(&policy_path).expect("default.rules should exist");
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
|
||||
@@ -174,7 +173,7 @@ mod tests {
|
||||
#[test]
|
||||
fn appends_rule_without_duplicate_newline() {
|
||||
let tmp = tempdir().expect("create temp dir");
|
||||
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
|
||||
let policy_path = tmp.path().join("rules").join("default.rules");
|
||||
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
|
||||
std::fs::write(
|
||||
&policy_path,
|
||||
@@ -201,7 +200,7 @@ prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
|
||||
#[test]
|
||||
fn inserts_newline_when_missing_before_append() {
|
||||
let tmp = tempdir().expect("create temp dir");
|
||||
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
|
||||
let policy_path = tmp.path().join("rules").join("default.rules");
|
||||
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
|
||||
std::fs::write(
|
||||
&policy_path,
|
||||
|
||||
@@ -14,9 +14,9 @@ use crate::RuleMatch;
|
||||
/// Arguments for evaluating a command against one or more execpolicy files.
|
||||
#[derive(Debug, Parser, Clone)]
|
||||
pub struct ExecPolicyCheckCommand {
|
||||
/// Paths to execpolicy files to evaluate (repeatable).
|
||||
#[arg(short = 'p', long = "policy", value_name = "PATH", required = true)]
|
||||
pub policies: Vec<PathBuf>,
|
||||
/// Paths to execpolicy rule files to evaluate (repeatable).
|
||||
#[arg(short = 'r', long = "rules", value_name = "PATH", required = true)]
|
||||
pub rules: Vec<PathBuf>,
|
||||
|
||||
/// Pretty-print the JSON output.
|
||||
#[arg(long)]
|
||||
@@ -35,7 +35,7 @@ pub struct ExecPolicyCheckCommand {
|
||||
impl ExecPolicyCheckCommand {
|
||||
/// Load the policies for this command, evaluate the command, and render JSON output.
|
||||
pub fn run(&self) -> Result<()> {
|
||||
let policy = load_policies(&self.policies)?;
|
||||
let policy = load_policies(&self.rules)?;
|
||||
let matched_rules = policy.matches_for_command(&self.command, None);
|
||||
|
||||
let json = format_matches_json(&matched_rules, self.pretty)?;
|
||||
|
||||
@@ -54,7 +54,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
let cmd = tokens(&["git", "status"]);
|
||||
let evaluation = policy.check(&cmd, &allow_all);
|
||||
@@ -129,8 +129,8 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("first.codexpolicy", first_policy)?;
|
||||
parser.parse("second.codexpolicy", second_policy)?;
|
||||
parser.parse("first.rules", first_policy)?;
|
||||
parser.parse("second.rules", second_policy)?;
|
||||
let policy = parser.build();
|
||||
|
||||
let git_rules = rule_snapshots(policy.rules().get_vec("git").context("missing git rules")?);
|
||||
@@ -194,7 +194,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
|
||||
let bash_rules = rule_snapshots(
|
||||
@@ -259,7 +259,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
|
||||
let rules = rule_snapshots(policy.rules().get_vec("npm").context("missing npm rules")?);
|
||||
@@ -323,7 +323,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
let match_eval = policy.check(&tokens(&["git", "status"]), &allow_all);
|
||||
assert_eq!(
|
||||
@@ -367,7 +367,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
|
||||
let commit = policy.check(&tokens(&["git", "commit", "-m", "hi"]), &allow_all);
|
||||
@@ -403,7 +403,7 @@ prefix_rule(
|
||||
)
|
||||
"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.codexpolicy", policy_src)?;
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
|
||||
let commands = vec for syntax details):
|
||||
|
||||
```shell
|
||||
codex execpolicy check --policy ~/.codex/policy/default.codexpolicy git push origin main
|
||||
codex execpolicy check --rules ~/.codex/rules/default.rules git push origin main
|
||||
```
|
||||
|
||||
Pass multiple `--policy` flags to test how several files combine, and use `--pretty` for formatted JSON output. See the [`codex-rs/execpolicy` README](../codex-rs/execpolicy/README.md) for a more detailed walkthrough of the available syntax.
|
||||
Pass multiple `--rules` flags to test how several files combine, and use `--pretty` for formatted JSON output. See the [`codex-rs/execpolicy` README](../codex-rs/execpolicy/README.md) for a more detailed walkthrough of the available syntax.
|
||||
|
||||
Example output when a rule matches:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user