Skip to content

Commit d4eb184

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

5 files changed

Lines changed: 360 additions & 175 deletions

File tree

codex-rs/config/src/config_toml.rs

Lines changed: 62 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,15 @@ 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 the configuration.
646+
pub async fn derive_permission_profile(
646647
&self,
647648
sandbox_mode_override: Option<SandboxMode>,
648649
profile_sandbox_mode: Option<SandboxMode>,
649650
windows_sandbox_level: WindowsSandboxLevel,
650651
active_project: Option<&ProjectConfig>,
651652
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
652-
) -> SandboxPolicy {
653+
) -> PermissionProfile {
653654
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
654655
|| profile_sandbox_mode.is_some()
655656
|| self.sandbox_mode.is_some();
@@ -677,50 +678,81 @@ impl ConfigToml {
677678
})
678679
})
679680
.unwrap_or_default();
680-
let mut sandbox_policy = match resolved_sandbox_mode {
681-
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
681+
let workspace_write_unsupported = cfg!(target_os = "windows")
682+
// If the experimental Windows sandbox is enabled, do not force a downgrade.
683+
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
684+
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite);
685+
let mut permission_profile = match resolved_sandbox_mode {
686+
SandboxMode::ReadOnly => PermissionProfile::read_only(),
682687
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
683688
Some(SandboxWorkspaceWrite {
684689
writable_roots,
685690
network_access,
686691
exclude_tmpdir_env_var,
687692
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(),
693+
}) => {
694+
let network_policy = if *network_access {
695+
NetworkSandboxPolicy::Enabled
696+
} else {
697+
NetworkSandboxPolicy::Restricted
698+
};
699+
PermissionProfile::workspace_write_with(
700+
writable_roots,
701+
network_policy,
702+
*exclude_tmpdir_env_var,
703+
*exclude_slash_tmp,
704+
)
705+
}
706+
None => PermissionProfile::workspace_write(),
695707
},
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-
}
708+
SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
706709
};
707-
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
708-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
710+
if workspace_write_unsupported {
711+
permission_profile = PermissionProfile::read_only();
709712
}
710713
if !sandbox_mode_was_explicit
711714
&& let Some(constraint) = permission_profile_constraint
712-
&& let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy(
713-
&sandbox_policy,
714-
))
715+
&& let Err(err) = constraint.can_set(&permission_profile)
715716
{
716717
tracing::warn!(
717718
error = %err,
718719
"default sandbox policy is disallowed by requirements; falling back to required default"
719720
);
720-
sandbox_policy = SandboxPolicy::new_read_only_policy();
721-
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
721+
permission_profile = PermissionProfile::read_only();
722+
}
723+
permission_profile
724+
}
725+
726+
/// Derive the legacy sandbox projection from configuration.
727+
///
728+
/// New callers should use [`Self::derive_permission_profile`] instead.
729+
pub async fn derive_sandbox_policy(
730+
&self,
731+
sandbox_mode_override: Option<SandboxMode>,
732+
profile_sandbox_mode: Option<SandboxMode>,
733+
windows_sandbox_level: WindowsSandboxLevel,
734+
active_project: Option<&ProjectConfig>,
735+
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
736+
) -> SandboxPolicy {
737+
let permission_profile = self
738+
.derive_permission_profile(
739+
sandbox_mode_override,
740+
profile_sandbox_mode,
741+
windows_sandbox_level,
742+
active_project,
743+
permission_profile_constraint,
744+
)
745+
.await;
746+
match permission_profile.to_legacy_sandbox_policy(Path::new("/")) {
747+
Ok(sandbox_policy) => sandbox_policy,
748+
Err(err) => {
749+
tracing::warn!(
750+
error = %err,
751+
"derived permission profile cannot be represented as a legacy sandbox policy; falling back to read-only"
752+
);
753+
SandboxPolicy::new_read_only_policy()
754+
}
722755
}
723-
sandbox_policy
724756
}
725757

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

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

Lines changed: 62 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,73 @@ 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+
assert!(
1826+
file_system_policy
1827+
.entries
1828+
.contains(&FileSystemSandboxEntry {
1829+
path: FileSystemPath::Special {
1830+
value: FileSystemSpecialPath::CurrentWorkingDirectory,
1831+
},
1832+
access: FileSystemAccessMode::Write,
1833+
})
1834+
);
1835+
assert!(
1836+
file_system_policy
1837+
.entries
1838+
.contains(&FileSystemSandboxEntry {
1839+
path: FileSystemPath::Path {
1840+
path: extra_root.clone(),
1841+
},
1842+
access: FileSystemAccessMode::Write,
1843+
})
1844+
);
1845+
assert!(
1846+
file_system_policy
1847+
.can_write_path_with_cwd(cwd.path().join(".git").as_path(), cwd.path()),
1848+
"legacy workspace-write should allow git init to create .git"
1849+
);
1850+
assert!(
1851+
file_system_policy
1852+
.can_write_path_with_cwd(cwd.path().join(".agents").as_path(), cwd.path()),
1853+
"legacy workspace-write should allow creating .agents"
1854+
);
1855+
assert!(
1856+
!file_system_policy
1857+
.can_write_path_with_cwd(cwd.path().join(".codex").as_path(), cwd.path()),
1858+
"legacy workspace-write should protect .codex before it exists"
1859+
);
1860+
}
1861+
_ => unreachable!("unexpected test case `{name}`"),
1862+
}
18161863
}
18171864

