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
@@ -272,8 +272,9 @@ async fn effective_patch_permissions(
session.granted_session_permissions().await.as_ref(),
session.granted_turn_permissions().await.as_ref(),
);
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&turn.file_system_sandbox_policy,
&base_file_system_sandbox_policy,
granted_permissions.as_ref(),
);
let effective_additional_permissions = apply_granted_turn_permissions(
+2 -1
View File
@@ -99,7 +99,8 @@ impl ToolHandler for ListDirHandler {
"dir_path must be an absolute path".to_string(),
));
}
let read_deny_matcher = ReadDenyMatcher::new(&turn.file_system_sandbox_policy, &turn.cwd);
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let read_deny_matcher = ReadDenyMatcher::new(&file_system_sandbox_policy, &turn.cwd);
if read_deny_matcher
.as_ref()
.is_some_and(|matcher| matcher.is_read_denied(&path))
@@ -37,6 +37,9 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::FileSystemAccessMode;
use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InterAgentCommunication;
@@ -2074,21 +2077,6 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
#[tokio::test]
async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
fn pick_allowed_sandbox_policy(
constraint: &crate::config::Constrained<SandboxPolicy>,
base: SandboxPolicy,
) -> SandboxPolicy {
let candidates = [
SandboxPolicy::DangerFullAccess,
SandboxPolicy::new_workspace_write_policy(),
SandboxPolicy::new_read_only_policy(),
];
candidates
.into_iter()
.find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok())
.unwrap_or(base)
}
#[derive(Debug, Deserialize)]
struct SpawnAgentResult {
agent_id: String,
@@ -2098,12 +2086,17 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let expected_sandbox = pick_allowed_sandbox_policy(
&turn.config.permissions.sandbox_policy,
turn.config.permissions.sandbox_policy.get().clone(),
);
let expected_file_system_sandbox_policy =
let expected_sandbox = turn.config.permissions.sandbox_policy.get().clone();
let mut expected_file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd);
expected_file_system_sandbox_policy
.entries
.push(FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/.env".to_string(),
},
access: FileSystemAccessMode::None,
});
let expected_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox);
let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&expected_sandbox),
@@ -2113,16 +2106,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
turn.approval_policy
.set(AskForApproval::OnRequest)
.expect("approval policy should be set");
turn.sandbox_policy
.set(expected_sandbox.clone())
.expect("sandbox policy should be set");
turn.file_system_sandbox_policy = expected_file_system_sandbox_policy.clone();
turn.network_sandbox_policy = expected_network_sandbox_policy;
turn.permission_profile = expected_permission_profile.clone();
assert_ne!(
expected_sandbox,
turn.config.permissions.sandbox_policy.get().clone(),
"test requires a runtime sandbox override that differs from base config"
expected_permission_profile,
turn.config.permissions.permission_profile(),
"test requires a runtime profile override that differs from base config"
);
let invocation = invocation(
@@ -2164,11 +2152,11 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
.expect("spawned agent thread should exist");
let child_turn = child_thread.codex.session.new_default_turn().await;
assert_eq!(
child_turn.file_system_sandbox_policy,
child_turn.file_system_sandbox_policy(),
expected_file_system_sandbox_policy
);
assert_eq!(
child_turn.network_sandbox_policy,
child_turn.network_sandbox_policy(),
expected_network_sandbox_policy
);
assert_eq!(child_turn.permission_profile(), expected_permission_profile);
@@ -3637,11 +3625,6 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
&file_system_sandbox_policy,
network_sandbox_policy,
);
turn.sandbox_policy
.set(sandbox_policy)
.expect("sandbox policy set");
turn.file_system_sandbox_policy = file_system_sandbox_policy;
turn.network_sandbox_policy = network_sandbox_policy;
turn.permission_profile = permission_profile.clone();
turn.approval_policy
.set(AskForApproval::OnRequest)
@@ -3718,7 +3701,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
expected
.permissions
.sandbox_policy
.set(turn.sandbox_policy.get().clone())
.set(turn.sandbox_policy())
.expect("sandbox policy set");
assert_eq!(config, expected);
}
+4 -2
View File
@@ -513,14 +513,16 @@ impl ShellHandler {
);
emitter.begin(event_ctx).await;
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let sandbox_policy = turn.sandbox_policy();
let exec_approval_requirement = session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &exec_params.command,
approval_policy: turn.approval_policy.value(),
sandbox_policy: turn.sandbox_policy.get(),
file_system_sandbox_policy: &turn.file_system_sandbox_policy,
sandbox_policy: &sandbox_policy,
file_system_sandbox_policy: &file_system_sandbox_policy,
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
codex_protocol::models::SandboxPermissions::UseDefault
} else {
+2 -1
View File
@@ -359,7 +359,8 @@ impl NetworkApprovalService {
.await;
return NetworkDecision::deny(REASON_NOT_ALLOWED);
};
if !sandbox_policy_allows_network_approval_flow(turn_context.sandbox_policy.get()) {
let sandbox_policy = turn_context.sandbox_policy();
if !sandbox_policy_allows_network_approval_flow(&sandbox_policy) {
pending.set_decision(PendingApprovalDecision::Deny).await;
self.pending_host_approvals.lock().await.remove(&key);
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
+6 -4
View File
@@ -122,8 +122,10 @@ impl ToolOrchestrator {
// 1) Approval
let mut already_approved = false;
let file_system_sandbox_policy = turn_ctx.file_system_sandbox_policy();
let network_sandbox_policy = turn_ctx.network_sandbox_policy();
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
default_exec_approval_requirement(approval_policy, &turn_ctx.file_system_sandbox_policy)
default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});
match requirement {
ExecApprovalRequirement::Skip { .. } => {
@@ -194,8 +196,8 @@ impl ToolOrchestrator {
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.file_system_sandbox_policy,
turn_ctx.network_sandbox_policy,
&file_system_sandbox_policy,
network_sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
managed_network_active,
@@ -268,7 +270,7 @@ impl ToolOrchestrator {
&& matches!(
default_exec_approval_requirement(
approval_policy,
&turn_ctx.file_system_sandbox_policy
&file_system_sandbox_policy
),
ExecApprovalRequirement::NeedsApproval { .. }
);
@@ -390,11 +390,12 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
..HooksConfig::default()
});
let sandbox_policy = SandboxPolicy::new_read_only_policy();
turn_context.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
turn_context.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
turn_context.file_system_sandbox_policy = read_only_file_system_sandbox_policy();
turn_context.network_sandbox_policy = NetworkSandboxPolicy::Restricted;
turn_context.permission_profile = PermissionProfile::from_runtime_permissions(
&read_only_file_system_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
);
let sandbox_policy = SandboxPolicy::new_read_only_policy();
let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?;
let target = std::env::temp_dir().join("execve-hook-short-circuit.txt");