Skip to content

Commit 755880e

Browse files
authored
permissions: derive config defaults as profiles (#19772)
## Why This continues the permissions migration by making legacy config default resolution produce the canonical `PermissionProfile` first. The legacy `SandboxPolicy` projection should stay available at compatibility boundaries, but config loading should not create a legacy policy just to immediately convert it back into a profile. Specifically, when `default_permissions` is not specified in `config.toml`, instead of creating a `SandboxPolicy` in `codex-rs/core/src/config/mod.rs` and then trying to derive a `PermissionProfile` from it, we use `derive_permission_profile()` to create a more faithful `PermissionProfile` using the values of `ConfigToml` directly. This also keeps the existing behavior of `sandbox_workspace_write` and extra writable roots after #19841 replaced `:cwd` with `:project_roots`. Legacy workspace-write defaults are represented as symbolic `:project_roots` write access plus symbolic project-root metadata carveouts. Extra absolute writable roots are still added directly and continue to get concrete metadata protections for paths that exist under those roots. The platform sandboxes differ when a symbolic project-root subpath does not exist yet. * **Seatbelt** can encode literal/subpath exclusions directly, so macOS emits project-root metadata subpath policies even if `.git`, `.agents`, or `.codex` do not exist. * **bwrap** has to materialize bind-mount targets. Binding `/dev/null` to a missing `.git` can create a host-visible placeholder that changes Git repo discovery. Binding missing `.agents` would not affect Git discovery, but it would still create a host-visible project metadata placeholder from an automatic compatibility carveout. Linux therefore skips only missing automatic `.git` and `.agents` read-only metadata masks; missing `.codex` remains protected so first-time project config creation goes through the protected-path approval flow. User-authored `read` and `none` subpath rules keep normal bwrap behavior, and `none` can still mask the first missing component to prevent creation under writable roots. ## What Changed - Adds profile-native helpers for legacy workspace-write semantics, including `PermissionProfile::workspace_write_with()`, `FileSystemSandboxPolicy::workspace_write()`, and `FileSystemSandboxPolicy::with_additional_legacy_workspace_writable_roots()`. - Makes `FileSystemSandboxPolicy::workspace_write()` the single legacy workspace-write constructor so both `from_legacy_sandbox_policy()` and `From<&SandboxPolicy>` include the project-root metadata carveouts. - Removes the no-carveout `legacy_workspace_write_base_policy()` path and the `prune_read_entries_under_writable_roots()` cleanup that was only needed by that split construction. - Adds `ConfigToml::derive_permission_profile()` for legacy sandbox-mode fallback resolution; named `default_permissions` profiles continue through the permissions profile pipeline instead of being reconstructed from `sandbox_mode`. - Updates `Config::load()` to start from the derived profile, validate that it still has a legacy compatibility projection, and apply additional writable roots directly to managed workspace-write filesystem policies. - Updates Linux bwrap argument construction so missing automatic `.git`/`.agents` symbolic project-root read-only carveouts are skipped before emitting bind args; missing `.codex`, user-authored `read`/`none` subpath rules, and existing missing writable-root behavior are preserved. - Adds coverage that legacy workspace-write config produces symbolic project-root metadata carveouts, extra legacy workspace writable roots still protect existing metadata paths such as `.git`, and bwrap skips missing `.git`/`.agents` project-root carveouts while preserving missing `.codex` and user-authored missing subpath rules. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19772). * #19776 * #19775 * #19774 * #19773 * __->__ #19772
1 parent c5a495c commit 755880e

8 files changed

Lines changed: 723 additions & 328 deletions

File tree

codex-rs/config/src/config_toml.rs

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ use codex_protocol::config_types::WebSearchToolConfig;
4949
use codex_protocol::config_types::WindowsSandboxLevel;
5050
use codex_protocol::models::PermissionProfile;
5151
use codex_protocol::openai_models::ReasoningEffort;
52+
use codex_protocol::permissions::NetworkSandboxPolicy;
5253
use codex_protocol::protocol::AskForApproval;
53-
use codex_protocol::protocol::SandboxPolicy;
5454
use codex_utils_absolute_path::AbsolutePathBuf;
5555
use codex_utils_path::normalize_for_path_comparison;
5656
use schemars::JsonSchema;
@@ -641,15 +641,19 @@ pub struct GhostSnapshotToml {
641641
}
642642

643643
impl ConfigToml {
644-
/// Derive the effective sandbox policy from the configuration.
645-
pub async fn derive_sandbox_policy(
644+
/// Derive the effective permission profile from legacy sandbox config.
645+
///
646+
/// Call this only after ruling out `default_permissions`: named
647+
/// `[permissions]` profiles must be compiled through the permissions
648+
/// profile pipeline, not reconstructed from `sandbox_mode`.
649+
pub async fn derive_permission_profile(
646650
&self,
647651
sandbox_mode_override: Option<SandboxMode>,
648652
profile_sandbox_mode: Option<SandboxMode>,
649653
windows_sandbox_level: WindowsSandboxLevel,
650654
active_project: Option<&ProjectConfig>,
651655
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
652-
) -> SandboxPolicy {
656+
) -> PermissionProfile {
653657
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
654658
|| profile_sandbox_mode.is_some()
655659
|| self.sandbox_mode.is_some();
@@ -677,50 +681,53 @@ impl ConfigToml {
677681
})
678682
})
679683
.unwrap_or_default();
680-
let mut sandbox_policy = match resolved_sandbox_mode {
681-
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
684+
let effective_sandbox_mode = if cfg!(target_os = "windows")
685+
// If the experimental Windows sandbox is enabled, do not force a downgrade.
686+
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
687+
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
688+
{
689+
SandboxMode::ReadOnly
690+
} else {
691+
resolved_sandbox_mode
692+
};
693+
694+
let permission_profile = match effective_sandbox_mode {
695+
SandboxMode::ReadOnly => PermissionProfile::read_only(),
682696
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
683697
Some(SandboxWorkspaceWrite {
684698
writable_roots,
685699
network_access,
686700
exclude_tmpdir_env_var,
687701
exclude_slash_tmp,
688-
}) => SandboxPolicy::WorkspaceWrite {
689-
writable_roots: writable_roots.clone(),
690-
network_access: *network_access,
691-
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
692-
exclude_slash_tmp: *exclude_slash_tmp,
693-
},
694-
None => SandboxPolicy::new_workspace_write_policy(),
702+
}) => {
703+
let network_policy = if *network_access {
704+
NetworkSandboxPolicy::Enabled
705+
} else {
706+
NetworkSandboxPolicy::Restricted
707+
};
708+
PermissionProfile::workspace_write_with(
709+
writable_roots,
710+
network_policy,
711+
*exclude_tmpdir_env_var,
712+
*exclude_slash_tmp,
713+
)
714+
}
715+
None => PermissionProfile::workspace_write(),
695716
},
696-
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
697-
};
698-
let downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| {
699-
if cfg!(target_os = "windows")
700-
// If the experimental Windows sandbox is enabled, do not force a downgrade.
701-
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
702-
&& matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. })
703-
{
704-
*policy = SandboxPolicy::new_read_only_policy();
705-
}
717+
SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
706718
};
707-
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
708-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
709-
}
710719
if !sandbox_mode_was_explicit
711720
&& let Some(constraint) = permission_profile_constraint
712-
&& let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy(
713-
&sandbox_policy,
714-
))
721+
&& let Err(err) = constraint.can_set(&permission_profile)
715722
{
716723
tracing::warn!(
717724
error = %err,
718725
"default sandbox policy is disallowed by requirements; falling back to required default"
719726
);
720-
sandbox_policy = SandboxPolicy::new_read_only_policy();
721-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
727+
PermissionProfile::read_only()
728+
} else {
729+
permission_profile
722730
}
723-
sandbox_policy
724731
}
725732

726733
/// Resolves the cwd to an existing project, or returns None if ConfigToml

0 commit comments

Comments
 (0)