Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions codex-rs/app-server-protocol/src/protocol/v2/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ pub struct ActivePermissionProfile {
/// Identifier from `default_permissions` or the implicit built-in default,
/// such as `:workspace` or a user-defined `[permissions.<id>]` profile.
pub id: String,
/// Parent profile identifier once permissions profiles support
/// inheritance. This is currently always `null`.
/// Parent profile identifier from the selected permissions profile's
/// `extends` setting, when present.
#[serde(default)]
pub extends: Option<String>,
}
Expand Down
218 changes: 218 additions & 0 deletions codex-rs/config/src/permissions_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use codex_protocol::permissions::FileSystemAccessMode;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct PermissionsToml {
Expand All @@ -20,16 +21,233 @@ impl PermissionsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}

pub fn resolve_profile<F>(
&self,
profile_name: &str,
mut parent_profile: F,
) -> Result<ResolvedPermissionProfileToml, PermissionProfileResolutionError>
where
F: FnMut(&str) -> Option<PermissionProfileToml>,
{
let mut profile_names = Vec::new();
let mut profiles = Vec::new();
let mut next_profile_name = profile_name.to_string();
let mut referenced_by: Option<String> = None;

loop {
if let Some(cycle_start) = profile_names
.iter()
.position(|name| name == &next_profile_name)
{
let cycle = profile_names[cycle_start..]
.iter()
.cloned()
.chain(std::iter::once(next_profile_name))
.collect::<Vec<_>>();
return Err(PermissionProfileResolutionError::Cycle { cycle });
}

let profile = self
.entries
.get(&next_profile_name)
.cloned()
.or_else(|| parent_profile(&next_profile_name))
.ok_or_else(|| {
referenced_by.as_deref().map_or_else(
|| PermissionProfileResolutionError::UndefinedProfile {
profile_name: next_profile_name.clone(),
},
|referenced_by| PermissionProfileResolutionError::UndefinedParent {
profile_name: referenced_by.to_string(),
parent_profile_name: next_profile_name.clone(),
},
)
})?;
let parent_profile_name = profile.extends.clone();

profile_names.push(next_profile_name.clone());

if let Some(parent_profile_name) = parent_profile_name {
profiles.push(profile);
referenced_by = Some(next_profile_name);
next_profile_name = parent_profile_name;
continue;
}

let profile = profiles
.into_iter()
.rev()
.fold(profile, merge_permission_profiles);
return Ok(ResolvedPermissionProfileToml {
profile,
inherited_profile_names: profile_names.into_iter().skip(1).collect(),
});
}
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct PermissionProfileToml {
pub extends: Option<String>,
pub workspace_roots: Option<WorkspaceRootsToml>,
pub filesystem: Option<FilesystemPermissionsToml>,
pub network: Option<NetworkToml>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPermissionProfileToml {
pub profile: PermissionProfileToml,
pub inherited_profile_names: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PermissionProfileResolutionError {
#[error("default_permissions refers to undefined profile `{profile_name}`")]
UndefinedProfile { profile_name: String },
#[error(
"permissions profile `{profile_name}` extends undefined profile `{parent_profile_name}`"
)]
UndefinedParent {
profile_name: String,
parent_profile_name: String,
},
#[error(
"permissions profile inheritance cycle detected: {}",
cycle.join(" -> ")
)]
Cycle { cycle: Vec<String> },
}

fn merge_permission_profiles(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any thoughts on whether we can do this using the raw TomlValues so that we don't have to special-case everything here?

parent: PermissionProfileToml,
child: PermissionProfileToml,
) -> PermissionProfileToml {
PermissionProfileToml {
extends: child.extends,
workspace_roots: merge_workspace_roots(parent.workspace_roots, child.workspace_roots),
filesystem: merge_filesystem_permissions(parent.filesystem, child.filesystem),
network: merge_network_permissions(parent.network, child.network),
}
}

fn merge_workspace_roots(
parent: Option<WorkspaceRootsToml>,
child: Option<WorkspaceRootsToml>,
) -> Option<WorkspaceRootsToml> {
match (parent, child) {
(Some(mut parent), Some(child)) => {
parent.entries.extend(child.entries);
Some(parent)
}
(Some(parent), None) => Some(parent),
(None, Some(child)) => Some(child),
(None, None) => None,
}
}

