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
+6
View File
@@ -1943,6 +1943,9 @@
},
"network": {
"$ref": "#/definitions/NetworkToml"
},
"workspace_roots": {
"$ref": "#/definitions/WorkspaceRootsToml"
}
},
"type": "object"
@@ -3930,6 +3933,9 @@
"type": "string"
}
]
},
"WorkspaceRootsToml": {
"type": "object"
}
},
"description": "Base config deserialized from ~/.codex/config.toml.",
+150 -32
View File
@@ -29,6 +29,7 @@ use codex_config::permissions_toml::NetworkDomainPermissionsToml;
use codex_config::permissions_toml::NetworkToml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::permissions_toml::WorkspaceRootsToml;
use codex_config::profile_toml::ConfigProfile;
use codex_config::types::AppToolApproval;
use codex_config::types::ApprovalsReviewer;
@@ -69,7 +70,6 @@ use codex_model_provider_info::WireApi;
use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
@@ -719,6 +719,10 @@ fn config_toml_deserializes_permission_profiles() {
let toml = r#"
default_permissions = "workspace"
[permissions.workspace.workspace_roots]
"~/code/openai" = true
"~/code/ignored" = false
[permissions.workspace.filesystem]
":minimal" = "read"
@@ -745,6 +749,12 @@ allow_upstream_proxy = false
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([
("~/code/ignored".to_string(), false),
("~/code/openai".to_string(), true),
]),
}),
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([
@@ -800,6 +810,7 @@ async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -953,6 +964,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1103,6 +1115,7 @@ async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io:
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1205,6 +1218,7 @@ enabled = false
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1253,6 +1267,7 @@ async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy()
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1299,6 +1314,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([
@@ -1332,6 +1348,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
)
.await?;
let cwd_root = cwd.path().abs();
let memories_root = codex_home.path().join("memories").abs();
assert_eq!(
config.permissions.file_system_sandbox_policy(),
@@ -1343,14 +1360,14 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
path: FileSystemPath::Path {
path: cwd_root.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(Some("docs".into())),
path: FileSystemPath::Path {
path: cwd_root.join("docs"),
},
access: FileSystemAccessMode::Read,
},
@@ -1371,6 +1388,12 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
exclude_slash_tmp: true,
}
);
assert!(
!config
.permissions
.file_system_sandbox_policy()
.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path())
);
assert_eq!(
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
@@ -1565,6 +1588,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1610,7 +1634,9 @@ async fn permission_profile_override_preserves_configured_network_policy_without
async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?;
tokio::fs::write(extra_root.path().join(".git"), "gitdir: nowhere").await?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
@@ -1619,6 +1645,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: Some(2),
entries: BTreeMap::from([(
@@ -1637,6 +1664,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
additional_writable_roots: vec![extra_root.path().to_path_buf()],
..Default::default()
},
codex_home.abs(),
@@ -1650,21 +1678,23 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std:
.glob_scan_max_depth,
Some(2)
);
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
.to_string_lossy()
.into_owned();
assert!(
config
.permissions
.file_system_sandbox_policy()
.entries
.contains(&FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: expected_pattern,
},
access: FileSystemAccessMode::None,
})
);
for root in [cwd.path(), extra_root.path()] {
let expected_pattern = AbsolutePathBuf::resolve_path_against_base("**/*.env", root)
.to_string_lossy()
.into_owned();
assert!(
config
.permissions
.file_system_sandbox_policy()
.entries
.contains(&FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: expected_pattern,
},
access: FileSystemAccessMode::None,
})
);
}
assert!(
!config
.permissions
@@ -1694,6 +1724,7 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -1764,8 +1795,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
}
#[tokio::test]
async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications()
-> std::io::Result<()> {
async fn default_permissions_read_only_keeps_add_dir_read_only() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
@@ -1787,20 +1817,88 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
let policy = config.permissions.file_system_sandbox_policy();
assert!(
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
"expected additional writable root to modify :read-only, policy: {policy:?}"
!policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
"expected :read-only to stay read-only for runtime workspace roots, policy: {policy:?}"
);
assert_eq!(
config.permissions.active_permission_profile(),
Some(
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY).with_modifications(
vec![
ActivePermissionProfileModification::AdditionalWritableRoot {
path: extra_root,
Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
))
);
Ok(())
}
#[tokio::test]
async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots()
-> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let codex_home = temp_dir.path().join("codex-home");
let cwd = temp_dir.path().join("frontend");
let runtime_root = temp_dir.path().join("backend");
let profile_root = temp_dir.path().join("shared");
for root in [&cwd, &runtime_root, &profile_root] {
std::fs::create_dir_all(root.join(".git"))?;
std::fs::create_dir_all(root.join(".codex"))?;
}
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some("dev".to_string()),
permissions: Some(PermissionsToml {
entries: BTreeMap::from([(
"dev".to_string(),
PermissionProfileToml {
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([(
profile_root.to_string_lossy().into_owned(),
true,
)]),
}),
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([
(".".to_string(), FileSystemAccessMode::Write),
(".git".to_string(), FileSystemAccessMode::Read),
(".codex".to_string(), FileSystemAccessMode::Read),
])),
)]),
}),
network: None,
},
]
)
)
)]),
}),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.clone()),
additional_writable_roots: vec![runtime_root.clone()],
..Default::default()
},
codex_home.abs(),
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
for root in [cwd.abs(), runtime_root.abs(), profile_root.abs()] {
assert!(
policy.can_write_path_with_cwd(root.as_path(), cwd.as_path()),
"expected workspace root to be writable, policy: {policy:?}"
);
assert!(
!policy.can_write_path_with_cwd(&root.join(".git"), cwd.as_path()),
"expected .git carveout under {root:?}, policy: {policy:?}"
);
assert!(
!policy.can_write_path_with_cwd(&root.join(".codex"), cwd.as_path()),
"expected .codex carveout under {root:?}, policy: {policy:?}"
);
}
assert_eq!(
config.permissions.active_permission_profile(),
Some(ActivePermissionProfile::new("dev"))
);
Ok(())
}
@@ -2185,6 +2283,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -2241,6 +2340,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() ->
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -2301,6 +2401,7 @@ async fn load_workspace_permission_profile(
#[tokio::test]
async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -2344,6 +2445,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
-> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -2380,6 +2482,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
#[tokio::test]
async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
workspace_roots: None,
filesystem: None,
network: None,
})
@@ -2408,6 +2511,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
#[tokio::test]
async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::Result<()> {
let config = load_workspace_permission_profile(PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::new(),
@@ -2443,6 +2547,7 @@ async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::i
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -2489,6 +2594,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
@@ -7356,6 +7462,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -7367,6 +7474,8 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
workspace_roots_explicit: false,
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -7805,6 +7914,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -7816,6 +7926,8 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
workspace_roots_explicit: false,
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -7968,6 +8080,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -7979,6 +8092,8 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
workspace_roots_explicit: false,
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -8116,6 +8231,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
workspace_roots: vec![fixture.cwd()],
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -8127,6 +8243,8 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
workspace_roots_explicit: false,
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
+228 -79
View File
@@ -89,7 +89,6 @@ use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelsResponse;
@@ -117,6 +116,7 @@ use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
use crate::config::permissions::apply_network_proxy_feature_config;
use crate::config::permissions::builtin_permission_profile;
use crate::config::permissions::compile_permission_profile_selection;
use crate::config::permissions::compile_permission_profile_workspace_roots;
use crate::config::permissions::default_builtin_permission_profile_name;
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
use crate::config::permissions::network_proxy_config_for_profile_selection;
@@ -248,11 +248,14 @@ pub struct Permissions {
/// Approval policy for executing commands.
pub approval_policy: Constrained<AskForApproval>,
/// Canonical effective runtime permissions after config requirements and
/// runtime readable-root additions have been applied.
pub permission_profile: Constrained<PermissionProfile>,
/// runtime workspace-root materialization have been applied.
permission_profile: Constrained<PermissionProfile>,
/// Named or implicit built-in profile selected by config, rather than an
/// ad-hoc override.
pub active_permission_profile: Option<ActivePermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
/// entries in `permission_profile` are materialized against these roots.
workspace_roots: Vec<AbsolutePathBuf>,
/// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxySpec>,
/// Whether the model may request a login shell for shell-based tools.
@@ -274,10 +277,89 @@ pub struct Permissions {
}
impl Permissions {
/// Build permissions from the constrained values required for a minimal
/// in-process configuration.
pub fn from_approval_and_profile(
approval_policy: Constrained<AskForApproval>,
permission_profile: Constrained<PermissionProfile>,
) -> Self {
Self {
approval_policy,
permission_profile,
active_permission_profile: None,
workspace_roots: Vec::new(),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
}
}
/// Borrow the constrained canonical profile. This preserves the raw
/// symbolic `:workspace_roots` form for session/thread state.
pub fn permission_profile_constraint(&self) -> &Constrained<PermissionProfile> {
&self.permission_profile
}
/// Replace the full constrained profile value and clear any profile-name
/// sidecar because the new constraint may no longer match that name.
pub fn replace_permission_profile_constraint(
&mut self,
permission_profile: Constrained<PermissionProfile>,
) {
self.permission_profile = permission_profile;
self.active_permission_profile = None;
}
/// Replace the full constrained profile value and preserve the active
/// profile sidecar when the caller has already validated both together.
pub fn replace_permission_profile_constraint_with_active_profile(
&mut self,
permission_profile: Constrained<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
) {
self.permission_profile = permission_profile;
self.active_permission_profile = active_permission_profile;
}
/// Record the active profile id only if it still describes the current
/// canonical profile.
pub fn set_active_permission_profile_for_current_profile(
&mut self,
active_permission_profile: Option<ActivePermissionProfile>,
expected_permission_profile: Option<&PermissionProfile>,
) {
self.active_permission_profile =
match (active_permission_profile, expected_permission_profile) {
(Some(active_permission_profile), Some(expected_permission_profile))
if self.permission_profile.get() == expected_permission_profile =>
{
Some(active_permission_profile)
}
_ => None,
};
}
pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
self.workspace_roots = workspace_roots;
}
pub fn workspace_roots(&self) -> &[AbsolutePathBuf] {
&self.workspace_roots
}
fn materialized_permission_profile(&self) -> PermissionProfile {
self.permission_profile
.get()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
}
/// Effective runtime permissions after config requirements and runtime
/// readable-root additions have been applied.
/// workspace-root materialization have been applied.
pub fn permission_profile(&self) -> PermissionProfile {
self.permission_profile.get().clone()
self.materialized_permission_profile()
}
/// Named profile selected by config, if the current profile has one.
@@ -287,7 +369,8 @@ impl Permissions {
/// Effective filesystem sandbox policy derived from the canonical profile.
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.get().file_system_sandbox_policy()
self.materialized_permission_profile()
.file_system_sandbox_policy()
}
/// Effective network sandbox policy derived from the canonical profile.
@@ -297,10 +380,10 @@ impl Permissions {
/// Legacy compatibility projection derived from the canonical profile.
pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy {
let permission_profile = self.permission_profile.get();
let permission_profile = self.materialized_permission_profile();
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
compatibility_sandbox_policy_for_permission_profile(
permission_profile,
&permission_profile,
&file_system_sandbox_policy,
permission_profile.network_sandbox_policy(),
cwd,
@@ -341,6 +424,26 @@ impl Permissions {
&file_system_sandbox_policy,
network_sandbox_policy,
);
self.workspace_roots = match &sandbox_policy {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
let mut workspace_roots = vec![
AbsolutePathBuf::from_absolute_path(cwd)
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
];
for root in writable_roots {
if !workspace_roots.iter().any(|existing| existing == root) {
workspace_roots.push(root.clone());
}
}
workspace_roots
}
SandboxPolicy::DangerFullAccess
| SandboxPolicy::ExternalSandbox { .. }
| SandboxPolicy::ReadOnly { .. } => vec![
AbsolutePathBuf::from_absolute_path(cwd)
.unwrap_or_else(|_| AbsolutePathBuf::resolve_path_against_base(cwd, "/")),
],
};
self.permission_profile.set(permission_profile)?;
self.active_permission_profile = None;
@@ -577,6 +680,15 @@ pub struct Config {
/// layer are resolved against this path.
pub cwd: AbsolutePathBuf,
/// Absolute runtime workspace roots for the session. Symbolic
/// `:workspace_roots` permission entries are materialized against these
/// roots while profile-defined workspace roots remain encoded directly in
/// the permission profile.
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Whether runtime workspace roots were supplied explicitly by the caller
/// or legacy config, rather than defaulting to `cwd`.
pub workspace_roots_explicit: bool,
/// Preferred store for CLI auth credentials.
/// file (default): Use a file in the Codex home directory.
/// keyring: Use an OS-specific keyring service.
@@ -1085,8 +1197,14 @@ impl Config {
&mut self,
sandbox_policy: SandboxPolicy,
) -> ConstraintResult<()> {
self.workspace_roots_explicit = matches!(
&sandbox_policy,
SandboxPolicy::WorkspaceWrite { writable_roots, .. } if !writable_roots.is_empty()
);
self.permissions
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())?;
self.workspace_roots = self.permissions.workspace_roots().to_vec();
Ok(())
}
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
@@ -1935,6 +2053,14 @@ pub struct ConfigOverrides {
pub bypass_hook_trust: Option<bool>,
/// Additional directories that should be treated as writable roots for this session.
pub additional_writable_roots: Vec<PathBuf>,
/// Explicit runtime workspace roots for this session. When set, this is
/// the full runtime root list rather than an additive override.
pub workspace_roots: Option<Vec<PathBuf>>,
}
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
let mut seen = HashSet::new();
paths.retain(|path| seen.insert(path.clone()));
}
/// Resolves the OSS provider from CLI override, profile config, or global config.
@@ -2248,6 +2374,7 @@ impl Config {
ephemeral,
bypass_hook_trust,
additional_writable_roots,
workspace_roots: workspace_roots_override,
} = overrides;
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
@@ -2340,11 +2467,10 @@ impl Config {
}
}
}))?;
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
.collect();
let requested_additional_writable_roots = additional_writable_roots.clone();
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
let active_project = cfg
.get_active_project(
@@ -2386,12 +2512,7 @@ impl Config {
};
let memories_root = memory_root(&codex_home);
std::fs::create_dir_all(&memories_root)?;
if !additional_writable_roots
.iter()
.any(|existing| existing == &memories_root)
{
additional_writable_roots.push(memories_root);
}
let internal_writable_roots = vec![memories_root];
let profiles_are_active = default_permissions_override.is_some()
|| matches!(
@@ -2401,6 +2522,40 @@ impl Config {
|| permission_config_syntax.is_none();
let using_implicit_builtin_profile =
permission_config_syntax.is_none() && default_permissions.is_none();
let should_seed_legacy_workspace_roots = default_permissions.is_none()
&& matches!(
permission_config_syntax,
None | Some(PermissionConfigSyntax::Legacy)
);
let legacy_workspace_roots_explicit = should_seed_legacy_workspace_roots
&& cfg
.sandbox_workspace_write
.as_ref()
.is_some_and(|sandbox_workspace_write| {
!sandbox_workspace_write.writable_roots.is_empty()
});
let workspace_roots_explicit = workspace_roots_override.is_some()
|| !requested_additional_writable_roots.is_empty()
|| legacy_workspace_roots_explicit;
let mut workspace_roots = match workspace_roots_override {
Some(workspace_roots) => workspace_roots
.into_iter()
.map(|path| {
AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path())
})
.collect(),
None => {
let mut workspace_roots = vec![resolved_cwd.clone()];
workspace_roots.extend(requested_additional_writable_roots.clone());
if should_seed_legacy_workspace_roots
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
{
workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
}
workspace_roots
}
};
dedupe_absolute_paths(&mut workspace_roots);
let (
mut configured_network_proxy_config,
permission_profile,
@@ -2429,18 +2584,24 @@ impl Config {
} else {
NetworkProxyConfig::default()
};
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots);
let materialized_permission_profile =
PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&materialized_file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
&file_system_sandbox_policy,
&materialized_permission_profile,
&materialized_file_system_sandbox_policy,
network_sandbox_policy,
resolved_cwd.as_path(),
);
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_writable_roots(
resolved_cwd.as_path(),
&additional_writable_roots,
);
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
@@ -2474,6 +2635,20 @@ impl Config {
resolved_cwd.as_path(),
&mut startup_warnings,
)?;
let mut configured_workspace_roots = compile_permission_profile_workspace_roots(
cfg.permissions.as_ref(),
default_permissions,
resolved_cwd.as_path(),
)?;
if using_implicit_builtin_profile
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
{
configured_workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
}
dedupe_absolute_paths(&mut configured_workspace_roots);
file_system_sandbox_policy = file_system_sandbox_policy
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
let mut permission_profile = if let Some(permission_profile) =
builtin_permission_profile(default_permissions, builtin_workspace_write_settings)
{
@@ -2484,36 +2659,26 @@ impl Config {
network_sandbox_policy,
)
};
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots);
let materialized_permission_profile =
PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&materialized_file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
&file_system_sandbox_policy,
&materialized_permission_profile,
&materialized_file_system_sandbox_policy,
network_sandbox_policy,
resolved_cwd.as_path(),
);
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
file_system_sandbox_policy = if using_implicit_builtin_profile {
file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(
&additional_writable_roots,
)
} else {
file_system_sandbox_policy.with_additional_writable_roots(
resolved_cwd.as_path(),
&additional_writable_roots,
)
};
permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
network_sandbox_policy,
);
} else if matches!(permission_profile, PermissionProfile::Managed { .. })
&& !requested_additional_writable_roots.is_empty()
{
file_system_sandbox_policy = file_system_sandbox_policy.with_additional_writable_roots(
resolved_cwd.as_path(),
&requested_additional_writable_roots,
);
permission_profile = PermissionProfile::from_runtime_permissions(
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
@@ -2529,22 +2694,7 @@ impl Config {
// when doing so would lose roots, network, or tmp settings.
None
} else {
let active_permission_profile = if !requested_additional_writable_roots.is_empty()
&& matches!(permission_profile, PermissionProfile::Managed { .. })
{
ActivePermissionProfile::new(default_permissions).with_modifications(
requested_additional_writable_roots
.iter()
.cloned()
.map(|path| {
ActivePermissionProfileModification::AdditionalWritableRoot { path }
})
.collect(),
)
} else {
ActivePermissionProfile::new(default_permissions)
};
Some(active_permission_profile)
Some(ActivePermissionProfile::new(default_permissions))
};
(
configured_network_proxy_config,
@@ -2583,25 +2733,21 @@ impl Config {
}
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
// `additional_writable_roots` is a legacy workspace-write knob. It
// only applies when the derived managed profile has workspace-style
// write access to the project roots; read-only, disabled, external,
// and future non-workspace profiles must not silently grow extra
// write access.
let materialized_file_system_sandbox_policy = permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots)
.file_system_sandbox_policy();
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
&& file_system_sandbox_policy.can_write_path_with_cwd(
&& materialized_file_system_sandbox_policy.can_write_path_with_cwd(
resolved_cwd.as_path(),
resolved_cwd.as_path(),
)
&& !file_system_sandbox_policy.has_full_disk_write_access()
&& !materialized_file_system_sandbox_policy.has_full_disk_write_access()
{
// Keep legacy behavior for extra writable roots while storing
// the result as the canonical permission profile. Explicit
// extra roots are concrete paths, so their metadata carveouts
// are also concrete rather than symbolic `:workspace_roots`
// entries.
// Keep Codex runtime write access while storing the runtime
// workspace roots separately on the thread.
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
@@ -3114,11 +3260,14 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
workspace_roots: workspace_roots.clone(),
workspace_roots_explicit,
startup_warnings,
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
permission_profile: constrained_permission_profile.value,
active_permission_profile,
workspace_roots,
network,
allow_login_shell,
shell_environment_policy,
@@ -3404,7 +3553,7 @@ impl Config {
pub fn managed_network_requirements_enabled(&self) -> bool {
!matches!(
self.permissions.permission_profile.get(),
self.permissions.permission_profile_constraint().get(),
PermissionProfile::Disabled
) && self
.config_layer_stack
+44 -12
View File
@@ -13,6 +13,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::permissions_toml::WorkspaceRootsToml;
use codex_config::types::SandboxWorkspaceWrite;
use codex_features::NetworkProxyConfigToml;
use codex_features::NetworkProxyDomainPermissionToml;
@@ -33,6 +34,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::permissions::project_roots_glob_pattern;
use codex_utils_absolute_path::AbsolutePathBuf;
use super::ProjectConfig;
@@ -72,12 +74,12 @@ pub(crate) fn builtin_permission_profile(
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
Some(SandboxWorkspaceWrite {
writable_roots,
writable_roots: _,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
}) => PermissionProfile::workspace_write_with(
writable_roots,
&[],
if *network_access {
NetworkSandboxPolicy::Enabled
} else {
@@ -303,6 +305,41 @@ pub(crate) fn compile_permission_profile_selection(
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
}
pub(crate) fn compile_permission_profile_workspace_roots(
permissions: Option<&PermissionsToml>,
profile_name: &str,
policy_cwd: &Path,
) -> io::Result<Vec<AbsolutePathBuf>> {
if is_builtin_permission_profile_name(profile_name) {
return Ok(Vec::new());
}
reject_unknown_builtin_permission_profile(profile_name)?;
let permissions = permissions.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"default_permissions requires a `[permissions]` table",
)
})?;
let profile = resolve_permission_profile(permissions, profile_name)?;
Ok(compile_workspace_roots(
profile.workspace_roots.as_ref(),
policy_cwd,
))
}
fn compile_workspace_roots(
workspace_roots: Option<&WorkspaceRootsToml>,
policy_cwd: &Path,
) -> Vec<AbsolutePathBuf> {
workspace_roots.map_or_else(Vec::new, |workspace_roots| {
workspace_roots
.enabled_roots()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, policy_cwd))
.collect()
})
}
fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> {
if profile_name.starts_with(':') {
return Err(io::Error::new(
@@ -478,7 +515,7 @@ fn compile_scoped_filesystem_pattern(
path: &str,
subpath: &str,
access: FileSystemAccessMode,
policy_cwd: &Path,
_policy_cwd: &Path,
) -> io::Result<String> {
// Pattern entries currently mean deny-read only. Supporting broader access
// modes here would imply glob-based read/write allow semantics that the
@@ -493,15 +530,10 @@ fn compile_scoped_filesystem_pattern(
match parse_special_path(path) {
Some(FileSystemSpecialPath::ProjectRoots { .. }) => {
// `:workspace_roots` is represented as a special path, but current
// filesystem-policy resolution defines it relative to the session
// cwd. Use the same policy cwd here so glob entries and exact
// scoped entries resolve consistently.
Ok(
AbsolutePathBuf::resolve_path_against_base(&subpath, policy_cwd)
.to_string_lossy()
.to_string(),
)
// Keep `:workspace_roots` glob patterns symbolic until the active
// workspace roots are known, then materialize them for cwd and any
// runtime/profile-added workspace roots together.
Ok(project_roots_glob_pattern(&subpath))
}
Some(_) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -11,6 +11,7 @@ use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::permissions_toml::WorkspaceRootsToml;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -66,6 +67,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::new(),
@@ -275,6 +277,39 @@ fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() {
);
}
#[test]
fn compile_permission_profile_workspace_roots_resolves_enabled_entries() -> std::io::Result<()> {
let cwd = TempDir::new()?;
let workspace_roots = compile_permission_profile_workspace_roots(
Some(&PermissionsToml {
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: Some(WorkspaceRootsToml {
entries: BTreeMap::from([
("backend".to_string(), true),
("disabled".to_string(), false),
]),
}),
filesystem: None,
network: None,
},
)]),
}),
"workspace",
cwd.path(),
)?;
assert_eq!(
workspace_roots,
vec![AbsolutePathBuf::resolve_path_against_base(
"backend",
cwd.path()
)]
);
Ok(())
}
#[test]
fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() {
let filesystem = FilesystemPermissionsToml {
@@ -359,6 +394,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
entries: BTreeMap::from([(
"workspace".to_string(),
PermissionProfileToml {
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
+9 -4
View File
@@ -896,9 +896,11 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.developer_instructions = None;
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
let sandbox_policy = SandboxPolicy::new_read_only_policy();
guardian_config.permissions.permission_profile = Constrained::allow_only(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
);
guardian_config
.permissions
.replace_permission_profile_constraint(Constrained::allow_only(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
));
guardian_config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path())
@@ -924,7 +926,10 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
live_network_config,
network_constraints,
guardian_config.permissions.permission_profile.get(),
guardian_config
.permissions
.permission_profile_constraint()
.get(),
)?);
}
for feature in [
+10 -4
View File
@@ -2163,7 +2163,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
}),
..Default::default()
}),
parent_config.permissions.permission_profile.get(),
parent_config
.permissions
.permission_profile_constraint()
.get(),
)
.expect("network proxy spec");
parent_config.permissions.network = Some(network.clone());
@@ -2190,8 +2193,8 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
Constrained::allow_only(AskForApproval::Never)
);
assert_eq!(
guardian_config.permissions.permission_profile,
Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
guardian_config.permissions.permission_profile_constraint(),
&Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
))
);
@@ -2230,7 +2233,10 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
NetworkProxySpec::from_config_and_constraints(
parent_network,
/*requirements*/ None,
parent_config.permissions.permission_profile.get(),
parent_config
.permissions
.permission_profile_constraint()
.get(),
)
.expect("parent network proxy spec"),
);
+1 -1
View File
@@ -617,7 +617,7 @@ impl Codex {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
+2 -2
View File
@@ -769,7 +769,7 @@ impl Session {
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
current_exec_policy.as_ref(),
config.permissions.permission_profile.get(),
config.permissions.permission_profile_constraint().get(),
network_policy_decider.as_ref().map(Arc::clone),
blocked_request_observer.as_ref().map(Arc::clone),
managed_network_requirements_configured,
@@ -833,7 +833,7 @@ impl Session {
// setup is straightforward enough and performs well.
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
+14 -12
View File
@@ -2128,8 +2128,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
};
let expected_sandbox_policy = sandbox_policy.clone();
let mut builder = test_codex().with_config(move |config| {
config.permissions.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
config.permissions.replace_permission_profile_constraint(
codex_config::Constrained::allow_any(PermissionProfile::from_legacy_sandbox_policy(
&sandbox_policy,
)),
);
config
.set_legacy_sandbox_policy(sandbox_policy)
@@ -2884,7 +2886,7 @@ async fn set_rate_limits_retains_previous_credits() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -2988,7 +2990,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -3461,7 +3463,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -3994,7 +3996,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -4103,7 +4105,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -4150,7 +4152,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -4335,7 +4337,7 @@ async fn make_session_with_config_and_rx(
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -4438,7 +4440,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -5955,7 +5957,7 @@ where
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
@@ -6002,7 +6004,7 @@ where
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
+12 -4
View File
@@ -438,8 +438,12 @@ impl Session {
per_turn_config.service_tier = session_configuration.service_tier.clone();
per_turn_config.personality = session_configuration.personality;
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
per_turn_config.permissions.permission_profile =
session_configuration.permission_profile.clone();
per_turn_config
.permissions
.replace_permission_profile_constraint_with_active_profile(
session_configuration.permission_profile.clone(),
session_configuration.active_permission_profile.clone(),
);
let permission_profile = session_configuration.permission_profile();
let resolved_web_search_mode =
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile);
@@ -466,8 +470,12 @@ impl Session {
Self::build_per_turn_config(session_configuration, session_configuration.cwd.clone());
config.model = Some(session_configuration.collaboration_mode.model().to_string());
config.permissions.approval_policy = session_configuration.approval_policy.clone();
config.permissions.active_permission_profile =
session_configuration.active_permission_profile.clone();
config
.permissions
.set_active_permission_profile_for_current_profile(
session_configuration.active_permission_profile.clone(),
Some(session_configuration.permission_profile.get()),
);
config
}
@@ -3948,7 +3948,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
#[allow(deprecated)]
let turn_cwd = turn.cwd.clone();
let sandbox_policy = pick_allowed_sandbox_policy(
&turn.config.permissions.permission_profile,
turn.config.permissions.permission_profile_constraint(),
turn.config.legacy_sandbox_policy(),
turn_cwd.as_path(),
);
+12 -7
View File
@@ -906,9 +906,11 @@ allow_local_binding = true
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
config.permissions.permission_profile = Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
);
config
.permissions
.replace_permission_profile_constraint(Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
));
});
let test = builder.build_remote_aware(server).await?;
assert!(
@@ -2752,10 +2754,13 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
},
access: FileSystemAccessMode::None,
});
config.permissions.permission_profile =
Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
config
.permissions
.replace_permission_profile_constraint(Constrained::allow_any(
PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
),
));
});
let TestCodex {
+3 -2
View File
@@ -344,11 +344,12 @@ async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Re
let server = responses::start_mock_server().await;
let mut builder = core_test_support::test_codex::test_codex().with_config(|config| {
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
config.permissions.permission_profile =
config.permissions.replace_permission_profile_constraint(
codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
)),
);
});
let test = builder.build(&server).await?;