permissions: support workspace roots in profiles

This commit is contained in:
Michael Bolin
2026-05-14 10:24:07 -07:00
parent 01d93fd9fc
commit 9ccddb30a7
40 changed files with 1011 additions and 537 deletions
+22 -24
View File
@@ -344,21 +344,6 @@ pub struct ActivePermissionProfile {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub extends: Option<String>,
/// Bounded user-requested modifications applied on top of the named
/// profile, if any.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modifications: Vec<ActivePermissionProfileModification>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum ActivePermissionProfileModification {
/// Additional concrete directory that should be writable.
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
AdditionalWritableRoot { path: AbsolutePathBuf },
}
impl ActivePermissionProfile {
@@ -366,17 +351,8 @@ impl ActivePermissionProfile {
Self {
id: id.into(),
extends: None,
modifications: Vec::new(),
}
}
pub fn with_modifications(
mut self,
modifications: Vec<ActivePermissionProfileModification>,
) -> Self {
self.modifications = modifications;
self
}
}
impl Default for PermissionProfile {
@@ -444,6 +420,28 @@ impl PermissionProfile {
}
}
pub fn materialize_project_roots_with_workspace_roots(
self,
workspace_roots: &[AbsolutePathBuf],
) -> Self {
match self {
Self::Managed {
file_system,
network,
} => {
let file_system = file_system
.to_sandbox_policy()
.materialize_project_roots_with_workspace_roots(workspace_roots);
Self::Managed {
file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
network,
}
}
Self::Disabled => Self::Disabled,
Self::External { network } => Self::External { network },
}
}
pub fn from_runtime_permissions(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
network_sandbox_policy: NetworkSandboxPolicy,
+220 -8
View File
@@ -350,6 +350,12 @@ pub enum FileSystemPath {
},
}
const PROJECT_ROOTS_GLOB_PATTERN_PREFIX: &str = "codex-project-roots://";
pub fn project_roots_glob_pattern(subpath: &Path) -> String {
format!("{PROJECT_ROOTS_GLOB_PATTERN_PREFIX}{}", subpath.display())
}
impl Default for FileSystemSandboxPolicy {
fn default() -> Self {
Self {
@@ -703,15 +709,100 @@ impl FileSystemSandboxPolicy {
pub fn materialize_project_roots_with_cwd(mut self, cwd: &Path) -> Self {
let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok();
for entry in &mut self.entries {
let FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots { .. },
} = &entry.path
else {
continue;
};
match &entry.path {
FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots { .. },
} => {
if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) {
entry.path = FileSystemPath::Path { path };
}
}
FileSystemPath::GlobPattern { pattern } => {
if let (Some(cwd), Some(subpath)) =
(cwd.as_ref(), parse_project_roots_glob_pattern(pattern))
{
entry.path = FileSystemPath::GlobPattern {
pattern: resolve_project_roots_glob_pattern(subpath, cwd),
};
}
}
FileSystemPath::Special { value: _ } => {}
FileSystemPath::Path { .. } => {}
}
}
self
}
if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) {
entry.path = FileSystemPath::Path { path };
/// Replaces symbolic `:workspace_roots` entries with concrete entries for
/// each workspace root.
pub fn materialize_project_roots_with_workspace_roots(
mut self,
workspace_roots: &[AbsolutePathBuf],
) -> Self {
let mut entries = Vec::with_capacity(self.entries.len());
for entry in self.entries {
match entry.path {
FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots { subpath },
} => {
entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: match subpath.as_ref() {
Some(subpath) => AbsolutePathBuf::resolve_path_against_base(
subpath,
root.as_path(),
),
None => root.clone(),
},
},
access: entry.access,
}));
}
FileSystemPath::GlobPattern { pattern } => {
if let Some(subpath) = parse_project_roots_glob_pattern(&pattern) {
entries.extend(workspace_roots.iter().map(|root| FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: resolve_project_roots_glob_pattern(subpath, root),
},
access: entry.access,
}));
} else {
entries.push(FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern { pattern },
access: entry.access,
});
}
}
FileSystemPath::Path { path } => {
entries.push(FileSystemSandboxEntry {
path: FileSystemPath::Path { path },
access: entry.access,
});
}
FileSystemPath::Special { value } => {
entries.push(FileSystemSandboxEntry {
path: FileSystemPath::Special { value },
access: entry.access,
});
}
}
}
self.entries = entries;
self
}
/// Preserves symbolic `:workspace_roots` entries while also adding concrete
/// entries for each provided workspace root.
pub fn with_materialized_project_roots_for_workspace_roots(
mut self,
workspace_roots: &[AbsolutePathBuf],
) -> Self {
let materialized = self
.clone()
.materialize_project_roots_with_workspace_roots(workspace_roots);
for entry in materialized.entries {
if !self.entries.contains(&entry) {
self.entries.push(entry);
}
}
self
@@ -1209,6 +1300,18 @@ fn resolve_entry_path(
}
}
fn parse_project_roots_glob_pattern(pattern: &str) -> Option<&Path> {
pattern
.strip_prefix(PROJECT_ROOTS_GLOB_PATTERN_PREFIX)
.map(Path::new)
}
fn resolve_project_roots_glob_pattern(subpath: &Path, root: &AbsolutePathBuf) -> String {
AbsolutePathBuf::resolve_path_against_base(subpath, root.as_path())
.to_string_lossy()
.into_owned()
}
fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option<AbsolutePathBuf> {
if path.is_absolute() {
AbsolutePathBuf::from_absolute_path(path).ok()
@@ -2750,6 +2853,115 @@ mod tests {
);
}
#[test]
fn materialize_project_roots_with_workspace_roots_expands_exact_and_glob_entries() {
let temp_dir = TempDir::new().expect("tempdir");
let first = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("first"))
.expect("resolve first root");
let second = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("second"))
.expect("resolve second root");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(Some(".git".into())),
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
},
access: FileSystemAccessMode::None,
},
]);
let actual =
policy.materialize_project_roots_with_workspace_roots(&[first.clone(), second.clone()]);
assert_eq!(
actual,
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.join(".git"),
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.join(".git"),
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
first.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::None,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
second.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::None,
},
])
);
}
#[test]
fn materialize_project_roots_with_cwd_expands_symbolic_glob_entries() {
let cwd = TempDir::new().expect("tempdir");
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
},
access: FileSystemAccessMode::None,
}]);
let actual = policy.materialize_project_roots_with_cwd(cwd.path());
assert_eq!(
actual,
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::None,
}])
);
}
#[test]
fn with_additional_legacy_workspace_writable_roots_protects_metadata() {
let temp_dir = TempDir::new().expect("tempdir");