mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user