Compare commits

...

1 Commits

Author SHA1 Message Date
juston
2b6054267b Lock approval/sandbox when managed 2025-12-01 15:34:02 -05:00
6 changed files with 178 additions and 5 deletions

View File

@@ -357,6 +357,7 @@ pub(crate) struct SessionConfiguration {
impl SessionConfiguration {
pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> Self {
let mut next_configuration = self.clone();
let managed_overrides = self.original_config_do_not_use.managed_overrides;
if let Some(model) = updates.model.clone() {
next_configuration.model = model;
}
@@ -366,10 +367,14 @@ impl SessionConfiguration {
if let Some(summary) = updates.reasoning_summary {
next_configuration.model_reasoning_summary = summary;
}
if let Some(approval_policy) = updates.approval_policy {
if let Some(approval_policy) = updates.approval_policy
&& !(managed_overrides.approval_policy && approval_policy != self.approval_policy)
{
next_configuration.approval_policy = approval_policy;
}
if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
if let Some(sandbox_policy) = updates.sandbox_policy.clone()
&& !(managed_overrides.sandbox_mode && sandbox_policy != self.sandbox_policy)
{
next_configuration.sandbox_policy = sandbox_policy;
}
if let Some(cwd) = updates.cwd.clone() {
@@ -2440,6 +2445,7 @@ mod tests {
use super::*;
use crate::config::ConfigOverrides;
use crate::config::ConfigToml;
use crate::config::ManagedConfigLocks;
use crate::exec::ExecToolCallOutput;
use crate::shell::default_user_shell;
use crate::tools::format_exec_output_str;
@@ -2515,6 +2521,44 @@ mod tests {
assert_eq!(expected, actual);
}
#[tokio::test]
async fn managed_policies_block_session_overrides() {
let (session, _) = make_session_and_context();
let (initial_approval, initial_sandbox) = {
let state = session.state.lock().await;
(
state.session_configuration.approval_policy,
state.session_configuration.sandbox_policy.clone(),
)
};
{
let mut state = session.state.lock().await;
let managed_overrides = ManagedConfigLocks {
approval_policy: true,
sandbox_mode: true,
};
let config = Arc::make_mut(&mut state.session_configuration.original_config_do_not_use);
config.managed_overrides = managed_overrides;
}
session
.update_settings(SessionSettingsUpdate {
approval_policy: Some(AskForApproval::OnRequest),
sandbox_policy: Some(SandboxPolicy::DangerFullAccess),
..Default::default()
})
.await;
let state = session.state.lock().await;
assert_eq!(
state.session_configuration.approval_policy,
initial_approval
);
assert_eq!(state.session_configuration.sandbox_policy, initial_sandbox);
}
#[test]
fn prefers_structured_content_when_present() {
let ctr = CallToolResult {

View File

@@ -72,6 +72,12 @@ pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB
pub const CONFIG_TOML_FILE: &str = "config.toml";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ManagedConfigLocks {
pub approval_policy: bool,
pub sandbox_mode: bool,
}
/// Application configuration loaded from disk and merged with overrides.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
@@ -104,6 +110,9 @@ pub struct Config {
/// for either of approval_policy or sandbox_mode.
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
/// Whether approval_policy or sandbox_mode were set by managed config.
pub managed_overrides: ManagedConfigLocks,
/// On Windows, indicates that a previously configured workspace-write sandbox
/// was coerced to read-only because native auto mode is unsupported.
pub forced_auto_mode_downgraded_on_windows: bool,
@@ -288,19 +297,28 @@ impl Config {
) -> std::io::Result<Self> {
let codex_home = find_codex_home()?;
let root_value = load_resolved_config(
let layers = load_config_layers_with_overrides(
&codex_home,
cli_overrides,
crate::config_loader::LoaderOverrides::default(),
)
.await?;
let managed_config = layers.managed_config.clone();
let managed_preferences = layers.managed_preferences.clone();
let root_value = apply_overlays(layers, cli_overrides);
let cfg: ConfigToml = root_value.try_into().map_err(|e| {
tracing::error!("Failed to deserialize overridden config: {e}");
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})?;
Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
let mut config = Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)?;
config.managed_overrides = managed_config_locks(
managed_config.as_ref(),
managed_preferences.as_ref(),
config.active_profile.as_deref(),
);
Ok(config)
}
}
@@ -353,6 +371,34 @@ fn apply_overlays(
base
}
fn managed_config_locks(
managed_config: Option<&TomlValue>,
managed_preferences: Option<&TomlValue>,
active_profile: Option<&str>,
) -> ManagedConfigLocks {
let mut locks = ManagedConfigLocks::default();
for overlay in [managed_config, managed_preferences].into_iter().flatten() {
if let Some(table) = overlay.as_table() {
locks.approval_policy |= table.contains_key("approval_policy");
locks.sandbox_mode |= table.contains_key("sandbox_mode");
if let Some(profile) = active_profile
&& let Some(profile_table) = table
.get("profiles")
.and_then(|profiles| profiles.as_table())
.and_then(|profiles| profiles.get(profile))
.and_then(|profile| profile.as_table())
{
locks.approval_policy |= profile_table.contains_key("approval_policy");
locks.sandbox_mode |= profile_table.contains_key("sandbox_mode");
}
}
}
locks
}
pub async fn load_global_mcp_servers(
codex_home: &Path,
) -> std::io::Result<BTreeMap<String, McpServerConfig>> {
@@ -1186,6 +1232,7 @@ impl Config {
approval_policy,
sandbox_policy,
did_user_set_custom_approval_policy_or_sandbox_mode,
managed_overrides: ManagedConfigLocks::default(),
forced_auto_mode_downgraded_on_windows,
shell_environment_policy,
notify: cfg.notify,
@@ -1712,6 +1759,27 @@ trust_level = "trusted"
Ok(())
}
#[test]
fn managed_config_locks_root_and_profile_keys() {
let managed = toml::from_str::<TomlValue>(
r#"
approval_policy = "never"
[profiles.work]
sandbox_mode = "read-only"
"#,
)
.expect("managed config should parse");
let locks_for_work = managed_config_locks(Some(&managed), None, Some("work"));
assert!(locks_for_work.approval_policy);
assert!(locks_for_work.sandbox_mode);
let locks_for_personal = managed_config_locks(Some(&managed), None, Some("personal"));
assert!(locks_for_personal.approval_policy);
assert!(!locks_for_personal.sandbox_mode);
}
#[test]
fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -2968,6 +3036,7 @@ model_verbosity = "high"
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
managed_overrides: ManagedConfigLocks::default(),
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
user_instructions: None,
@@ -3041,6 +3110,7 @@ model_verbosity = "high"
approval_policy: AskForApproval::UnlessTrusted,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
managed_overrides: ManagedConfigLocks::default(),
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
user_instructions: None,
@@ -3129,6 +3199,7 @@ model_verbosity = "high"
approval_policy: AskForApproval::OnFailure,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
managed_overrides: ManagedConfigLocks::default(),
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
user_instructions: None,
@@ -3203,6 +3274,7 @@ model_verbosity = "high"
approval_policy: AskForApproval::OnFailure,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
managed_overrides: ManagedConfigLocks::default(),
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
user_instructions: None,

View File

@@ -703,9 +703,29 @@ impl App {
}
}
AppEvent::UpdateAskForApprovalPolicy(policy) => {
if self.config.managed_overrides.approval_policy
&& policy != self.config.approval_policy
{
self.chat_widget.add_info_message(
"Managed configuration locks the approval policy for this session."
.to_string(),
None,
);
return Ok(true);
}
self.chat_widget.set_approval_policy(policy);
}
AppEvent::UpdateSandboxPolicy(policy) => {
if self.config.managed_overrides.sandbox_mode
&& policy != self.config.sandbox_policy
{
self.chat_widget.add_info_message(
"Managed configuration locks the sandbox mode for this session."
.to_string(),
None,
);
return Ok(true);
}
#[cfg(target_os = "windows")]
let policy_is_workspace_write_or_ro = matches!(
policy,

View File

@@ -2326,6 +2326,17 @@ impl ChatWidget {
/// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy).
pub(crate) fn open_approvals_popup(&mut self) {
if self.config.managed_overrides.approval_policy
|| self.config.managed_overrides.sandbox_mode
{
self.add_info_message(
"Managed configuration locks approval and sandbox settings for this session."
.to_string(),
None,
);
return;
}
let current_approval = self.config.approval_policy;
let current_sandbox = self.config.sandbox_policy.clone();
let mut items: Vec<SelectionItem> = Vec::new();

View File

@@ -12,6 +12,7 @@ use codex_core::CodexAuth;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
use codex_core::config::ManagedConfigLocks;
use codex_core::protocol::AgentMessageDeltaEvent;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::AgentReasoningDeltaEvent;
@@ -145,6 +146,28 @@ fn resumed_initial_messages_render_history() {
);
}
#[test]
fn approvals_popup_respects_managed_config() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
chat.config.managed_overrides = ManagedConfigLocks {
approval_policy: true,
sandbox_mode: true,
};
chat.open_approvals_popup();
let history_cells = drain_insert_history(&mut rx);
let merged = lines_to_single_string(
history_cells
.last()
.expect("managed notice should render a history cell"),
);
assert!(
merged.contains("Managed configuration locks approval and sandbox settings"),
"expected managed notice, got {merged:?}"
);
}
/// Entering review mode uses the hint provided by the review request.
#[test]
fn entered_review_mode_uses_request_hint() {

View File

@@ -25,6 +25,9 @@ Codex supports several mechanisms for setting config values:
- Because quotes are interpreted by one's shell, `-c key="true"` will be correctly interpreted in TOML as `key = true` (a boolean) and not `key = "true"` (a string). If for some reason you needed the string `"true"`, you would need to use `-c key='"true"'` (note the two sets of quotes).
- The `$CODEX_HOME/config.toml` configuration file where the `CODEX_HOME` environment value defaults to `~/.codex`. (Note `CODEX_HOME` will also be where logs and other Codex-related information are stored.)
> [!NOTE]
> Managed configs (`/etc/codex/managed_config.toml`, or the managed preferences payload on macOS) sit above CLI overrides. Keys set there are locked for the lifetime of a session; in-product approval/sandbox toggles are ignored until you remove or change the managed file and restart Codex.
Both the `--config` flag and the `config.toml` file support the following options:
## Feature flags