config: express implicit sandbox defaults as permission profiles (#25926)

## Why

`PermissionProfile` is becoming the default way to represent Codex
permissions, but the implicit default behavior should stay the same for
now:

- trusted projects use `:workspace`
- untrusted projects also use `:workspace`
- roots without a trust decision use `:read-only`
- unsandboxed Windows falls back to `:read-only`

This keeps the existing sandbox semantics while making silent config
defaults observable as built-in permission profiles instead of treating
the legacy `SandboxPolicy` projection as the primary shape.

## What Changed

- Refactored legacy sandbox derivation to resolve the configured sandbox
mode once, then apply the implicit project fallback only when no sandbox
mode was configured.
- Preserved the existing trust-decision fallback: trusted and untrusted
projects default to workspace-write where supported.
- Added empty-config coverage asserting that an untrusted project
resolves to the built-in active permission profile (`:workspace` outside
unsandboxed Windows).

## Verification

- `just fmt`
- `just test -p codex-core 'config::'`
- `just test -p codex-config`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/25926).
* __->__ #25926
This commit is contained in:
Michael Bolin
2026-06-02 16:26:36 -07:00
committed by GitHub
parent 6471f8b31a
commit a28b32a835
2 changed files with 69 additions and 16 deletions
+10 -16
View File
@@ -728,29 +728,23 @@ impl ConfigToml {
active_project: Option<&ProjectConfig>,
permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
) -> PermissionProfile {
let sandbox_mode_was_explicit =
sandbox_mode_override.is_some() || self.sandbox_mode.is_some();
let resolved_sandbox_mode = sandbox_mode_override
.or(self.sandbox_mode)
.or(if sandbox_mode_was_explicit {
None
} else {
let configured_sandbox_mode = sandbox_mode_override.or(self.sandbox_mode);
let resolved_sandbox_mode = configured_sandbox_mode
.or_else(|| {
// If no sandbox_mode is set but this directory has a trust decision,
// default to workspace-write except on unsandboxed Windows where we
// default to read-only.
active_project.and_then(|p| {
if p.is_trusted() || p.is_untrusted() {
active_project
.filter(|project| project.is_trusted() || project.is_untrusted())
.map(|_| {
if cfg!(target_os = "windows")
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
{
Some(SandboxMode::ReadOnly)
SandboxMode::ReadOnly
} else {
Some(SandboxMode::WorkspaceWrite)
SandboxMode::WorkspaceWrite
}
} else {
None
}
})
})
})
.unwrap_or_default();
let effective_sandbox_mode = if cfg!(target_os = "windows")
@@ -788,7 +782,7 @@ impl ConfigToml {
},
SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
};
if !sandbox_mode_was_explicit
if configured_sandbox_mode.is_none()
&& let Some(constraint) = permission_profile_constraint
&& let Err(err) = constraint.can_set(&permission_profile)
{