Stricter `disable_ai` overrides (#35977)

Richard Feldman , Assistant , and David Kleingeld created

Settings overrides (e.g. local project settings, server settings) can no
longer change `disable_ai` to `false` if it was `true`; they can only
change it to `true`. In other words, settings can only cause AI to be
*more* disabled, they can't undo the user's preference for no AI (or the
project's requirement not to use AI).

Release Notes:

- Settings overrides (such as local project settings) can now only
override `disable_ai` to become `true`; they can no longer cause
otherwise-disabled AI to become re-enabled.

---------

Co-authored-by: Assistant <assistant@anthropic.com>
Co-authored-by: David Kleingeld <git@davidsk.dev>

Change summary

crates/project/src/project.rs | 171 +++++++++++++++++++++++++++++++++++-
1 file changed, 163 insertions(+), 8 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -962,14 +962,19 @@ impl settings::Settings for DisableAiSettings {
     type FileContent = Option<bool>;
 
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        Ok(Self {
-            disable_ai: sources
-                .user
-                .or(sources.server)
-                .copied()
-                .flatten()
-                .unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
-        })
+        // For security reasons, settings can only make AI restrictions MORE strict, not less.
+        // (For example, if someone is working on a project that contractually
+        // requires no AI use, that should override the user's setting which
+        // permits AI use.)
+        // This also prevents an attacker from using project or server settings to enable AI when it should be disabled.
+        let disable_ai = sources
+            .project
+            .iter()
+            .chain(sources.user.iter())
+            .chain(sources.server.iter())
+            .any(|disabled| **disabled == Some(true));
+
+        Ok(Self { disable_ai })
     }
 
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
@@ -5508,3 +5513,153 @@ fn provide_inline_values(
 
     variables
 }
+
+#[cfg(test)]
+mod disable_ai_settings_tests {
+    use super::*;
+    use gpui::TestAppContext;
+    use settings::{Settings, SettingsSources};
+
+    #[gpui::test]
+    async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            // Test 1: Default is false (AI enabled)
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: None,
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: None,
+                project: &[],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(settings.disable_ai, false, "Default should allow AI");
+
+            // Test 2: Global true, local false -> still disabled (local cannot re-enable)
+            let global_true = Some(true);
+            let local_false = Some(false);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&global_true),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: None,
+                project: &[&local_false],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Local false cannot override global true"
+            );
+
+            // Test 3: Global false, local true -> disabled (local can make more restrictive)
+            let global_false = Some(false);
+            let local_true = Some(true);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&global_false),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: None,
+                project: &[&local_true],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Local true can override global false"
+            );
+
+            // Test 4: Server can only make more restrictive (set to true)
+            let user_false = Some(false);
+            let server_true = Some(true);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&user_false),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: Some(&server_true),
+                project: &[],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Server can set to true even if user is false"
+            );
+
+            // Test 5: Server false cannot override user true
+            let user_true = Some(true);
+            let server_false = Some(false);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&user_true),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: Some(&server_false),
+                project: &[],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Server false cannot override user true"
+            );
+
+            // Test 6: Multiple local settings, any true disables AI
+            let global_false = Some(false);
+            let local_false3 = Some(false);
+            let local_true2 = Some(true);
+            let local_false4 = Some(false);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&global_false),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: None,
+                project: &[&local_false3, &local_true2, &local_false4],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Any local true should disable AI"
+            );
+
+            // Test 7: All three sources can independently disable AI
+            let user_false2 = Some(false);
+            let server_false2 = Some(false);
+            let local_true3 = Some(true);
+            let sources = SettingsSources {
+                default: &Some(false),
+                global: None,
+                extensions: None,
+                user: Some(&user_false2),
+                release_channel: None,
+                operating_system: None,
+                profile: None,
+                server: Some(&server_false2),
+                project: &[&local_true3],
+            };
+            let settings = DisableAiSettings::load(sources, cx).unwrap();
+            assert_eq!(
+                settings.disable_ai, true,
+                "Local can disable even if user and server are false"
+            );
+        });
+    }
+}