mirror of
https://github.com/openai/codex.git
synced 2026-03-03 05:03:20 +00:00
Compare commits
2 Commits
fix/notify
...
dh--codex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff901ce04a | ||
|
|
0ab58c871c |
4
MODULE.bazel.lock
generated
4
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
@@ -49,6 +49,7 @@ use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_core::config::ConstraintResult;
|
||||
use codex_core::config::types::Notifications;
|
||||
use codex_core::config::types::WindowsSandboxModeToml;
|
||||
@@ -121,6 +122,7 @@ use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::Settings;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -5281,6 +5283,7 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.permissions.sandbox_policy.get();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
|
||||
let mut displayed_presets: Vec<ApprovalPreset> = Vec::new();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
|
||||
@@ -5298,6 +5301,7 @@ impl ChatWidget {
|
||||
if !include_read_only && preset.id == "read-only" {
|
||||
continue;
|
||||
}
|
||||
displayed_presets.push(preset.clone());
|
||||
let is_current =
|
||||
Self::preset_matches_current(current_approval, current_sandbox, &preset);
|
||||
let name = if preset.id == "auto" && windows_degraded_sandbox_enabled {
|
||||
@@ -5305,7 +5309,7 @@ impl ChatWidget {
|
||||
} else {
|
||||
preset.label.to_string()
|
||||
};
|
||||
let description = Some(preset.description.replace(" (Identical to Agent mode)", ""));
|
||||
let description = Some(preset.description.to_string());
|
||||
let disabled_reason = match self
|
||||
.config
|
||||
.permissions
|
||||
@@ -5388,6 +5392,31 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
let custom_config_permissions = Self::custom_permissions_from_user_config(&self.config);
|
||||
if let Some((approval, sandbox)) = custom_config_permissions {
|
||||
let matches_displayed_preset = displayed_presets
|
||||
.iter()
|
||||
.any(|preset| preset.approval == approval && preset.sandbox == sandbox);
|
||||
if !matches_displayed_preset {
|
||||
let is_current = current_approval == approval && *current_sandbox == sandbox;
|
||||
let disabled_reason =
|
||||
match self.config.permissions.approval_policy.can_set(&approval) {
|
||||
Ok(()) => None,
|
||||
Err(err) => Some(err.to_string()),
|
||||
};
|
||||
let description = crate::status::permissions_display_text_for(approval, &sandbox);
|
||||
items.push(SelectionItem {
|
||||
name: "Custom".to_string(),
|
||||
description: Some(description),
|
||||
is_current,
|
||||
actions: Self::approval_preset_actions(approval, sandbox),
|
||||
dismiss_on_select: true,
|
||||
disabled_reason,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let footer_note = show_elevate_sandbox_hint.then(|| {
|
||||
vec![
|
||||
"The non-admin sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the default sandbox, run ".dim(),
|
||||
@@ -5456,6 +5485,48 @@ impl ChatWidget {
|
||||
current_approval == preset.approval && *current_sandbox == preset.sandbox
|
||||
}
|
||||
|
||||
fn custom_permissions_from_user_config(
|
||||
config: &Config,
|
||||
) -> Option<(AskForApproval, SandboxPolicy)> {
|
||||
let user_layer = config.config_layer_stack.get_user_layer()?;
|
||||
let config_toml: ConfigToml = user_layer.config.clone().try_into().ok()?;
|
||||
let profile = config_toml
|
||||
.get_config_profile(config.active_profile.clone())
|
||||
.ok()?;
|
||||
let approval_explicit =
|
||||
profile.approval_policy.is_some() || config_toml.approval_policy.is_some();
|
||||
let sandbox_mode_explicit =
|
||||
profile.sandbox_mode.is_some() || config_toml.sandbox_mode.is_some();
|
||||
let sandbox_workspace_write_explicit = config_toml.sandbox_workspace_write.is_some();
|
||||
if !approval_explicit && !sandbox_mode_explicit && !sandbox_workspace_write_explicit {
|
||||
return None;
|
||||
}
|
||||
|
||||
let approval = profile
|
||||
.approval_policy
|
||||
.or(config_toml.approval_policy)
|
||||
.unwrap_or_default();
|
||||
let sandbox_mode = profile
|
||||
.sandbox_mode
|
||||
.or(config_toml.sandbox_mode)
|
||||
.unwrap_or(SandboxMode::WorkspaceWrite);
|
||||
let sandbox = match sandbox_mode {
|
||||
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
|
||||
SandboxMode::WorkspaceWrite => match config_toml.sandbox_workspace_write {
|
||||
Some(settings) => SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: settings.writable_roots,
|
||||
read_only_access: Default::default(),
|
||||
network_access: settings.network_access,
|
||||
exclude_tmpdir_env_var: settings.exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp: settings.exclude_slash_tmp,
|
||||
},
|
||||
None => SandboxPolicy::new_workspace_write_policy(),
|
||||
},
|
||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
};
|
||||
Some((approval, sandbox))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {
|
||||
if self
|
||||
|
||||
@@ -107,15 +107,24 @@ use tokio::sync::mpsc::unbounded_channel;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
async fn test_config() -> Config {
|
||||
// Use base defaults to avoid depending on host state.
|
||||
let codex_home = std::env::temp_dir();
|
||||
// Use an isolated codex_home to avoid depending on host state.
|
||||
let codex_home = tempdir().expect("tempdir").keep();
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.clone())
|
||||
.codex_home(codex_home)
|
||||
.build()
|
||||
.await
|
||||
.expect("config")
|
||||
}
|
||||
|
||||
fn set_user_config_layer(chat: &mut ChatWidget, config_toml: TomlValue) {
|
||||
let user_config_path = AbsolutePathBuf::try_from(std::env::temp_dir().join("config.toml"))
|
||||
.expect("absolute user config path");
|
||||
chat.config.config_layer_stack = chat
|
||||
.config
|
||||
.config_layer_stack
|
||||
.with_user_config(&user_config_path, config_toml);
|
||||
}
|
||||
|
||||
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError {
|
||||
ConstraintError::InvalidValue {
|
||||
field_name: "<unknown>",
|
||||
@@ -4171,6 +4180,69 @@ async fn approvals_selection_popup_snapshot() {
|
||||
assert_snapshot!("approvals_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approvals_popup_shows_custom_option_for_custom_user_config() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
let user_config_toml: TomlValue = toml::from_str(
|
||||
r#"
|
||||
approval_policy = "on-request"
|
||||
sandbox_mode = "workspace-write"
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
"#,
|
||||
)
|
||||
.expect("valid user config.toml");
|
||||
set_user_config_layer(&mut chat, user_config_toml);
|
||||
|
||||
chat.open_permissions_popup();
|
||||
let popup = render_bottom_popup(&chat, 150);
|
||||
|
||||
assert!(
|
||||
popup.contains("Custom"),
|
||||
"expected popup to include a Custom option, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("workspace-write with network access, on-request"),
|
||||
"expected Custom option description to match status-card details, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approvals_popup_hides_custom_option_for_default_or_full_access_user_config() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
let user_default_toml: TomlValue = toml::from_str(
|
||||
r#"
|
||||
approval_policy = "on-request"
|
||||
sandbox_mode = "workspace-write"
|
||||
"#,
|
||||
)
|
||||
.expect("valid default-equivalent user config.toml");
|
||||
set_user_config_layer(&mut chat, user_default_toml);
|
||||
|
||||
chat.open_permissions_popup();
|
||||
let default_popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
!default_popup.contains("Custom"),
|
||||
"default-equivalent config.toml should not surface Custom, got:\n{default_popup}"
|
||||
);
|
||||
|
||||
let user_full_toml: TomlValue = toml::from_str(
|
||||
r#"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
"#,
|
||||
)
|
||||
.expect("valid full-access-equivalent user config.toml");
|
||||
set_user_config_layer(&mut chat, user_full_toml);
|
||||
|
||||
chat.open_permissions_popup();
|
||||
let full_popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
!full_popup.contains("Custom"),
|
||||
"full-access-equivalent config.toml should not surface Custom, got:\n{full_popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
||||
@@ -8,7 +8,6 @@ use chrono::Local;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::NetworkAccess;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::protocol::TokenUsageInfo;
|
||||
@@ -168,14 +167,6 @@ impl StatusHistoryCell {
|
||||
("workdir", config.cwd.display().to_string()),
|
||||
("model", model_name.to_string()),
|
||||
("provider", config.model_provider_id.clone()),
|
||||
(
|
||||
"approval",
|
||||
config.permissions.approval_policy.value().to_string(),
|
||||
),
|
||||
(
|
||||
"sandbox",
|
||||
summarize_sandbox_policy(config.permissions.sandbox_policy.get()),
|
||||
),
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
let effort_value = reasoning_effort_override
|
||||
@@ -189,39 +180,7 @@ impl StatusHistoryCell {
|
||||
));
|
||||
}
|
||||
let (model_name, model_details) = compose_model_display(model_name, &config_entries);
|
||||
let approval = config_entries
|
||||
.iter()
|
||||
.find(|(k, _)| *k == "approval")
|
||||
.map(|(_, v)| v.clone())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let sandbox = match config.permissions.sandbox_policy.get() {
|
||||
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 permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest
|
||||
&& *config.permissions.sandbox_policy.get()
|
||||
== SandboxPolicy::new_workspace_write_policy()
|
||||
{
|
||||
"Default".to_string()
|
||||
} else if config.permissions.approval_policy.value() == AskForApproval::Never
|
||||
&& *config.permissions.sandbox_policy.get() == SandboxPolicy::DangerFullAccess
|
||||
{
|
||||
"Full Access".to_string()
|
||||
} else {
|
||||
format!("Custom ({sandbox}, {approval})")
|
||||
};
|
||||
let permissions = permissions_display_text(config);
|
||||
let agents_summary = compose_agents_summary(config);
|
||||
let model_provider = format_model_provider(config);
|
||||
let account = compose_account_display(auth_manager, plan_type);
|
||||
@@ -406,6 +365,26 @@ impl StatusHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn permissions_display_text(config: &Config) -> String {
|
||||
permissions_display_text_for(
|
||||
config.permissions.approval_policy.value(),
|
||||
config.permissions.sandbox_policy.get(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn permissions_display_text_for(
|
||||
approval: AskForApproval,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
) -> String {
|
||||
let approval = approval.to_string();
|
||||
let sandbox = summarize_sandbox_policy(sandbox_policy, true);
|
||||
match (approval.as_str(), sandbox.as_str()) {
|
||||
("on-request", "workspace-write") => "Default".to_string(),
|
||||
("never", "danger-full-access") => "Full Access".to_string(),
|
||||
_ => format!("Custom ({sandbox}, {approval})"),
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCell for StatusHistoryCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
@@ -15,6 +15,7 @@ mod rate_limits;
|
||||
#[cfg(test)]
|
||||
pub(crate) use card::new_status_output;
|
||||
pub(crate) use card::new_status_output_with_rate_limits;
|
||||
pub(crate) use card::permissions_display_text_for;
|
||||
pub(crate) use helpers::format_directory_display;
|
||||
pub(crate) use helpers::format_tokens_compact;
|
||||
pub(crate) use rate_limits::RateLimitSnapshotDisplay;
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
|
||||
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)",
|
||||
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.",
|
||||
approval: AskForApproval::OnRequest,
|
||||
sandbox: SandboxPolicy::new_workspace_write_policy(),
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta
|
||||
),
|
||||
(
|
||||
"sandbox",
|
||||
summarize_sandbox_policy(config.permissions.sandbox_policy.get()),
|
||||
summarize_sandbox_policy(config.permissions.sandbox_policy.get(), false),
|
||||
),
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use codex_core::protocol::NetworkAccess;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
|
||||
pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||
pub fn summarize_sandbox_policy(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
ignore_writable_roots: bool,
|
||||
) -> String {
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
||||
SandboxPolicy::ReadOnly { .. } => "read-only".to_string(),
|
||||
@@ -35,9 +38,11 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||
.map(|p| p.to_string_lossy().to_string()),
|
||||
);
|
||||
|
||||
summary.push_str(&format!(" [{}]", writable_entries.join(", ")));
|
||||
if *network_access {
|
||||
summary.push_str(" (network access enabled)");
|
||||
summary.push_str(" with network access");
|
||||
}
|
||||
if !ignore_writable_roots {
|
||||
summary.push_str(&format!(" [{}]", writable_entries.join(", ")));
|
||||
}
|
||||
summary
|
||||
}
|
||||
@@ -52,17 +57,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn summarizes_external_sandbox_without_network_access_suffix() {
|
||||
let summary = summarize_sandbox_policy(&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
});
|
||||
let summary = summarize_sandbox_policy(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
false,
|
||||
);
|
||||
assert_eq!(summary, "external-sandbox");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summarizes_external_sandbox_with_enabled_network() {
|
||||
let summary = summarize_sandbox_policy(&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Enabled,
|
||||
});
|
||||
let summary = summarize_sandbox_policy(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Enabled,
|
||||
},
|
||||
false,
|
||||
);
|
||||
assert_eq!(summary, "external-sandbox (network access enabled)");
|
||||
}
|
||||
|
||||
@@ -70,13 +81,16 @@ mod tests {
|
||||
fn workspace_write_summary_still_includes_network_access() {
|
||||
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||
let writable_root = AbsolutePathBuf::try_from(root).unwrap();
|
||||
let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root.clone()],
|
||||
read_only_access: Default::default(),
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
});
|
||||
let summary = summarize_sandbox_policy(
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root.clone()],
|
||||
read_only_access: Default::default(),
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
summary,
|
||||
format!(
|
||||
|
||||
Reference in New Issue
Block a user