tui: use permission profiles for sandbox state (#20008)

## Summary
- Move TUI permission state from legacy `SandboxPolicy` values to
canonical `PermissionProfile` values across presets, app events, chat
widget state, app commands, thread routing, and cached thread session
state.
- Keep app-server compatibility boundaries explicit: embedded sessions
send `permissionProfile`, while remote sessions send only a legacy
`sandbox` projection and fall back to read-only when a custom profile
cannot be projected.
- Update status/add-dir UI summaries and snapshots to render the active
permission profile, including workspace profiles selected by the new
built-in defaults.

## Verification
- `rg '\bSandboxPolicy\b' codex-rs/tui -n` returns no matches.
- `cargo test -p codex-tui`
- `cargo check -p codex-tui --tests`
- `cargo test -p codex-tui additional_dirs`
- `just fmt`
- `just fix -p codex-tui`




































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20008).
* #20041
* #20040
* #20037
* #20035
* #20034
* #20033
* #20032
* #20030
* #20028
* #20027
* #20026
* #20024
* #20021
* #20018
* #20016
* #20015
* #20013
* #20011
* #20010
* __->__ #20008
This commit is contained in:
Michael Bolin
2026-04-28 13:36:48 -07:00
committed by GitHub
Unverified
parent 34d71d43eb
commit 3b74a4d3b1
32 changed files with 878 additions and 683 deletions
+83 -23
View File
@@ -1,23 +1,35 @@
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use std::path::PathBuf;
/// Returns a warning describing why `--add-dir` entries will be ignored for the
/// resolved sandbox policy. The caller is responsible for presenting the
/// resolved permission profile. The caller is responsible for presenting the
/// warning to the user (for example, printing to stderr).
pub fn add_dir_warning_message(
additional_dirs: &[PathBuf],
sandbox_policy: &SandboxPolicy,
permission_profile: &PermissionProfile,
cwd: &std::path::Path,
) -> Option<String> {
if additional_dirs.is_empty() {
return None;
}
match sandbox_policy {
SandboxPolicy::WorkspaceWrite { .. }
| SandboxPolicy::DangerFullAccess
| SandboxPolicy::ExternalSandbox { .. } => None,
SandboxPolicy::ReadOnly { .. } => Some(format_warning(additional_dirs)),
if matches!(
permission_profile,
PermissionProfile::Disabled | PermissionProfile::External { .. }
) {
return None;
}
let file_system_policy = permission_profile.file_system_sandbox_policy();
if file_system_policy.has_full_disk_write_access() {
return None;
}
if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
return None;
}
Some(format_warning(additional_dirs))
}
fn format_warning(additional_dirs: &[PathBuf]) -> String {
@@ -27,57 +39,105 @@ fn format_warning(additional_dirs: &[PathBuf]) -> String {
.collect::<Vec<_>>()
.join(", ");
format!(
"Ignoring --add-dir ({joined_paths}) because the effective sandbox mode is read-only. Switch to workspace-write or danger-full-access to allow additional writable roots."
"Ignoring --add-dir ({joined_paths}) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them."
)
}
#[cfg(test)]
mod tests {
use super::add_dir_warning_message;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn returns_none_for_workspace_write() {
let sandbox = SandboxPolicy::new_workspace_write_policy();
let profile = PermissionProfile::workspace_write();
let dirs = vec![PathBuf::from("/tmp/example")];
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
assert_eq!(
add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")),
None
);
}
#[test]
fn returns_none_for_danger_full_access() {
let sandbox = SandboxPolicy::DangerFullAccess;
let profile = PermissionProfile::Disabled;
let dirs = vec![PathBuf::from("/tmp/example")];
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
assert_eq!(
add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")),
None
);
}
#[test]
fn returns_none_for_external_sandbox() {
let sandbox = SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Enabled,
let profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Enabled,
};
let dirs = vec![PathBuf::from("/tmp/example")];
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
assert_eq!(
add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")),
None
);
}
#[test]
fn warns_for_read_only() {
let sandbox = SandboxPolicy::new_read_only_policy();
let profile = PermissionProfile::read_only();
let dirs = vec![PathBuf::from("relative"), PathBuf::from("/abs")];
let message = add_dir_warning_message(&dirs, &sandbox)
let message = add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project"))
.expect("expected warning for read-only sandbox");
assert_eq!(
message,
"Ignoring --add-dir (relative, /abs) because the effective sandbox mode is read-only. Switch to workspace-write or danger-full-access to allow additional writable roots."
"Ignoring --add-dir (relative, /abs) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them."
);
}
#[test]
fn warns_when_profile_can_write_elsewhere_but_not_cwd() {
let profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: "/tmp/writable".try_into().expect("absolute path"),
},
access: FileSystemAccessMode::Write,
},
],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let dirs = vec![PathBuf::from("/tmp/extra")];
assert_eq!(
add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")),
Some("Ignoring --add-dir (/tmp/extra) because the effective permissions do not allow additional writable roots. Switch to workspace-write or danger-full-access to allow them.".to_string())
);
}
#[test]
fn returns_none_when_no_additional_dirs() {
let sandbox = SandboxPolicy::new_read_only_policy();
let profile = PermissionProfile::read_only();
let dirs: Vec<PathBuf> = Vec::new();
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
assert_eq!(
add_dir_warning_message(&dirs, &profile, Path::new("/tmp/project")),
None
);
}
}
+27 -17
View File
@@ -131,11 +131,14 @@ use codex_protocol::approvals::ExecApprovalRequestEvent;
use codex_protocol::config_types::Personality;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::AskForApproval;
#[cfg(target_os = "windows")]
use codex_protocol::protocol::FileSystemSandboxKind;
use codex_protocol::protocol::FinalOutput;
use codex_protocol::protocol::GetHistoryEntryResponseEvent;
use codex_protocol::protocol::ListSkillsResponseEvent;
@@ -143,7 +146,6 @@ use codex_protocol::protocol::ListSkillsResponseEvent;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SkillErrorInfo;
use codex_protocol::protocol::TokenUsage;
@@ -308,7 +310,7 @@ fn default_exec_approval_decisions(
struct AutoReviewMode {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
}
/// Enabling the Auto-review experiment in the TUI should also switch the
@@ -319,9 +321,18 @@ fn auto_review_mode() -> AutoReviewMode {
AutoReviewMode {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: PermissionProfile::workspace_write(),
}
}
#[cfg(target_os = "windows")]
fn managed_filesystem_sandbox_is_restricted(permission_profile: &PermissionProfile) -> bool {
matches!(
permission_profile.file_system_sandbox_policy().kind,
FileSystemSandboxKind::Restricted
)
}
/// Baseline cadence for periodic stream commit animation ticks.
///
/// Smooth-mode streaming drains one line per tick, so this interval controls
@@ -508,7 +519,7 @@ pub(crate) struct App {
cli_kv_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
runtime_approval_policy_override: Option<AskForApproval>,
runtime_sandbox_policy_override: Option<SandboxPolicy>,
runtime_permission_profile_override: Option<PermissionProfile>,
pub(crate) file_search: FileSearchManager,
@@ -906,7 +917,7 @@ See the Codex keymap documentation for supported actions and examples."
cli_kv_overrides,
harness_overrides,
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
runtime_permission_profile_override: None,
file_search,
enhanced_keys_supported,
keymap: runtime_keymap,
@@ -947,20 +958,14 @@ See the Codex keymap documentation for supported actions and examples."
.await?;
}
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
// On startup, if a managed filesystem sandbox is active, warn about
// world-writable dirs on Windows.
#[cfg(target_os = "windows")]
{
let startup_sandbox_policy = app
.config
.permissions
.legacy_sandbox_policy(app.config.cwd.as_path());
let startup_permission_profile = app.config.permissions.permission_profile();
let should_check = WindowsSandboxLevel::from_config(&app.config)
!= WindowsSandboxLevel::Disabled
&& matches!(
&startup_sandbox_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::ReadOnly { .. }
)
&& managed_filesystem_sandbox_is_restricted(&startup_permission_profile)
&& !app
.config
.notices
@@ -971,8 +976,13 @@ See the Codex keymap documentation for supported actions and examples."
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let tx = app.app_event_tx.clone();
let logs_base_dir = app.config.codex_home.clone();
let sandbox_policy = startup_sandbox_policy;
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx);
Self::spawn_world_writable_scan(
cwd,
env_map,
logs_base_dir,
startup_permission_profile,
tx,
);
}
}
+23 -30
View File
@@ -72,14 +72,12 @@ impl App {
"Failed to carry forward approval policy override: {err}"
));
}
if let Some(policy) = self.runtime_sandbox_policy_override.as_ref()
&& let Err(err) = config
.permissions
.set_legacy_sandbox_policy(policy.clone(), config.cwd.as_path())
if let Some(profile) = self.runtime_permission_profile_override.as_ref()
&& let Err(err) = config.permissions.set_permission_profile(profile.clone())
{
tracing::warn!(%err, "failed to carry forward sandbox policy override");
tracing::warn!(%err, "failed to carry forward permission profile override");
self.chat_widget.add_error_message(format!(
"Failed to carry forward sandbox policy override: {err}"
"Failed to carry forward permission profile override: {err}"
));
}
}
@@ -106,16 +104,16 @@ impl App {
true
}
pub(super) fn try_set_sandbox_policy_on_config(
pub(super) fn try_set_permission_profile_on_config(
&mut self,
config: &mut Config,
policy: SandboxPolicy,
permission_profile: PermissionProfile,
user_message_prefix: &str,
log_message: &str,
) -> bool {
if let Err(err) = config
.permissions
.set_legacy_sandbox_policy(policy, config.cwd.as_path())
.set_permission_profile(permission_profile)
{
tracing::warn!(error = %err, "{log_message}");
self.chat_widget
@@ -149,7 +147,7 @@ impl App {
});
let mut approval_policy_override = None;
let mut approvals_reviewer_override = None;
let mut sandbox_policy_override = None;
let mut permission_profile_override = None;
let mut feature_updates_to_apply = Vec::with_capacity(updates.len());
// Auto-Review owns `approvals_reviewer`, but disabling the feature
// from inside a profile should not silently clear a value configured at
@@ -241,11 +239,11 @@ impl App {
) {
continue;
}
if !self.try_set_sandbox_policy_on_config(
if !self.try_set_permission_profile_on_config(
&mut feature_config,
auto_review_preset.sandbox_policy.clone(),
auto_review_preset.permission_profile.clone(),
"Failed to enable Auto-review",
"failed to set auto-review sandbox policy on staged config",
"failed to set auto-review permission profile on staged config",
) {
continue;
}
@@ -260,7 +258,7 @@ impl App {
},
]);
approval_policy_override = Some(auto_review_preset.approval_policy);
sandbox_policy_override = Some(auto_review_preset.sandbox_policy.clone());
permission_profile_override = Some(auto_review_preset.permission_profile.clone());
}
next_config = feature_config;
feature_updates_to_apply.push((feature, effective_enabled));
@@ -299,31 +297,26 @@ impl App {
self.chat_widget
.set_approval_policy(self.config.permissions.approval_policy.value());
}
if sandbox_policy_override.is_some()
&& let Err(err) = self.chat_widget.set_sandbox_policy(
self.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
)
if permission_profile_override.is_some()
&& let Err(err) = self
.chat_widget
.set_permission_profile(self.config.permissions.permission_profile())
{
tracing::error!(
error = %err,
"failed to set auto-review sandbox policy on chat config"
"failed to set auto-review permission profile on chat config"
);
self.chat_widget
.add_error_message(format!("Failed to enable Auto-review: {err}"));
}
if sandbox_policy_override.is_some() {
self.runtime_sandbox_policy_override = Some(
self.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
);
if permission_profile_override.is_some() {
self.runtime_permission_profile_override =
Some(self.config.permissions.permission_profile());
}
if approval_policy_override.is_some()
|| approvals_reviewer_override.is_some()
|| sandbox_policy_override.is_some()
|| permission_profile_override.is_some()
{
self.sync_active_thread_permission_settings_to_cached_session()
.await;
@@ -336,7 +329,7 @@ impl App {
/*cwd*/ None,
approval_policy_override,
approvals_reviewer_override,
sandbox_policy_override,
permission_profile_override,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
@@ -363,7 +356,7 @@ impl App {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
/*permission_profile*/ None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
/*model*/ None,
+52 -32
View File
@@ -703,7 +703,7 @@ impl App {
AppEvent::BeginWindowsSandboxElevatedSetup { preset } => {
#[cfg(target_os = "windows")]
{
let policy = preset.sandbox.clone();
let permission_profile = preset.permission_profile.clone();
let policy_cwd = self.config.cwd.clone();
let command_cwd = policy_cwd.clone();
let env_map: std::collections::HashMap<String, String> =
@@ -726,6 +726,18 @@ impl App {
self.chat_widget.show_windows_sandbox_setup_status();
self.windows_sandbox.setup_started_at = Some(Instant::now());
let session_telemetry = self.session_telemetry.clone();
let Ok(policy) = permission_profile
.to_legacy_sandbox_policy(policy_cwd.as_path())
.inspect_err(|err| {
tracing::error!(
%err,
"approval preset permissions cannot be projected for elevated Windows sandbox setup"
);
})
else {
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset });
return Ok(AppRunControl::Continue);
};
tokio::task::spawn_blocking(move || {
let result = crate::legacy_core::windows_sandbox::run_elevated_setup(
&policy,
@@ -789,7 +801,7 @@ impl App {
AppEvent::BeginWindowsSandboxLegacySetup { preset } => {
#[cfg(target_os = "windows")]
{
let policy = preset.sandbox.clone();
let permission_profile = preset.permission_profile.clone();
let policy_cwd = self.config.cwd.clone();
let command_cwd = policy_cwd.clone();
let env_map: std::collections::HashMap<String, String> =
@@ -799,6 +811,18 @@ impl App {
let session_telemetry = self.session_telemetry.clone();
self.chat_widget.show_windows_sandbox_setup_status();
let Ok(policy) = permission_profile
.to_legacy_sandbox_policy(policy_cwd.as_path())
.inspect_err(|err| {
tracing::error!(
%err,
"approval preset permissions cannot be projected for legacy Windows sandbox setup"
);
})
else {
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset });
return Ok(AppRunControl::Continue);
};
tokio::task::spawn_blocking(move || {
if let Err(err) =
crate::legacy_core::windows_sandbox::run_legacy_setup_preflight(
@@ -935,7 +959,7 @@ impl App {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
/*permission_profile*/ None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
/*model*/ None,
@@ -961,7 +985,7 @@ impl App {
/*cwd*/ None,
Some(preset.approval),
Some(self.config.approvals_reviewer),
Some(preset.sandbox.clone()),
Some(preset.permission_profile.clone()),
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
/*model*/ None,
@@ -975,8 +999,9 @@ impl App {
));
self.app_event_tx
.send(AppEvent::UpdateAskForApprovalPolicy(preset.approval));
self.app_event_tx
.send(AppEvent::UpdateSandboxPolicy(preset.sandbox.clone()));
self.app_event_tx.send(AppEvent::UpdatePermissionProfile(
preset.permission_profile.clone(),
));
let _ = mode;
self.chat_widget.add_plain_history_lines(vec![
Line::from(vec!["".dim(), "Sandbox ready".into()]),
@@ -1228,40 +1253,38 @@ impl App {
self.sync_active_thread_permission_settings_to_cached_session()
.await;
}
AppEvent::UpdateSandboxPolicy(policy) => {
AppEvent::UpdatePermissionProfile(permission_profile) => {
#[cfg(target_os = "windows")]
let policy_is_workspace_write_or_ro = matches!(
&policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::ReadOnly { .. }
);
let policy_for_chat = policy.clone();
let permission_profile_is_managed_restricted =
managed_filesystem_sandbox_is_restricted(&permission_profile);
let permission_profile_for_chat = permission_profile.clone();
let mut config = self.config.clone();
if !self.try_set_sandbox_policy_on_config(
if !self.try_set_permission_profile_on_config(
&mut config,
policy,
"Failed to set sandbox policy",
"failed to set sandbox policy on app config",
permission_profile,
"Failed to set permission profile",
"failed to set permission profile on app config",
) {
return Ok(AppRunControl::Continue);
}
self.config = config;
if let Err(err) = self.chat_widget.set_sandbox_policy(policy_for_chat) {
tracing::warn!(%err, "failed to set sandbox policy on chat config");
if let Err(err) = self
.chat_widget
.set_permission_profile(permission_profile_for_chat)
{
tracing::warn!(%err, "failed to set permission profile on chat config");
self.chat_widget
.add_error_message(format!("Failed to set sandbox policy: {err}"));
.add_error_message(format!("Failed to set permission profile: {err}"));
return Ok(AppRunControl::Continue);
}
self.runtime_sandbox_policy_override = Some(
self.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
);
self.runtime_permission_profile_override =
Some(self.config.permissions.permission_profile());
self.sync_active_thread_permission_settings_to_cached_session()
.await;
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
// If a managed filesystem sandbox is active, run the Windows
// world-writable scan.
#[cfg(target_os = "windows")]
{
// One-shot suppression if the user just confirmed continue.
@@ -1272,7 +1295,7 @@ impl App {
let should_check = WindowsSandboxLevel::from_config(&self.config)
!= WindowsSandboxLevel::Disabled
&& policy_is_workspace_write_or_ro
&& permission_profile_is_managed_restricted
&& !self.chat_widget.world_writable_warning_hidden();
if should_check {
let cwd = self.config.cwd.clone();
@@ -1280,15 +1303,12 @@ impl App {
std::env::vars().collect();
let tx = self.app_event_tx.clone();
let logs_base_dir = self.config.codex_home.clone();
let sandbox_policy = self
.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path());
let permission_profile = self.config.permissions.permission_profile();
Self::spawn_world_writable_scan(
cwd,
env_map,
logs_base_dir,
sandbox_policy,
permission_profile,
tx,
);
}
+17 -7
View File
@@ -18,9 +18,14 @@ impl App {
cwd: AbsolutePathBuf,
env_map: std::collections::HashMap<String, String>,
logs_base_dir: AbsolutePathBuf,
sandbox_policy: codex_protocol::protocol::SandboxPolicy,
permission_profile: PermissionProfile,
tx: AppEventSender,
) {
let Ok(sandbox_policy) = permission_profile.to_legacy_sandbox_policy(cwd.as_path()) else {
send_world_writable_scan_failed(&tx);
return;
};
tokio::task::spawn_blocking(move || {
let logs_base_dir_path = logs_base_dir.as_path();
let result = codex_windows_sandbox::apply_world_writable_scan_and_denies(
@@ -32,17 +37,22 @@ impl App {
);
if result.is_err() {
// Scan failed: warn without examples.
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
preset: None,
sample_paths: Vec::new(),
extra_count: 0usize,
failed_scan: true,
});
send_world_writable_scan_failed(&tx);
}
});
}
}
#[cfg(target_os = "windows")]
fn send_world_writable_scan_failed(tx: &AppEventSender) {
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
preset: None,
sample_paths: Vec::new(),
extra_count: 0usize,
failed_scan: true,
});
}
pub(super) fn side_return_shortcut_matches(key_event: KeyEvent) -> bool {
match key_event {
KeyEvent {
+1 -1
View File
@@ -24,7 +24,7 @@ pub(super) async fn make_test_app() -> App {
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
runtime_permission_profile_override: None,
file_search,
transcript_cells: Vec::new(),
overlay: None,
+28 -19
View File
@@ -79,7 +79,6 @@ use codex_protocol::protocol::NetworkApprovalContext;
use codex_protocol::protocol::NetworkApprovalProtocol;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnContextItem;
@@ -1638,8 +1637,11 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
auto_review.approval_policy
);
assert_eq!(
&app.chat_widget.config_ref().legacy_sandbox_policy(),
&auto_review.sandbox_policy
app.chat_widget
.config_ref()
.permissions
.permission_profile(),
auto_review.permission_profile
);
assert_eq!(
app.chat_widget.config_ref().approvals_reviewer,
@@ -1647,8 +1649,8 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
);
assert_eq!(app.runtime_approval_policy_override, None);
assert_eq!(
app.runtime_sandbox_policy_override,
Some(auto_review.sandbox_policy.clone())
app.runtime_permission_profile_override,
Some(auto_review.permission_profile.clone())
);
assert_eq!(
op_rx.try_recv(),
@@ -1656,8 +1658,8 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
cwd: None,
approval_policy: Some(auto_review.approval_policy),
approvals_reviewer: Some(auto_review.approvals_reviewer),
sandbox_policy: Some(auto_review.sandbox_policy.clone()),
permission_profile: None,
sandbox_policy: None,
permission_profile: Some(auto_review.permission_profile.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
@@ -1714,11 +1716,12 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor
.approval_policy
.set(AskForApproval::OnRequest)?;
app.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())?;
.permissions
.set_permission_profile(PermissionProfile::workspace_write())?;
app.chat_widget
.set_approval_policy(AskForApproval::OnRequest);
app.chat_widget
.set_sandbox_policy(SandboxPolicy::new_workspace_write_policy())?;
.set_permission_profile(PermissionProfile::workspace_write())?;
app.update_feature_flags(vec![(Feature::GuardianApproval, false)])
.await;
@@ -1813,8 +1816,11 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
auto_review.approval_policy
);
assert_eq!(
&app.chat_widget.config_ref().legacy_sandbox_policy(),
&auto_review.sandbox_policy
app.chat_widget
.config_ref()
.permissions
.permission_profile(),
auto_review.permission_profile
);
assert_eq!(
op_rx.try_recv(),
@@ -1822,8 +1828,8 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
cwd: None,
approval_policy: Some(auto_review.approval_policy),
approvals_reviewer: Some(auto_review.approvals_reviewer),
sandbox_policy: Some(auto_review.sandbox_policy.clone()),
permission_profile: None,
sandbox_policy: None,
permission_profile: Some(auto_review.permission_profile.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
@@ -1940,8 +1946,8 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev
cwd: None,
approval_policy: Some(auto_review.approval_policy),
approvals_reviewer: Some(auto_review.approvals_reviewer),
sandbox_policy: Some(auto_review.sandbox_policy.clone()),
permission_profile: None,
sandbox_policy: None,
permission_profile: Some(auto_review.permission_profile.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
@@ -2731,6 +2737,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
);
let rollout_path = temp_dir.path().join("agent-rollout.jsonl");
let permission_profile = PermissionProfile::workspace_write();
let turn_context = TurnContextItem {
turn_id: None,
trace_id: None,
@@ -2738,8 +2745,10 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
current_date: None,
timezone: None,
approval_policy: primary_session.approval_policy,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: None,
sandbox_policy: permission_profile
.to_legacy_sandbox_policy(test_path_buf("/tmp/agent").as_path())
.expect("workspace profile must be legacy-compatible"),
permission_profile: Some(permission_profile),
network: None,
file_system_sandbox_policy: None,
model: "gpt-agent".to_string(),
@@ -3687,7 +3696,7 @@ async fn make_test_app() -> App {
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
runtime_permission_profile_override: None,
file_search,
transcript_cells: Vec::new(),
overlay: None,
@@ -3747,7 +3756,7 @@ async fn make_test_app_with_channels() -> (
cli_kv_overrides: Vec::new(),
harness_overrides: ConfigOverrides::default(),
runtime_approval_policy_override: None,
runtime_sandbox_policy_override: None,
runtime_permission_profile_override: None,
file_search,
transcript_cells: Vec::new(),
overlay: None,
-2
View File
@@ -529,7 +529,6 @@ impl App {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
model,
effort,
@@ -613,7 +612,6 @@ impl App {
approval_policy,
approvals_reviewer
.unwrap_or(self.chat_widget.config_ref().approvals_reviewer),
sandbox_policy.clone(),
permission_profile.clone(),
model.to_string(),
effort,
+6 -15
View File
@@ -110,7 +110,6 @@ mod tests {
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
@@ -174,23 +173,15 @@ mod tests {
app.config.permissions.approval_policy =
codex_config::Constrained::allow_any(AskForApproval::OnRequest);
app.config.approvals_reviewer = ApprovalsReviewer::AutoReview;
let expected_sandbox_policy = SandboxPolicy::new_workspace_write_policy();
let expected_file_system_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&expected_sandbox_policy,
&main_session.cwd,
);
let expected_permission_profile = PermissionProfile::from_runtime_permissions(
&expected_file_system_policy,
NetworkSandboxPolicy::from(&expected_sandbox_policy),
);
let expected_permission_profile = PermissionProfile::workspace_write();
app.chat_widget.handle_thread_session(main_session.clone());
app.chat_widget
.set_sandbox_policy(expected_sandbox_policy.clone())
.expect("set widget sandbox policy");
.set_permission_profile(expected_permission_profile.clone())
.expect("set widget permission profile");
app.config
.set_legacy_sandbox_policy(expected_sandbox_policy.clone())
.expect("set sandbox policy");
.permissions
.set_permission_profile(expected_permission_profile.clone())
.expect("set permission profile");
app.sync_active_thread_permission_settings_to_cached_session()
.await;
+28 -16
View File
@@ -17,7 +17,6 @@ use codex_protocol::protocol::ConversationTextParams;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
@@ -44,8 +43,7 @@ pub(crate) enum AppCommandView<'a> {
cwd: &'a PathBuf,
approval_policy: AskForApproval,
approvals_reviewer: &'a Option<ApprovalsReviewer>,
sandbox_policy: &'a SandboxPolicy,
permission_profile: &'a Option<PermissionProfile>,
permission_profile: &'a PermissionProfile,
model: &'a str,
effort: Option<ReasoningEffortConfig>,
summary: &'a Option<ReasoningSummaryConfig>,
@@ -58,7 +56,7 @@ pub(crate) enum AppCommandView<'a> {
cwd: &'a Option<PathBuf>,
approval_policy: &'a Option<AskForApproval>,
approvals_reviewer: &'a Option<ApprovalsReviewer>,
sandbox_policy: &'a Option<SandboxPolicy>,
permission_profile: &'a Option<PermissionProfile>,
windows_sandbox_level: &'a Option<WindowsSandboxLevel>,
model: &'a Option<String>,
effort: &'a Option<Option<ReasoningEffortConfig>>,
@@ -141,8 +139,7 @@ impl AppCommand {
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
permission_profile: PermissionProfile,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
@@ -151,6 +148,19 @@ impl AppCommand {
collaboration_mode: Option<CollaborationMode>,
personality: Option<Personality>,
) -> Self {
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(cwd.as_path())
.unwrap_or_else(|err| {
tracing::warn!(
%err,
"permission profile cannot be projected to legacy UserTurn sandbox; using read-only compatibility fallback"
);
PermissionProfile::read_only()
.to_legacy_sandbox_policy(cwd.as_path())
.unwrap_or_else(|err| {
unreachable!("read-only permissions must be legacy-compatible: {err}")
})
});
Self(Op::UserTurn {
items,
environments: None,
@@ -158,7 +168,7 @@ impl AppCommand {
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
permission_profile: Some(permission_profile),
model,
effort,
summary,
@@ -174,7 +184,7 @@ impl AppCommand {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
sandbox_policy: Option<SandboxPolicy>,
permission_profile: Option<PermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
@@ -187,8 +197,8 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile: None,
sandbox_policy: None,
permission_profile,
windows_sandbox_level,
model,
effort,
@@ -294,7 +304,7 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
sandbox_policy: _,
permission_profile,
model,
effort,
@@ -309,8 +319,10 @@ impl AppCommand {
cwd,
approval_policy: *approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
permission_profile: match permission_profile.as_ref() {
Some(permission_profile) => permission_profile,
None => unreachable!("AppCommand::user_turn always sets permission_profile"),
},
model,
effort: *effort,
summary,
@@ -323,8 +335,8 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile: _,
sandbox_policy: _,
permission_profile,
windows_sandbox_level,
model,
effort,
@@ -336,7 +348,7 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
windows_sandbox_level,
model,
effort,
+3 -3
View File
@@ -41,9 +41,9 @@ use codex_plugin::PluginCapabilitySummary;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_realtime_webrtc::RealtimeWebrtcEvent;
use codex_realtime_webrtc::RealtimeWebrtcSessionHandle;
@@ -581,8 +581,8 @@ pub(crate) enum AppEvent {
/// Update the current approval policy in the running app and widget.
UpdateAskForApprovalPolicy(AskForApproval),
/// Update the current sandbox policy in the running app and widget.
UpdateSandboxPolicy(SandboxPolicy),
/// Update the current permission profile in the running app and widget.
UpdatePermissionProfile(PermissionProfile),
/// Update the current approvals reviewer in the running app and widget.
UpdateApprovalsReviewer(ApprovalsReviewer),
+99 -133
View File
@@ -109,7 +109,6 @@ use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget as CoreReviewTarget;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionNetworkProxyRuntime;
use codex_utils_absolute_path::AbsolutePathBuf;
use color_eyre::eyre::ContextCompat;
@@ -532,8 +531,7 @@ impl AppServerSession {
cwd: PathBuf,
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
permission_profile: PermissionProfile,
model: String,
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
summary: Option<codex_protocol::config_types::ReasoningSummary>,
@@ -543,11 +541,33 @@ impl AppServerSession {
output_schema: Option<serde_json::Value>,
) -> Result<TurnStartResponse> {
let request_id = self.next_request_id();
let (sandbox_policy, permission_profile) = turn_start_permission_overrides(
self.thread_params_mode(),
sandbox_policy,
permission_profile,
);
let sandbox_policy = if matches!(self.thread_params_mode(), ThreadParamsMode::Remote) {
let policy =
permission_profile
.to_legacy_sandbox_policy(cwd.as_path())
.unwrap_or_else(|err| {
tracing::warn!(
%err,
"permission profile cannot be projected for remote turn/start; using read-only compatibility fallback"
);
PermissionProfile::read_only()
.to_legacy_sandbox_policy(cwd.as_path())
.unwrap_or_else(|err| {
unreachable!(
"read-only permissions must be legacy-compatible: {err}"
)
})
});
Some(policy.into())
} else {
None
};
let permission_profile = if matches!(self.thread_params_mode(), ThreadParamsMode::Embedded)
{
Some(permission_profile.into())
} else {
None
};
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
@@ -1088,35 +1108,28 @@ fn config_request_overrides_from_config(
})
}
fn sandbox_mode_from_policy(
policy: SandboxPolicy,
fn sandbox_mode_from_permission_profile(
permission_profile: &PermissionProfile,
cwd: &std::path::Path,
) -> Option<codex_app_server_protocol::SandboxMode> {
match policy {
SandboxPolicy::DangerFullAccess => {
match permission_profile {
PermissionProfile::Disabled => {
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
}
SandboxPolicy::ReadOnly { .. } => Some(codex_app_server_protocol::SandboxMode::ReadOnly),
SandboxPolicy::WorkspaceWrite { .. } => {
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
PermissionProfile::External { .. } => None,
PermissionProfile::Managed { .. } => {
let file_system_policy = permission_profile.file_system_sandbox_policy();
if file_system_policy.has_full_disk_write_access() {
permission_profile
.network_sandbox_policy()
.is_enabled()
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
} else if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
} else {
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
}
}
SandboxPolicy::ExternalSandbox { .. } => None,
}
}
fn turn_start_permission_overrides(
mode: ThreadParamsMode,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
) -> (
Option<codex_app_server_protocol::SandboxPolicy>,
Option<codex_app_server_protocol::PermissionProfile>,
) {
match (mode, permission_profile) {
(ThreadParamsMode::Embedded, Some(permission_profile)) => {
(None, Some(permission_profile.into()))
}
(ThreadParamsMode::Embedded, None) => (None, None),
(ThreadParamsMode::Remote, _) => (Some(sandbox_policy.into()), None),
}
}
@@ -1141,10 +1154,9 @@ fn thread_start_params_from_config(
let sandbox = permission_profile
.is_none()
.then(|| {
sandbox_mode_from_policy(
config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
)
})
.flatten();
@@ -1174,10 +1186,9 @@ fn thread_resume_params_from_config(
let sandbox = permission_profile
.is_none()
.then(|| {
sandbox_mode_from_policy(
config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
)
})
.flatten();
@@ -1206,10 +1217,9 @@ fn thread_fork_params_from_config(
let sandbox = permission_profile
.is_none()
.then(|| {
sandbox_mode_from_policy(
config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
)
})
.flatten();
@@ -1297,8 +1307,16 @@ async fn thread_session_state_from_thread_start_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1321,8 +1339,16 @@ async fn thread_session_state_from_thread_resume_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1345,8 +1371,16 @@ async fn thread_session_state_from_thread_fork_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1388,8 +1422,7 @@ async fn thread_session_state_from_thread_response(
service_tier: Option<codex_protocol::config_types::ServiceTier>,
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
permission_profile: PermissionProfile,
cwd: AbsolutePathBuf,
instruction_source_paths: Vec<AbsolutePathBuf>,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -1404,9 +1437,6 @@ async fn thread_session_state_from_thread_response(
.map_err(|err| format!("forked_from_id is invalid: {err}"))?;
let (history_log_id, history_entry_count) = message_history_metadata(config).await;
let history_entry_count = u64::try_from(history_entry_count).unwrap_or(u64::MAX);
let permission_profile =
permission_profile_from_response_permissions(&sandbox_policy, permission_profile, &cwd);
Ok(ThreadSessionState {
thread_id,
forked_from_id,
@@ -1428,16 +1458,6 @@ async fn thread_session_state_from_thread_response(
})
}
fn permission_profile_from_response_permissions(
sandbox_policy: &SandboxPolicy,
permission_profile: Option<PermissionProfile>,
cwd: &AbsolutePathBuf,
) -> PermissionProfile {
permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.as_path())
})
}
pub(crate) fn app_server_rate_limit_snapshots_to_core(
response: GetAccountRateLimitsResponse,
) -> Vec<RateLimitSnapshot> {
@@ -1548,10 +1568,9 @@ mod tests {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let thread_id = ThreadId::new();
let expected_sandbox = sandbox_mode_from_policy(
config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
let expected_sandbox = sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
);
let start = thread_start_params_from_config(
@@ -1593,10 +1612,9 @@ mod tests {
let config = build_config(&temp_dir).await;
let thread_id = ThreadId::new();
let remote_cwd = PathBuf::from("repo/on/server");
let expected_sandbox = sandbox_mode_from_policy(
config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
let expected_sandbox = sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
);
let start = thread_start_params_from_config(
@@ -1632,55 +1650,6 @@ mod tests {
assert_eq!(fork.permission_profile, None);
}
#[test]
fn turn_start_permission_overrides_send_profiles_only_for_embedded_runtime_overrides() {
let workspace_write = SandboxPolicy::new_workspace_write_policy();
let workspace_write_profile =
PermissionProfile::from_legacy_sandbox_policy(&workspace_write);
let (sandbox, profile) = turn_start_permission_overrides(
ThreadParamsMode::Embedded,
workspace_write.clone(),
Some(workspace_write_profile.clone()),
);
assert_eq!(sandbox, None);
assert_eq!(profile, Some(workspace_write_profile.into()));
let (sandbox, profile) = turn_start_permission_overrides(
ThreadParamsMode::Embedded,
workspace_write.clone(),
/*permission_profile*/ None,
);
assert_eq!(sandbox, None);
assert_eq!(profile, None);
let (sandbox, profile) = turn_start_permission_overrides(
ThreadParamsMode::Remote,
workspace_write.clone(),
Some(PermissionProfile::from_legacy_sandbox_policy(
&workspace_write,
)),
);
assert_eq!(sandbox, Some(workspace_write.into()));
assert_eq!(profile, None);
let external_sandbox = SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
};
let (sandbox, profile) = turn_start_permission_overrides(
ThreadParamsMode::Embedded,
external_sandbox.clone(),
Some(PermissionProfile::from_legacy_sandbox_policy(
&external_sandbox,
)),
);
assert_eq!(sandbox, None);
assert_eq!(
profile,
Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox).into())
);
}
#[tokio::test]
async fn thread_fork_params_forward_instruction_overrides() {
let temp_dir = tempfile::tempdir().expect("tempdir");
@@ -1709,6 +1678,7 @@ mod tests {
let config = build_config(&temp_dir).await;
let thread_id = ThreadId::new();
let forked_from_id = ThreadId::new();
let read_only_profile = PermissionProfile::read_only();
let response = ThreadResumeResponse {
thread: codex_app_server_protocol::Thread {
id: thread_id.to_string(),
@@ -1758,13 +1728,11 @@ mod tests {
instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()],
approval_policy: codex_protocol::protocol::AskForApproval::Never.into(),
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
sandbox: codex_protocol::protocol::SandboxPolicy::new_read_only_policy().into(),
permission_profile: Some(
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
)
sandbox: read_only_profile
.to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path())
.expect("read-only profile must be legacy-compatible")
.into(),
),
permission_profile: Some(read_only_profile.into()),
reasoning_effort: None,
};
@@ -1811,8 +1779,7 @@ mod tests {
/*service_tier*/ None,
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
SandboxPolicy::new_read_only_policy(),
Some(PermissionProfile::read_only()),
PermissionProfile::read_only(),
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,
@@ -1842,8 +1809,7 @@ mod tests {
/*service_tier*/ None,
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
SandboxPolicy::new_read_only_policy(),
Some(PermissionProfile::read_only()),
PermissionProfile::read_only(),
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,
+91 -84
View File
@@ -422,13 +422,13 @@ use crate::streaming::controller::StreamController;
use chrono::Local;
use codex_file_search::FileMatch;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::plan_tool::StepStatus;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_approval_presets::ApprovalPreset;
use codex_utils_approval_presets::builtin_approval_presets;
use strum::IntoEnumIterator;
@@ -6480,14 +6480,11 @@ impl ChatWidget {
None if self.config.notices.fast_default_opt_out == Some(true) => Some(None),
None => None,
};
let permission_profile = Some(self.config.permissions.permission_profile());
let permission_profile = self.config.permissions.permission_profile();
let op = AppCommand::user_turn(
items,
self.config.cwd.to_path_buf(),
self.config.permissions.approval_policy.value(),
self.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
permission_profile,
effective_mode.model().to_string(),
effective_mode.reasoning_effort(),
@@ -8567,7 +8564,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
/*permission_profile*/ None,
/*windows_sandbox_level*/ None,
Some(switch_model_for_events.clone()),
Some(Some(default_effort)),
@@ -8791,7 +8788,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
/*permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
@@ -9540,14 +9537,11 @@ impl ChatWidget {
self.open_permissions_popup();
}
/// Open a popup to choose the permissions mode (approval policy + sandbox policy).
/// Open a popup to choose the permissions mode.
pub(crate) fn open_permissions_popup(&mut self) {
let include_read_only = cfg!(target_os = "windows");
let current_approval = self.config.permissions.approval_policy.value();
let current_sandbox = self
.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path());
let current_permission_profile = self.config.permissions.permission_profile();
let guardian_approval_enabled = self.config.features.enabled(Feature::GuardianApproval);
let current_review_policy = self.config.approvals_reviewer;
let mut items: Vec<SelectionItem> = Vec::new();
@@ -9653,7 +9647,7 @@ impl ChatWidget {
} else {
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -9663,7 +9657,7 @@ impl ChatWidget {
{
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -9671,7 +9665,7 @@ impl ChatWidget {
} else {
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -9683,7 +9677,8 @@ impl ChatWidget {
is_current: current_review_policy == ApprovalsReviewer::User
&& Self::preset_matches_current(
current_approval,
&current_sandbox,
&current_permission_profile,
self.config.cwd.as_path(),
&preset,
),
actions: default_actions,
@@ -9701,13 +9696,14 @@ impl ChatWidget {
),
is_current: current_review_policy == ApprovalsReviewer::AutoReview
&& Self::preset_matches_current(
current_approval,
&current_sandbox,
&preset,
),
current_approval,
&current_permission_profile,
self.config.cwd.as_path(),
&preset,
),
actions: Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.permission_profile.clone(),
"Auto-review".to_string(),
ApprovalsReviewer::AutoReview,
),
@@ -9723,7 +9719,8 @@ impl ChatWidget {
description: base_description,
is_current: Self::preset_matches_current(
current_approval,
&current_sandbox,
&current_permission_profile,
self.config.cwd.as_path(),
&preset,
),
actions: default_actions,
@@ -9848,18 +9845,18 @@ impl ChatWidget {
fn approval_preset_actions(
approval: AskForApproval,
sandbox: SandboxPolicy,
permission_profile: PermissionProfile,
label: String,
approvals_reviewer: ApprovalsReviewer,
) -> Vec<SelectionAction> {
vec![Box::new(move |tx| {
let sandbox_clone = sandbox.clone();
let permission_profile_clone = permission_profile.clone();
tx.send(AppEvent::CodexOp(
AppCommand::override_turn_context(
/*cwd*/ None,
Some(approval),
Some(approvals_reviewer),
Some(sandbox_clone.clone()),
Some(permission_profile_clone.clone()),
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
@@ -9871,7 +9868,7 @@ impl ChatWidget {
.into_core(),
));
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone));
tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer));
tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_info_event(
@@ -9884,36 +9881,39 @@ impl ChatWidget {
fn preset_matches_current(
current_approval: AskForApproval,
current_sandbox: &SandboxPolicy,
current_permission_profile: &PermissionProfile,
cwd: &std::path::Path,
preset: &ApprovalPreset,
) -> bool {
if current_approval != preset.approval {
return false;
}
match (current_sandbox, &preset.sandbox) {
(SandboxPolicy::DangerFullAccess, SandboxPolicy::DangerFullAccess) => true,
(
SandboxPolicy::ReadOnly {
network_access: current_network_access,
..
},
SandboxPolicy::ReadOnly {
network_access: preset_network_access,
..
},
) => current_network_access == preset_network_access,
(
SandboxPolicy::WorkspaceWrite {
network_access: current_network_access,
..
},
SandboxPolicy::WorkspaceWrite {
network_access: preset_network_access,
..
},
) => current_network_access == preset_network_access,
_ => false,
match preset.id {
"full-access" => matches!(current_permission_profile, PermissionProfile::Disabled),
"read-only" => {
let file_system_policy = current_permission_profile.file_system_sandbox_policy();
matches!(
current_permission_profile,
PermissionProfile::Managed { .. }
) && !file_system_policy.has_full_disk_write_access()
&& file_system_policy
.get_writable_roots_with_cwd(cwd)
.is_empty()
&& current_permission_profile.network_sandbox_policy()
== preset.permission_profile.network_sandbox_policy()
}
"auto" => {
let file_system_policy = current_permission_profile.file_system_sandbox_policy();
matches!(
current_permission_profile,
PermissionProfile::Managed { .. }
) && file_system_policy.can_write_path_with_cwd(cwd, cwd)
&& !file_system_policy.has_full_disk_write_access()
&& current_permission_profile.network_sandbox_policy()
== preset.permission_profile.network_sandbox_policy()
}
_ => current_permission_profile == &preset.permission_profile,
}
}
@@ -9929,14 +9929,19 @@ impl ChatWidget {
}
let cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let Ok(policy) = self
.config
.permissions
.permission_profile()
.to_legacy_sandbox_policy(self.config.cwd.as_path())
else {
return Some((Vec::new(), 0, true));
};
match codex_windows_sandbox::apply_world_writable_scan_and_denies(
self.config.codex_home.as_path(),
cwd.as_path(),
&env_map,
&self
.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
&policy,
Some(self.config.codex_home.as_path()),
) {
Ok(_) => None,
@@ -9957,7 +9962,7 @@ impl ChatWidget {
) {
let selected_name = preset.label.to_string();
let approval = preset.approval;
let sandbox = preset.sandbox;
let permission_profile = preset.permission_profile;
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
let title_line = Line::from("Enable full access?").bold();
let info_line = Line::from(vec![
@@ -9974,7 +9979,7 @@ impl ChatWidget {
let mut accept_actions = Self::approval_preset_actions(
approval,
sandbox.clone(),
permission_profile.clone(),
selected_name.clone(),
ApprovalsReviewer::User,
);
@@ -9984,7 +9989,7 @@ impl ChatWidget {
let mut accept_and_remember_actions = Self::approval_preset_actions(
approval,
sandbox,
permission_profile,
selected_name,
ApprovalsReviewer::User,
);
@@ -10041,27 +10046,27 @@ impl ChatWidget {
extra_count: usize,
failed_scan: bool,
) {
let (approval, sandbox) = match &preset {
Some(p) => (Some(p.approval), Some(p.sandbox.clone())),
let (approval, permission_profile) = match &preset {
Some(p) => (Some(p.approval), Some(p.permission_profile.clone())),
None => (None, None),
};
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
let describe_policy = |policy: &SandboxPolicy| match policy {
SandboxPolicy::WorkspaceWrite { .. } => "Agent mode",
SandboxPolicy::ReadOnly { .. } => "Read-Only mode",
_ => "Agent mode",
let describe_profile = |profile: &PermissionProfile| {
if matches!(profile, PermissionProfile::Disabled) {
"Full Access mode"
} else if profile
.file_system_sandbox_policy()
.can_write_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path())
{
"Agent mode"
} else {
"Read-Only mode"
}
};
let mode_label = preset
.as_ref()
.map(|p| describe_policy(&p.sandbox))
.unwrap_or_else(|| {
describe_policy(
&self
.config
.permissions
.legacy_sandbox_policy(self.config.cwd.as_path()),
)
});
.map(|p| describe_profile(&p.permission_profile))
.unwrap_or_else(|| describe_profile(&self.config.permissions.permission_profile()));
let info_line = if failed_scan {
Line::from(vec![
"We couldn't complete the world-writable scan, so protections cannot be verified. "
@@ -10093,8 +10098,9 @@ impl ChatWidget {
}
let header = ColumnRenderable::with(header_children);
// Build actions ensuring acknowledgement happens before applying the new sandbox policy,
// so downstream policy-change hooks don't re-trigger the warning.
// Build actions ensuring acknowledgement happens before applying the
// new permission profile, so downstream policy-change hooks don't
// re-trigger the warning.
let mut accept_actions: Vec<SelectionAction> = Vec::new();
// Suppress the immediate re-scan only when a preset will be applied (i.e., via /approvals or
// /permissions), to avoid duplicate warnings from the ensuing policy change.
@@ -10103,10 +10109,10 @@ impl ChatWidget {
tx.send(AppEvent::SkipNextWorldWritableScan);
}));
}
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
if let (Some(approval), Some(permission_profile)) = (approval, permission_profile.clone()) {
accept_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
permission_profile,
mode_label.to_string(),
ApprovalsReviewer::User,
));
@@ -10117,10 +10123,10 @@ impl ChatWidget {
tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
if let (Some(approval), Some(permission_profile)) = (approval, permission_profile) {
accept_and_remember_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
permission_profile,
mode_label.to_string(),
ApprovalsReviewer::User,
));
@@ -10439,12 +10445,13 @@ impl ChatWidget {
}
}
/// Set the sandbox policy in the widget's config copy.
/// Set the permission profile in the widget's config copy.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> {
self.config
.permissions
.set_legacy_sandbox_policy(policy, self.config.cwd.as_path())
pub(crate) fn set_permission_profile(
&mut self,
profile: PermissionProfile,
) -> ConstraintResult<()> {
self.config.permissions.set_permission_profile(profile)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
@@ -10701,7 +10708,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
/*permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
@@ -5,7 +5,6 @@ use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSandboxKind;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
@@ -249,8 +248,9 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
chat.config.cwd = test_path_buf("/home/user/main").abs();
let expected_cwd = test_path_buf("/home/user/sub-agent").abs();
@@ -314,23 +314,13 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
);
assert_eq!(&chat.config_ref().cwd, &expected_cwd);
let updated_sandbox = SandboxPolicy::new_workspace_write_policy();
chat.set_sandbox_policy(updated_sandbox.clone())
.expect("set sandbox policy");
let updated_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&updated_sandbox,
&expected_cwd,
);
let updated_profile = PermissionProfile::workspace_write();
chat.set_permission_profile(updated_profile.clone())
.expect("set permission profile");
assert_eq!(
chat.config_ref().permissions.permission_profile(),
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&updated_sandbox
),
&updated_file_system_policy,
NetworkSandboxPolicy::from(&updated_sandbox),
),
"local sandbox changes should replace SessionConfigured profile-derived runtime permissions using the widget cwd"
updated_profile,
"local permission changes should replace SessionConfigured profile-derived runtime permissions"
);
}
@@ -338,9 +328,12 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
let expected_sandbox = SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Restricted,
let expected_permission_profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
};
let expected_sandbox = expected_permission_profile
.to_legacy_sandbox_policy(test_path_buf("/home/user/external").as_path())
.expect("external profile should project to legacy sandbox policy");
let configured = codex_protocol::protocol::SessionConfiguredEvent {
session_id: ThreadId::new(),
forked_from_id: None,
@@ -350,9 +343,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
permission_profile: expected_permission_profile,
cwd: test_path_buf("/home/user/external").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1,4 +1,10 @@
use super::*;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
#[tokio::test]
@@ -51,24 +57,73 @@ async fn preset_matching_accepts_workspace_write_with_extra_roots() {
.into_iter()
.find(|p| p.id == "auto")
.expect("auto preset exists");
let extra_root = test_path_buf("/tmp/extra").abs();
let current_sandbox = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extra_root],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let current_profile = PermissionProfile::workspace_write_with(
&[test_path_buf("/tmp/extra").abs()],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
let cwd = test_path_buf("/tmp/project").abs();
assert!(
ChatWidget::preset_matches_current(AskForApproval::OnRequest, &current_sandbox, &preset),
ChatWidget::preset_matches_current(
AskForApproval::OnRequest,
&current_profile,
cwd.as_path(),
&preset
),
"WorkspaceWrite with extra roots should still match the Default preset"
);
assert!(
!ChatWidget::preset_matches_current(AskForApproval::Never, &current_sandbox, &preset),
!ChatWidget::preset_matches_current(
AskForApproval::Never,
&current_profile,
cwd.as_path(),
&preset
),
"approval mismatch should prevent matching the preset"
);
}
#[tokio::test]
async fn preset_matching_does_not_treat_non_cwd_writable_profile_as_read_only() {
let preset = builtin_approval_presets()
.into_iter()
.find(|p| p.id == "read-only")
.expect("read-only preset exists");
let current_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: test_path_buf("/tmp/writable").abs(),
},
access: FileSystemAccessMode::Write,
},
],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let cwd = test_path_buf("/tmp/project").abs();
assert!(
!ChatWidget::preset_matches_current(
AskForApproval::OnRequest,
&current_profile,
cwd.as_path(),
&preset
),
"profiles with any writable root should not be classified as Read Only"
);
}
#[tokio::test]
async fn full_access_confirmation_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -348,8 +403,9 @@ async fn permissions_selection_history_snapshot_full_access_to_default() {
.set(AskForApproval::Never)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess)
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("set permission profile");
chat.open_permissions_popup();
let popup = render_bottom_popup(&chat, /*width*/ 120);
@@ -389,8 +445,9 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() {
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
@@ -446,8 +503,9 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_a
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
chat.open_permissions_popup();
let popup = render_bottom_popup(&chat, /*width*/ 120);
@@ -521,7 +579,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
let cwd = test_project_path().abs();
let permission_profile = PermissionProfile::workspace_write_with(
&[extra_root],
codex_protocol::protocol::NetworkSandboxPolicy::Restricted,
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
@@ -573,8 +631,9 @@ async fn permissions_selection_can_disable_auto_review() {
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::from(KeyCode::Up));
@@ -612,8 +671,9 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
.set(AskForApproval::OnRequest)
.expect("set approval policy");
chat.config
.set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
chat.set_approvals_reviewer(ApprovalsReviewer::User);
chat.open_permissions_popup();
@@ -648,8 +708,8 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
cwd: None,
approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
sandbox_policy: Some(SandboxPolicy::new_workspace_write_policy()),
permission_profile: None,
sandbox_policy: None,
permission_profile: Some(PermissionProfile::workspace_write()),
windows_sandbox_level: None,
model: None,
effort: None,
+8 -7
View File
@@ -882,9 +882,8 @@ pub async fn run_main(
if let Some(warning) = add_dir_warning_message(
&cli.add_dir,
&config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
&config.permissions.permission_profile(),
config.cwd.as_path(),
) {
#[allow(clippy::print_stderr)]
{
@@ -2211,6 +2210,10 @@ mod tests {
.model
.clone()
.unwrap_or_else(|| "gpt-5.1".to_string());
let permission_profile = config.permissions.permission_profile();
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(config.cwd.as_path())
.expect("configured permissions must have a legacy compatibility projection");
TurnContextItem {
turn_id: None,
trace_id: None,
@@ -2218,10 +2221,8 @@ mod tests {
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
permission_profile: None,
sandbox_policy,
permission_profile: Some(permission_profile),
network: None,
file_system_sandbox_policy: None,
model,
+24 -28
View File
@@ -9,16 +9,16 @@ use chrono::Local;
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use codex_utils_sandbox_summary::summarize_sandbox_policy;
use codex_utils_sandbox_summary::summarize_permission_profile;
use ratatui::prelude::*;
use ratatui::style::Stylize;
use std::collections::BTreeSet;
use std::path::Path;
use std::path::PathBuf;
use url::Url;
@@ -254,10 +254,9 @@ impl StatusHistoryCell {
),
(
"sandbox",
summarize_sandbox_policy(
&config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
summarize_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
),
),
];
@@ -281,31 +280,14 @@ impl StatusHistoryCell {
.find(|(k, _)| *k == "approval")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let sandbox_policy = config
.permissions
.legacy_sandbox_policy(config.cwd.as_path());
let sandbox = match &sandbox_policy {
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
SandboxPolicy::ReadOnly { .. } => "read-only".to_string(),
SandboxPolicy::WorkspaceWrite {
network_access: true,
..
} => "workspace-write with network access".to_string(),
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
SandboxPolicy::ExternalSandbox { network_access } => {
if matches!(network_access, NetworkAccess::Enabled) {
"external-sandbox (network access enabled)".to_string()
} else {
"external-sandbox".to_string()
}
}
};
let permission_profile = config.permissions.permission_profile();
let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path());
let permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest
&& sandbox_policy == SandboxPolicy::new_workspace_write_policy()
&& permission_profile == PermissionProfile::workspace_write()
{
"Default".to_string()
} else if config.permissions.approval_policy.value() == AskForApproval::Never
&& sandbox_policy == SandboxPolicy::DangerFullAccess
&& permission_profile == PermissionProfile::Disabled
{
"Full Access".to_string()
} else {
@@ -553,6 +535,20 @@ impl StatusHistoryCell {
}
}
fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path) -> String {
let summary = summarize_permission_profile(permission_profile, cwd);
if let Some(details) = summary.strip_prefix("workspace-write") {
if details.contains("(network access enabled)") {
return "workspace-write with network access".to_string();
}
return "workspace-write".to_string();
}
if summary == "custom permissions (network access enabled)" {
return "custom permissions with network access".to_string();
}
summary
}
impl HistoryCell for StatusHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 1.05K total (700 input + 350 output) │
│ Context window: 100% left (1.45K used / 272K) │
│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │
│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │
│ Warning: limits may be stale - start new turn to refresh. │
╰─────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 1.05K total (700 input + 350 output)
│ Context window: 100% left (1.45K used / 272K)
│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32)
│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52)
│ Warning: limits may be stale - start new turn to refresh.
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 2K total (1.4K input + 600 output) │
│ Context window: 100% left (2.2K used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │
│ Credits: 38 credits │
╰───────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 2K total (1.4K input + 600 output)
│ Context window: 100% left (2.2K used / 272K)
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25)
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55)
│ Credits: 38 credits
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd
│ Token usage: 1.2K total (800 input + 400 output)
│ Context window: 100% left (1.2K used / 272K)
│ Limits: data not available yet
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May) │
╰────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 1.2K total (800 input + 400 output)
│ Context window: 100% left (1.2K used / 272K)
│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May)
╰────────────────────────────────────────────────────────────────────────────
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 750 total (500 input + 250 output)
│ Context window: 100% left (750 used / 272K)
│ Limits: data not available yet
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,19 +4,19 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54) │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 750 total (500 input + 250 output)
│ Context window: 100% left (750 used / 272K)
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24)
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54)
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │
│ Warning: limits may be stale - start new turn to refresh. │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 1.9K total (1K input + 900 output)
│ Context window: 100% left (2.25K used / 272K)
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14)
│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34)
│ Warning: limits may be stale - start new turn to refresh.
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 750 total (500 input + 250 output)
│ Context window: 100% left (750 used / 272K)
│ Limits: not available for this account
╰─────────────────────────────────────────────────────────────────────────────
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰───────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 750 total (500 input + 250 output)
│ Context window: 100% left (750 used / 272K)
│ Limits: not available for this account
╰─────────────────────────────────────────────────────────────────────────────
@@ -12,7 +12,7 @@ expression: sanitized
│ │
│ Model: gpt-5.1-codex-max (reasoning high, summaries de │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Permissions: Custom (workspace-write with network access, on
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (read-only, on-request)
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────────
│ >_ OpenAI Codex (v0.0.0)
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date
│ information on rate limits and credits
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto)
│ Directory: [[workspace]]
│ Permissions: Custom (workspace-write with network access, on-request)
│ Agents.md: <none>
│ Token usage: 750 total (500 input + 250 output)
│ Context window: 100% left (750 used / 272K)
│ Limits: data not available yet
╰─────────────────────────────────────────────────────────────────────────────
+108 -53
View File
@@ -13,12 +13,14 @@ use chrono::TimeZone;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::NetworkSandboxPolicy;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
use insta::assert_snapshot;
@@ -27,11 +29,21 @@ use ratatui::prelude::*;
use tempfile::TempDir;
async fn test_config(temp_home: &TempDir) -> Config {
ConfigBuilder::default()
let mut config = ConfigBuilder::default()
.codex_home(temp_home.path().to_path_buf())
.build()
.await
.expect("load config")
.expect("load config");
config
.permissions
.set_permission_profile(PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
))
.expect("set permission profile");
config
}
fn test_status_account_display() -> Option<StatusAccountDisplay> {
@@ -90,6 +102,41 @@ fn reset_at_from(captured_at: &chrono::DateTime<chrono::Local>, seconds: i64) ->
.timestamp()
}
fn permissions_text_for(config: &Config) -> Option<String> {
let usage = TokenUsage::default();
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
.single()
.expect("timestamp");
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
let composite = new_status_output(
config,
test_status_account_display().as_ref(),
/*token_info*/ None,
&usage,
&None,
/*thread_name*/ None,
/*forked_from*/ None,
/*rate_limits*/ None,
None,
captured_at,
&model_slug,
/*collaboration_mode*/ None,
/*reasoning_effort_override*/ None,
);
render_lines(&composite.display_lines(/*width*/ 80))
.iter()
.find(|line| line.contains("Permissions:"))
.and_then(|line| {
line.split("Permissions:")
.nth(1)
.map(str::trim)
.map(|text| text.trim_end_matches('│'))
.map(str::trim)
.map(ToString::to_string)
})
}
#[tokio::test]
async fn status_snapshot_includes_reasoning_details() {
let temp_home = TempDir::new().expect("temp home");
@@ -99,13 +146,9 @@ async fn status_snapshot_includes_reasoning_details() {
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
config.cwd = test_path_buf("/workspace/tests").abs();
config
.set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.expect("set sandbox policy");
.permissions
.set_permission_profile(PermissionProfile::workspace_write())
.expect("set permission profile");
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -181,55 +224,67 @@ async fn status_permissions_non_default_workspace_write_is_custom() {
.expect("set approval policy");
config.cwd = test_path_buf("/workspace/tests").abs();
config
.set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.expect("set sandbox policy");
let account_display = test_status_account_display();
let usage = TokenUsage::default();
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
.single()
.expect("timestamp");
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
let composite = new_status_output(
&config,
account_display.as_ref(),
/*token_info*/ None,
&usage,
&None,
/*thread_name*/ None,
/*forked_from*/ None,
/*rate_limits*/ None,
None,
captured_at,
&model_slug,
/*collaboration_mode*/ None,
/*reasoning_effort_override*/ None,
);
let rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
let permissions_line = rendered_lines
.iter()
.find(|line| line.contains("Permissions:"))
.expect("permissions line");
let permissions_text = permissions_line
.split("Permissions:")
.nth(1)
.map(str::trim)
.map(|text| text.trim_end_matches('│'))
.map(str::trim);
.permissions
.set_permission_profile(PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
))
.expect("set permission profile");
assert_eq!(
permissions_text,
permissions_text_for(&config).as_deref(),
Some("Custom (workspace-write with network access, on-request)")
);
}
#[tokio::test]
async fn status_permissions_full_disk_managed_with_network_is_danger_full_access() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.expect("set approval policy");
config
.permissions
.set_permission_profile(PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Enabled,
})
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Custom (danger-full-access, on-request)")
);
}
#[tokio::test]
async fn status_permissions_full_disk_managed_without_network_is_external_sandbox() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.expect("set approval policy");
config
.permissions
.set_permission_profile(PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
})
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Custom (external-sandbox, on-request)")
);
}
#[tokio::test]
async fn status_snapshot_includes_forked_from() {
let temp_home = TempDir::new().expect("temp home");
+8 -8
View File
@@ -1,7 +1,7 @@
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
/// A simple preset pairing an approval policy with a sandbox policy.
/// A simple preset pairing an approval policy with a permission profile.
#[derive(Debug, Clone)]
pub struct ApprovalPreset {
/// Stable identifier for the preset.
@@ -12,11 +12,11 @@ pub struct ApprovalPreset {
pub description: &'static str,
/// Approval policy to apply.
pub approval: AskForApproval,
/// Sandbox policy to apply.
pub sandbox: SandboxPolicy,
/// Permission profile to apply.
pub permission_profile: PermissionProfile,
}
/// Built-in list of approval presets that pair approval and sandbox policy.
/// Built-in list of approval presets that pair approval and permissions.
///
/// Keep this UI-agnostic so it can be reused by both TUI and MCP server.
pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
@@ -26,21 +26,21 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
label: "Read Only",
description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.",
approval: AskForApproval::OnRequest,
sandbox: SandboxPolicy::new_read_only_policy(),
permission_profile: PermissionProfile::read_only(),
},
ApprovalPreset {
id: "auto",
label: "Default",
description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files. (Identical to Agent mode)",
approval: AskForApproval::OnRequest,
sandbox: SandboxPolicy::new_workspace_write_policy(),
permission_profile: PermissionProfile::workspace_write(),
},
ApprovalPreset {
id: "full-access",
label: "Full Access",
description: "Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using.",
approval: AskForApproval::Never,
sandbox: SandboxPolicy::DangerFullAccess,
permission_profile: PermissionProfile::Disabled,
},
]
}
@@ -2,4 +2,5 @@ mod config_summary;
mod sandbox_summary;
pub use config_summary::create_config_summary_entries;
pub use sandbox_summary::summarize_permission_profile;
pub use sandbox_summary::summarize_sandbox_policy;
@@ -1,5 +1,7 @@
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::SandboxPolicy;
use std::path::Path;
pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
match sandbox_policy {
@@ -49,6 +51,19 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
}
}
pub fn summarize_permission_profile(permission_profile: &PermissionProfile, cwd: &Path) -> String {
match permission_profile.to_legacy_sandbox_policy(cwd) {
Ok(policy) => summarize_sandbox_policy(&policy),
Err(_) => {
if permission_profile.network_sandbox_policy().is_enabled() {
"custom permissions (network access enabled)".to_string()
} else {
"custom permissions".to_string()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;