mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
Align Rust plugin skill and agent loading with upstream routing semantics
Rust was still treating local skills as flat roots and had no plugin-backed discovery for /skills, /agents, or Skill tool resolution. This patch adds plugin manifest component paths, recursive namespaced discovery, plugin-prefixed skill/agent listing, and bare-name invoke routing that falls back to unique namespaced suffix matches. The implementation stays narrow to loading and routing: plugin tools and UI flows remain unchanged. Focused tests cover manifest parsing, plugin/local discovery, plugin-prefixed reports, unique plugin suffix resolution, and ambiguous bare-name failures. Constraint: Keep scope limited to plugin/skill/agent loading and invoke routing parity; no UI work Rejected: Introduce a new shared discovery crate | unnecessary drift for a parity patch Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep plugin skill and agent names prefixed with the plugin manifest name so bare-name suffix resolution stays deterministic Tested: cargo check; cargo test Not-tested: Runtime interactive UI rendering for /skills and /agents beyond report output
This commit is contained in:
@@ -119,6 +119,10 @@ pub struct PluginManifest {
|
||||
pub tools: Vec<PluginToolManifest>,
|
||||
#[serde(default)]
|
||||
pub commands: Vec<PluginCommandManifest>,
|
||||
#[serde(default)]
|
||||
pub agents: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub skills: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
@@ -228,6 +232,10 @@ struct RawPluginManifest {
|
||||
pub tools: Vec<RawPluginToolManifest>,
|
||||
#[serde(default)]
|
||||
pub commands: Vec<PluginCommandManifest>,
|
||||
#[serde(default, deserialize_with = "deserialize_string_list")]
|
||||
pub agents: Vec<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_string_list")]
|
||||
pub skills: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -246,6 +254,24 @@ struct RawPluginToolManifest {
|
||||
pub required_permission: String,
|
||||
}
|
||||
|
||||
fn deserialize_string_list<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StringList {
|
||||
One(String),
|
||||
Many(Vec<String>),
|
||||
}
|
||||
|
||||
Ok(match Option::<StringList>::deserialize(deserializer)? {
|
||||
Some(StringList::One(value)) => vec![value],
|
||||
Some(StringList::Many(values)) => values,
|
||||
None => Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PluginTool {
|
||||
plugin_id: String,
|
||||
@@ -1461,6 +1487,8 @@ fn build_plugin_manifest(
|
||||
"lifecycle command",
|
||||
&mut errors,
|
||||
);
|
||||
let agents = build_manifest_paths(root, raw.agents, "agent", &mut errors);
|
||||
let skills = build_manifest_paths(root, raw.skills, "skill", &mut errors);
|
||||
let tools = build_manifest_tools(root, raw.tools, &mut errors);
|
||||
let commands = build_manifest_commands(root, raw.commands, &mut errors);
|
||||
|
||||
@@ -1478,6 +1506,8 @@ fn build_plugin_manifest(
|
||||
lifecycle: raw.lifecycle,
|
||||
tools,
|
||||
commands,
|
||||
agents,
|
||||
skills,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1593,6 +1623,47 @@ fn build_manifest_tools(
|
||||
validated
|
||||
}
|
||||
|
||||
fn build_manifest_paths(
|
||||
root: &Path,
|
||||
paths: Vec<String>,
|
||||
kind: &'static str,
|
||||
errors: &mut Vec<PluginManifestValidationError>,
|
||||
) -> Vec<String> {
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut validated = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
errors.push(PluginManifestValidationError::EmptyEntryField {
|
||||
kind,
|
||||
field: "path",
|
||||
name: None,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let resolved = if Path::new(trimmed).is_absolute() {
|
||||
PathBuf::from(trimmed)
|
||||
} else {
|
||||
root.join(trimmed)
|
||||
};
|
||||
if !resolved.exists() {
|
||||
errors.push(PluginManifestValidationError::MissingPath {
|
||||
kind,
|
||||
path: resolved,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if seen.insert(trimmed.to_string()) {
|
||||
validated.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
validated
|
||||
}
|
||||
|
||||
fn build_manifest_commands(
|
||||
root: &Path,
|
||||
commands: Vec<PluginCommandManifest>,
|
||||
@@ -2227,6 +2298,38 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_plugin_from_directory_parses_agent_and_skill_paths() {
|
||||
let root = temp_dir("manifest-component-paths");
|
||||
write_file(
|
||||
root.join("agents").join("ops").join("triage.md").as_path(),
|
||||
"---\nname: triage\ndescription: triage agent\n---\n",
|
||||
);
|
||||
write_file(
|
||||
root.join("skills")
|
||||
.join("review")
|
||||
.join("SKILL.md")
|
||||
.as_path(),
|
||||
"---\nname: review\ndescription: review skill\n---\n",
|
||||
);
|
||||
write_file(
|
||||
root.join(MANIFEST_FILE_NAME).as_path(),
|
||||
r#"{
|
||||
"name": "component-paths",
|
||||
"version": "1.0.0",
|
||||
"description": "Manifest component paths",
|
||||
"agents": "./agents/ops/triage.md",
|
||||
"skills": ["./skills"]
|
||||
}"#,
|
||||
);
|
||||
|
||||
let manifest = load_plugin_from_directory(&root).expect("manifest should load");
|
||||
assert_eq!(manifest.agents, vec!["./agents/ops/triage.md"]);
|
||||
assert_eq!(manifest.skills, vec!["./skills"]);
|
||||
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_plugin_from_directory_defaults_optional_fields() {
|
||||
let root = temp_dir("manifest-defaults");
|
||||
|
||||
Reference in New Issue
Block a user