From 3bf71c690f53554d4bb44291b4875db40ee5e967 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 3 Oct 2025 11:55:01 -0400 Subject: [PATCH] extension_host: Load granted extension capabilities from settings (#39472) 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. --- assets/settings/default.json | 8 +++ .../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 +----- .../src/settings_content/extension.rs | 59 +++++++++++++++++++ 6 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 crates/settings/src/settings_content/extension.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 21cd3b84041b89344516145dbdfe79151199bf65..52f2973b2b29ddb5af9297eb20239d47c61a66bd 100644 --- a/assets/settings/default.json +++ b/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. diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index 0f15add8cbf7e422b2d90c684a46c464672ad1d6..f1c23877c0e48945958dc56de14a21a0e2533404 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/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, bool>, pub auto_update_extensions: HashMap, bool>, + pub granted_capabilities: Vec, } 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(), } } } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index a14bb860dbf19b3a42a989a33fa7fd6ea84d26a5..f77258e8957fa1be7579b931de82fd633a0f6ae4 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/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, }) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 96b33cdca3a39f24c1d06b7cb33fe97776cd942f..1d5e72ff9bd2457e6b1d4fbf313669742737379b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/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); } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index be725660e8bffdb50715b784bef8766f7e5e88eb..bc315553ca35b71f42bb8461bc6b92fb62a73818 100644 --- a/crates/settings/src/settings_content.rs +++ b/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, } -#[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, bool>, - #[serde(default)] - pub auto_update_extensions: HashMap, bool>, -} - #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct GitPanelSettingsContent { diff --git a/crates/settings/src/settings_content/extension.rs b/crates/settings/src/settings_content/extension.rs new file mode 100644 index 0000000000000000000000000000000000000000..1b5ce12a832e710b0be6cd6a5ee9cb97b95eeab9 --- /dev/null +++ b/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, bool>, + #[serde(default)] + pub auto_update_extensions: HashMap, bool>, + /// The capabilities granted to extensions. + #[serde(default)] + pub granted_extension_capabilities: Option>, +} + +/// 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, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DownloadFileCapabilityContent { + pub host: String, + pub path: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct NpmInstallPackageCapabilityContent { + pub package: String, +}