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
parent fed0a8f4fa
commit 4d7ce3447d
62 changed files with 1601 additions and 671 deletions

View File

@@ -2880,7 +2880,11 @@ allow_local_binding = true
};
let mut builder = test_codex().with_home(home).with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config.permissions.sandbox_policy = Constrained::allow_any(SandboxPolicy::DangerFullAccess);
let cwd = config.cwd.clone();
config
.permissions
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path())
.expect("test setup should allow sandbox policy");
let layers = config
.config_layer_stack
.get_layers(

View File

@@ -8,8 +8,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::error::Result;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxType;
use codex_sandboxing::get_platform_sandbox;
@@ -52,12 +51,11 @@ where
};
let policy = SandboxPolicy::new_read_only_policy();
let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&policy);
process_exec_tool_call(
params,
&policy,
&FileSystemSandboxPolicy::from(&policy),
NetworkSandboxPolicy::from(&policy),
&permission_profile,
&cwd,
&None,
/*use_legacy_landlock*/ false,

View File

@@ -672,12 +672,24 @@ async fn steered_user_input_follows_compact_when_only_the_steer_needs_follow_up(
async fn steered_user_input_waits_when_tool_output_triggers_compact_before_next_request() {
let (gate_first_completed_tx, gate_first_completed_rx) = oneshot::channel();
let large_output_command = if cfg!(windows) {
"[Console]::Out.Write([string]::new([char]'0', 4000))"
} else {
"printf '%04000d' 0"
};
let large_output_args = json!({
"command": large_output_command,
"login": false,
"timeout_ms": 2000,
})
.to_string();
let first_chunks = vec![
chunk(ev_response_created("resp-1")),
chunk(ev_function_call(
"call-1",
"shell_command",
r#"{"command":"printf '%04000d' 0","login":false,"timeout_ms":2000}"#,
&large_output_args,
)),
gated_chunk(
gate_first_completed_rx,

View File

@@ -550,7 +550,10 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let mut builder = test_codex().with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy_for_config, config.cwd.as_path())
.expect("test sandbox policy should be allowed");
config.config_layer_stack = ConfigLayerStack::default();
});
let test = builder.build(&server).await?;