Skip to content

Commit 1d09ac8

Browse files
authored
execpolicy helpers (#7032)
this PR - adds a helper function to amend `.codexpolicy` files with new prefix rules - adds a utility to `Policy` allowing prefix rules to be added to existing `Policy` structs both additions will be helpful as we thread codexpolicy into the TUI workflow
1 parent 127e307 commit 1d09ac8

File tree

6 files changed

+336
-35
lines changed

6 files changed

+336
-35
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
@@ -28,3 +28,4 @@ thiserror = { workspace = true }
2828

2929
[dev-dependencies]
3030
pretty_assertions = { workspace = true }
31+
tempfile = { workspace = true }

codex-rs/execpolicy/src/amend.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
use std::fs::OpenOptions;
2+
use std::io::Read;
3+
use std::io::Seek;
4+
use std::io::SeekFrom;
5+
use std::io::Write;
6+
use std::path::Path;
7+
use std::path::PathBuf;
8+
9+
use serde_json;
10+
use thiserror::Error;
11+
12+
#[derive(Debug, Error)]
13+
pub enum AmendError {
14+
#[error("prefix rule requires at least one token")]
15+
EmptyPrefix,
16+
#[error("policy path has no parent: {path}")]
17+
MissingParent { path: PathBuf },
18+
#[error("failed to create policy directory {dir}: {source}")]
19+
CreatePolicyDir {
20+
dir: PathBuf,
21+
source: std::io::Error,
22+
},
23+
#[error("failed to format prefix tokens: {source}")]
24+
SerializePrefix { source: serde_json::Error },
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 lock policy file {path}: {source}")]
36+
LockPolicyFile {
37+
path: PathBuf,
38+
source: std::io::Error,
39+
},
40+
#[error("failed to seek policy file {path}: {source}")]
41+
SeekPolicyFile {
42+
path: PathBuf,
43+
source: std::io::Error,
44+
},
45+
#[error("failed to read policy file {path}: {source}")]
46+
ReadPolicyFile {
47+
path: PathBuf,
48+
source: std::io::Error,
49+
},
50+
#[error("failed to read metadata for policy file {path}: {source}")]
51+
PolicyMetadata {
52+
path: PathBuf,
53+
source: std::io::Error,
54+
},
55+
}
56+
57+
/// Note this thread uses advisory file locking and performs blocking I/O, so it should be used with
58+
/// [`tokio::task::spawn_blocking`] when called from an async context.
59+
pub fn blocking_append_allow_prefix_rule(
60+
policy_path: &Path,
61+
prefix: &[String],
62+
) -> Result<(), AmendError> {
63+
if prefix.is_empty() {
64+
return Err(AmendError::EmptyPrefix);
65+
}
66+
67+
let tokens = prefix
68+
.iter()
69+
.map(serde_json::to_string)
70+
.collect::<Result<Vec<_>, _>>()
71+
.map_err(|source| AmendError::SerializePrefix { source })?;
72+
let pattern = format!("[{}]", tokens.join(", "));
73+
let rule = format!(r#"prefix_rule(pattern={pattern}, decision="allow")"#);
74+
75+
let dir = policy_path
76+
.parent()
77+
.ok_or_else(|| AmendError::MissingParent {
78+
path: policy_path.to_path_buf(),
79+
})?;
80+
match std::fs::create_dir(dir) {
81+
Ok(()) => {}
82+
Err(ref source) if source.kind() == std::io::ErrorKind::AlreadyExists => {}
83+
Err(source) => {
84+
return Err(AmendError::CreatePolicyDir {
85+
dir: dir.to_path_buf(),
86+
source,
87+
});
88+
}
89+
}
90+
append_locked_line(policy_path, &rule)
91+
}
92+
93+
fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> {
94+
let mut file = OpenOptions::new()
95+
.create(true)
96+
.read(true)
97+
.append(true)
98+
.open(policy_path)
99+
.map_err(|source| AmendError::OpenPolicyFile {
100+
path: policy_path.to_path_buf(),
101+
source,
102+
})?;
103+
file.lock().map_err(|source| AmendError::LockPolicyFile {
104+
path: policy_path.to_path_buf(),
105+
source,
106+
})?;
107+
108+
let len = file
109+
.metadata()
110+
.map_err(|source| AmendError::PolicyMetadata {
111+
path: policy_path.to_path_buf(),
112+
source,
113+
})?
114+
.len();
115+
116+
// Ensure file ends in a newline before appending.
117+
if len > 0 {
118+
file.seek(SeekFrom::End(-1))
119+
.map_err(|source| AmendError::SeekPolicyFile {
120+
path: policy_path.to_path_buf(),
121+
source,
122+
})?;
123+
let mut last = [0; 1];
124+
file.read_exact(&mut last)
125+
.map_err(|source| AmendError::ReadPolicyFile {
126+
path: policy_path.to_path_buf(),
127+
source,
128+
})?;
129+
130+
if last[0] != b'\n' {
131+
file.write_all(b"\n")
132+
.map_err(|source| AmendError::WritePolicyFile {
133+
path: policy_path.to_path_buf(),
134+
source,
135+
})?;
136+
}
137+
}
138+
139+
file.write_all(format!("{line}\n").as_bytes())
140+
.map_err(|source| AmendError::WritePolicyFile {
141+
path: policy_path.to_path_buf(),
142+
source,
143+
})?;
144+
145+
Ok(())
146+
}
147+
148+
#[cfg(test)]
149+
mod tests {
150+
use super::*;
151+
use pretty_assertions::assert_eq;
152+
use tempfile::tempdir;
153+
154+
#[test]
155+
fn appends_rule_and_creates_directories() {
156+
let tmp = tempdir().expect("create temp dir");
157+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
158+
159+
blocking_append_allow_prefix_rule(
160+
&policy_path,
161+
&[String::from("echo"), String::from("Hello, world!")],
162+
)
163+
.expect("append rule");
164+
165+
let contents =
166+
std::fs::read_to_string(&policy_path).expect("default.codexpolicy should exist");
167+
assert_eq!(
168+
contents,
169+
r#"prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
170+
"#
171+
);
172+
}
173+
174+
#[test]
175+
fn appends_rule_without_duplicate_newline() {
176+
let tmp = tempdir().expect("create temp dir");
177+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
178+
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
179+
std::fs::write(
180+
&policy_path,
181+
r#"prefix_rule(pattern=["ls"], decision="allow")
182+
"#,
183+
)
184+
.expect("write seed rule");
185+
186+
blocking_append_allow_prefix_rule(
187+
&policy_path,
188+
&[String::from("echo"), String::from("Hello, world!")],
189+
)
190+
.expect("append rule");
191+
192+
let contents = std::fs::read_to_string(&policy_path).expect("read policy");
193+
assert_eq!(
194+
contents,
195+
r#"prefix_rule(pattern=["ls"], decision="allow")
196+
prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
197+
"#
198+
);
199+
}
200+
201+
#[test]
202+
fn inserts_newline_when_missing_before_append() {
203+
let tmp = tempdir().expect("create temp dir");
204+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
205+
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
206+
std::fs::write(
207+
&policy_path,
208+
r#"prefix_rule(pattern=["ls"], decision="allow")"#,
209+
)
210+
.expect("write seed rule without newline");
211+
212+
blocking_append_allow_prefix_rule(
213+
&policy_path,
214+
&[String::from("echo"), String::from("Hello, world!")],
215+
)
216+
.expect("append rule");
217+
218+
let contents = std::fs::read_to_string(&policy_path).expect("read policy");
219+
assert_eq!(
220+
contents,
221+
r#"prefix_rule(pattern=["ls"], decision="allow")
222+
prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
223+
"#
224+
);
225+
}
226+
}

codex-rs/execpolicy/src/lib.rs

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

9+
pub use amend::AmendError;
10+
pub use amend::blocking_append_allow_prefix_rule;
811
pub use decision::Decision;
912
pub use error::Error;
1013
pub use error::Result;

codex-rs/execpolicy/src/policy.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
use crate::decision::Decision;
2+
use crate::error::Error;
3+
use crate::error::Result;
4+
use crate::rule::PatternToken;
5+
use crate::rule::PrefixPattern;
6+
use crate::rule::PrefixRule;
27
use crate::rule::RuleMatch;
38
use crate::rule::RuleRef;
49
use multimap::MultiMap;
510
use serde::Deserialize;
611
use serde::Serialize;
12+
use std::sync::Arc;
713

814
#[derive(Clone, Debug)]
915
pub struct Policy {
@@ -23,6 +29,27 @@ impl Policy {
2329
&self.rules_by_program
2430
}
2531

32+
pub fn add_prefix_rule(&mut self, prefix: &[String], decision: Decision) -> Result<()> {
33+
let (first_token, rest) = prefix
34+
.split_first()
35+
.ok_or_else(|| Error::InvalidPattern("prefix cannot be empty".to_string()))?;
36+
37+
let rule: RuleRef = Arc::new(PrefixRule {
38+
pattern: PrefixPattern {
39+
first: Arc::from(first_token.as_str()),
40+
rest: rest
41+
.iter()
42+
.map(|token| PatternToken::Single(token.clone()))
43+
.collect::<Vec<_>>()
44+
.into(),
45+
},
46+
decision,
47+
});
48+
49+
self.rules_by_program.insert(first_token.clone(), rule);
50+
Ok(())
51+
}
52+
2653
pub fn check(&self, cmd: &[String]) -> Evaluation {
2754
let rules = match cmd.first() {
2855
Some(first) => match self.rules_by_program.get_vec(first) {

0 commit comments

Comments
 (0)