Skip to content

Commit d0c5aa8

Browse files
committed
permissions: derive config defaults as profiles
1 parent a6ca39c commit d0c5aa8

5 files changed

Lines changed: 384 additions & 175 deletions

File tree

codex-rs/config/src/config_toml.rs

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ 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;
5354
use codex_protocol::protocol::SandboxPolicy;
5455
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -641,15 +642,19 @@ pub struct GhostSnapshotToml {
641642
}
642643

643644
impl ConfigToml {
644-
/// Derive the effective sandbox policy from the configuration.
645-
pub async fn derive_sandbox_policy(
645+
/// Derive the effective permission profile from legacy sandbox config.
646+
///
647+
/// Call this only after ruling out `default_permissions`: named
648+
/// `[permissions]` profiles must be compiled through the permissions
649+
/// profile pipeline, not reconstructed from `sandbox_mode`.
650+
pub async fn derive_permission_profile(
646651
&self,
647652
sandbox_mode_override: Option<SandboxMode>,
648653
profile_sandbox_mode: Option<SandboxMode>,
649654
windows_sandbox_level: WindowsSandboxLevel,
650655
active_project: Option<&ProjectConfig>,
651656
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
652-
) -> SandboxPolicy {
657+
) -> PermissionProfile {
653658
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
654659
|| profile_sandbox_mode.is_some()
655660
|| self.sandbox_mode.is_some();
@@ -677,50 +682,81 @@ impl ConfigToml {
677682
})
678683
})
679684
.unwrap_or_default();
680-
let mut sandbox_policy = match resolved_sandbox_mode {
681-
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
685+
let workspace_write_unsupported = cfg!(target_os = "windows")
686+
// If the experimental Windows sandbox is enabled, do not force a downgrade.
687+
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
688+
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite);
689+
let mut permission_profile = match resolved_sandbox_mode {
690+
SandboxMode::ReadOnly => PermissionProfile::read_only(),
682691
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
683692
Some(SandboxWorkspaceWrite {
684693
writable_roots,
685694
network_access,
686695
exclude_tmpdir_env_var,
687696
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(),
697+
}) => {
698+
let network_policy = if *network_access {
699+
NetworkSandboxPolicy::Enabled
700+
} else {
701+
NetworkSandboxPolicy::Restricted
702+
};
703+
PermissionProfile::workspace_write_with(
704+
writable_roots,
705+
network_policy,
706+
*exclude_tmpdir_env_var,
707+
*exclude_slash_tmp,
708+
)
709+
}
710+
None => PermissionProfile::workspace_write(),
695711
},
696-
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
712+
SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
697713
};
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-
}
706-
};
707-
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
708-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
714+
if workspace_write_unsupported {
715+
permission_profile = PermissionProfile::read_only();
709716
}
710717
if !sandbox_mode_was_explicit
711718
&& let Some(constraint) = permission_profile_constraint
712-
&& let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy(
713-
&sandbox_policy,
714-
))
719+
&& let Err(err) = constraint.can_set(&permission_profile)
715720
{
716721
tracing::warn!(
717722
error = %err,
718723
"default sandbox policy is disallowed by requirements; falling back to required default"
719724
);
720-
sandbox_policy = SandboxPolicy::new_read_only_policy();
721-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
725+
permission_profile = PermissionProfile::read_only();
726+
}
727+
permission_profile
728+
}
729+
730+
/// Derive the legacy sandbox projection from configuration.
731+
///
732+
/// New callers should use [`Self::derive_permission_profile`] instead.
733+
pub async fn derive_sandbox_policy(
734+
&self,
735+
sandbox_mode_override: Option<SandboxMode>,
736+
profile_sandbox_mode: Option<SandboxMode>,
737+
windows_sandbox_level: WindowsSandboxLevel,
738+
active_project: Option<&ProjectConfig>,
739+
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
740+
) -> SandboxPolicy {
741+
let permission_profile = self
742+
.derive_permission_profile(
743+
sandbox_mode_override,
744+
profile_sandbox_mode,
745+
windows_sandbox_level,
746+
active_project,
747+
permission_profile_constraint,
748+
)
749+
.await;
750+
match permission_profile.to_legacy_sandbox_policy(Path::new("/")) {
751+
Ok(sandbox_policy) => sandbox_policy,
752+
Err(err) => {
753+
tracing::warn!(
754+
error = %err,
755+
"derived permission profile cannot be represented as a legacy sandbox policy; falling back to read-only"
756+
);
757+
SandboxPolicy::new_read_only_policy()
758+
}
722759
}
723-
sandbox_policy
724760
}
725761

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

codex-rs/core/src/config/config_tests.rs

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ exclude_slash_tmp = true
17481748
}
17491749

