Fix multiple env var migrations

Richard Feldman created

Change summary

crates/extension_host/src/extension_host.rs             | 15 +++++-
crates/extension_host/src/extension_settings.rs         |  6 +-
crates/extension_host/src/wasm_host/llm_provider.rs     | 24 +++++++++-
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs | 19 ++++++-
crates/settings/src/settings_content/extension.rs       |  2 
5 files changed, 51 insertions(+), 15 deletions(-)

Detailed changes

crates/extension_host/src/extension_host.rs 🔗

@@ -88,7 +88,7 @@ const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
 /// Extension IDs that are being migrated from hardcoded LLM providers.
 /// For backwards compatibility, if the user has the corresponding env var set,
 /// we automatically enable env var reading for these extensions on first install.
-const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[
+pub const LEGACY_LLM_EXTENSION_IDS: &[&str] = &[
     "anthropic",
     "copilot-chat",
     "google-ai",
@@ -133,7 +133,7 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap
 
             // Check if already enabled in settings
             let already_enabled = ExtensionSettings::get_global(cx)
-                .allowed_env_vars
+                .allowed_env_var_providers
                 .contains(settings_key.as_ref());
 
             if already_enabled {
@@ -146,7 +146,7 @@ fn migrate_legacy_llm_provider_env_var(manifest: &ExtensionManifest, cx: &mut Ap
                 move |settings, _| {
                     let allowed = settings
                         .extension
-                        .allowed_env_vars
+                        .allowed_env_var_providers
                         .get_or_insert_with(Vec::new);
 
                     if !allowed
@@ -1192,9 +1192,16 @@ impl ExtensionStore {
                 }
             }
 
-            fs.create_symlink(output_path, extension_source_path)
+            fs.create_symlink(output_path, extension_source_path.clone())
                 .await?;
 
+            // Re-load manifest and run migration before reload so settings are updated before providers are registered
+            let manifest_for_migration =
+                ExtensionManifest::load(fs.clone(), &extension_source_path).await?;
+            this.update(cx, |_this, cx| {
+                migrate_legacy_llm_provider_env_var(&manifest_for_migration, cx);
+            })?;
+
             this.update(cx, |this, cx| this.reload(None, cx))?.await;
             this.update(cx, |this, cx| {
                 cx.emit(Event::ExtensionInstalled(extension_id.clone()));

crates/extension_host/src/extension_settings.rs 🔗

@@ -19,7 +19,7 @@ pub struct ExtensionSettings {
     /// The extension language model providers that are allowed to read API keys
     /// from environment variables. Each entry is in the format
     /// "extension_id:provider_id:ENV_VAR_NAME".
-    pub allowed_env_vars: HashSet<Arc<str>>,
+    pub allowed_env_var_providers: HashSet<Arc<str>>,
 }
 
 impl ExtensionSettings {
@@ -64,9 +64,9 @@ impl Settings for ExtensionSettings {
                     }
                 })
                 .collect(),
-            allowed_env_vars: content
+            allowed_env_var_providers: content
                 .extension
-                .allowed_env_vars
+                .allowed_env_var_providers
                 .clone()
                 .unwrap_or_default()
                 .into_iter()

crates/extension_host/src/wasm_host/llm_provider.rs 🔗

@@ -1,4 +1,5 @@
 use crate::ExtensionSettings;
+use crate::LEGACY_LLM_EXTENSION_IDS;
 use crate::wasm_host::WasmExtension;
 use collections::HashSet;
 
@@ -69,11 +70,20 @@ impl ExtensionLanguageModelProvider {
 
         // Build set of allowed env vars for this provider
         let settings = ExtensionSettings::get_global(cx);
+        let is_legacy_extension =
+            LEGACY_LLM_EXTENSION_IDS.contains(&extension.manifest.id.as_ref());
+
         let mut allowed_env_vars = HashSet::default();
         if let Some(env_vars) = auth_config.as_ref().and_then(|c| c.env_vars.as_ref()) {
             for env_var_name in env_vars {
                 let key = format!("{}:{}", provider_id_string, env_var_name);
-                if settings.allowed_env_vars.contains(key.as_str()) {
+                // For legacy extensions, auto-allow if env var is set (migration will persist this)
+                let env_var_is_set = std::env::var(env_var_name)
+                    .map(|v| !v.is_empty())
+                    .unwrap_or(false);
+                if settings.allowed_env_var_providers.contains(key.as_str())
+                    || (is_legacy_extension && env_var_is_set)
+                {
                     allowed_env_vars.insert(env_var_name.clone());
                 }
             }
@@ -201,10 +211,18 @@ impl LanguageModelProvider for ExtensionLanguageModelProvider {
             if let Some(ref env_vars) = auth_config.env_vars {
                 let provider_id_string = self.provider_id_string();
                 let settings = ExtensionSettings::get_global(cx);
+                let is_legacy_extension =
+                    LEGACY_LLM_EXTENSION_IDS.contains(&self.extension.manifest.id.as_ref());
 
                 for env_var_name in env_vars {
                     let key = format!("{}:{}", provider_id_string, env_var_name);
-                    if settings.allowed_env_vars.contains(key.as_str()) {
+                    // For legacy extensions, auto-allow if env var is set
+                    let env_var_is_set = std::env::var(env_var_name)
+                        .map(|v| !v.is_empty())
+                        .unwrap_or(false);
+                    if settings.allowed_env_var_providers.contains(key.as_str())
+                        || (is_legacy_extension && env_var_is_set)
+                    {
                         if let Ok(value) = std::env::var(env_var_name) {
                             if !value.is_empty() {
                                 return true;
@@ -474,7 +492,7 @@ impl ExtensionProviderConfigurationView {
             move |settings, _| {
                 let allowed = settings
                     .extension
-                    .allowed_env_vars
+                    .allowed_env_var_providers
                     .get_or_insert_with(Vec::new);
 
                 if currently_allowed {

crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs 🔗

@@ -1114,6 +1114,7 @@ impl ExtensionImports for WasmState {
 impl llm_provider::Host for WasmState {
     async fn get_credential(&mut self, provider_id: String) -> wasmtime::Result<Option<String>> {
         let extension_id = self.manifest.id.clone();
+        let is_legacy_extension = crate::LEGACY_LLM_EXTENSION_IDS.contains(&extension_id.as_ref());
 
         // Check if this provider has env vars configured and if the user has allowed any of them
         let env_vars = self
@@ -1131,6 +1132,11 @@ impl llm_provider::Host for WasmState {
                 let settings_key: Arc<str> =
                     format!("{}:{}", full_provider_id, env_var_name).into();
 
+                // For legacy extensions, auto-allow if env var is set
+                let env_var_is_set = env::var(env_var_name)
+                    .map(|v| !v.is_empty())
+                    .unwrap_or(false);
+
                 let is_allowed = self
                     .on_main_thread({
                         let settings_key = settings_key.clone();
@@ -1138,7 +1144,7 @@ impl llm_provider::Host for WasmState {
                             async move {
                                 cx.update(|cx| {
                                     crate::extension_settings::ExtensionSettings::get_global(cx)
-                                        .allowed_env_vars
+                                        .allowed_env_var_providers
                                         .contains(&settings_key)
                                 })
                             }
@@ -1148,7 +1154,7 @@ impl llm_provider::Host for WasmState {
                     .await
                     .unwrap_or(false);
 
-                if is_allowed {
+                if is_allowed || (is_legacy_extension && env_var_is_set) {
                     if let Ok(value) = env::var(env_var_name) {
                         if !value.is_empty() {
                             return Ok(Some(value));
@@ -1247,6 +1253,11 @@ impl llm_provider::Host for WasmState {
 
         // Check if the user has allowed this specific env var
         let settings_key: Arc<str> = format!("{}:{}:{}", extension_id, provider_id, name).into();
+        let is_legacy_extension = crate::LEGACY_LLM_EXTENSION_IDS.contains(&extension_id.as_ref());
+
+        // For legacy extensions, auto-allow if env var is set
+        let env_var_is_set = env::var(&name).map(|v| !v.is_empty()).unwrap_or(false);
+
         let is_allowed = self
             .on_main_thread({
                 let settings_key = settings_key.clone();
@@ -1254,7 +1265,7 @@ impl llm_provider::Host for WasmState {
                     async move {
                         cx.update(|cx| {
                             crate::extension_settings::ExtensionSettings::get_global(cx)
-                                .allowed_env_vars
+                                .allowed_env_var_providers
                                 .contains(&settings_key)
                         })
                     }
@@ -1264,7 +1275,7 @@ impl llm_provider::Host for WasmState {
             .await
             .unwrap_or(false);
 
-        if !is_allowed {
+        if !is_allowed && !(is_legacy_extension && env_var_is_set) {
             log::debug!(
                 "Extension {} provider {} is not allowed to read env var {}",
                 extension_id,

crates/settings/src/settings_content/extension.rs 🔗

@@ -25,7 +25,7 @@ pub struct ExtensionSettingsContent {
     /// "extension_id:provider_id:ENV_VAR_NAME" (e.g., "google-ai:google-ai:GEMINI_API_KEY").
     ///
     /// Default: []
-    pub allowed_env_vars: Option<Vec<Arc<str>>>,
+    pub allowed_env_var_providers: Option<Vec<Arc<str>>>,
 }
 
 /// A capability for an extension.