fn merge_filesystem_permissions(
parent: Option<FilesystemPermissionsToml>,
child: Option<FilesystemPermissionsToml>,
) -> Option<FilesystemPermissionsToml> {
match (parent, child) {
(Some(mut parent), Some(child)) => {
if child.glob_scan_max_depth.is_some() {
parent.glob_scan_max_depth = child.glob_scan_max_depth;
}
for (path, child_permission) in child.entries {
match (parent.entries.remove(&path), child_permission) {
(
Some(FilesystemPermissionToml::Scoped(mut parent_entries)),
FilesystemPermissionToml::Scoped(child_entries),
) => {
parent_entries.extend(child_entries);
parent
.entries
.insert(path, FilesystemPermissionToml::Scoped(parent_entries));
}
(_, child_permission) => {
parent.entries.insert(path, child_permission);
}
}
}
Some(parent)
}
(Some(parent), None) => Some(parent),
(None, Some(child)) => Some(child),
(None, None) => None,
}
}

fn merge_network_permissions(
parent: Option<NetworkToml>,
child: Option<NetworkToml>,
) -> Option<NetworkToml> {
match (parent, child) {
(Some(mut parent), Some(child)) => {
parent.enabled = child.enabled.or(parent.enabled);
parent.proxy_url = child.proxy_url.or(parent.proxy_url);
parent.enable_socks5 = child.enable_socks5.or(parent.enable_socks5);
parent.socks_url = child.socks_url.or(parent.socks_url);
parent.enable_socks5_udp = child.enable_socks5_udp.or(parent.enable_socks5_udp);
parent.allow_upstream_proxy =
child.allow_upstream_proxy.or(parent.allow_upstream_proxy);
parent.dangerously_allow_non_loopback_proxy = child
.dangerously_allow_non_loopback_proxy
.or(parent.dangerously_allow_non_loopback_proxy);
parent.dangerously_allow_all_unix_sockets = child
.dangerously_allow_all_unix_sockets
.or(parent.dangerously_allow_all_unix_sockets);
parent.mode = child.mode.or(parent.mode);
parent.allow_local_binding = child.allow_local_binding.or(parent.allow_local_binding);
parent.domains = merge_network_domain_permissions(parent.domains, child.domains);
parent.unix_sockets =
merge_network_unix_socket_permissions(parent.unix_sockets, child.unix_sockets);
Some(parent)
}
(Some(parent), None) => Some(parent),
(None, Some(child)) => Some(child),
(None, None) => None,
}
}

fn merge_network_domain_permissions(
parent: Option<NetworkDomainPermissionsToml>,
child: Option<NetworkDomainPermissionsToml>,
) -> Option<NetworkDomainPermissionsToml> {
match (parent, child) {
(Some(parent), Some(child)) => {
let mut entries = BTreeMap::new();
for (pattern, permission) in parent.entries {
entries.insert(normalize_host(&pattern), permission);
}
for (pattern, permission) in child.entries {
entries.insert(normalize_host(&pattern), permission);
}
Some(NetworkDomainPermissionsToml { entries })
}
(Some(parent), None) => Some(parent),
(None, Some(child)) => Some(child),
(None, None) => None,
}
}

fn merge_network_unix_socket_permissions(
parent: Option<NetworkUnixSocketPermissionsToml>,
child: Option<NetworkUnixSocketPermissionsToml>,
) -> Option<NetworkUnixSocketPermissionsToml> {
match (parent, child) {
(Some(mut parent), Some(child)) => {
parent.entries.extend(child.entries);
Some(parent)
}
(Some(parent), None) => Some(parent),
(None, Some(child)) => Some(child),
(None, None) => None,
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct WorkspaceRootsToml {
#[serde(flatten)]
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,9 @@
"PermissionProfileToml": {
"additionalProperties": false,
"properties": {
"extends": {
"type": "string"
},
"filesystem": {
"$ref": "#/definitions/FilesystemPermissionsToml"
},
Expand Down
Loading
Loading