permissions: make runtime config profile-backed (#19606)

## Why

This supersedes #19391. During stack repair, GitHub marked #19391 as
merged into a temporary stack branch rather than into `main`, so the
runtime-config change needed a fresh PR.

`PermissionProfile` is now the canonical permissions shape after #19231
because it can distinguish `Managed`, `Disabled`, and `External`
enforcement while also carrying filesystem rules that legacy
`SandboxPolicy` cannot represent cleanly. Core config and session state
still needed to accept profile-backed permissions without forcing every
profile through the strict legacy bridge, which rejected valid runtime
profiles such as direct write roots.

The unrelated CI/test hardening that previously rode along with this PR
has been split into #19683 so this PR stays focused on the permissions
model migration.

## What Changed

- Adds `Permissions.permission_profile` and
`SessionConfiguration.permission_profile` as constrained runtime state,
while keeping `sandbox_policy` as a legacy compatibility projection.
- Introduces profile setters that keep `PermissionProfile`, split
filesystem/network policies, and legacy `SandboxPolicy` projections
synchronized.
- Uses a compatibility projection for requirement checks and legacy
consumers instead of rejecting profiles that cannot round-trip through
`SandboxPolicy` exactly.
- Updates config loading, config overrides, session updates, turn
context plumbing, prompt permission text, sandbox tags, and exec request
construction to carry profile-backed runtime permissions.
- Preserves configured deny-read entries and `glob_scan_max_depth` when
command/session profiles are narrowed.
- Adds `PermissionProfile::read_only()` and
`PermissionProfile::workspace_write()` presets that match legacy
defaults.

## 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/19606).
* #19395
* #19394
* #19393
* #19392
* __->__ #19606
This commit is contained in:
Michael Bolin
2026-04-26 13:29:54 -07:00
committed by GitHub
Unverified
parent fed0a8f4fa
commit 4d7ce3447d
62 changed files with 1601 additions and 671 deletions
@@ -359,6 +359,7 @@ use codex_rmcp_client::perform_oauth_login_return_url;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::state_db::get_state_db;
use codex_rollout::state_db::reconcile_rollout;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_state::StateRuntime;
use codex_state::ThreadMetadata;
use codex_state::ThreadMetadataBuilder;
@@ -2272,44 +2273,34 @@ impl CodexMessageProcessor {
arg0: None,
};
let (
effective_policy,
effective_file_system_sandbox_policy,
effective_network_sandbox_policy,
) = if let Some(permission_profile) = permission_profile {
let effective_permission_profile = if let Some(permission_profile) = permission_profile {
let permission_profile =
codex_protocol::models::PermissionProfile::from(permission_profile);
let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) {
Ok(sandbox_policy) => sandbox_policy,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid permission profile: {err}"),
data: None,
};
self.outgoing.send_error(request, error).await;
return;
}
};
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy,
);
let effective_permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&effective_permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_cwd.as_path(),
);
match self
.config
.permissions
.sandbox_policy
.can_set(&sandbox_policy)
{
Ok(()) => {
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy,
);
(
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
)
}
Ok(()) => effective_permission_profile,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
@@ -2327,7 +2318,13 @@ impl CodexMessageProcessor {
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);
let network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
(policy, file_system_sandbox_policy, network_sandbox_policy)
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&policy,
),
&file_system_sandbox_policy,
network_sandbox_policy,
)
}
Err(err) => {
let error = JSONRPCErrorError {
@@ -2340,11 +2337,7 @@ impl CodexMessageProcessor {
}
}
} else {
(
self.config.permissions.sandbox_policy.get().clone(),
self.config.permissions.file_system_sandbox_policy.clone(),
self.config.permissions.network_sandbox_policy,
)
self.config.permissions.permission_profile()
};
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
@@ -2363,9 +2356,7 @@ impl CodexMessageProcessor {
match codex_core::exec::build_exec_request(
exec_params,
&effective_policy,
&effective_file_system_sandbox_policy,
effective_network_sandbox_policy,
&effective_permission_profile,
&sandbox_cwd,
&codex_linux_sandbox_exe,
use_legacy_landlock,
@@ -10161,16 +10152,20 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path)
.permission_profile
.as_ref()
.is_some_and(|profile| {
profile
.to_legacy_sandbox_policy(cwd)
.is_ok_and(|sandbox_policy| {
matches!(
sandbox_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
)
})
let (file_system_sandbox_policy, network_sandbox_policy) =
profile.to_runtime_permissions();
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
profile,
&file_system_sandbox_policy,
network_sandbox_policy,
cwd,
);
matches!(
sandbox_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
)
})
}
@@ -10672,16 +10667,10 @@ mod tests {
#[test]
fn thread_response_permission_profile_preserves_enforcement() {
let full_access_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
);
let external_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
},
);
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
let external_profile = codex_protocol::models::PermissionProfile::External {
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
};
assert_eq!(
thread_response_permission_profile(external_profile.clone()),
@@ -10696,17 +10685,20 @@ mod tests {
#[test]
fn requested_permissions_trust_project_uses_permission_profile_intent() {
let cwd = test_path_buf("/tmp/project").abs();
let full_access_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
);
let workspace_write_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
);
let read_only_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
let workspace_write_profile = codex_protocol::models::PermissionProfile::workspace_write();
let read_only_profile = codex_protocol::models::PermissionProfile::read_only();
let direct_write_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions(
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: test_path_buf("/tmp/other").abs(),
},
access: FileSystemAccessMode::Write,
},
]),
codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
);
assert!(requested_permissions_trust_project(
@@ -10723,6 +10715,13 @@ mod tests {
},
cwd.as_path()
));
assert!(requested_permissions_trust_project(
&ConfigOverrides {
permission_profile: Some(direct_write_profile),
..Default::default()
},
cwd.as_path()
));
assert!(!requested_permissions_trust_project(
&ConfigOverrides {
permission_profile: Some(read_only_profile),
@@ -10915,10 +10914,7 @@ mod tests {
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
),
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
cwd,
ephemeral: false,
reasoning_effort: None,