18181865
Ok(())

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

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,33 +1965,43 @@ 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(
1986-
&sandbox_policy,
1987-
resolved_cwd.as_path(),
1988-
);
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),
1977+
let (mut file_system_sandbox_policy, network_sandbox_policy) =
1978+
permission_profile.to_runtime_permissions();
1979+
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
1980+
&permission_profile,
19921981
&file_system_sandbox_policy,
19931982
network_sandbox_policy,
1983+
resolved_cwd.as_path(),
19941984
);
1985+
permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
1986+
&sandbox_policy,
1987+
resolved_cwd.as_path(),
1988+
);
1989+
(file_system_sandbox_policy, _) = permission_profile.to_runtime_permissions();
1990+
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
1991+
&& file_system_sandbox_policy.can_write_path_with_cwd(
1992+
resolved_cwd.as_path(),
1993+
resolved_cwd.as_path(),
1994+
)
1995+
&& !file_system_sandbox_policy.has_full_disk_write_access()
1996+
{
1997+
file_system_sandbox_policy = file_system_sandbox_policy
1998+
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
1999+
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
2000+
permission_profile.enforcement(),
2001+
&file_system_sandbox_policy,
2002+
network_sandbox_policy,
2003+
);
2004+
}
19952005
(
19962006
configured_network_proxy_config,
19972007
permission_profile,

codex-rs/protocol/src/models.rs

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -404,55 +404,30 @@ impl PermissionProfile {
404404

405405
/// Managed workspace-write filesystem access with restricted network access.
406406
pub fn workspace_write() -> Self {
407+
Self::workspace_write_with(
408+
&[],
409+
NetworkSandboxPolicy::Restricted,
410+
/*exclude_tmpdir_env_var*/ false,
411+
/*exclude_slash_tmp*/ false,
412+
)
413+
}
414+
415+
/// Managed workspace-write filesystem access with the legacy
416+
/// `sandbox_workspace_write` knobs applied directly to the profile.
417+
pub fn workspace_write_with(
418+
writable_roots: &[AbsolutePathBuf],
419+
network: NetworkSandboxPolicy,
420+
exclude_tmpdir_env_var: bool,
421+
exclude_slash_tmp: bool,
422+
) -> Self {
423+
let file_system = FileSystemSandboxPolicy::legacy_workspace_write(
424+
writable_roots,
425+
exclude_tmpdir_env_var,
426+
exclude_slash_tmp,
427+
);
407428
Self::Managed {
408-
file_system: ManagedFileSystemPermissions::Restricted {
409-
entries: vec![
410-
FileSystemSandboxEntry {
411-
path: FileSystemPath::Special {
412-
value: FileSystemSpecialPath::Root,
413-
},
414-
access: FileSystemAccessMode::Read,
415-
},
416-
FileSystemSandboxEntry {
417-
path: FileSystemPath::Special {
418-
value: FileSystemSpecialPath::CurrentWorkingDirectory,
419-
},
420-
access: FileSystemAccessMode::Write,
421-
},
422-
FileSystemSandboxEntry {
423-
path: FileSystemPath::Special {
424-
value: FileSystemSpecialPath::SlashTmp,
425-
},
426-
access: FileSystemAccessMode::Write,
427-
},
428-
FileSystemSandboxEntry {
429-
path: FileSystemPath::Special {
430-
value: FileSystemSpecialPath::Tmpdir,
431-
},
432-
access: FileSystemAccessMode::Write,
433-
},
434-
FileSystemSandboxEntry {
435-
path: FileSystemPath::Special {
436-
value: FileSystemSpecialPath::project_roots(Some(".git".into())),
437-
},
438-
access: FileSystemAccessMode::Read,
439-
},
440-
FileSystemSandboxEntry {
441-
path: FileSystemPath::Special {
442-
value: FileSystemSpecialPath::project_roots(Some(".agents".into())),
443-
},
444-
access: FileSystemAccessMode::Read,
445-
},
446-
FileSystemSandboxEntry {
447-
path: FileSystemPath::Special {
448-
value: FileSystemSpecialPath::project_roots(Some(".codex".into())),
449-
},
450-
access: FileSystemAccessMode::Read,
451-
},
452-
],
453-
glob_scan_max_depth: None,
454-
},
455-
network: NetworkSandboxPolicy::Restricted,
429+
file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
430+
network,
456431
}
457432
}
458433

@@ -504,6 +479,14 @@ impl PermissionProfile {
504479
)
505480
}
506481

482+
pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
483+
Self::from_runtime_permissions_with_enforcement(
484+
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
485+
&FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd),
486+
NetworkSandboxPolicy::from(sandbox_policy),
487+
)
488+
}
489+
507490
pub fn enforcement(&self) -> SandboxEnforcement {
508491
match self {
509492
Self::Managed { .. } => SandboxEnforcement::Managed,

0 commit comments

Comments
 (0)