codex_execpolicy/
sandbox_migration.rs1use std::collections::HashSet;
2use std::io;
3use std::io::SeekFrom;
4use std::io::Write as _;
5use std::path::Path;
6use tokio::io::AsyncReadExt;
7use tokio::io::AsyncSeekExt;
8use tokio::io::AsyncWriteExt;
9
10const MIGRATION_MARKER_FILENAME: &str = ".sandbox_migration";
11
12pub async fn prefix_rule_migration(
17 codex_home: &Path,
18 policy_path: &Path,
19 banned_prefixes: &[&[&str]],
20) -> io::Result<()> {
21 let marker_path = codex_home.join(MIGRATION_MARKER_FILENAME);
22 if tokio::fs::try_exists(&marker_path).await? {
23 return Ok(());
24 }
25 clean_rules_file(policy_path, banned_prefixes).await?;
26
27 write_migration_marker(codex_home, &marker_path).await?;
28 Ok(())
29}
30
31async fn write_migration_marker(codex_home: &Path, marker_path: &Path) -> io::Result<()> {
33 tokio::fs::create_dir_all(codex_home).await?;
34 let codex_home = codex_home.to_owned();
35 let marker_path = marker_path.to_owned();
36 tokio::task::spawn_blocking(move || {
37 let mut marker = tempfile::NamedTempFile::new_in(codex_home)?;
38 marker.write_all(b"v1\n")?;
39 match marker.persist_noclobber(marker_path) {
40 Ok(_) => Ok(()),
41 Err(err) if err.error.kind() == io::ErrorKind::AlreadyExists => Ok(()),
42 Err(err) => Err(err.error),
43 }
44 })
45 .await
46 .map_err(io::Error::other)?
47}
48
49async fn clean_rules_file(policy_path: &Path, banned_prefixes: &[&[&str]]) -> io::Result<()> {
51 let contents = match tokio::fs::read_to_string(policy_path).await {
52 Ok(contents) => contents,
53 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
54 Err(err) => return Err(err),
55 };
56 if strip_banned_allow_rules(&contents, banned_prefixes) == contents {
57 return Ok(());
58 }
59
60 let mut file = match tokio::fs::OpenOptions::new()
61 .read(true)
62 .write(true)
63 .open(policy_path)
64 .await
65 {
66 Ok(file) => file,
67 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
68 Err(err) => return Err(err),
69 };
70
71 let mut contents = String::new();
72 file.read_to_string(&mut contents).await?;
73 let retained = strip_banned_allow_rules(&contents, banned_prefixes);
74 if retained == contents {
75 return Ok(());
76 }
77
78 file.seek(SeekFrom::Start(0)).await?;
79 file.write_all(retained.as_bytes()).await?;
80 file.set_len(retained.len() as u64).await?;
81 Ok(())
82}
83
84fn strip_banned_allow_rules(contents: &str, banned_prefixes: &[&[&str]]) -> String {
86 let banned_prefixes = banned_prefixes
87 .iter()
88 .map(|prefix| {
89 prefix
90 .iter()
91 .map(|token| token.to_ascii_lowercase())
92 .collect::<Vec<_>>()
93 })
94 .collect::<HashSet<_>>();
95 contents
96 .split_inclusive('\n')
97 .filter(|line| !should_remove_rule(line, &banned_prefixes))
98 .collect()
99}
100
101fn should_remove_rule(line: &str, banned_prefixes: &HashSet<Vec<String>>) -> bool {
103 let line = line.strip_suffix('\n').unwrap_or(line);
104 let line = line.strip_suffix('\r').unwrap_or(line);
105 let Some(pattern) = line
106 .strip_prefix("prefix_rule(pattern=")
107 .and_then(|line| line.strip_suffix(r#", decision="allow")"#))
108 else {
109 return false;
110 };
111 let Ok(prefix) = serde_json::from_str::<Vec<String>>(pattern) else {
112 return false;
113 };
114 let prefix = prefix
115 .iter()
116 .map(|token| token.to_ascii_lowercase())
117 .collect::<Vec<_>>();
118 banned_prefixes.contains(&prefix)
119}
120
121#[cfg(test)]
122#[path = "sandbox_migration_tests.rs"]
123mod tests;