mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-03 21:38:48 +03:00
feat: plugin subsystem final in-flight progress
This commit is contained in:
@@ -149,6 +149,12 @@ impl PluginPermission {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for PluginPermission {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PluginToolManifest {
|
pub struct PluginToolManifest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -224,7 +230,7 @@ struct RawPluginManifest {
|
|||||||
pub commands: Vec<PluginCommandManifest>,
|
pub commands: Vec<PluginCommandManifest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
struct RawPluginToolManifest {
|
struct RawPluginToolManifest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@@ -233,7 +239,10 @@ struct RawPluginToolManifest {
|
|||||||
pub command: String,
|
pub command: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
#[serde(rename = "requiredPermission", default = "default_raw_tool_permission")]
|
#[serde(
|
||||||
|
rename = "requiredPermission",
|
||||||
|
default = "default_tool_permission_label"
|
||||||
|
)]
|
||||||
pub required_permission: String,
|
pub required_permission: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +340,7 @@ impl PluginTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_raw_tool_permission() -> String {
|
fn default_tool_permission_label() -> String {
|
||||||
"danger-full-access".to_string()
|
"danger-full-access".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,17 +782,31 @@ pub struct UpdateOutcome {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum PluginManifestValidationError {
|
pub enum PluginManifestValidationError {
|
||||||
EmptyField { field: &'static str },
|
EmptyField {
|
||||||
|
field: &'static str,
|
||||||
|
},
|
||||||
EmptyEntryField {
|
EmptyEntryField {
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
InvalidPermission { permission: String },
|
InvalidPermission {
|
||||||
DuplicatePermission { permission: String },
|
permission: String,
|
||||||
DuplicateEntry { kind: &'static str, name: String },
|
},
|
||||||
MissingPath { kind: &'static str, path: PathBuf },
|
DuplicatePermission {
|
||||||
InvalidToolInputSchema { tool_name: String },
|
permission: String,
|
||||||
|
},
|
||||||
|
DuplicateEntry {
|
||||||
|
kind: &'static str,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
MissingPath {
|
||||||
|
kind: &'static str,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
InvalidToolInputSchema {
|
||||||
|
tool_name: String,
|
||||||
|
},
|
||||||
InvalidToolRequiredPermission {
|
InvalidToolRequiredPermission {
|
||||||
tool_name: String,
|
tool_name: String,
|
||||||
permission: String,
|
permission: String,
|
||||||
@@ -1316,89 +1339,34 @@ fn load_plugin_definition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_plugin_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
pub fn load_plugin_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
||||||
let manifest = load_manifest_from_directory(root)?;
|
load_manifest_from_directory(root)
|
||||||
validate_plugin_manifest(root, &manifest)?;
|
|
||||||
Ok(manifest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_validated_package_manifest_from_root(
|
fn load_validated_package_manifest_from_root(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
) -> Result<PluginPackageManifest, PluginError> {
|
) -> Result<PluginPackageManifest, PluginError> {
|
||||||
let manifest = load_package_manifest_from_root(root)?;
|
load_package_manifest_from_root(root)
|
||||||
validate_package_manifest(root, &manifest)?;
|
|
||||||
validate_hook_paths(Some(root), &manifest.hooks)?;
|
|
||||||
validate_lifecycle_paths(Some(root), &manifest.lifecycle)?;
|
|
||||||
Ok(manifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_plugin_manifest(root: &Path, manifest: &PluginManifest) -> Result<(), PluginError> {
|
|
||||||
if manifest.name.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest name cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if manifest.version.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest version cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if manifest.description.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest description cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
validate_named_strings(&manifest.permissions, "permission")?;
|
|
||||||
validate_hook_paths(Some(root), &manifest.hooks)?;
|
|
||||||
validate_named_commands(root, &manifest.tools, "tool")?;
|
|
||||||
validate_tool_manifest_entries(&manifest.tools)?;
|
|
||||||
validate_named_commands(root, &manifest.commands, "command")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_package_manifest(
|
|
||||||
root: &Path,
|
|
||||||
manifest: &PluginPackageManifest,
|
|
||||||
) -> Result<(), PluginError> {
|
|
||||||
if manifest.name.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest name cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if manifest.version.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest version cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if manifest.description.trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(
|
|
||||||
"plugin manifest description cannot be empty".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
validate_named_commands(root, &manifest.tools, "tool")?;
|
|
||||||
validate_tool_manifest_entries(&manifest.tools)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_manifest_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
fn load_manifest_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
||||||
let manifest_path = plugin_manifest_path(root)?;
|
let manifest_path = plugin_manifest_path(root)?;
|
||||||
let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
load_manifest_from_path(root, &manifest_path)
|
||||||
PluginError::NotFound(format!(
|
|
||||||
"plugin manifest not found at {}: {error}",
|
|
||||||
manifest_path.display()
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
Ok(serde_json::from_str(&contents)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_package_manifest_from_root(root: &Path) -> Result<PluginPackageManifest, PluginError> {
|
fn load_package_manifest_from_root(root: &Path) -> Result<PluginPackageManifest, PluginError> {
|
||||||
let manifest_path = root.join(MANIFEST_RELATIVE_PATH);
|
let manifest_path = root.join(MANIFEST_RELATIVE_PATH);
|
||||||
|
load_manifest_from_path(root, &manifest_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_manifest_from_path(root: &Path, manifest_path: &Path) -> Result<PluginManifest, PluginError> {
|
||||||
let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
||||||
PluginError::NotFound(format!(
|
PluginError::NotFound(format!(
|
||||||
"plugin manifest not found at {}: {error}",
|
"plugin manifest not found at {}: {error}",
|
||||||
manifest_path.display()
|
manifest_path.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(serde_json::from_str(&contents)?)
|
let raw_manifest: RawPluginManifest = serde_json::from_str(&contents)?;
|
||||||
|
build_plugin_manifest(root, raw_manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
|
fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
|
||||||
@@ -1419,76 +1387,238 @@ fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_named_strings(entries: &[String], kind: &str) -> Result<(), PluginError> {
|
fn build_plugin_manifest(root: &Path, raw: RawPluginManifest) -> Result<PluginManifest, PluginError> {
|
||||||
let mut seen = BTreeSet::<&str>::new();
|
let mut errors = Vec::new();
|
||||||
for entry in entries {
|
|
||||||
let trimmed = entry.trim();
|
validate_required_manifest_field("name", &raw.name, &mut errors);
|
||||||
if trimmed.is_empty() {
|
validate_required_manifest_field("version", &raw.version, &mut errors);
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
validate_required_manifest_field("description", &raw.description, &mut errors);
|
||||||
"plugin manifest {kind} cannot be empty"
|
|
||||||
)));
|
let permissions = build_manifest_permissions(&raw.permissions, &mut errors);
|
||||||
}
|
validate_command_entries(root, raw.hooks.pre_tool_use.iter(), "hook", &mut errors);
|
||||||
if !seen.insert(trimmed) {
|
validate_command_entries(root, raw.hooks.post_tool_use.iter(), "hook", &mut errors);
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
validate_command_entries(root, raw.lifecycle.init.iter(), "lifecycle command", &mut errors);
|
||||||
"plugin manifest {kind} `{trimmed}` is duplicated"
|
validate_command_entries(
|
||||||
)));
|
root,
|
||||||
}
|
raw.lifecycle.shutdown.iter(),
|
||||||
}
|
"lifecycle command",
|
||||||
Ok(())
|
&mut errors,
|
||||||
|
);
|
||||||
|
let tools = build_manifest_tools(root, raw.tools, &mut errors);
|
||||||
|
let commands = build_manifest_commands(root, raw.commands, &mut errors);
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Err(PluginError::ManifestValidation(errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_named_commands(
|
Ok(PluginManifest {
|
||||||
root: &Path,
|
name: raw.name,
|
||||||
entries: &[impl NamedCommand],
|
version: raw.version,
|
||||||
kind: &str,
|
description: raw.description,
|
||||||
) -> Result<(), PluginError> {
|
permissions,
|
||||||
let mut seen = BTreeSet::<&str>::new();
|
default_enabled: raw.default_enabled,
|
||||||
for entry in entries {
|
hooks: raw.hooks,
|
||||||
let name = entry.name().trim();
|
lifecycle: raw.lifecycle,
|
||||||
if name.is_empty() {
|
tools,
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
commands,
|
||||||
"plugin {kind} name cannot be empty"
|
})
|
||||||
)));
|
|
||||||
}
|
|
||||||
if !seen.insert(name) {
|
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
|
||||||
"plugin {kind} `{name}` is duplicated"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if entry.description().trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
|
||||||
"plugin {kind} `{name}` description cannot be empty"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if entry.command().trim().is_empty() {
|
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
|
||||||
"plugin {kind} `{name}` command cannot be empty"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
validate_command_path(root, entry.command(), kind)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_tool_manifest_entries(entries: &[PluginToolManifest]) -> Result<(), PluginError> {
|
fn validate_required_manifest_field(
|
||||||
for entry in entries {
|
field: &'static str,
|
||||||
if !entry.input_schema.is_object() {
|
value: &str,
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
"plugin tool `{}` inputSchema must be a JSON object",
|
|
||||||
entry.name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if !matches!(
|
|
||||||
entry.required_permission.as_str(),
|
|
||||||
"read-only" | "workspace-write" | "danger-full-access"
|
|
||||||
) {
|
) {
|
||||||
return Err(PluginError::InvalidManifest(format!(
|
if value.trim().is_empty() {
|
||||||
"plugin tool `{}` requiredPermission must be read-only, workspace-write, or danger-full-access",
|
errors.push(PluginManifestValidationError::EmptyField { field });
|
||||||
entry.name
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
fn build_manifest_permissions(
|
||||||
|
permissions: &[String],
|
||||||
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
|
) -> Vec<PluginPermission> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut validated = Vec::new();
|
||||||
|
|
||||||
|
for permission in permissions {
|
||||||
|
let permission = permission.trim();
|
||||||
|
if permission.is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "permission",
|
||||||
|
field: "value",
|
||||||
|
name: None,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert(permission.to_string()) {
|
||||||
|
errors.push(PluginManifestValidationError::DuplicatePermission {
|
||||||
|
permission: permission.to_string(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match PluginPermission::parse(permission) {
|
||||||
|
Some(permission) => validated.push(permission),
|
||||||
|
None => errors.push(PluginManifestValidationError::InvalidPermission {
|
||||||
|
permission: permission.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validated
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_manifest_tools(
|
||||||
|
root: &Path,
|
||||||
|
tools: Vec<RawPluginToolManifest>,
|
||||||
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
|
) -> Vec<PluginToolManifest> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut validated = Vec::new();
|
||||||
|
|
||||||
|
for tool in tools {
|
||||||
|
let name = tool.name.trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "tool",
|
||||||
|
field: "name",
|
||||||
|
name: None,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert(name.clone()) {
|
||||||
|
errors.push(PluginManifestValidationError::DuplicateEntry {
|
||||||
|
kind: "tool",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if tool.description.trim().is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "tool",
|
||||||
|
field: "description",
|
||||||
|
name: Some(name.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if tool.command.trim().is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "tool",
|
||||||
|
field: "command",
|
||||||
|
name: Some(name.clone()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
validate_command_entry(root, &tool.command, "tool", errors);
|
||||||
|
}
|
||||||
|
if !tool.input_schema.is_object() {
|
||||||
|
errors.push(PluginManifestValidationError::InvalidToolInputSchema {
|
||||||
|
tool_name: name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let Some(required_permission) = PluginToolPermission::parse(tool.required_permission.trim()) else {
|
||||||
|
errors.push(PluginManifestValidationError::InvalidToolRequiredPermission {
|
||||||
|
tool_name: name.clone(),
|
||||||
|
permission: tool.required_permission.trim().to_string(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
validated.push(PluginToolManifest {
|
||||||
|
name,
|
||||||
|
description: tool.description,
|
||||||
|
input_schema: tool.input_schema,
|
||||||
|
command: tool.command,
|
||||||
|
args: tool.args,
|
||||||
|
required_permission,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validated
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_manifest_commands(
|
||||||
|
root: &Path,
|
||||||
|
commands: Vec<PluginCommandManifest>,
|
||||||
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
|
) -> Vec<PluginCommandManifest> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut validated = Vec::new();
|
||||||
|
|
||||||
|
for command in commands {
|
||||||
|
let name = command.name.trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "command",
|
||||||
|
field: "name",
|
||||||
|
name: None,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !seen.insert(name.clone()) {
|
||||||
|
errors.push(PluginManifestValidationError::DuplicateEntry {
|
||||||
|
kind: "command",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if command.description.trim().is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "command",
|
||||||
|
field: "description",
|
||||||
|
name: Some(name.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if command.command.trim().is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind: "command",
|
||||||
|
field: "command",
|
||||||
|
name: Some(name.clone()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
validate_command_entry(root, &command.command, "command", errors);
|
||||||
|
}
|
||||||
|
validated.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
validated
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_command_entries<'a>(
|
||||||
|
root: &Path,
|
||||||
|
entries: impl Iterator<Item = &'a String>,
|
||||||
|
kind: &'static str,
|
||||||
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
|
) {
|
||||||
|
for entry in entries {
|
||||||
|
validate_command_entry(root, entry, kind, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_command_entry(
|
||||||
|
root: &Path,
|
||||||
|
entry: &str,
|
||||||
|
kind: &'static str,
|
||||||
|
errors: &mut Vec<PluginManifestValidationError>,
|
||||||
|
) {
|
||||||
|
if entry.trim().is_empty() {
|
||||||
|
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||||
|
kind,
|
||||||
|
field: "command",
|
||||||
|
name: None,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if is_literal_command(entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = if Path::new(entry).is_absolute() {
|
||||||
|
PathBuf::from(entry)
|
||||||
|
} else {
|
||||||
|
root.join(entry)
|
||||||
|
};
|
||||||
|
if !path.exists() {
|
||||||
|
errors.push(PluginManifestValidationError::MissingPath { kind, path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait NamedCommand {
|
trait NamedCommand {
|
||||||
@@ -1574,7 +1704,7 @@ fn resolve_tools(
|
|||||||
},
|
},
|
||||||
resolve_hook_entry(root, &tool.command),
|
resolve_hook_entry(root, &tool.command),
|
||||||
tool.args.clone(),
|
tool.args.clone(),
|
||||||
tool.required_permission.clone(),
|
tool.required_permission,
|
||||||
Some(root.to_path_buf()),
|
Some(root.to_path_buf()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -2030,7 +2160,14 @@ mod tests {
|
|||||||
let manifest = load_plugin_from_directory(&root).expect("manifest should load");
|
let manifest = load_plugin_from_directory(&root).expect("manifest should load");
|
||||||
assert_eq!(manifest.name, "loader-demo");
|
assert_eq!(manifest.name, "loader-demo");
|
||||||
assert_eq!(manifest.version, "1.2.3");
|
assert_eq!(manifest.version, "1.2.3");
|
||||||
assert_eq!(manifest.permissions, vec!["read", "write"]);
|
assert_eq!(
|
||||||
|
manifest
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.map(|permission| permission.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["read", "write"]
|
||||||
|
);
|
||||||
assert_eq!(manifest.hooks.pre_tool_use, vec!["./hooks/pre.sh"]);
|
assert_eq!(manifest.hooks.pre_tool_use, vec!["./hooks/pre.sh"]);
|
||||||
assert_eq!(manifest.tools.len(), 1);
|
assert_eq!(manifest.tools.len(), 1);
|
||||||
assert_eq!(manifest.tools[0].name, "echo_tool");
|
assert_eq!(manifest.tools[0].name, "echo_tool");
|
||||||
|
|||||||
Reference in New Issue
Block a user