permissions: derive compatibility policies from profiles (#19392)

## Why

After #19391, `PermissionProfile` and the split filesystem/network
policies could still be stored in parallel. That creates drift risk: a
profile can preserve deny globs, external enforcement, or split
filesystem entries while a cached projection silently loses those
details. This PR makes the profile the runtime source and derives
compatibility views from it.

## What Changed

- Removes stored filesystem/network sandbox projections from
`Permissions` and `SessionConfiguration`; their accessors now derive
from the canonical `PermissionProfile`.
- Derives legacy `SandboxPolicy` snapshots from profiles only where an
older API still needs that field.
- Updates MCP connection and elicitation state to track
`PermissionProfile` instead of `SandboxPolicy` for auto-approval
decisions.
- Adds semantic filesystem-policy comparison so cwd changes can preserve
richer profiles while still recognizing equivalent legacy projections
independent of entry ordering.
- Updates config/session tests to assert profile-derived projections
instead of parallel stored fields.

## 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/19392).
* #19395
* #19394
* #19393
* __->__ #19392
This commit is contained in:
Michael Bolin
2026-04-26 15:06:42 -07:00
committed by GitHub
parent 4d7ce3447d
commit deaa307fb2
39 changed files with 568 additions and 439 deletions
+14 -9
View File
@@ -26,10 +26,10 @@ use codex_plugin::PluginCapabilitySummary;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::mcp::Tool;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpListToolsResponseEvent;
use codex_protocol::protocol::SandboxPolicy;
use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ReadResourceResult;
use serde_json::Value;
@@ -66,13 +66,18 @@ pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String {
/// of being shown to the user.
pub fn mcp_permission_prompt_is_auto_approved(
approval_policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
) -> bool {
approval_policy == AskForApproval::Never
&& matches!(
sandbox_policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
)
if approval_policy != AskForApproval::Never {
return false;
}
match permission_profile {
PermissionProfile::Disabled | PermissionProfile::External { .. } => true,
PermissionProfile::Managed { file_system, .. } => {
file_system.to_sandbox_policy().has_full_disk_write_access()
}
}
}
/// MCP runtime settings derived from `codex_core::config::Config`.
@@ -229,7 +234,7 @@ pub async fn read_mcp_resource(
&config.approval_policy,
String::new(),
tx_event,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::default(),
runtime_environment,
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
@@ -294,7 +299,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
&config.approval_policy,
submit_id,
tx_event,
SandboxPolicy::new_read_only_policy(),
PermissionProfile::default(),
runtime_environment,
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
+31
View File
@@ -3,6 +3,9 @@ use codex_config::Constrained;
use codex_login::CodexAuth;
use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
@@ -33,6 +36,34 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
);
}
#[test]
fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() {
assert!(mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
},
));
assert!(mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
},
));
assert!(!mcp_permission_prompt_is_auto_approved(
AskForApproval::Never,
&PermissionProfile::read_only(),
));
assert!(!mcp_permission_prompt_is_auto_approved(
AskForApproval::OnRequest,
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
},
));
}
#[test]
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
let provenance = ToolPluginProvenance::from_capability_summaries(&[
@@ -334,15 +334,15 @@ fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) ->
struct ElicitationRequestManager {
requests: Arc<Mutex<ResponderMap>>,
approval_policy: Arc<StdMutex<AskForApproval>>,
sandbox_policy: Arc<StdMutex<SandboxPolicy>>,
permission_profile: Arc<StdMutex<PermissionProfile>>,
}
impl ElicitationRequestManager {
fn new(approval_policy: AskForApproval, sandbox_policy: SandboxPolicy) -> Self {
fn new(approval_policy: AskForApproval, permission_profile: PermissionProfile) -> Self {
Self {
requests: Arc::new(Mutex::new(HashMap::new())),
approval_policy: Arc::new(StdMutex::new(approval_policy)),
sandbox_policy: Arc::new(StdMutex::new(sandbox_policy)),
permission_profile: Arc::new(StdMutex::new(permission_profile)),
}
}
@@ -364,23 +364,23 @@ impl ElicitationRequestManager {
fn make_sender(&self, server_name: String, tx_event: Sender<Event>) -> SendElicitation {
let elicitation_requests = self.requests.clone();
let approval_policy = self.approval_policy.clone();
let sandbox_policy = self.sandbox_policy.clone();
let permission_profile = self.permission_profile.clone();
Box::new(move |id, elicitation| {
let elicitation_requests = elicitation_requests.clone();
let tx_event = tx_event.clone();
let server_name = server_name.clone();
let approval_policy = approval_policy.clone();
let sandbox_policy = sandbox_policy.clone();
let permission_profile = permission_profile.clone();
async move {
let approval_policy = approval_policy
.lock()
.map(|policy| *policy)
.unwrap_or(AskForApproval::Never);
let sandbox_policy = sandbox_policy
let permission_profile = permission_profile
.lock()
.map(|policy| policy.clone())
.unwrap_or_else(|_| SandboxPolicy::new_read_only_policy());
if mcp_permission_prompt_is_auto_approved(approval_policy, &sandbox_policy)
.map(|profile| profile.clone())
.unwrap_or_default();
if mcp_permission_prompt_is_auto_approved(approval_policy, &permission_profile)
&& can_auto_accept_elicitation(&elicitation)
{
return Ok(ElicitationResponse {
@@ -666,14 +666,14 @@ impl AsyncManagedClient {
impl McpConnectionManager {
pub fn new_uninitialized(
approval_policy: &Constrained<AskForApproval>,
sandbox_policy: &Constrained<SandboxPolicy>,
permission_profile: &Constrained<PermissionProfile>,
) -> Self {
Self {
clients: HashMap::new(),
server_origins: HashMap::new(),
elicitation_requests: ElicitationRequestManager::new(
approval_policy.value(),
sandbox_policy.get().clone(),
permission_profile.get().clone(),
),
}
}
@@ -692,9 +692,9 @@ impl McpConnectionManager {
}
}
pub fn set_sandbox_policy(&self, sandbox_policy: &SandboxPolicy) {
if let Ok(mut policy) = self.elicitation_requests.sandbox_policy.lock() {
*policy = sandbox_policy.clone();
pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
*profile = permission_profile;
}
}
@@ -706,7 +706,7 @@ impl McpConnectionManager {
approval_policy: &Constrained<AskForApproval>,
submit_id: String,
tx_event: Sender<Event>,
initial_sandbox_policy: SandboxPolicy,
initial_permission_profile: PermissionProfile,
runtime_environment: McpRuntimeEnvironment,
codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
@@ -718,7 +718,7 @@ impl McpConnectionManager {
let mut server_origins = HashMap::new();
let mut join_set = JoinSet::new();
let elicitation_requests =
ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy);
ElicitationRequestManager::new(approval_policy.value(), initial_permission_profile);
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
let startup_submit_id = submit_id.clone();
let codex_apps_auth_provider = auth
@@ -1,5 +1,6 @@
use super::*;
use codex_protocol::ToolName;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpAuthStatus;
use pretty_assertions::assert_eq;
@@ -179,9 +180,9 @@ fn elicitation_granular_policy_respects_never_and_config() {
}
#[tokio::test]
async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
async fn disabled_permissions_auto_accept_elicitation_with_empty_form_schema() {
let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event);
@@ -209,9 +210,9 @@ async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
}
#[tokio::test]
async fn full_access_does_not_auto_accept_elicitation_with_requested_fields() {
async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fields() {
let manager =
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
let (tx_event, _rx_event) = async_channel::bounded(1);
let sender = manager.make_sender("server".to_string(), tx_event);
@@ -627,8 +628,9 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -654,8 +656,9 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
"rmcp".to_string(),
AsyncManagedClient {
@@ -689,8 +692,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -712,8 +716,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
@@ -744,8 +749,9 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
.boxed()
.shared();
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager =
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),