mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
34d71d43eb
commit
3b74a4d3b1
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
¤t_sandbox,
|
||||
¤t_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,
|
||||
¤t_sandbox,
|
||||
&preset,
|
||||
),
|
||||
current_approval,
|
||||
¤t_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,
|
||||
¤t_sandbox,
|
||||
¤t_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, ¤t_sandbox, &preset),
|
||||
ChatWidget::preset_matches_current(
|
||||
AskForApproval::OnRequest,
|
||||
¤t_profile,
|
||||
cwd.as_path(),
|
||||
&preset
|
||||
),
|
||||
"WorkspaceWrite with extra roots should still match the Default preset"
|
||||
);
|
||||
assert!(
|
||||
!ChatWidget::preset_matches_current(AskForApproval::Never, ¤t_sandbox, &preset),
|
||||
!ChatWidget::preset_matches_current(
|
||||
AskForApproval::Never,
|
||||
¤t_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,
|
||||
¤t_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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
+17
-17
@@ -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. │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+17
-17
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+17
-17
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+15
-15
@@ -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) │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+15
-15
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+16
-16
@@ -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) │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+17
-17
@@ -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. │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+15
-15
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+15
-15
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
+1
-1
@@ -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) │
|
||||
|
||||
+15
-15
@@ -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 │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user