extension: Add capabilities for the process API (#26224)
This PR adds support for capabilities for the extension process API. In order to use the process API, an extension must declare which commands it wants to use, with arguments: ```toml [[capabilities]] kind = "process:exec" command = "echo" args = ["hello!"] ``` A `*` can be used to denote a single wildcard in the argument list: ```toml [[capabilities]] kind = "process:exec" command = "echo" args = ["*"] ``` And `**` can be used to denote a wildcard for the remaining arguments: ```toml [[capabilities]] kind = "process:exec" command = "ls" args = ["-a", "**"] ``` Release Notes: - N/A --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
99216acdec
commit
6fd9708eee
5 changed files with 170 additions and 1 deletions
|
@ -1,4 +1,4 @@
|
|||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use fs::Fs;
|
||||
use language::LanguageName;
|
||||
|
@ -85,6 +85,61 @@ pub struct ExtensionManifest {
|
|||
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
|
||||
#[serde(default)]
|
||||
pub snippets: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<ExtensionCapability>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub fn allow_exec(
|
||||
&self,
|
||||
desired_command: &str,
|
||||
desired_args: &[impl AsRef<str> + std::fmt::Debug],
|
||||
) -> Result<()> {
|
||||
let is_allowed = self.capabilities.iter().any(|capability| match capability {
|
||||
ExtensionCapability::ProcessExec { command, args } if command == desired_command => {
|
||||
for (ix, arg) in args.iter().enumerate() {
|
||||
if arg == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if arg != "*" && arg != desired_args[ix].as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if args.len() < desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if !is_allowed {
|
||||
bail!(
|
||||
"capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A capability for an extension.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ExtensionCapability {
|
||||
#[serde(rename = "process:exec")]
|
||||
ProcessExec {
|
||||
/// The command to execute.
|
||||
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.
|
||||
args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
|
@ -218,5 +273,104 @@ fn manifest_from_old_manifest(
|
|||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn extension_manifest() -> ExtensionManifest {
|
||||
ExtensionManifest {
|
||||
id: "test".into(),
|
||||
name: "Test".to_string(),
|
||||
version: "1.0.0".into(),
|
||||
schema_version: SchemaVersion::ZERO,
|
||||
description: None,
|
||||
repository: None,
|
||||
authors: vec![],
|
||||
lib: Default::default(),
|
||||
themes: vec![],
|
||||
icon_themes: vec![],
|
||||
languages: vec![],
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_exact_match() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "ls".to_string(),
|
||||
args: vec!["-la".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
|
||||
assert!(manifest.allow_exec("ls", &["-l"]).is_err());
|
||||
assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_wildcard_arg() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "git".to_string(),
|
||||
args: vec!["*".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("git", &["status"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["commit"]).is_ok());
|
||||
assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
|
||||
assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_double_wildcard() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "cargo".to_string(),
|
||||
args: vec!["test".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_mixed_wildcards() {
|
||||
let manifest = ExtensionManifest {
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "docker".to_string(),
|
||||
args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
|
||||
}],
|
||||
..extension_manifest()
|
||||
};
|
||||
|
||||
assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["run"]).is_err());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "ubuntu", "bash"])
|
||||
.is_ok());
|
||||
assert!(manifest
|
||||
.allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
|
||||
.is_ok());
|
||||
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,6 +163,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
@ -191,6 +192,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
@ -356,6 +358,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: Vec::new(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
|
|
@ -592,6 +592,8 @@ impl process::Host for WasmState {
|
|||
command: process::Command,
|
||||
) -> wasmtime::Result<Result<process::Output, String>> {
|
||||
maybe!(async {
|
||||
self.manifest.allow_exec(&command.command, &command.args)?;
|
||||
|
||||
let output = util::command::new_smol_command(command.command.as_str())
|
||||
.args(&command.args)
|
||||
.envs(command.env)
|
||||
|
|
|
@ -13,3 +13,8 @@ language = "Gleam"
|
|||
[grammars.gleam]
|
||||
repository = "https://github.com/gleam-lang/tree-sitter-gleam"
|
||||
commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
|
||||
|
||||
[[capabilities]]
|
||||
kind = "process:exec"
|
||||
command = "echo"
|
||||
args = ["hello!"]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::fs;
|
||||
use zed::lsp::CompletionKind;
|
||||
use zed::{CodeLabel, CodeLabelSpan, LanguageServerId};
|
||||
use zed_extension_api::process::Command;
|
||||
use zed_extension_api::{self as zed, Result};
|
||||
|
||||
struct TestExtension {
|
||||
|
@ -13,6 +14,10 @@ impl TestExtension {
|
|||
language_server_id: &LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
) -> Result<String> {
|
||||
let echo_output = Command::new("echo").arg("hello!").output()?;
|
||||
|
||||
println!("{}", String::from_utf8_lossy(&echo_output.stdout));
|
||||
|
||||
if let Some(path) = &self.cached_binary_path {
|
||||
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
|
||||
return Ok(path.clone());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue