Skip to content

Commit 3c00714

Browse files
committed
Add allow-prefix amendment helper to execpolicy
1 parent 54e6e4a commit 3c00714

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/execpolicy/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ thiserror = { workspace = true }
2727

2828
[dev-dependencies]
2929
pretty_assertions = { workspace = true }
30+
tempfile = { workspace = true }

codex-rs/execpolicy/src/amend.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::fs::OpenOptions;
2+
use std::io::Write;
3+
use std::path::Path;
4+
use std::path::PathBuf;
5+
6+
use serde_json;
7+
use thiserror::Error;
8+
9+
#[derive(Debug, Error)]
10+
pub enum AmendError {
11+
#[error("prefix rule requires at least one token")]
12+
EmptyPrefix,
13+
#[error("policy path has no parent: {path}")]
14+
MissingParent { path: PathBuf },
15+
#[error("failed to create policy directory {dir}: {source}")]
16+
CreatePolicyDir {
17+
dir: PathBuf,
18+
source: std::io::Error,
19+
},
20+
#[error("failed to format prefix token {token}: {source}")]
21+
SerializeToken {
22+
token: String,
23+
source: serde_json::Error,
24+
},
25+
#[error("failed to open policy file {path}: {source}")]
26+
OpenPolicyFile {
27+
path: PathBuf,
28+
source: std::io::Error,
29+
},
30+
#[error("failed to write to policy file {path}: {source}")]
31+
WritePolicyFile {
32+
path: PathBuf,
33+
source: std::io::Error,
34+
},
35+
#[error("failed to read metadata for policy file {path}: {source}")]
36+
PolicyMetadata {
37+
path: PathBuf,
38+
source: std::io::Error,
39+
},
40+
}
41+
42+
pub fn append_allow_prefix_rule(policy_path: &Path, prefix: &[String]) -> Result<(), AmendError> {
43+
if prefix.is_empty() {
44+
return Err(AmendError::EmptyPrefix);
45+
}
46+
47+
let tokens: Vec<String> = prefix
48+
.iter()
49+
.map(|token| {
50+
serde_json::to_string(token).map_err(|source| AmendError::SerializeToken {
51+
token: token.clone(),
52+
source,
53+
})
54+
})
55+
.collect::<Result<_, _>>()?;
56+
let pattern = tokens.join(", ");
57+
let rule = format!("prefix_rule(pattern=[{pattern}], decision=\"allow\")\n");
58+
59+
let dir = policy_path
60+
.parent()
61+
.ok_or_else(|| AmendError::MissingParent {
62+
path: policy_path.to_path_buf(),
63+
})?;
64+
match std::fs::create_dir(dir) {
65+
Ok(()) => {}
66+
Err(ref source) if source.kind() == std::io::ErrorKind::AlreadyExists => {}
67+
Err(source) => {
68+
return Err(AmendError::CreatePolicyDir {
69+
dir: dir.to_path_buf(),
70+
source,
71+
});
72+
}
73+
}
74+
let mut file = OpenOptions::new()
75+
.create(true)
76+
.append(true)
77+
.open(policy_path)
78+
.map_err(|source| AmendError::OpenPolicyFile {
79+
path: policy_path.to_path_buf(),
80+
source,
81+
})?;
82+
let needs_newline = file
83+
.metadata()
84+
.map(|metadata| metadata.len() > 0)
85+
.map_err(|source| AmendError::PolicyMetadata {
86+
path: policy_path.to_path_buf(),
87+
source,
88+
})?;
89+
let final_rule = if needs_newline {
90+
format!("\n{rule}")
91+
} else {
92+
rule
93+
};
94+
95+
file.write_all(final_rule.as_bytes())
96+
.map_err(|source| AmendError::WritePolicyFile {
97+
path: policy_path.to_path_buf(),
98+
source,
99+
})
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
use pretty_assertions::assert_eq;
106+
use tempfile::tempdir;
107+
108+
#[test]
109+
fn appends_rule_and_creates_directories() {
110+
let tmp = tempdir().expect("create temp dir");
111+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
112+
113+
append_allow_prefix_rule(&policy_path, &[String::from("bash"), String::from("-lc")])
114+
.expect("append rule");
115+
116+
let contents =
117+
std::fs::read_to_string(&policy_path).expect("default.codexpolicy should exist");
118+
assert_eq!(
119+
contents,
120+
"prefix_rule(pattern=[\"bash\", \"-lc\"], decision=\"allow\")\n"
121+
);
122+
}
123+
124+
#[test]
125+
fn separates_rules_with_newlines_when_appending() {
126+
let tmp = tempdir().expect("create temp dir");
127+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
128+
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
129+
std::fs::write(
130+
&policy_path,
131+
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")\n",
132+
)
133+
.expect("write seed rule");
134+
135+
append_allow_prefix_rule(&policy_path, &[String::from("git")]).expect("append rule");
136+
137+
let contents = std::fs::read_to_string(&policy_path).expect("read policy");
138+
assert_eq!(
139+
contents,
140+
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\"], decision=\"allow\")\n"
141+
);
142+
}
143+
}

codex-rs/execpolicy/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
pub mod amend;
12
pub mod decision;
23
pub mod error;
34
pub mod parser;
45
pub mod policy;
56
pub mod rule;
67

8+
pub use amend::AmendError;
9+
pub use amend::append_allow_prefix_rule;
710
pub use decision::Decision;
811
pub use error::Error;
912
pub use error::Result;

0 commit comments

Comments
 (0)