permissions: migrate approval and sandbox consumers to profiles (#19393)

## Why

Runtime decisions should not infer permissions from the lossy legacy
sandbox projection once `PermissionProfile` is available. In particular,
`Disabled` and `External` need to remain distinct, and managed profiles
with split filesystem or deny-read rules should not be collapsed before
approval, network, safety, or analytics code makes decisions.

## What Changed

- Changes managed network proxy setup and network approval logic to use
`PermissionProfile` when deciding whether a managed sandbox is active.
- Migrates patch safety, Guardian/user-shell approval paths, Landlock
helper setup, analytics sandbox classification, and selected
turn/session code to profile-backed permissions.
- Validates command-level profile overrides against the constrained
`PermissionProfile` rather than a strict `SandboxPolicy` round trip.
- Preserves configured deny-read restrictions when command profiles are
narrowed.
- Adds coverage for profile-backed trust, network proxy/approval
behavior, patch safety, analytics classification, and command-profile
narrowing.

## Verification

- `cargo test -p codex-core direct_write_roots`
- `cargo test -p codex-core runtime_roots_to_legacy_projection`
- `cargo test -p codex-app-server
requested_permissions_trust_project_uses_permission_profile_intent`




































































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19393).
* #19395
* #19394
* __->__ #19393
This commit is contained in:
Michael Bolin
2026-04-26 15:30:40 -07:00
committed by GitHub
Unverified
parent 9c3abcd46c
commit dda8199b73
24 changed files with 367 additions and 164 deletions
+17 -13
View File
@@ -845,8 +845,10 @@ impl Session {
}
}
fn managed_network_proxy_active_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> bool {
!matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
fn managed_network_proxy_active_for_permission_profile(
permission_profile: &PermissionProfile,
) -> bool {
!matches!(permission_profile, PermissionProfile::Disabled)
}
/// Builds the `x-codex-beta-features` header value for this session.
@@ -879,7 +881,7 @@ impl Session {
async fn start_managed_network_proxy(
spec: &crate::config::NetworkProxySpec,
exec_policy: &codex_execpolicy::Policy,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
network_policy_decider: Option<Arc<dyn codex_network_proxy::NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn codex_network_proxy::BlockedRequestObserver>>,
managed_network_requirements_enabled: bool,
@@ -896,7 +898,7 @@ impl Session {
.unwrap_or_else(|_| spec.clone());
let network_proxy = spec
.start_proxy(
sandbox_policy,
permission_profile,
network_policy_decider,
blocked_request_observer,
managed_network_requirements_enabled,
@@ -914,7 +916,7 @@ impl Session {
Ok((network_proxy, session_network_proxy))
}
async fn refresh_managed_network_proxy_for_current_sandbox_policy(&self) {
async fn refresh_managed_network_proxy_for_current_permission_profile(&self) {
let Some(started_proxy) = self.services.network_proxy.as_ref() else {
return;
};
@@ -935,7 +937,8 @@ impl Session {
return;
};
let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy())
let spec = match spec
.recompute_for_permission_profile(&session_configuration.permission_profile())
{
Ok(spec) => spec,
Err(err) => {
@@ -1285,7 +1288,7 @@ impl Session {
&self,
updates: SessionSettingsUpdate,
) -> ConstraintResult<()> {
let (previous_cwd, sandbox_policy_changed, next_cwd, codex_home, session_source) = {
let (previous_cwd, permission_profile_changed, next_cwd, codex_home, session_source) = {
let mut state = self.state.lock().await;
let updated = match state.session_configuration.apply(&updates) {
Ok(updated) => updated,
@@ -1296,16 +1299,17 @@ impl Session {
};
let previous_cwd = state.session_configuration.cwd.clone();
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let updated_sandbox_policy = updated.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != updated_sandbox_policy;
let previous_permission_profile = state.session_configuration.permission_profile();
let updated_permission_profile = updated.permission_profile();
let permission_profile_changed =
previous_permission_profile != updated_permission_profile;
let next_cwd = updated.cwd.clone();
let codex_home = updated.codex_home.clone();
let session_source = updated.session_source.clone();
state.session_configuration = updated;
(
previous_cwd,
sandbox_policy_changed,
permission_profile_changed,
next_cwd,
codex_home,
session_source,
@@ -1318,8 +1322,8 @@ impl Session {
&codex_home,
&session_source,
);
if sandbox_policy_changed {
self.refresh_managed_network_proxy_for_current_sandbox_policy()
if permission_profile_changed {
self.refresh_managed_network_proxy_for_current_permission_profile()
.await;
}
+3 -3
View File
@@ -730,7 +730,7 @@ impl Session {
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
current_exec_policy.as_ref(),
config.permissions.sandbox_policy.get(),
config.permissions.permission_profile.get(),
network_policy_decider.as_ref().map(Arc::clone),
blocked_request_observer.as_ref().map(Arc::clone),
managed_network_requirements_configured,
@@ -885,8 +885,8 @@ impl Session {
history_entry_count,
initial_messages,
network_proxy: session_network_proxy.filter(|_| {
Self::managed_network_proxy_active_for_sandbox_policy(
&session_sandbox_policy,
Self::managed_network_proxy_active_for_permission_profile(
session_configuration.permission_profile.get(),
)
}),
rollout_path,
+39 -23
View File
@@ -159,6 +159,10 @@ use std::time::Duration as StdDuration;
mod guardian_tests;
fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
}
struct InstructionsTestCase {
slug: &'static str,
expects_apply_patch_description: bool,
@@ -593,7 +597,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
/*requirements*/ None,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)?;
let mut exec_policy = Policy::empty();
exec_policy.add_network_rule(
@@ -606,7 +610,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -637,7 +641,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
managed_allowed_domains_only: Some(true),
..Default::default()
}),
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
)?;
let mut exec_policy = Policy::empty();
exec_policy.add_network_rule(
@@ -650,7 +654,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::new_workspace_write_policy(),
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -674,7 +678,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let exec_policy = Policy::empty();
let decider_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
@@ -689,7 +693,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&exec_policy,
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
Some(network_policy_decider),
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ true,
@@ -697,7 +701,9 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
)
.await?;
let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())?;
let spec = spec.recompute_for_permission_profile(&permission_profile_for_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
))?;
spec.apply_to_started_proxy(&started_proxy).await?;
let current_cfg = started_proxy.proxy().current_cfg().await?;
assert_eq!(current_cfg.network.allowed_domains(), None);
@@ -754,12 +760,12 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
let spec = crate::config::NetworkProxySpec::from_config_and_constraints(
network_config,
Some(requirements),
&initial_policy,
&permission_profile_for_sandbox_policy(&initial_policy),
)?;
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
&Policy::empty(),
&initial_policy,
&permission_profile_for_sandbox_policy(&initial_policy),
/*network_policy_decider*/ None,
/*blocked_request_observer*/ None,
/*managed_network_requirements_enabled*/ false,
@@ -832,14 +838,15 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
@@ -897,14 +904,15 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
enabled: Some(true),
..Default::default()
}),
&SandboxPolicy::DangerFullAccess,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess);
config.permissions.permission_profile =
codex_config::Constrained::allow_any(PermissionProfile::Disabled);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
let layers = config
@@ -971,11 +979,15 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any
enabled: Some(true),
..Default::default()
}),
&sandbox_policy,
&permission_profile_for_sandbox_policy(&sandbox_policy),
)?;
let session = make_session_with_config(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
@@ -994,11 +1006,15 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
enabled: Some(true),
..Default::default()
}),
&sandbox_policy,
&permission_profile_for_sandbox_policy(&sandbox_policy),
)?;
let (session, rx) = make_session_with_config_and_rx(move |config| {
config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, cwd.as_path())
.expect("test setup should allow sandbox policy");
config.permissions.network = Some(network_spec);
})
.await?;
+2 -1
View File
@@ -692,7 +692,8 @@ async fn track_turn_resolved_config_analytics(
session_source: thread_config.session_source,
model: turn_context.model_info.slug.clone(),
model_provider: turn_context.config.model_provider_id.clone(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: turn_context.permission_profile(),
permission_profile_cwd: turn_context.cwd.to_path_buf(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context.config.service_tier,
+11 -9
View File
@@ -538,16 +538,18 @@ impl Session {
let turn_environments =
self.resolve_turn_environments(&effective_environments)?;
let previous_cwd = state.session_configuration.cwd.clone();
let previous_sandbox_policy = state.session_configuration.sandbox_policy();
let next_sandbox_policy = next.sandbox_policy();
let sandbox_policy_changed = previous_sandbox_policy != next_sandbox_policy;
let previous_permission_profile =
state.session_configuration.permission_profile();
let next_permission_profile = next.permission_profile();
let permission_profile_changed =
previous_permission_profile != next_permission_profile;
let codex_home = next.codex_home.clone();
let session_source = next.session_source.clone();
state.session_configuration = next.clone();
Ok((
next,
turn_environments,
sandbox_policy_changed,
permission_profile_changed,
previous_cwd,
codex_home,
session_source,
@@ -560,7 +562,7 @@ impl Session {
let (
session_configuration,
turn_environments,
sandbox_policy_changed,
permission_profile_changed,
previous_cwd,
codex_home,
session_source,
@@ -587,8 +589,8 @@ impl Session {
&session_source,
);
if sandbox_policy_changed {
self.refresh_managed_network_proxy_for_current_sandbox_policy()
if permission_profile_changed {
self.refresh_managed_network_proxy_for_current_permission_profile()
.await;
}
@@ -691,8 +693,8 @@ impl Session {
.network_proxy
.as_ref()
.and_then(|started_proxy| {
Self::managed_network_proxy_active_for_sandbox_policy(
&session_configuration.sandbox_policy(),
Self::managed_network_proxy_active_for_permission_profile(
&session_configuration.permission_profile(),
)
.then(|| started_proxy.proxy())
}),