use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokenPlugin {
pub path: PathBuf,
pub error: String,
}
#[derive(Debug, Clone, Default)]
pub struct PluginDiscovery {
pub plugins: Vec<rightkit_agent_plugins::PluginManifest>,
pub broken: Vec<BrokenPlugin>,
}
pub fn discover_plugin_report(dir: &Path) -> Result<PluginDiscovery, String> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(PluginDiscovery::default())
}
Err(error) => return Err(error.to_string()),
};
let mut report = PluginDiscovery::default();
for entry in entries {
let entry = entry.map_err(|error| error.to_string())?;
let root = entry.path();
let hidden = entry.file_name().to_string_lossy().starts_with('.');
if hidden || !root.is_dir() {
continue;
}
let manifest_path = root.join("plugin.json");
if !manifest_path.exists() {
continue;
}
let parsed = std::fs::read_to_string(&manifest_path)
.map_err(|error| error.to_string())
.and_then(|raw| {
rightkit_agent_plugins::PluginManifest::from_json(&raw)
.map_err(|error| error.to_string())
});
match parsed {
Ok(manifest) => {
if report.plugins.iter().all(|plugin| plugin.id != manifest.id) {
report.plugins.push(manifest);
}
}
Err(error) => report.broken.push(BrokenPlugin {
path: manifest_path,
error,
}),
}
}
report.plugins.sort_by(|left, right| {
left.id
.cmp(&right.id)
.then_with(|| left.version.cmp(&right.version))
});
report
.broken
.sort_by(|left, right| left.path.cmp(&right.path));
Ok(report)
}
pub fn discover_plugins(dir: &Path) -> Result<Vec<rightkit_agent_plugins::PluginManifest>, String> {
Ok(discover_plugin_report(dir)?.plugins)
}
pub fn plugin_manifests_with_roots(
dir: &Path,
) -> Vec<(rightkit_agent_plugins::PluginManifest, PathBuf)> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut manifests = Vec::new();
for entry in entries.flatten() {
let root = entry.path();
if !root.is_dir() {
continue;
}
let manifest_path = root.join("plugin.json");
let Ok(raw) = std::fs::read_to_string(&manifest_path) else {
continue;
};
let Ok(manifest) = rightkit_agent_plugins::PluginManifest::from_json(&raw) else {
continue;
};
manifests.push((manifest, root));
}
manifests.sort_by(|(left, _), (right, _)| {
left.id
.cmp(&right.id)
.then_with(|| left.version.cmp(&right.version))
});
manifests
}
pub fn provider_extension_documents(dir: &Path) -> Result<Vec<String>, String> {
let mut documents = Vec::new();
for (manifest, root) in plugin_manifests_with_roots(dir) {
if !manifest.enabled {
continue;
}
let path = root.join("provider.json");
if path.exists() {
documents.push(std::fs::read_to_string(path).map_err(|error| error.to_string())?);
}
}
Ok(documents)
}
pub fn local_plugin_middleware_definitions(
dir: &Path,
) -> Vec<rightkit_agent_engine_runtime_middleware::MiddlewareDefinition> {
let mut definitions = Vec::new();
for (manifest, _) in plugin_manifests_with_roots(dir) {
if !manifest.enabled {
continue;
}
for contribution in manifest.middleware {
if let Some(definition) = plugin_middleware_definition(&manifest.id, contribution) {
definitions.push(definition);
}
}
}
definitions.sort_by(|left, right| {
left.order
.cmp(&right.order)
.then_with(|| left.id.cmp(&right.id))
});
definitions
}
fn plugin_middleware_definition(
plugin_id: &str,
contribution: rightkit_agent_plugins::PluginMiddlewareContribution,
) -> Option<rightkit_agent_engine_runtime_middleware::MiddlewareDefinition> {
use rightkit_agent_engine_runtime_middleware::{
MiddlewareCondition, MiddlewareDefinition, MiddlewareSource,
};
let phase = parse_middleware_phase(&contribution.phase)?;
let effect = parse_middleware_effect(&contribution.effect)?;
let id = contribution.id.trim();
if id.is_empty() {
return None;
}
Some(MiddlewareDefinition {
id: format!("plugin.{plugin_id}.{id}"),
title: if contribution.title.trim().is_empty() {
format!("{plugin_id}: {id}")
} else {
contribution.title
},
source: MiddlewareSource::LocalPlugin,
phase,
effect,
tool: contribution
.tool
.map(|tool| tool.trim().to_string())
.filter(|tool| !tool.is_empty()),
requires_toolset: contribution
.requires_toolset
.map(|toolset| toolset.trim().to_string())
.filter(|toolset| !toolset.is_empty()),
condition: contribution.condition.map(|condition| MiddlewareCondition {
argument: condition
.argument
.map(|argument| argument.trim().to_string())
.filter(|argument| !argument.is_empty()),
contains: condition
.contains
.map(|contains| contains.trim().to_string())
.filter(|contains| !contains.is_empty()),
equals: condition.equals,
}),
mutate_args: contribution.mutate_args,
reason: contribution
.reason
.map(|reason| reason.trim().to_string())
.filter(|reason| !reason.is_empty()),
enabled: contribution.enabled.unwrap_or(true),
order: contribution.order.unwrap_or(100),
description: contribution.description,
})
}
fn parse_middleware_phase(
value: &str,
) -> Option<rightkit_agent_engine_runtime_middleware::MiddlewarePhase> {
use rightkit_agent_engine_runtime_middleware::MiddlewarePhase;
match value {
"sessionStart" => Some(MiddlewarePhase::SessionStart),
"preLlmCall" => Some(MiddlewarePhase::PreLlmCall),
"postLlmCall" => Some(MiddlewarePhase::PostLlmCall),
"preToolCall" => Some(MiddlewarePhase::PreToolCall),
"postToolCall" => Some(MiddlewarePhase::PostToolCall),
"approvalRequested" => Some(MiddlewarePhase::ApprovalRequested),
"subagentStart" => Some(MiddlewarePhase::SubagentStart),
"subagentStop" => Some(MiddlewarePhase::SubagentStop),
"scheduledJobStart" => Some(MiddlewarePhase::ScheduledJobStart),
"scheduledJobEnd" => Some(MiddlewarePhase::ScheduledJobEnd),
"sessionEnd" => Some(MiddlewarePhase::SessionEnd),
_ => None,
}
}
fn parse_middleware_effect(
value: &str,
) -> Option<rightkit_agent_engine_runtime_middleware::MiddlewareEffect> {
use rightkit_agent_engine_runtime_middleware::MiddlewareEffect;
match value {
"observe" => Some(MiddlewareEffect::Observe),
"mutateArgs" => Some(MiddlewareEffect::MutateArgs),
"requireApproval" => Some(MiddlewareEffect::RequireApproval),
"deny" => Some(MiddlewareEffect::Deny),
_ => None,
}
}