Skip to content
Merged
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.

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 @@ -329,8 +329,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
20 changes: 20 additions & 0 deletions codex-rs/config/src/merge.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::key_aliases::normalize_key_aliases;
use crate::key_aliases::normalized_with_key_aliases;
use codex_network_proxy::normalize_host;
use toml::Value as TomlValue;

/// Merge config `overlay` into `base`, giving `overlay` precedence.
Expand All @@ -14,6 +15,10 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m
normalize_key_aliases(path, base_table);
let mut overlay_table = overlay_table.clone();
normalize_key_aliases(path, &mut overlay_table);
if is_permission_network_domains_path(path) {
normalize_network_domain_keys(base_table);
normalize_network_domain_keys(&mut overlay_table);
}

for (key, value) in overlay_table {
path.push(key.clone());
Expand All @@ -29,6 +34,21 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m
}
}

fn is_permission_network_domains_path(path: &[String]) -> bool {
matches!(
path,
[permissions, _, network, domains]
if permissions == "permissions" && network == "network" && domains == "domains"
)
}

fn normalize_network_domain_keys(table: &mut toml::map::Map<String, TomlValue>) {
let entries = std::mem::take(table);
for (pattern, value) in entries {
table.insert(normalize_host(&pattern), value);
}
}

#[cfg(test)]
#[path = "merge_tests.rs"]
mod tests;
26 changes: 26 additions & 0 deletions codex-rs/config/src/merge_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,29 @@ disable_on_external_context = true
);
assert_eq!(base, expected);
}

#[test]
fn merge_toml_values_normalizes_permission_network_domains_before_overlaying() {
let mut base = parse_toml(
r#"
[permissions.dev.network.domains]
"example.com" = "deny"
"#,
);
let overlay = parse_toml(
r#"
[permissions.dev.network.domains]
"EXAMPLE.COM" = "allow"
"#,
);

merge_toml_values(&mut base, &overlay);

let expected = parse_toml(
r#"
[permissions.dev.network.domains]
"example.com" = "allow"
"#,
);
assert_eq!(base, expected);
}
176 changes: 176 additions & 0 deletions codex-rs/config/src/permissions_toml.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;

use crate::merge::merge_toml_values;
use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission;
use codex_network_proxy::NetworkMode;
use codex_network_proxy::NetworkProxyConfig;
Expand All @@ -9,6 +10,8 @@ use codex_protocol::permissions::FileSystemAccessMode;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
use toml::Value as TomlValue;

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct PermissionsToml {
Expand All @@ -20,17 +23,190 @@ 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| {
if next_profile_name.starts_with(':') {
PermissionProfileResolutionError::UnsupportedBuiltInParent {
Comment thread
viyatb-oai marked this conversation as resolved.
profile_name: referenced_by.to_string(),
parent_profile_name: next_profile_name.clone(),
}
} else {
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()
.try_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 description: Option<String>,
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,
/// Names of profiles inherited while resolving `profile`, ordered from the
/// selected profile's direct parent to the farthest ancestor.
///
/// Callers use this to preserve which built-in baseline contributed the
/// resolved permissions after the parent profiles have been merged away.
pub inherited_profile_names: Vec<String>,
Comment thread
viyatb-oai marked this conversation as resolved.
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PermissionProfileResolutionError {
Comment thread
viyatb-oai marked this conversation as resolved.
#[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 `{profile_name}` cannot extend unsupported built-in profile `{parent_profile_name}`"
)]
UnsupportedBuiltInParent {
profile_name: String,
parent_profile_name: String,
},
#[error(
"permissions profile inheritance cycle detected: {}",
cycle.join(" -> ")
)]
Cycle { cycle: Vec<String> },
#[error("failed to serialize permissions profile while resolving inheritance: {source}")]
SerializeProfileToml {
#[source]
source: toml::ser::Error,
},
#[error(
"failed to deserialize merged permissions profile while resolving inheritance: {source}"
)]
DeserializeProfileToml {
#[source]
source: toml::de::Error,
},
}

fn merge_permission_profiles(
Comment thread
viyatb-oai marked this conversation as resolved.
mut parent: PermissionProfileToml,
mut child: PermissionProfileToml,
) -> Result<PermissionProfileToml, PermissionProfileResolutionError> {
let merges_network_domains = parent
.network
.as_ref()
.and_then(|network| network.domains.as_ref())
.is_some()
&& child
.network
.as_ref()
.and_then(|network| network.domains.as_ref())
.is_some();

// Description and inheritance metadata belong to the selected profile
// declaration, so an inherited profile must not fill those gaps.
parent.description = None;
Comment thread
viyatb-oai marked this conversation as resolved.
parent.extends = None;

if merges_network_domains {
normalize_profile_network_domains(&mut parent);
normalize_profile_network_domains(&mut child);
}

let mut merged = TomlValue::try_from(parent)
.map_err(|source| PermissionProfileResolutionError::SerializeProfileToml { source })?;
let child = TomlValue::try_from(child)
.map_err(|source| PermissionProfileResolutionError::SerializeProfileToml { source })?;
merge_toml_values(&mut merged, &child);
merged
.try_into()
.map_err(|source| PermissionProfileResolutionError::DeserializeProfileToml { source })
}

fn normalize_profile_network_domains(profile: &mut PermissionProfileToml) {
let Some(domains) = profile
.network
.as_mut()
.and_then(|network| network.domains.as_mut())
else {
return;
};

let entries = std::mem::take(&mut domains.entries);
domains.entries = entries
.into_iter()
.map(|(pattern, permission)| (normalize_host(&pattern), permission))
.collect();
}

#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
pub struct WorkspaceRootsToml {
#[serde(flatten)]
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,9 @@
"description": {
"type": "string"
},
"extends": {
"type": "string"
},
"filesystem": {
"$ref": "#/definitions/FilesystemPermissionsToml"
},
Expand Down Expand Up @@ -4873,4 +4876,4 @@
},
"title": "ConfigToml",
"type": "object"
}
}
Loading
Loading