use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use crate::capabilities::{CapabilityDenial, CapabilityGrant, CapabilityRequest, CapabilityStatus};
pub type ExtensionId = String;
pub type ApiVersion = String;
pub type InferenceEngineId = String;
pub type InferenceRouterId = String;
pub type ContextProviderId = String;
pub type ContextPlannerId = String;
pub type ThreadStoreId = String;
pub type CheckpointStoreId = String;
pub type MemoryStoreId = String;
pub type KnowledgeStoreId = String;
pub type EmbeddingProviderId = String;
pub type MediaGeneratorProviderId = String;
pub type ToolProviderId = String;
pub type SubagentDispatcherId = String;
pub type PolicyContributorId = String;
pub type EventSinkId = String;
pub type TaskExecutorId = String;
pub type NotificationSinkId = String;
pub type InteractiveRegionHandlerId = String;
pub type SpeechTranscriberId = String;
pub type SpeechSynthesizerId = String;
pub type VersionControlProviderId = crate::version_control::VcsProviderId;
pub const SUPPORTED_EXTENSION_API_VERSION: &str = "0.1.0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ProvidedService {
InferenceEngine(InferenceEngineId),
InferenceRouter(InferenceRouterId),
ContextProvider(ContextProviderId),
ContextPlanner(ContextPlannerId),
ThreadStore(ThreadStoreId),
CheckpointStore(CheckpointStoreId),
MemoryStore(MemoryStoreId),
KnowledgeStore(KnowledgeStoreId),
EmbeddingProvider(EmbeddingProviderId),
MediaGenerator(MediaGeneratorProviderId),
ToolProvider(ToolProviderId),
SubagentDispatcher(SubagentDispatcherId),
PolicyContributor(PolicyContributorId),
EventSink(EventSinkId),
ForkProvider(crate::forks::ForkProviderId),
TaskExecutor(TaskExecutorId),
NotificationSink(NotificationSinkId),
InteractiveRegionHandler(InteractiveRegionHandlerId),
SpeechTranscriber(SpeechTranscriberId),
SpeechSynthesizer(SpeechSynthesizerId),
VersionControlProvider(VersionControlProviderId),
RemoteRunnerProvider(crate::remote_runner::RemoteRunnerProviderId),
StatusSegment(crate::tui_status::StatusSegmentId),
PaletteSource(crate::tui_status::PaletteSourceId),
CodeIndexProvider(crate::code_index::CodeIndexProviderId),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionManifest {
pub id: ExtensionId,
pub name: String,
pub version: Version,
pub api_version: ApiVersion,
pub description: Option<String>,
pub provides: Vec<ProvidedService>,
pub required_capabilities: Vec<CapabilityRequest>,
}
pub trait RoderExtension: Send + Sync + 'static {
fn manifest(&self) -> ExtensionManifest;
fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()>;
}
impl<E: RoderExtension + ?Sized> RoderExtension for Arc<E> {
fn manifest(&self) -> ExtensionManifest {
(**self).manifest()
}
fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
(**self).install(registry)
}
}
#[derive(Clone)]
pub struct ExtensionRegistry {
pub manifests: Vec<ExtensionManifest>,
pub capability_statuses: BTreeMap<ExtensionId, Vec<CapabilityStatus>>,
pub inference_engines: Vec<Arc<dyn crate::inference::InferenceEngine>>,
pub inference_routers: Vec<Arc<dyn crate::inference_routing::InferenceRouter>>,
pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
pub context_planners: Vec<Arc<dyn crate::context::ContextPlanner>>,
pub thread_stores: Vec<Arc<dyn crate::thread::ThreadStoreFactory>>,
pub checkpoint_stores: Vec<Arc<dyn crate::thread::CheckpointStoreFactory>>,
pub memory_stores: Vec<Arc<dyn crate::memory::MemoryStoreFactory>>,
pub knowledge_stores: Vec<Arc<dyn crate::knowledge::KnowledgeStoreFactory>>,
pub embedding_providers: Vec<Arc<dyn crate::embeddings::EmbeddingProvider>>,
pub media_generator_providers: Vec<Arc<dyn crate::media::MediaGeneratorProvider>>,
pub tools: Vec<Arc<dyn crate::tools::ToolContributor>>,
pub subagent_dispatchers: Vec<Arc<dyn crate::subagents::SubagentDispatcher>>,
pub policy_contributors: Vec<Arc<dyn crate::context::PolicyContributor>>,
pub event_sinks: Vec<Arc<dyn crate::extension::EventSink>>,
pub fork_providers: Vec<Arc<dyn crate::forks::ForkProvider>>,
pub task_executors: Vec<Arc<dyn crate::tasks::TaskExecutor>>,
pub notification_sinks: Vec<Arc<dyn crate::notifications::NotificationSink>>,
pub interactive_region_handlers: Vec<Arc<dyn crate::interactive::InteractiveRegionHandler>>,
pub speech_transcribers: Vec<Arc<dyn crate::speech::SpeechTranscriber>>,
pub speech_synthesizers: Vec<Arc<dyn crate::speech::SpeechSynthesizer>>,
pub version_control_providers: Vec<Arc<dyn crate::version_control::VcsProvider>>,
pub remote_runner_providers: Vec<Arc<dyn crate::remote_runner::RemoteRunnerProvider>>,
pub status_segments: Vec<crate::tui_status::StatusSegment>,
pub palette_sources: Vec<crate::tui_status::PaletteSourceDescriptor>,
pub code_index_providers: Vec<Arc<dyn crate::code_index::CodeIndexProvider>>,
}
impl ExtensionRegistry {
pub fn media_generator(
&self,
id: &str,
) -> Option<Arc<dyn crate::media::MediaGeneratorProvider>> {
self.media_generator_providers
.iter()
.find(|provider| provider.provider_id() == id)
.cloned()
}
pub fn inference_engine(&self, id: &str) -> Option<Arc<dyn crate::inference::InferenceEngine>> {
self.inference_engines
.iter()
.find(|engine| engine.id() == id)
.cloned()
}
pub fn default_inference_engine(&self) -> Option<Arc<dyn crate::inference::InferenceEngine>> {
self.inference_engines.first().cloned()
}
pub fn inference_router(
&self,
id: &str,
) -> Option<Arc<dyn crate::inference_routing::InferenceRouter>> {
self.inference_routers
.iter()
.find(|router| router.id() == id)
.cloned()
}
pub fn speech_transcriber(
&self,
id: &str,
) -> Option<Arc<dyn crate::speech::SpeechTranscriber>> {
self.speech_transcribers
.iter()
.find(|transcriber| transcriber.id() == id)
.cloned()
}
pub fn speech_synthesizer(
&self,
id: &str,
) -> Option<Arc<dyn crate::speech::SpeechSynthesizer>> {
self.speech_synthesizers
.iter()
.find(|synthesizer| synthesizer.id() == id)
.cloned()
}
pub fn fork_provider(
&self,
id: &str,
) -> Option<Arc<dyn crate::forks::ForkProvider>> {
self.fork_providers
.iter()
.find(|provider| provider.descriptor().id == id)
.cloned()
}
pub fn provided_services(&self) -> Vec<ProvidedService> {
self.manifests
.iter()
.flat_map(|manifest| manifest.provides.iter().cloned())
.collect()
}
pub fn capability_statuses(&self, extension_id: &str) -> &[CapabilityStatus] {
self.capability_statuses
.get(extension_id)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn subagent_dispatcher(
&self,
id: &str,
) -> Option<Arc<dyn crate::subagents::SubagentDispatcher>> {
self.subagent_dispatchers
.iter()
.find(|dispatcher| dispatcher.id() == id)
.cloned()
}
pub fn version_control_provider(
&self,
id: &str,
) -> Option<Arc<dyn crate::version_control::VcsProvider>> {
self.version_control_providers
.iter()
.find(|provider| provider.id() == id)
.cloned()
}
pub fn version_control_resolver(&self) -> crate::version_control::RegistryVcsProviderResolver {
crate::version_control::RegistryVcsProviderResolver::new(
self.version_control_providers.clone(),
)
}
}
pub struct ExtensionRegistryBuilder {
manifests: Vec<ExtensionManifest>,
granted_capabilities: BTreeMap<ExtensionId, BTreeSet<String>>,
denied_capabilities: BTreeMap<ExtensionId, BTreeMap<String, String>>,
pub inference_engines: Vec<Arc<dyn crate::inference::InferenceEngine>>,
pub inference_routers: Vec<Arc<dyn crate::inference_routing::InferenceRouter>>,
pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
pub context_planners: Vec<Arc<dyn crate::context::ContextPlanner>>,
pub thread_stores: Vec<Arc<dyn crate::thread::ThreadStoreFactory>>,
pub checkpoint_stores: Vec<Arc<dyn crate::thread::CheckpointStoreFactory>>,
pub memory_stores: Vec<Arc<dyn crate::memory::MemoryStoreFactory>>,
pub knowledge_stores: Vec<Arc<dyn crate::knowledge::KnowledgeStoreFactory>>,
pub embedding_providers: Vec<Arc<dyn crate::embeddings::EmbeddingProvider>>,
pub media_generator_providers: Vec<Arc<dyn crate::media::MediaGeneratorProvider>>,
pub tools: Vec<Arc<dyn crate::tools::ToolContributor>>,
pub subagent_dispatchers: Vec<Arc<dyn crate::subagents::SubagentDispatcher>>,
pub policy_contributors: Vec<Arc<dyn crate::context::PolicyContributor>>,
pub event_sinks: Vec<Arc<dyn crate::extension::EventSink>>,
pub fork_providers: Vec<Arc<dyn crate::forks::ForkProvider>>,
pub task_executors: Vec<Arc<dyn crate::tasks::TaskExecutor>>,
pub notification_sinks: Vec<Arc<dyn crate::notifications::NotificationSink>>,
pub interactive_region_handlers: Vec<Arc<dyn crate::interactive::InteractiveRegionHandler>>,
pub speech_transcribers: Vec<Arc<dyn crate::speech::SpeechTranscriber>>,
pub speech_synthesizers: Vec<Arc<dyn crate::speech::SpeechSynthesizer>>,
pub version_control_providers: Vec<Arc<dyn crate::version_control::VcsProvider>>,
pub remote_runner_providers: Vec<Arc<dyn crate::remote_runner::RemoteRunnerProvider>>,
pub status_segments: Vec<crate::tui_status::StatusSegment>,
pub palette_sources: Vec<crate::tui_status::PaletteSourceDescriptor>,
pub code_index_providers: Vec<Arc<dyn crate::code_index::CodeIndexProvider>>,
}
impl Default for ExtensionRegistryBuilder {
fn default() -> Self {
Self::new()
}
}
impl ExtensionRegistryBuilder {
pub fn new() -> Self {
Self {
manifests: Vec::new(),
granted_capabilities: BTreeMap::new(),
denied_capabilities: BTreeMap::new(),
inference_engines: Vec::new(),
inference_routers: Vec::new(),
context_providers: Vec::new(),
context_planners: Vec::new(),
thread_stores: Vec::new(),
checkpoint_stores: Vec::new(),
memory_stores: Vec::new(),
knowledge_stores: Vec::new(),
embedding_providers: Vec::new(),
media_generator_providers: Vec::new(),
tools: Vec::new(),
subagent_dispatchers: Vec::new(),
policy_contributors: Vec::new(),
event_sinks: Vec::new(),
fork_providers: Vec::new(),
task_executors: Vec::new(),
notification_sinks: Vec::new(),
interactive_region_handlers: Vec::new(),
speech_transcribers: Vec::new(),
speech_synthesizers: Vec::new(),
version_control_providers: Vec::new(),
remote_runner_providers: Vec::new(),
status_segments: Vec::new(),
palette_sources: Vec::new(),
code_index_providers: Vec::new(),
}
}
pub fn install<E: RoderExtension>(&mut self, ext: E) -> anyhow::Result<()> {
let manifest = ext.manifest();
if self
.manifests
.iter()
.any(|existing| existing.id == manifest.id)
{
anyhow::bail!("extension {} is already installed", manifest.id);
}
let before = service_counts(self)?;
ext.install(self)?;
let declared: BTreeSet<ProvidedService> = manifest.provides.iter().cloned().collect();
for (service, count) in service_counts(self)? {
let prior = before.get(&service).copied().unwrap_or(0);
if count > prior && !declared.contains(&service) {
anyhow::bail!(
"extension {} installed undeclared service {}; declare it in the manifest provides list",
manifest.id,
service_label(&service)
);
}
}
self.manifests.push(manifest);
Ok(())
}
pub fn build(self) -> anyhow::Result<ExtensionRegistry> {
let validation = self.validate()?;
Ok(ExtensionRegistry {
manifests: self.manifests,
capability_statuses: validation.capability_statuses,
inference_engines: self.inference_engines,
inference_routers: self.inference_routers,
context_providers: self.context_providers,
context_planners: self.context_planners,
thread_stores: self.thread_stores,
checkpoint_stores: self.checkpoint_stores,
memory_stores: self.memory_stores,
knowledge_stores: self.knowledge_stores,
embedding_providers: self.embedding_providers,
media_generator_providers: self.media_generator_providers,
tools: self.tools,
subagent_dispatchers: self.subagent_dispatchers,
policy_contributors: self.policy_contributors,
event_sinks: self.event_sinks,
fork_providers: self.fork_providers,
task_executors: self.task_executors,
notification_sinks: self.notification_sinks,
interactive_region_handlers: self.interactive_region_handlers,
speech_transcribers: self.speech_transcribers,
speech_synthesizers: self.speech_synthesizers,
version_control_providers: self.version_control_providers,
remote_runner_providers: self.remote_runner_providers,
status_segments: self.status_segments,
palette_sources: self.palette_sources,
code_index_providers: self.code_index_providers,
})
}
pub fn manifest(&mut self, manifest: ExtensionManifest) {
self.manifests.push(manifest);
}
pub fn grant_capability(&mut self, extension_id: impl Into<String>, grant: CapabilityGrant) {
self.granted_capabilities
.entry(extension_id.into())
.or_default()
.insert(grant.id);
}
pub fn deny_capability(&mut self, extension_id: impl Into<String>, denial: CapabilityDenial) {
self.denied_capabilities
.entry(extension_id.into())
.or_default()
.insert(denial.id, denial.reason);
}
pub fn inference_engine(&mut self, engine: Arc<dyn crate::inference::InferenceEngine>) {
self.inference_engines.push(engine);
}
pub fn inference_router(&mut self, router: Arc<dyn crate::inference_routing::InferenceRouter>) {
self.inference_routers.push(router);
}
pub fn context_provider(&mut self, provider: Arc<dyn crate::context::ContextProvider>) {
self.context_providers.push(provider);
}
pub fn context_planner(&mut self, planner: Arc<dyn crate::context::ContextPlanner>) {
self.context_planners.push(planner);
}
pub fn thread_store_factory(&mut self, store: Arc<dyn crate::thread::ThreadStoreFactory>) {
self.thread_stores.push(store);
}
pub fn checkpoint_store_factory(
&mut self,
store: Arc<dyn crate::thread::CheckpointStoreFactory>,
) {
self.checkpoint_stores.push(store);
}
pub fn memory_store_factory(&mut self, store: Arc<dyn crate::memory::MemoryStoreFactory>) {
self.memory_stores.push(store);
}
pub fn knowledge_store_factory(
&mut self,
store: Arc<dyn crate::knowledge::KnowledgeStoreFactory>,
) {
self.knowledge_stores.push(store);
}
pub fn embedding_provider(&mut self, provider: Arc<dyn crate::embeddings::EmbeddingProvider>) {
self.embedding_providers.push(provider);
}
pub fn media_generator_provider(
&mut self,
provider: Arc<dyn crate::media::MediaGeneratorProvider>,
) {
self.media_generator_providers.push(provider);
}
pub fn tool_contributor(&mut self, contributor: Arc<dyn crate::tools::ToolContributor>) {
self.tools.push(contributor);
}
pub fn subagent_dispatcher(
&mut self,
dispatcher: Arc<dyn crate::subagents::SubagentDispatcher>,
) {
self.subagent_dispatchers.push(dispatcher);
}
pub fn policy_contributor(&mut self, contributor: Arc<dyn crate::context::PolicyContributor>) {
self.policy_contributors.push(contributor);
}
pub fn event_sink(&mut self, sink: Arc<dyn crate::extension::EventSink>) {
self.event_sinks.push(sink);
}
pub fn fork_provider(&mut self, provider: Arc<dyn crate::forks::ForkProvider>) {
self.fork_providers.push(provider);
}
pub fn task_executor(&mut self, executor: Arc<dyn crate::tasks::TaskExecutor>) {
self.task_executors.push(executor);
}
pub fn notification_sink(&mut self, sink: Arc<dyn crate::notifications::NotificationSink>) {
self.notification_sinks.push(sink);
}
pub fn interactive_region_handler(
&mut self,
handler: Arc<dyn crate::interactive::InteractiveRegionHandler>,
) {
self.interactive_region_handlers.push(handler);
}
pub fn speech_transcriber(&mut self, transcriber: Arc<dyn crate::speech::SpeechTranscriber>) {
self.speech_transcribers.push(transcriber);
}
pub fn speech_synthesizer(&mut self, synthesizer: Arc<dyn crate::speech::SpeechSynthesizer>) {
self.speech_synthesizers.push(synthesizer);
}
pub fn version_control_provider(
&mut self,
provider: Arc<dyn crate::version_control::VcsProvider>,
) {
self.version_control_providers.push(provider);
}
pub fn remote_runner_provider(
&mut self,
provider: Arc<dyn crate::remote_runner::RemoteRunnerProvider>,
) {
self.remote_runner_providers.push(provider);
}
pub fn status_segment(&mut self, segment: crate::tui_status::StatusSegment) {
self.status_segments.push(segment);
}
pub fn palette_source(&mut self, source: crate::tui_status::PaletteSourceDescriptor) {
self.palette_sources.push(source);
}
pub fn code_index_provider(&mut self, provider: Arc<dyn crate::code_index::CodeIndexProvider>) {
self.code_index_providers.push(provider);
}
fn validate(&self) -> anyhow::Result<RegistryValidation> {
validate_manifests(&self.manifests)?;
validate_actual_services(self)?;
validate_tool_contributors(&self.tools)?;
let capability_statuses = validate_capabilities(
&self.manifests,
&self.granted_capabilities,
&self.denied_capabilities,
)?;
Ok(RegistryValidation {
capability_statuses,
})
}
}
#[async_trait::async_trait]
pub trait EventSink: Send + Sync + 'static {
fn id(&self) -> EventSinkId;
async fn handle_event(&self, envelope: &crate::events::EventEnvelope) -> anyhow::Result<()>;
}
struct RegistryValidation {
capability_statuses: BTreeMap<ExtensionId, Vec<CapabilityStatus>>,
}
fn validate_manifests(manifests: &[ExtensionManifest]) -> anyhow::Result<()> {
let mut extension_ids = BTreeSet::new();
let mut services = BTreeMap::<ProvidedService, ExtensionId>::new();
for manifest in manifests {
if manifest.id.trim().is_empty() {
anyhow::bail!("extension manifest has an empty id");
}
if !extension_ids.insert(manifest.id.clone()) {
anyhow::bail!("duplicate extension id {}", manifest.id);
}
validate_api_version(manifest)?;
for service in &manifest.provides {
if let Some(existing) = services.insert(service.clone(), manifest.id.clone()) {
anyhow::bail!(
"duplicate provided service {} declared by {} and {}",
service_label(service),
existing,
manifest.id
);
}
}
}
Ok(())
}
fn validate_api_version(manifest: &ExtensionManifest) -> anyhow::Result<()> {
let supported = Version::parse(SUPPORTED_EXTENSION_API_VERSION)?;
let requirement = VersionReq::parse(&manifest.api_version).or_else(|_| {
Version::parse(&manifest.api_version).map(|version| VersionReq {
comparators: vec![semver::Comparator {
op: semver::Op::Exact,
major: version.major,
minor: Some(version.minor),
patch: Some(version.patch),
pre: version.pre,
}],
})
})?;
if requirement.matches(&supported) {
Ok(())
} else {
anyhow::bail!(
"extension {} requires unsupported API version {}; supported {}",
manifest.id,
manifest.api_version,
SUPPORTED_EXTENSION_API_VERSION
)
}
}
fn validate_actual_services(builder: &ExtensionRegistryBuilder) -> anyhow::Result<()> {
let declared = builder
.manifests
.iter()
.flat_map(|manifest| manifest.provides.iter().cloned())
.collect::<BTreeSet<_>>();
let actual = actual_services(builder)?;
for service in &declared {
if !actual.contains(service) {
anyhow::bail!(
"manifest declares provided service {} but no matching service was installed",
service_label(service)
);
}
}
validate_duplicate_actual_services(&actual)
}
fn validate_duplicate_actual_services(actual: &[ProvidedService]) -> anyhow::Result<()> {
let mut seen = BTreeSet::new();
for service in actual {
if !seen.insert(service.clone()) {
anyhow::bail!("duplicate installed service {}", service_label(service));
}
}
Ok(())
}
fn service_counts(
builder: &ExtensionRegistryBuilder,
) -> anyhow::Result<BTreeMap<ProvidedService, usize>> {
let mut counts = BTreeMap::new();
for service in actual_services(builder)? {
*counts.entry(service).or_insert(0) += 1;
}
Ok(counts)
}
fn actual_services(builder: &ExtensionRegistryBuilder) -> anyhow::Result<Vec<ProvidedService>> {
let mut services = Vec::new();
services.extend(
builder
.inference_engines
.iter()
.map(|service| ProvidedService::InferenceEngine(service.id())),
);
services.extend(
builder
.inference_routers
.iter()
.map(|service| ProvidedService::InferenceRouter(service.id())),
);
services.extend(
builder
.context_providers
.iter()
.map(|service| ProvidedService::ContextProvider(service.id())),
);
services.extend(
builder
.context_planners
.iter()
.map(|service| ProvidedService::ContextPlanner(service.id())),
);
services.extend(
builder
.thread_stores
.iter()
.map(|service| ProvidedService::ThreadStore(service.id())),
);
services.extend(
builder
.checkpoint_stores
.iter()
.map(|service| ProvidedService::CheckpointStore(service.id())),
);
services.extend(
builder
.memory_stores
.iter()
.map(|service| ProvidedService::MemoryStore(service.id())),
);
services.extend(
builder
.knowledge_stores
.iter()
.map(|service| ProvidedService::KnowledgeStore(service.id())),
);
services.extend(
builder
.embedding_providers
.iter()
.map(|service| ProvidedService::EmbeddingProvider(service.descriptor().id)),
);
services.extend(
builder
.media_generator_providers
.iter()
.map(|service| ProvidedService::MediaGenerator(service.provider_id().to_string())),
);
services.extend(
builder
.tools
.iter()
.map(|service| ProvidedService::ToolProvider(service.id())),
);
services.extend(
builder
.subagent_dispatchers
.iter()
.map(|service| ProvidedService::SubagentDispatcher(service.id())),
);
services.extend(
builder
.policy_contributors
.iter()
.map(|service| ProvidedService::PolicyContributor(service.id())),
);
services.extend(
builder
.event_sinks
.iter()
.map(|service| ProvidedService::EventSink(service.id())),
);
services.extend(
builder
.fork_providers
.iter()
.map(|service| ProvidedService::ForkProvider(service.descriptor().id)),
);
services.extend(
builder
.task_executors
.iter()
.map(|service| ProvidedService::TaskExecutor(service.id())),
);
services.extend(
builder
.notification_sinks
.iter()
.map(|service| ProvidedService::NotificationSink(service.id())),
);
services.extend(
builder
.interactive_region_handlers
.iter()
.map(|service| ProvidedService::InteractiveRegionHandler(service.id())),
);
services.extend(
builder
.speech_transcribers
.iter()
.map(|service| ProvidedService::SpeechTranscriber(service.id())),
);
services.extend(
builder
.speech_synthesizers
.iter()
.map(|service| ProvidedService::SpeechSynthesizer(service.id())),
);
services.extend(
builder
.version_control_providers
.iter()
.map(|service| ProvidedService::VersionControlProvider(service.id())),
);
services.extend(
builder
.remote_runner_providers
.iter()
.map(|service| ProvidedService::RemoteRunnerProvider(service.id())),
);
services.extend(
builder
.status_segments
.iter()
.map(|service| ProvidedService::StatusSegment(service.id.clone())),
);
services.extend(
builder
.palette_sources
.iter()
.map(|service| ProvidedService::PaletteSource(service.id.clone())),
);
services.extend(
builder
.code_index_providers
.iter()
.map(|service| ProvidedService::CodeIndexProvider(service.id())),
);
Ok(services)
}
fn validate_tool_contributors(
contributors: &[Arc<dyn crate::tools::ToolContributor>],
) -> anyhow::Result<()> {
let mut registry = crate::tools::ToolRegistry::default();
for contributor in contributors {
contributor.contribute(&mut registry)?;
}
Ok(())
}
fn validate_capabilities(
manifests: &[ExtensionManifest],
granted: &BTreeMap<ExtensionId, BTreeSet<String>>,
denied: &BTreeMap<ExtensionId, BTreeMap<String, String>>,
) -> anyhow::Result<BTreeMap<ExtensionId, Vec<CapabilityStatus>>> {
let mut statuses = BTreeMap::new();
for manifest in manifests {
let mut seen = BTreeSet::new();
let mut extension_statuses = Vec::new();
for request in &manifest.required_capabilities {
if !seen.insert(request.id.clone()) {
anyhow::bail!(
"extension {} declares capability {} more than once",
manifest.id,
request.id
);
}
if let Some(reason) = denied
.get(&manifest.id)
.and_then(|denials| denials.get(&request.id))
{
anyhow::bail!(
"extension {} requires denied capability {}: {}",
manifest.id,
request.id,
reason
);
}
let decision = if granted
.get(&manifest.id)
.is_some_and(|grants| grants.contains(&request.id))
{
crate::capabilities::CapabilityDecision::Granted
} else {
crate::capabilities::CapabilityDecision::Requested
};
extension_statuses.push(CapabilityStatus {
id: request.id.clone(),
decision,
reason: request.reason.clone(),
});
}
statuses.insert(manifest.id.clone(), extension_statuses);
}
Ok(statuses)
}
fn service_label(service: &ProvidedService) -> String {
match service {
ProvidedService::InferenceEngine(id) => format!("InferenceEngine({id})"),
ProvidedService::InferenceRouter(id) => format!("InferenceRouter({id})"),
ProvidedService::ContextProvider(id) => format!("ContextProvider({id})"),
ProvidedService::ContextPlanner(id) => format!("ContextPlanner({id})"),
ProvidedService::ThreadStore(id) => format!("ThreadStore({id})"),
ProvidedService::CheckpointStore(id) => format!("CheckpointStore({id})"),
ProvidedService::MemoryStore(id) => format!("MemoryStore({id})"),
ProvidedService::KnowledgeStore(id) => format!("KnowledgeStore({id})"),
ProvidedService::EmbeddingProvider(id) => format!("EmbeddingProvider({id})"),
ProvidedService::MediaGenerator(id) => format!("MediaGenerator({id})"),
ProvidedService::ToolProvider(id) => format!("ToolProvider({id})"),
ProvidedService::SubagentDispatcher(id) => format!("SubagentDispatcher({id})"),
ProvidedService::PolicyContributor(id) => format!("PolicyContributor({id})"),
ProvidedService::EventSink(id) => format!("EventSink({id})"),
ProvidedService::ForkProvider(id) => format!("ForkProvider({id})"),
ProvidedService::TaskExecutor(id) => format!("TaskExecutor({id})"),
ProvidedService::NotificationSink(id) => format!("NotificationSink({id})"),
ProvidedService::InteractiveRegionHandler(id) => {
format!("InteractiveRegionHandler({id})")
}
ProvidedService::SpeechTranscriber(id) => format!("SpeechTranscriber({id})"),
ProvidedService::SpeechSynthesizer(id) => format!("SpeechSynthesizer({id})"),
ProvidedService::VersionControlProvider(id) => {
format!("VersionControlProvider({id})")
}
ProvidedService::RemoteRunnerProvider(id) => format!("RemoteRunnerProvider({id})"),
ProvidedService::StatusSegment(id) => format!("StatusSegment({id})"),
ProvidedService::PaletteSource(id) => format!("PaletteSource({id})"),
ProvidedService::CodeIndexProvider(id) => format!("CodeIndexProvider({id})"),
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::tui_status::{PaletteSourceDescriptor, StatusCell, StatusSegment, StatusStyle};
use crate::version_control::{
VcsCapabilities, VcsChangedContentPage, VcsChangedFile, VcsDetectionClaim, VcsError,
VcsListChangesRequest, VcsProvider, VcsReadChangedContentRequest, VcsStatus,
VcsStatusRequest, VcsWorkspace,
};
use super::*;
#[test]
fn provided_service_status_segment_round_trips_json() {
let service = ProvidedService::StatusSegment("mode".to_string());
let encoded = serde_json::to_value(&service).expect("serialize status segment service");
assert_eq!(encoded, serde_json::json!({ "StatusSegment": "mode" }));
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize status segment service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_inference_router_round_trips_json() {
let service = ProvidedService::InferenceRouter("adaptive".to_string());
let encoded = serde_json::to_value(&service).expect("serialize inference router service");
assert_eq!(
encoded,
serde_json::json!({ "InferenceRouter": "adaptive" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize inference router service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_palette_source_round_trips_json() {
let service = ProvidedService::PaletteSource("commands".to_string());
let encoded = serde_json::to_value(&service).expect("serialize palette source service");
assert_eq!(encoded, serde_json::json!({ "PaletteSource": "commands" }));
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize palette source service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_media_generator_round_trips_json() {
let service = ProvidedService::MediaGenerator("openai".to_string());
let encoded = serde_json::to_value(&service).expect("serialize media generator service");
assert_eq!(encoded, serde_json::json!({ "MediaGenerator": "openai" }));
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize media generator service");
assert_eq!(decoded, service);
}
#[test]
fn registering_media_generator_advertises_service_and_resolves_provider() {
struct FakeImageExtension;
struct FakeImageProvider;
#[async_trait::async_trait]
impl crate::media::MediaGeneratorProvider for FakeImageProvider {
fn provider_id(&self) -> &str {
"fake"
}
fn descriptor(&self) -> crate::media::MediaProviderDescriptor {
crate::media::MediaProviderDescriptor {
id: "fake".to_string(),
display_name: "Fake Image Provider".to_string(),
supports_images: true,
configured: true,
..crate::media::MediaProviderDescriptor::default()
}
}
}
impl RoderExtension for FakeImageExtension {
fn manifest(&self) -> ExtensionManifest {
ExtensionManifest {
id: "fake-image-extension".to_string(),
name: "Fake Image".to_string(),
version: Version::new(0, 1, 0),
api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
description: None,
provides: vec![ProvidedService::MediaGenerator("fake".to_string())],
required_capabilities: Vec::new(),
}
}
fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
registry.media_generator_provider(Arc::new(FakeImageProvider));
Ok(())
}
}
let mut builder = ExtensionRegistryBuilder::new();
builder
.install(FakeImageExtension)
.expect("install image extension");
let registry = builder.build().expect("build registry");
assert!(
registry
.provided_services()
.contains(&ProvidedService::MediaGenerator("fake".to_string()))
);
let provider = registry.media_generator("fake").expect("resolve provider");
assert!(provider.descriptor().supports_images);
assert!(registry.media_generator("missing").is_none());
}
#[test]
fn provided_service_task_executor_round_trips_json() {
let service = ProvidedService::TaskExecutor("process".to_string());
let encoded = serde_json::to_value(&service).expect("serialize task executor service");
assert_eq!(encoded, serde_json::json!({ "TaskExecutor": "process" }));
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize task executor service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_code_index_provider_round_trips_json() {
let service = ProvidedService::CodeIndexProvider("local-code-index".to_string());
let encoded =
serde_json::to_value(&service).expect("serialize code index provider service");
assert_eq!(
encoded,
serde_json::json!({ "CodeIndexProvider": "local-code-index" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize code index provider service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_notification_sink_round_trips_json() {
let service = ProvidedService::NotificationSink("terminal-bell".to_string());
let encoded = serde_json::to_value(&service).expect("serialize notification sink service");
assert_eq!(
encoded,
serde_json::json!({ "NotificationSink": "terminal-bell" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize notification sink service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_interactive_region_handler_round_trips_json() {
let service = ProvidedService::InteractiveRegionHandler("links".to_string());
let encoded =
serde_json::to_value(&service).expect("serialize interactive region handler service");
assert_eq!(
encoded,
serde_json::json!({ "InteractiveRegionHandler": "links" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize interactive region handler service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_remote_runner_provider_round_trips_json() {
let service = ProvidedService::RemoteRunnerProvider("unix-local".to_string());
let encoded =
serde_json::to_value(&service).expect("serialize remote runner provider service");
assert_eq!(
encoded,
serde_json::json!({ "RemoteRunnerProvider": "unix-local" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize remote runner provider service");
assert_eq!(decoded, service);
}
#[test]
fn provided_service_version_control_provider_round_trips_json() {
let service = ProvidedService::VersionControlProvider("git".to_string());
let encoded =
serde_json::to_value(&service).expect("serialize version control provider service");
assert_eq!(
encoded,
serde_json::json!({ "VersionControlProvider": "git" })
);
let decoded = serde_json::from_value::<ProvidedService>(encoded)
.expect("deserialize version control provider service");
assert_eq!(decoded, service);
}
#[test]
fn registry_builder_records_status_segments() {
let mut builder = ExtensionRegistryBuilder::new();
builder.status_segment(StatusSegment::new("custom", 42, 6, |_| StatusCell {
text: "ready".to_string(),
style: StatusStyle::Accent,
tooltip: None,
}));
let registry = builder.build().expect("build registry");
assert_eq!(registry.status_segments.len(), 1);
assert_eq!(registry.status_segments[0].id, "custom");
assert_eq!(registry.status_segments[0].priority, 42);
assert_eq!(registry.status_segments[0].min_width, 6);
}
#[test]
fn registry_builder_records_palette_sources() {
let mut builder = ExtensionRegistryBuilder::new();
builder.palette_source(PaletteSourceDescriptor {
id: "commands".to_string(),
label: "Commands".to_string(),
priority: 100,
});
let registry = builder.build().expect("build registry");
assert_eq!(registry.palette_sources.len(), 1);
assert_eq!(registry.palette_sources[0].id, "commands");
assert_eq!(registry.palette_sources[0].label, "Commands");
assert_eq!(registry.palette_sources[0].priority, 100);
}
#[test]
fn registering_vcs_provider_advertises_service_and_builds_registry() {
let mut builder = ExtensionRegistryBuilder::new();
builder
.install(FakeVcsExtension::new("git"))
.expect("install vcs extension");
let registry = builder.build().expect("build registry");
assert!(
registry
.provided_services()
.contains(&ProvidedService::VersionControlProvider("git".to_string()))
);
assert!(registry.version_control_provider("git").is_some());
}
#[test]
fn duplicate_vcs_provider_ids_fail_registry_validation() {
let mut builder = ExtensionRegistryBuilder::new();
builder.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
builder.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
let error = match builder.build() {
Ok(_) => panic!("duplicate provider should fail"),
Err(error) => error,
};
assert!(
error
.to_string()
.contains("duplicate installed service VersionControlProvider(git)")
);
}
#[test]
fn installing_an_undeclared_service_fails_install() {
let mut builder = ExtensionRegistryBuilder::new();
let error = match builder.install(UndeclaredServiceExtension) {
Ok(()) => panic!("undeclared service should fail install"),
Err(error) => error,
};
assert!(
error
.to_string()
.contains("installed undeclared service VersionControlProvider(git)"),
"unexpected error: {error}"
);
assert!(builder.manifests.is_empty());
}
struct UndeclaredServiceExtension;
impl RoderExtension for UndeclaredServiceExtension {
fn manifest(&self) -> ExtensionManifest {
ExtensionManifest {
id: "undeclared-service-extension".to_string(),
name: "Undeclared Service".to_string(),
version: Version::new(0, 1, 0),
api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
description: None,
provides: Vec::new(),
required_capabilities: Vec::new(),
}
}
fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
registry.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
Ok(())
}
}
struct FakeVcsExtension {
id: String,
}
impl FakeVcsExtension {
fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
impl RoderExtension for FakeVcsExtension {
fn manifest(&self) -> ExtensionManifest {
ExtensionManifest {
id: format!("{}-extension", self.id),
name: "Fake VCS".to_string(),
version: Version::new(0, 1, 0),
api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
description: None,
provides: vec![ProvidedService::VersionControlProvider(self.id.clone())],
required_capabilities: Vec::new(),
}
}
fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
registry.version_control_provider(Arc::new(FakeVcsProvider::new(self.id.clone())));
Ok(())
}
}
struct FakeVcsProvider {
id: String,
}
impl FakeVcsProvider {
fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
#[async_trait::async_trait]
impl VcsProvider for FakeVcsProvider {
fn id(&self) -> crate::version_control::VcsProviderId {
self.id.clone()
}
fn display_name(&self) -> String {
self.id.clone()
}
async fn detect(
&self,
workspace_root: &Path,
) -> Result<Option<VcsDetectionClaim>, VcsError> {
Ok(Some(VcsDetectionClaim {
workspace: VcsWorkspace {
root: workspace_root.to_path_buf(),
id: None,
},
priority: 0,
metadata: serde_json::Value::Null,
}))
}
async fn status(&self, request: VcsStatusRequest) -> Result<VcsStatus, VcsError> {
Ok(VcsStatus {
provider: crate::version_control::VcsProviderIdentity {
id: self.id.clone(),
display_name: self.id.clone(),
},
workspace: VcsWorkspace {
root: request.workspace_root,
id: None,
},
active_line: None,
base: None,
capabilities: VcsCapabilities::default(),
changed_file_count: 0,
})
}
async fn list_changes(
&self,
_request: VcsListChangesRequest,
) -> Result<Vec<VcsChangedFile>, VcsError> {
Ok(Vec::new())
}
async fn read_changed_content(
&self,
request: VcsReadChangedContentRequest,
) -> Result<VcsChangedContentPage, VcsError> {
Ok(VcsChangedContentPage {
path: PathBuf::from(request.path),
content: Some(String::new()),
offset: request.offset,
total_lines: 0,
next_offset: None,
binary: false,
})
}
}
}