17501750
#[tokio::test]
1751-
async fn legacy_sandbox_mode_config_builds_split_policies_without_drift() -> std::io::Result<()> {
1751+
async fn legacy_sandbox_mode_builds_profiles_with_compatible_projection() -> std::io::Result<()> {
17521752
let codex_home = TempDir::new()?;
17531753
let cwd = TempDir::new()?;
17541754
let extra_root = test_absolute_path("/tmp/legacy-extra-root");
@@ -1793,26 +1793,90 @@ exclude_slash_tmp = true
17931793
)
17941794
.await?;
17951795

1796-
let sandbox_policy = &config.legacy_sandbox_policy();
1797-
assert_eq!(
1798-
config.permissions.file_system_sandbox_policy(),
1799-
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()),
1800-
"case `{name}` should preserve filesystem semantics from legacy config"
1801-
);
1796+
let sandbox_policy = config.legacy_sandbox_policy();
1797+
let file_system_policy = config.permissions.file_system_sandbox_policy();
1798+
let network_policy = config.permissions.network_sandbox_policy();
1799+
18021800
assert_eq!(
1803-
config.permissions.network_sandbox_policy(),
1804-
NetworkSandboxPolicy::from(sandbox_policy),
1801+
network_policy,
1802+
NetworkSandboxPolicy::from(&sandbox_policy),
18051803
"case `{name}` should preserve network semantics from legacy config"
18061804
);
18071805
assert_eq!(
1808-
config
1809-
.permissions
1810-
.file_system_sandbox_policy()
1811-
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy(), cwd.path())
1806+
file_system_policy
1807+
.to_legacy_sandbox_policy(network_policy, cwd.path())
18121808
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
1813-
sandbox_policy.clone(),
1814-
"case `{name}` should round-trip through split policies without drift"
1809+
sandbox_policy,
1810+
"case `{name}` should preserve its legacy compatibility projection"
18151811
);
1812+
1813+
match name.as_str() {
1814+
"danger-full-access" | "read-only" => {
1815+
assert_eq!(
1816+
file_system_policy,
1817+
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
1818+
&sandbox_policy,
1819+
cwd.path()
1820+
),
1821+
"case `{name}` should match the legacy filesystem projection exactly"
1822+
);
1823+
}
1824+
"workspace-write" => {
1825+
if cfg!(target_os = "windows") {
1826+
assert_eq!(
1827+
sandbox_policy,
1828+
SandboxPolicy::new_read_only_policy(),
1829+
"legacy workspace-write should keep the existing Windows downgrade when \
1830+
the experimental Windows sandbox is disabled"
1831+
);
1832+
assert_eq!(
1833+
file_system_policy,
1834+
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
1835+
&sandbox_policy,
1836+
cwd.path()
1837+
),
1838+
"downgraded workspace-write should match the legacy read-only projection"
1839+
);
1840+
continue;
1841+
}
1842+
assert!(
1843+
file_system_policy
1844+
.entries
1845+
.contains(&FileSystemSandboxEntry {
1846+
path: FileSystemPath::Special {
1847+
value: FileSystemSpecialPath::CurrentWorkingDirectory,
1848+
},
1849+
access: FileSystemAccessMode::Write,
1850+
})
1851+
);
1852+
assert!(
1853+
file_system_policy
1854+
.entries
1855+
.contains(&FileSystemSandboxEntry {
1856+
path: FileSystemPath::Path {
1857+
path: extra_root.clone(),
1858+
},
1859+
access: FileSystemAccessMode::Write,
1860+
})
1861+
);
1862+
assert!(
1863+
file_system_policy
1864+
.can_write_path_with_cwd(cwd.path().join(".git").as_path(), cwd.path()),
1865+
"legacy workspace-write should allow git init to create .git"
1866+
);
1867+
assert!(
1868+
file_system_policy
1869+
.can_write_path_with_cwd(cwd.path().join(".agents").as_path(), cwd.path()),
1870+
"legacy workspace-write should allow creating .agents"
1871+
);
1872+
assert!(
1873+
!file_system_policy
1874+
.can_write_path_with_cwd(cwd.path().join(".codex").as_path(), cwd.path()),
1875+
"legacy workspace-write should protect .codex before it exists"
1876+
);
1877+
}
1878+
_ => unreachable!("unexpected test case `{name}`"),
1879+
}
18161880
}
18171881

18181882
Ok(())

codex-rs/core/src/config/mod.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,33 +1965,46 @@ impl Config {
19651965
)
19661966
} else {
19671967
let configured_network_proxy_config = NetworkProxyConfig::default();
1968-
let mut sandbox_policy = cfg
1969-
.derive_sandbox_policy(
1968+
let mut permission_profile = cfg
1969+
.derive_permission_profile(
19701970
sandbox_mode,
19711971
config_profile.sandbox_mode,
19721972
windows_sandbox_level,
19731973
Some(&active_project),
19741974
Some(&constrained_permission_profile),
19751975
)
19761976
.await;
1977-
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
1978-
for path in &additional_writable_roots {
1979-
if !writable_roots.iter().any(|existing| existing == path) {
1980-
writable_roots.push(path.clone());
1981-
}
1982-
}
1983-
}
1984-
let file_system_sandbox_policy =
1985-
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
1977+
let sandbox_policy =
1978+
permission_profile
1979+
.to_legacy_sandbox_policy(resolved_cwd.as_path())
1980+
.unwrap_or_else(|err| {
1981+
tracing::warn!(
1982+
error = %err,
1983+
"derived permission profile cannot be represented as a legacy sandbox policy; falling back to read-only"
1984+
);
1985+
SandboxPolicy::new_read_only_policy()
1986+
});
1987+
permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
19861988
&sandbox_policy,
19871989
resolved_cwd.as_path(),
19881990
);
1989-
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
1990-
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
1991-
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
1992-
&file_system_sandbox_policy,
1993-
network_sandbox_policy,
1994-
);
1991+
let (mut file_system_sandbox_policy, network_sandbox_policy) =
1992+
permission_profile.to_runtime_permissions();
1993+
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
1994+
&& file_system_sandbox_policy.can_write_path_with_cwd(
1995+
resolved_cwd.as_path(),
1996+
resolved_cwd.as_path(),
1997+
)
1998+
&& !file_system_sandbox_policy.has_full_disk_write_access()
1999+
{
2000+
file_system_sandbox_policy = file_system_sandbox_policy
2001+
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
2002+
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
2003+
permission_profile.enforcement(),
2004+
&file_system_sandbox_policy,
2005+
network_sandbox_policy,
2006+
);
2007+
}
19952008
(
19962009
configured_network_proxy_config,
19972010
permission_profile,

0 commit comments

Comments
 (0)