feat: introduce Permissions (#11633)

## Why
We currently carry multiple permission-related concepts directly on
`Config` for shell/unified-exec behavior (`approval_policy`,
`sandbox_policy`, `network`, `shell_environment_policy`,
`windows_sandbox_mode`).

Consolidating these into one in-memory struct makes permission handling
easier to reason about and sets up the next step: supporting named
permission profiles (`[permissions.PROFILE_NAME]`) without changing
behavior now.

This change is mostly mechanical: it updates existing callsites to go
through `config.permissions`, but it does not yet refactor those
callsites to take a single `Permissions` value in places where multiple
permission fields are still threaded separately.

This PR intentionally **does not** change the on-disk `config.toml`
format yet and keeps compatibility with legacy config keys.

## What Changed
- Introduced `Permissions` in `core/src/config/mod.rs`.
- Added `Config::permissions` and moved effective runtime permission
fields under it:
  - `approval_policy`
  - `sandbox_policy`
  - `network`
  - `shell_environment_policy`
  - `windows_sandbox_mode`
- Updated config loading/building so these effective values are still
derived from the same existing config inputs and constraints.
- Updated Windows sandbox helpers/resolution to read/write via
`permissions`.
- Threaded the new field through all permission consumers across core
runtime, app-server, CLI/exec, TUI, and sandbox summary code.
- Updated affected tests to reference `config.permissions.*`.
- Renamed the struct/field from
`EffectivePermissions`/`effective_permissions` to
`Permissions`/`permissions` and aligned variable naming accordingly.

## Verification
- `just fix -p codex-core -p codex-tui -p codex-cli -p codex-app-server
-p codex-exec -p codex-utils-sandbox-summary`
- `cargo build -p codex-core -p codex-tui -p codex-cli -p
codex-app-server -p codex-exec -p codex-utils-sandbox-summary`
This commit is contained in:
Michael Bolin
2026-02-12 14:42:54 -08:00
committed by GitHub
parent d7cb70ed26
commit a4cc1a4a85
30 changed files with 280 additions and 193 deletions

View File

@@ -4510,7 +4510,7 @@ async fn disabled_slash_command_while_task_running_snapshot() {
async fn approvals_popup_shows_disabled_presets() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.config.approval_policy =
chat.config.permissions.approval_policy =
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
AskForApproval::OnRequest => Ok(()),
_ => Err(invalid_value(
@@ -4546,7 +4546,7 @@ async fn approvals_popup_shows_disabled_presets() {
async fn approvals_popup_navigation_skips_disabled() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
chat.config.approval_policy =
chat.config.permissions.approval_policy =
Constrained::new(AskForApproval::OnRequest, |candidate| match candidate {
AskForApproval::OnRequest => Ok(()),
_ => Err(invalid_value(candidate.to_string(), "[on-request]")),
@@ -4623,7 +4623,10 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
// Build a chat widget with manual channels to avoid spawning the agent.
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
// Ensure policy allows surfacing approvals explicitly (not strictly required for direct event).
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
// Inject an exec approval request to display the approval modal.
let ev = ExecApprovalRequestEvent {
call_id: "call-approve-cmd".into(),
@@ -4678,7 +4681,10 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
#[tokio::test]
async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
let ev = ExecApprovalRequestEvent {
call_id: "call-approve-cmd-noreason".into(),
@@ -4720,7 +4726,10 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
-> anyhow::Result<()> {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
let script = "python - <<'PY'\nprint('hello')\nPY".to_string();
let command = vec!["bash".into(), "-lc".into(), script];
@@ -4760,7 +4769,10 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
#[tokio::test]
async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
// Build a small changeset and a reason/grant_root to exercise the prompt text.
let mut changes = HashMap::new();
@@ -5529,7 +5541,10 @@ async fn apply_patch_full_flow_integration_like() {
async fn apply_patch_untrusted_shows_approval_modal() -> anyhow::Result<()> {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
// Ensure approval policy is untrusted (OnRequest)
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
// Simulate a patch approval request from backend
let mut changes = HashMap::new();
@@ -5577,7 +5592,10 @@ async fn apply_patch_request_shows_diff_summary() -> anyhow::Result<()> {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
// Ensure we are in OnRequest so an approval is surfaced
chat.config.approval_policy.set(AskForApproval::OnRequest)?;
chat.config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)?;
// Simulate backend asking to apply a patch adding two lines to README.md
let mut changes = HashMap::new();