extension_host: Load granted extension capabilities from settings (#39472)

Marshall Bowers created

This PR adds the ability to control the capabilities granted to
extensions by the extension host via the new
`granted_extension_capabilities` setting.

This setting is a list of the capabilities granted to any extension
running in Zed.

The currently available capabilities are:

- `process:exec` - Grants extensions the ability to invoke commands
using
[`zed_extension_api::process::Command`](https://docs.rs/zed_extension_api/latest/zed_extension_api/process/struct.Command.html)
- `download_file` - Grants extensions the ability to download files
using
[`zed_extension_api::download_file`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.download_file.html)
- `npm:install` - Grants extensions the ability to install npm packages
using
[`zed_extension_api::npm_install_package`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.npm_install_package.html)

Each of these capabilities has parameters that can be used to customize
the permissions.

For instance, to only allow downloads from GitHub, the `download_file`
capability can specify an allowed `host`:

```json
[
  { "kind": "download_file", "host": "github.com", "path": ["**"] }
]
```

The same capability can also be granted multiple times with different
parameters to build up an allowlist:

```json
[
  { "kind": "download_file", "host": "github.com", "path": ["**"] },
  { "kind": "download_file", "host": "gitlab.com", "path": ["**"] }
]
```

When an extension is not granted a capability, the associated extension
APIs protected by that capability will fail.

For instance, trying to use `zed_extension_api::download_file` when the
`download_file` capability is not granted will result in an error that
will be surfaced by the extension:

```
Language server phpactor:

from extension "PHP" version 0.4.3: failed to download file: capability for download_file https://github.com/phpactor/phpactor/releases/download/2025.07.25.0/phpactor.phar is not granted by the extension host
```

Release Notes:

- Added a `granted_extension_capabilities` setting to control the
capabilities granted to extensions.

Change summary

assets/settings/default.json                      |  8 ++
crates/extension_host/src/extension_settings.rs   | 30 ++++++++
crates/extension_host/src/wasm_host.rs            | 26 ++----
crates/remote_server/src/headless_project.rs      |  3 
crates/settings/src/settings_content.rs           | 17 ----
crates/settings/src/settings_content/extension.rs | 59 +++++++++++++++++
6 files changed, 110 insertions(+), 33 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1562,6 +1562,14 @@
   "auto_install_extensions": {
     "html": true
   },
+  // The capabilities granted to extensions.
+  //
+  // This list can be customized to restrict what extensions are able to do.
+  "granted_extension_capabilities": [
+    { "kind": "process:exec", "command": "*", "args": ["**"] },
+    { "kind": "download_file", "host": "*", "path": ["**"] },
+    { "kind": "npm:install", "package": "*" }
+  ],
   // Controls how completions are processed for this language.
   "completions": {
     // Controls how words are completed.

crates/extension_host/src/extension_settings.rs 🔗

@@ -1,4 +1,7 @@
 use collections::HashMap;
+use extension::{
+    DownloadFileCapability, ExtensionCapability, NpmInstallPackageCapability, ProcessExecCapability,
+};
 use gpui::App;
 use settings::Settings;
 use std::sync::Arc;
@@ -13,6 +16,7 @@ pub struct ExtensionSettings {
     /// Default: { "html": true }
     pub auto_install_extensions: HashMap<Arc<str>, bool>,
     pub auto_update_extensions: HashMap<Arc<str>, bool>,
+    pub granted_capabilities: Vec<ExtensionCapability>,
 }
 
 impl ExtensionSettings {
@@ -37,6 +41,32 @@ impl Settings for ExtensionSettings {
         Self {
             auto_install_extensions: content.extension.auto_install_extensions.clone(),
             auto_update_extensions: content.extension.auto_update_extensions.clone(),
+            granted_capabilities: content
+                .extension
+                .granted_extension_capabilities
+                .clone()
+                .unwrap_or_default()
+                .into_iter()
+                .map(|capability| match capability {
+                    settings::ExtensionCapabilityContent::ProcessExec(capability) => {
+                        ExtensionCapability::ProcessExec(ProcessExecCapability {
+                            command: capability.command,
+                            args: capability.args,
+                        })
+                    }
+                    settings::ExtensionCapabilityContent::DownloadFile(capability) => {
+                        ExtensionCapability::DownloadFile(DownloadFileCapability {
+                            host: capability.host,
+                            path: capability.path,
+                        })
+                    }
+                    settings::ExtensionCapabilityContent::NpmInstallPackage(capability) => {
+                        ExtensionCapability::NpmInstallPackage(NpmInstallPackageCapability {
+                            package: capability.package,
+                        })
+                    }
+                })
+                .collect(),
         }
     }
 }

crates/extension_host/src/wasm_host.rs 🔗

@@ -1,15 +1,15 @@
 pub mod wit;
 
-use crate::ExtensionManifest;
 use crate::capability_granter::CapabilityGranter;
+use crate::{ExtensionManifest, ExtensionSettings};
 use anyhow::{Context as _, Result, anyhow, bail};
 use async_trait::async_trait;
 use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
 use extension::{
     CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
-    DebugTaskDefinition, DownloadFileCapability, ExtensionCapability, ExtensionHostProxy,
-    KeyValueStoreDelegate, NpmInstallPackageCapability, ProcessExecCapability, ProjectDelegate,
-    SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
+    DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate,
+    ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol,
+    WorktreeDelegate,
 };
 use fs::{Fs, normalize_path};
 use futures::future::LocalBoxFuture;
@@ -29,6 +29,7 @@ use moka::sync::Cache;
 use node_runtime::NodeRuntime;
 use release_channel::ReleaseChannel;
 use semantic_version::SemanticVersion;
+use settings::Settings;
 use std::borrow::Cow;
 use std::sync::{LazyLock, OnceLock};
 use std::time::Duration;
@@ -569,6 +570,9 @@ impl WasmHost {
                 message(cx).await;
             }
         });
+
+        let extension_settings = ExtensionSettings::get_global(cx);
+
         Arc::new(Self {
             engine: wasm_engine(cx.background_executor()),
             fs,
@@ -577,19 +581,7 @@ impl WasmHost {
             node_runtime,
             proxy,
             release_channel: ReleaseChannel::global(cx),
-            granted_capabilities: vec![
-                ExtensionCapability::ProcessExec(ProcessExecCapability {
-                    command: "*".to_string(),
-                    args: vec!["**".to_string()],
-                }),
-                ExtensionCapability::DownloadFile(DownloadFileCapability {
-                    host: "*".to_string(),
-                    path: vec!["**".to_string()],
-                }),
-                ExtensionCapability::NpmInstallPackage(NpmInstallPackageCapability {
-                    package: "*".to_string(),
-                }),
-            ],
+            granted_capabilities: extension_settings.granted_capabilities.clone(),
             _main_thread_message_task: task,
             main_thread_message_tx: tx,
         })

crates/remote_server/src/headless_project.rs 🔗

@@ -26,7 +26,7 @@ use rpc::{
     proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID},
 };
 
-use settings::initial_server_settings_content;
+use settings::{Settings as _, initial_server_settings_content};
 use smol::stream::StreamExt;
 use std::{
     path::{Path, PathBuf},
@@ -69,6 +69,7 @@ impl HeadlessProject {
         settings::init(cx);
         language::init(cx);
         project::Project::init_settings(cx);
+        extension_host::ExtensionSettings::register(cx);
         log_store::init(true, cx);
     }
 

crates/settings/src/settings_content.rs 🔗

@@ -1,5 +1,6 @@
 mod agent;
 mod editor;
+mod extension;
 mod language;
 mod language_model;
 mod project;
@@ -9,6 +10,7 @@ mod workspace;
 
 pub use agent::*;
 pub use editor::*;
+pub use extension::*;
 pub use language::*;
 pub use language_model::*;
 pub use project::*;
@@ -433,21 +435,6 @@ pub struct CallSettingsContent {
     pub share_on_join: Option<bool>,
 }
 
-#[skip_serializing_none]
-#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema, MergeFrom)]
-pub struct ExtensionSettingsContent {
-    /// The extensions that should be automatically installed by Zed.
-    ///
-    /// This is used to make functionality provided by extensions (e.g., language support)
-    /// available out-of-the-box.
-    ///
-    /// Default: { "html": true }
-    #[serde(default)]
-    pub auto_install_extensions: HashMap<Arc<str>, bool>,
-    #[serde(default)]
-    pub auto_update_extensions: HashMap<Arc<str>, bool>,
-}
-
 #[skip_serializing_none]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct GitPanelSettingsContent {

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

@@ -0,0 +1,59 @@
+use std::sync::Arc;
+
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use settings_macros::MergeFrom;
+
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
+pub struct ExtensionSettingsContent {
+    /// The extensions that should be automatically installed by Zed.
+    ///
+    /// This is used to make functionality provided by extensions (e.g., language support)
+    /// available out-of-the-box.
+    ///
+    /// Default: { "html": true }
+    #[serde(default)]
+    pub auto_install_extensions: HashMap<Arc<str>, bool>,
+    #[serde(default)]
+    pub auto_update_extensions: HashMap<Arc<str>, bool>,
+    /// The capabilities granted to extensions.
+    #[serde(default)]
+    pub granted_extension_capabilities: Option<Vec<ExtensionCapabilityContent>>,
+}
+
+/// A capability for an extension.
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub enum ExtensionCapabilityContent {
+    #[serde(rename = "process:exec")]
+    ProcessExec(ProcessExecCapabilityContent),
+    DownloadFile(DownloadFileCapabilityContent),
+    #[serde(rename = "npm:install")]
+    NpmInstallPackage(NpmInstallPackageCapabilityContent),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct ProcessExecCapabilityContent {
+    /// The command to execute.
+    pub command: String,
+    /// The arguments to pass to the command. Use `*` for a single wildcard argument.
+    /// If the last element is `**`, then any trailing arguments are allowed.
+    pub args: Vec<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct DownloadFileCapabilityContent {
+    pub host: String,
+    pub path: Vec<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct NpmInstallPackageCapabilityContent {
+    pub package: String,
+}