Skip to main content

roder_api/
extension.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::Arc;
3
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Serialize};
6
7use crate::capabilities::{CapabilityDenial, CapabilityGrant, CapabilityRequest, CapabilityStatus};
8
9pub type ExtensionId = String;
10pub type ApiVersion = String;
11pub type InferenceEngineId = String;
12pub type InferenceRouterId = String;
13pub type ContextProviderId = String;
14pub type ContextPlannerId = String;
15pub type ThreadStoreId = String;
16pub type CheckpointStoreId = String;
17pub type MemoryStoreId = String;
18pub type KnowledgeStoreId = String;
19pub type EmbeddingProviderId = String;
20pub type MediaGeneratorProviderId = String;
21pub type ToolProviderId = String;
22pub type SubagentDispatcherId = String;
23pub type PolicyContributorId = String;
24pub type EventSinkId = String;
25pub type TaskExecutorId = String;
26pub type NotificationSinkId = String;
27pub type InteractiveRegionHandlerId = String;
28pub type SpeechTranscriberId = String;
29pub type SpeechSynthesizerId = String;
30pub type VersionControlProviderId = crate::version_control::VcsProviderId;
31
32pub const SUPPORTED_EXTENSION_API_VERSION: &str = "0.1.0";
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub enum ProvidedService {
36    InferenceEngine(InferenceEngineId),
37    InferenceRouter(InferenceRouterId),
38    ContextProvider(ContextProviderId),
39    ContextPlanner(ContextPlannerId),
40    ThreadStore(ThreadStoreId),
41    CheckpointStore(CheckpointStoreId),
42    MemoryStore(MemoryStoreId),
43    KnowledgeStore(KnowledgeStoreId),
44    EmbeddingProvider(EmbeddingProviderId),
45    MediaGenerator(MediaGeneratorProviderId),
46    ToolProvider(ToolProviderId),
47    SubagentDispatcher(SubagentDispatcherId),
48    PolicyContributor(PolicyContributorId),
49    EventSink(EventSinkId),
50    ForkProvider(crate::forks::ForkProviderId),
51    TaskExecutor(TaskExecutorId),
52    NotificationSink(NotificationSinkId),
53    InteractiveRegionHandler(InteractiveRegionHandlerId),
54    SpeechTranscriber(SpeechTranscriberId),
55    SpeechSynthesizer(SpeechSynthesizerId),
56    VersionControlProvider(VersionControlProviderId),
57    RemoteRunnerProvider(crate::remote_runner::RemoteRunnerProviderId),
58    StatusSegment(crate::tui_status::StatusSegmentId),
59    PaletteSource(crate::tui_status::PaletteSourceId),
60    CodeIndexProvider(crate::code_index::CodeIndexProviderId),
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ExtensionManifest {
65    pub id: ExtensionId,
66    pub name: String,
67    pub version: Version,
68    pub api_version: ApiVersion,
69    pub description: Option<String>,
70    pub provides: Vec<ProvidedService>,
71    pub required_capabilities: Vec<CapabilityRequest>,
72}
73
74pub trait RoderExtension: Send + Sync + 'static {
75    fn manifest(&self) -> ExtensionManifest;
76
77    fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()>;
78}
79
80/// Lets shared extension handles (e.g. distribution-supplied extension lists)
81/// be installed through the same `ExtensionRegistryBuilder::install` path as
82/// concrete extension values.
83impl<E: RoderExtension + ?Sized> RoderExtension for Arc<E> {
84    fn manifest(&self) -> ExtensionManifest {
85        (**self).manifest()
86    }
87
88    fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
89        (**self).install(registry)
90    }
91}
92
93#[derive(Clone)]
94pub struct ExtensionRegistry {
95    pub manifests: Vec<ExtensionManifest>,
96    pub capability_statuses: BTreeMap<ExtensionId, Vec<CapabilityStatus>>,
97    pub inference_engines: Vec<Arc<dyn crate::inference::InferenceEngine>>,
98    pub inference_routers: Vec<Arc<dyn crate::inference_routing::InferenceRouter>>,
99    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
100    pub context_planners: Vec<Arc<dyn crate::context::ContextPlanner>>,
101    pub thread_stores: Vec<Arc<dyn crate::thread::ThreadStoreFactory>>,
102    pub checkpoint_stores: Vec<Arc<dyn crate::thread::CheckpointStoreFactory>>,
103    pub memory_stores: Vec<Arc<dyn crate::memory::MemoryStoreFactory>>,
104    pub knowledge_stores: Vec<Arc<dyn crate::knowledge::KnowledgeStoreFactory>>,
105    pub embedding_providers: Vec<Arc<dyn crate::embeddings::EmbeddingProvider>>,
106    pub media_generator_providers: Vec<Arc<dyn crate::media::MediaGeneratorProvider>>,
107    pub tools: Vec<Arc<dyn crate::tools::ToolContributor>>,
108    pub subagent_dispatchers: Vec<Arc<dyn crate::subagents::SubagentDispatcher>>,
109    pub policy_contributors: Vec<Arc<dyn crate::context::PolicyContributor>>,
110    pub event_sinks: Vec<Arc<dyn crate::extension::EventSink>>,
111    pub fork_providers: Vec<Arc<dyn crate::forks::ForkProvider>>,
112    pub task_executors: Vec<Arc<dyn crate::tasks::TaskExecutor>>,
113    pub notification_sinks: Vec<Arc<dyn crate::notifications::NotificationSink>>,
114    pub interactive_region_handlers: Vec<Arc<dyn crate::interactive::InteractiveRegionHandler>>,
115    pub speech_transcribers: Vec<Arc<dyn crate::speech::SpeechTranscriber>>,
116    pub speech_synthesizers: Vec<Arc<dyn crate::speech::SpeechSynthesizer>>,
117    pub version_control_providers: Vec<Arc<dyn crate::version_control::VcsProvider>>,
118    pub remote_runner_providers: Vec<Arc<dyn crate::remote_runner::RemoteRunnerProvider>>,
119    pub status_segments: Vec<crate::tui_status::StatusSegment>,
120    pub palette_sources: Vec<crate::tui_status::PaletteSourceDescriptor>,
121    pub code_index_providers: Vec<Arc<dyn crate::code_index::CodeIndexProvider>>,
122}
123
124impl ExtensionRegistry {
125    pub fn media_generator(
126        &self,
127        id: &str,
128    ) -> Option<Arc<dyn crate::media::MediaGeneratorProvider>> {
129        self.media_generator_providers
130            .iter()
131            .find(|provider| provider.provider_id() == id)
132            .cloned()
133    }
134
135    pub fn inference_engine(&self, id: &str) -> Option<Arc<dyn crate::inference::InferenceEngine>> {
136        self.inference_engines
137            .iter()
138            .find(|engine| engine.id() == id)
139            .cloned()
140    }
141
142    pub fn default_inference_engine(&self) -> Option<Arc<dyn crate::inference::InferenceEngine>> {
143        self.inference_engines.first().cloned()
144    }
145
146    pub fn inference_router(
147        &self,
148        id: &str,
149    ) -> Option<Arc<dyn crate::inference_routing::InferenceRouter>> {
150        self.inference_routers
151            .iter()
152            .find(|router| router.id() == id)
153            .cloned()
154    }
155
156    pub fn speech_transcriber(
157        &self,
158        id: &str,
159    ) -> Option<Arc<dyn crate::speech::SpeechTranscriber>> {
160        self.speech_transcribers
161            .iter()
162            .find(|transcriber| transcriber.id() == id)
163            .cloned()
164    }
165
166    pub fn speech_synthesizer(
167        &self,
168        id: &str,
169    ) -> Option<Arc<dyn crate::speech::SpeechSynthesizer>> {
170        self.speech_synthesizers
171            .iter()
172            .find(|synthesizer| synthesizer.id() == id)
173            .cloned()
174    }
175
176    pub fn fork_provider(&self, id: &str) -> Option<Arc<dyn crate::forks::ForkProvider>> {
177        self.fork_providers
178            .iter()
179            .find(|provider| provider.descriptor().id == id)
180            .cloned()
181    }
182
183    pub fn provided_services(&self) -> Vec<ProvidedService> {
184        self.manifests
185            .iter()
186            .flat_map(|manifest| manifest.provides.iter().cloned())
187            .collect()
188    }
189
190    pub fn capability_statuses(&self, extension_id: &str) -> &[CapabilityStatus] {
191        self.capability_statuses
192            .get(extension_id)
193            .map(Vec::as_slice)
194            .unwrap_or(&[])
195    }
196
197    pub fn subagent_dispatcher(
198        &self,
199        id: &str,
200    ) -> Option<Arc<dyn crate::subagents::SubagentDispatcher>> {
201        self.subagent_dispatchers
202            .iter()
203            .find(|dispatcher| dispatcher.id() == id)
204            .cloned()
205    }
206
207    pub fn version_control_provider(
208        &self,
209        id: &str,
210    ) -> Option<Arc<dyn crate::version_control::VcsProvider>> {
211        self.version_control_providers
212            .iter()
213            .find(|provider| provider.id() == id)
214            .cloned()
215    }
216
217    pub fn version_control_resolver(&self) -> crate::version_control::RegistryVcsProviderResolver {
218        crate::version_control::RegistryVcsProviderResolver::new(
219            self.version_control_providers.clone(),
220        )
221    }
222}
223
224pub struct ExtensionRegistryBuilder {
225    manifests: Vec<ExtensionManifest>,
226    granted_capabilities: BTreeMap<ExtensionId, BTreeSet<String>>,
227    denied_capabilities: BTreeMap<ExtensionId, BTreeMap<String, String>>,
228    pub inference_engines: Vec<Arc<dyn crate::inference::InferenceEngine>>,
229    pub inference_routers: Vec<Arc<dyn crate::inference_routing::InferenceRouter>>,
230    pub context_providers: Vec<Arc<dyn crate::context::ContextProvider>>,
231    pub context_planners: Vec<Arc<dyn crate::context::ContextPlanner>>,
232    pub thread_stores: Vec<Arc<dyn crate::thread::ThreadStoreFactory>>,
233    pub checkpoint_stores: Vec<Arc<dyn crate::thread::CheckpointStoreFactory>>,
234    pub memory_stores: Vec<Arc<dyn crate::memory::MemoryStoreFactory>>,
235    pub knowledge_stores: Vec<Arc<dyn crate::knowledge::KnowledgeStoreFactory>>,
236    pub embedding_providers: Vec<Arc<dyn crate::embeddings::EmbeddingProvider>>,
237    pub media_generator_providers: Vec<Arc<dyn crate::media::MediaGeneratorProvider>>,
238    pub tools: Vec<Arc<dyn crate::tools::ToolContributor>>,
239    pub subagent_dispatchers: Vec<Arc<dyn crate::subagents::SubagentDispatcher>>,
240    pub policy_contributors: Vec<Arc<dyn crate::context::PolicyContributor>>,
241    pub event_sinks: Vec<Arc<dyn crate::extension::EventSink>>,
242    pub fork_providers: Vec<Arc<dyn crate::forks::ForkProvider>>,
243    pub task_executors: Vec<Arc<dyn crate::tasks::TaskExecutor>>,
244    pub notification_sinks: Vec<Arc<dyn crate::notifications::NotificationSink>>,
245    pub interactive_region_handlers: Vec<Arc<dyn crate::interactive::InteractiveRegionHandler>>,
246    pub speech_transcribers: Vec<Arc<dyn crate::speech::SpeechTranscriber>>,
247    pub speech_synthesizers: Vec<Arc<dyn crate::speech::SpeechSynthesizer>>,
248    pub version_control_providers: Vec<Arc<dyn crate::version_control::VcsProvider>>,
249    pub remote_runner_providers: Vec<Arc<dyn crate::remote_runner::RemoteRunnerProvider>>,
250    pub status_segments: Vec<crate::tui_status::StatusSegment>,
251    pub palette_sources: Vec<crate::tui_status::PaletteSourceDescriptor>,
252    pub code_index_providers: Vec<Arc<dyn crate::code_index::CodeIndexProvider>>,
253}
254
255impl Default for ExtensionRegistryBuilder {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl ExtensionRegistryBuilder {
262    pub fn new() -> Self {
263        Self {
264            manifests: Vec::new(),
265            granted_capabilities: BTreeMap::new(),
266            denied_capabilities: BTreeMap::new(),
267            inference_engines: Vec::new(),
268            inference_routers: Vec::new(),
269            context_providers: Vec::new(),
270            context_planners: Vec::new(),
271            thread_stores: Vec::new(),
272            checkpoint_stores: Vec::new(),
273            memory_stores: Vec::new(),
274            knowledge_stores: Vec::new(),
275            embedding_providers: Vec::new(),
276            media_generator_providers: Vec::new(),
277            tools: Vec::new(),
278            subagent_dispatchers: Vec::new(),
279            policy_contributors: Vec::new(),
280            event_sinks: Vec::new(),
281            fork_providers: Vec::new(),
282            task_executors: Vec::new(),
283            notification_sinks: Vec::new(),
284            interactive_region_handlers: Vec::new(),
285            speech_transcribers: Vec::new(),
286            speech_synthesizers: Vec::new(),
287            version_control_providers: Vec::new(),
288            remote_runner_providers: Vec::new(),
289            status_segments: Vec::new(),
290            palette_sources: Vec::new(),
291            code_index_providers: Vec::new(),
292        }
293    }
294
295    /// Installs the extension and checks that every service it registers is
296    /// declared in its manifest `provides` list, so a manifest is a complete
297    /// inventory of what an installed extension contributes. Host code that
298    /// registers services directly on the builder (outside `install`) is not
299    /// subject to this check.
300    pub fn install<E: RoderExtension>(&mut self, ext: E) -> anyhow::Result<()> {
301        let manifest = ext.manifest();
302        if self
303            .manifests
304            .iter()
305            .any(|existing| existing.id == manifest.id)
306        {
307            anyhow::bail!("extension {} is already installed", manifest.id);
308        }
309        let before = service_counts(self)?;
310        ext.install(self)?;
311        let declared: BTreeSet<ProvidedService> = manifest.provides.iter().cloned().collect();
312        for (service, count) in service_counts(self)? {
313            let prior = before.get(&service).copied().unwrap_or(0);
314            if count > prior && !declared.contains(&service) {
315                anyhow::bail!(
316                    "extension {} installed undeclared service {}; declare it in the manifest provides list",
317                    manifest.id,
318                    service_label(&service)
319                );
320            }
321        }
322        self.manifests.push(manifest);
323        Ok(())
324    }
325
326    pub fn build(self) -> anyhow::Result<ExtensionRegistry> {
327        let validation = self.validate()?;
328        Ok(ExtensionRegistry {
329            manifests: self.manifests,
330            capability_statuses: validation.capability_statuses,
331            inference_engines: self.inference_engines,
332            inference_routers: self.inference_routers,
333            context_providers: self.context_providers,
334            context_planners: self.context_planners,
335            thread_stores: self.thread_stores,
336            checkpoint_stores: self.checkpoint_stores,
337            memory_stores: self.memory_stores,
338            knowledge_stores: self.knowledge_stores,
339            embedding_providers: self.embedding_providers,
340            media_generator_providers: self.media_generator_providers,
341            tools: self.tools,
342            subagent_dispatchers: self.subagent_dispatchers,
343            policy_contributors: self.policy_contributors,
344            event_sinks: self.event_sinks,
345            fork_providers: self.fork_providers,
346            task_executors: self.task_executors,
347            notification_sinks: self.notification_sinks,
348            interactive_region_handlers: self.interactive_region_handlers,
349            speech_transcribers: self.speech_transcribers,
350            speech_synthesizers: self.speech_synthesizers,
351            version_control_providers: self.version_control_providers,
352            remote_runner_providers: self.remote_runner_providers,
353            status_segments: self.status_segments,
354            palette_sources: self.palette_sources,
355            code_index_providers: self.code_index_providers,
356        })
357    }
358
359    pub fn manifest(&mut self, manifest: ExtensionManifest) {
360        self.manifests.push(manifest);
361    }
362
363    /// Capability grants are advisory metadata surfaced through
364    /// `extensions/list` (statically linked extensions run in-process);
365    /// nothing enforces them at runtime.
366    pub fn grant_capability(&mut self, extension_id: impl Into<String>, grant: CapabilityGrant) {
367        self.granted_capabilities
368            .entry(extension_id.into())
369            .or_default()
370            .insert(grant.id);
371    }
372
373    /// Advisory, like [`Self::grant_capability`]: a denial fails `build()` for
374    /// extensions that require the capability but does not constrain runtime
375    /// behavior.
376    pub fn deny_capability(&mut self, extension_id: impl Into<String>, denial: CapabilityDenial) {
377        self.denied_capabilities
378            .entry(extension_id.into())
379            .or_default()
380            .insert(denial.id, denial.reason);
381    }
382
383    pub fn inference_engine(&mut self, engine: Arc<dyn crate::inference::InferenceEngine>) {
384        self.inference_engines.push(engine);
385    }
386
387    pub fn inference_router(&mut self, router: Arc<dyn crate::inference_routing::InferenceRouter>) {
388        self.inference_routers.push(router);
389    }
390
391    pub fn context_provider(&mut self, provider: Arc<dyn crate::context::ContextProvider>) {
392        self.context_providers.push(provider);
393    }
394
395    pub fn context_planner(&mut self, planner: Arc<dyn crate::context::ContextPlanner>) {
396        self.context_planners.push(planner);
397    }
398
399    pub fn thread_store_factory(&mut self, store: Arc<dyn crate::thread::ThreadStoreFactory>) {
400        self.thread_stores.push(store);
401    }
402
403    pub fn checkpoint_store_factory(
404        &mut self,
405        store: Arc<dyn crate::thread::CheckpointStoreFactory>,
406    ) {
407        self.checkpoint_stores.push(store);
408    }
409
410    pub fn memory_store_factory(&mut self, store: Arc<dyn crate::memory::MemoryStoreFactory>) {
411        self.memory_stores.push(store);
412    }
413
414    pub fn knowledge_store_factory(
415        &mut self,
416        store: Arc<dyn crate::knowledge::KnowledgeStoreFactory>,
417    ) {
418        self.knowledge_stores.push(store);
419    }
420
421    pub fn embedding_provider(&mut self, provider: Arc<dyn crate::embeddings::EmbeddingProvider>) {
422        self.embedding_providers.push(provider);
423    }
424
425    pub fn media_generator_provider(
426        &mut self,
427        provider: Arc<dyn crate::media::MediaGeneratorProvider>,
428    ) {
429        self.media_generator_providers.push(provider);
430    }
431
432    pub fn tool_contributor(&mut self, contributor: Arc<dyn crate::tools::ToolContributor>) {
433        self.tools.push(contributor);
434    }
435
436    pub fn subagent_dispatcher(
437        &mut self,
438        dispatcher: Arc<dyn crate::subagents::SubagentDispatcher>,
439    ) {
440        self.subagent_dispatchers.push(dispatcher);
441    }
442
443    pub fn policy_contributor(&mut self, contributor: Arc<dyn crate::context::PolicyContributor>) {
444        self.policy_contributors.push(contributor);
445    }
446
447    pub fn event_sink(&mut self, sink: Arc<dyn crate::extension::EventSink>) {
448        self.event_sinks.push(sink);
449    }
450
451    pub fn fork_provider(&mut self, provider: Arc<dyn crate::forks::ForkProvider>) {
452        self.fork_providers.push(provider);
453    }
454
455    pub fn task_executor(&mut self, executor: Arc<dyn crate::tasks::TaskExecutor>) {
456        self.task_executors.push(executor);
457    }
458
459    pub fn notification_sink(&mut self, sink: Arc<dyn crate::notifications::NotificationSink>) {
460        self.notification_sinks.push(sink);
461    }
462
463    pub fn interactive_region_handler(
464        &mut self,
465        handler: Arc<dyn crate::interactive::InteractiveRegionHandler>,
466    ) {
467        self.interactive_region_handlers.push(handler);
468    }
469
470    pub fn speech_transcriber(&mut self, transcriber: Arc<dyn crate::speech::SpeechTranscriber>) {
471        self.speech_transcribers.push(transcriber);
472    }
473
474    pub fn speech_synthesizer(&mut self, synthesizer: Arc<dyn crate::speech::SpeechSynthesizer>) {
475        self.speech_synthesizers.push(synthesizer);
476    }
477
478    pub fn version_control_provider(
479        &mut self,
480        provider: Arc<dyn crate::version_control::VcsProvider>,
481    ) {
482        self.version_control_providers.push(provider);
483    }
484
485    pub fn remote_runner_provider(
486        &mut self,
487        provider: Arc<dyn crate::remote_runner::RemoteRunnerProvider>,
488    ) {
489        self.remote_runner_providers.push(provider);
490    }
491
492    pub fn status_segment(&mut self, segment: crate::tui_status::StatusSegment) {
493        self.status_segments.push(segment);
494    }
495
496    pub fn palette_source(&mut self, source: crate::tui_status::PaletteSourceDescriptor) {
497        self.palette_sources.push(source);
498    }
499
500    pub fn code_index_provider(&mut self, provider: Arc<dyn crate::code_index::CodeIndexProvider>) {
501        self.code_index_providers.push(provider);
502    }
503
504    fn validate(&self) -> anyhow::Result<RegistryValidation> {
505        validate_manifests(&self.manifests)?;
506        validate_actual_services(self)?;
507        validate_tool_contributors(&self.tools)?;
508        let capability_statuses = validate_capabilities(
509            &self.manifests,
510            &self.granted_capabilities,
511            &self.denied_capabilities,
512        )?;
513        Ok(RegistryValidation {
514            capability_statuses,
515        })
516    }
517}
518
519#[async_trait::async_trait]
520pub trait EventSink: Send + Sync + 'static {
521    fn id(&self) -> EventSinkId;
522
523    async fn handle_event(&self, envelope: &crate::events::EventEnvelope) -> anyhow::Result<()>;
524}
525
526struct RegistryValidation {
527    capability_statuses: BTreeMap<ExtensionId, Vec<CapabilityStatus>>,
528}
529
530fn validate_manifests(manifests: &[ExtensionManifest]) -> anyhow::Result<()> {
531    let mut extension_ids = BTreeSet::new();
532    let mut services = BTreeMap::<ProvidedService, ExtensionId>::new();
533    for manifest in manifests {
534        if manifest.id.trim().is_empty() {
535            anyhow::bail!("extension manifest has an empty id");
536        }
537        if !extension_ids.insert(manifest.id.clone()) {
538            anyhow::bail!("duplicate extension id {}", manifest.id);
539        }
540        validate_api_version(manifest)?;
541        for service in &manifest.provides {
542            if let Some(existing) = services.insert(service.clone(), manifest.id.clone()) {
543                anyhow::bail!(
544                    "duplicate provided service {} declared by {} and {}",
545                    service_label(service),
546                    existing,
547                    manifest.id
548                );
549            }
550        }
551    }
552    Ok(())
553}
554
555fn validate_api_version(manifest: &ExtensionManifest) -> anyhow::Result<()> {
556    let supported = Version::parse(SUPPORTED_EXTENSION_API_VERSION)?;
557    let requirement = VersionReq::parse(&manifest.api_version).or_else(|_| {
558        Version::parse(&manifest.api_version).map(|version| VersionReq {
559            comparators: vec![semver::Comparator {
560                op: semver::Op::Exact,
561                major: version.major,
562                minor: Some(version.minor),
563                patch: Some(version.patch),
564                pre: version.pre,
565            }],
566        })
567    })?;
568    if requirement.matches(&supported) {
569        Ok(())
570    } else {
571        anyhow::bail!(
572            "extension {} requires unsupported API version {}; supported {}",
573            manifest.id,
574            manifest.api_version,
575            SUPPORTED_EXTENSION_API_VERSION
576        )
577    }
578}
579
580fn validate_actual_services(builder: &ExtensionRegistryBuilder) -> anyhow::Result<()> {
581    let declared = builder
582        .manifests
583        .iter()
584        .flat_map(|manifest| manifest.provides.iter().cloned())
585        .collect::<BTreeSet<_>>();
586    let actual = actual_services(builder)?;
587    for service in &declared {
588        if !actual.contains(service) {
589            anyhow::bail!(
590                "manifest declares provided service {} but no matching service was installed",
591                service_label(service)
592            );
593        }
594    }
595    validate_duplicate_actual_services(&actual)
596}
597
598fn validate_duplicate_actual_services(actual: &[ProvidedService]) -> anyhow::Result<()> {
599    let mut seen = BTreeSet::new();
600    for service in actual {
601        if !seen.insert(service.clone()) {
602            anyhow::bail!("duplicate installed service {}", service_label(service));
603        }
604    }
605    Ok(())
606}
607
608/// Multiset of the services currently registered on the builder; diffed
609/// around each extension install to attribute new services to that extension.
610fn service_counts(
611    builder: &ExtensionRegistryBuilder,
612) -> anyhow::Result<BTreeMap<ProvidedService, usize>> {
613    let mut counts = BTreeMap::new();
614    for service in actual_services(builder)? {
615        *counts.entry(service).or_insert(0) += 1;
616    }
617    Ok(counts)
618}
619
620fn actual_services(builder: &ExtensionRegistryBuilder) -> anyhow::Result<Vec<ProvidedService>> {
621    let mut services = Vec::new();
622    services.extend(
623        builder
624            .inference_engines
625            .iter()
626            .map(|service| ProvidedService::InferenceEngine(service.id())),
627    );
628    services.extend(
629        builder
630            .inference_routers
631            .iter()
632            .map(|service| ProvidedService::InferenceRouter(service.id())),
633    );
634    services.extend(
635        builder
636            .context_providers
637            .iter()
638            .map(|service| ProvidedService::ContextProvider(service.id())),
639    );
640    services.extend(
641        builder
642            .context_planners
643            .iter()
644            .map(|service| ProvidedService::ContextPlanner(service.id())),
645    );
646    services.extend(
647        builder
648            .thread_stores
649            .iter()
650            .map(|service| ProvidedService::ThreadStore(service.id())),
651    );
652    services.extend(
653        builder
654            .checkpoint_stores
655            .iter()
656            .map(|service| ProvidedService::CheckpointStore(service.id())),
657    );
658    services.extend(
659        builder
660            .memory_stores
661            .iter()
662            .map(|service| ProvidedService::MemoryStore(service.id())),
663    );
664    services.extend(
665        builder
666            .knowledge_stores
667            .iter()
668            .map(|service| ProvidedService::KnowledgeStore(service.id())),
669    );
670    services.extend(
671        builder
672            .embedding_providers
673            .iter()
674            .map(|service| ProvidedService::EmbeddingProvider(service.descriptor().id)),
675    );
676    services.extend(
677        builder
678            .media_generator_providers
679            .iter()
680            .map(|service| ProvidedService::MediaGenerator(service.provider_id().to_string())),
681    );
682    services.extend(
683        builder
684            .tools
685            .iter()
686            .map(|service| ProvidedService::ToolProvider(service.id())),
687    );
688    services.extend(
689        builder
690            .subagent_dispatchers
691            .iter()
692            .map(|service| ProvidedService::SubagentDispatcher(service.id())),
693    );
694    services.extend(
695        builder
696            .policy_contributors
697            .iter()
698            .map(|service| ProvidedService::PolicyContributor(service.id())),
699    );
700    services.extend(
701        builder
702            .event_sinks
703            .iter()
704            .map(|service| ProvidedService::EventSink(service.id())),
705    );
706    services.extend(
707        builder
708            .fork_providers
709            .iter()
710            .map(|service| ProvidedService::ForkProvider(service.descriptor().id)),
711    );
712    services.extend(
713        builder
714            .task_executors
715            .iter()
716            .map(|service| ProvidedService::TaskExecutor(service.id())),
717    );
718    services.extend(
719        builder
720            .notification_sinks
721            .iter()
722            .map(|service| ProvidedService::NotificationSink(service.id())),
723    );
724    services.extend(
725        builder
726            .interactive_region_handlers
727            .iter()
728            .map(|service| ProvidedService::InteractiveRegionHandler(service.id())),
729    );
730    services.extend(
731        builder
732            .speech_transcribers
733            .iter()
734            .map(|service| ProvidedService::SpeechTranscriber(service.id())),
735    );
736    services.extend(
737        builder
738            .speech_synthesizers
739            .iter()
740            .map(|service| ProvidedService::SpeechSynthesizer(service.id())),
741    );
742    services.extend(
743        builder
744            .version_control_providers
745            .iter()
746            .map(|service| ProvidedService::VersionControlProvider(service.id())),
747    );
748    services.extend(
749        builder
750            .remote_runner_providers
751            .iter()
752            .map(|service| ProvidedService::RemoteRunnerProvider(service.id())),
753    );
754    services.extend(
755        builder
756            .status_segments
757            .iter()
758            .map(|service| ProvidedService::StatusSegment(service.id.clone())),
759    );
760    services.extend(
761        builder
762            .palette_sources
763            .iter()
764            .map(|service| ProvidedService::PaletteSource(service.id.clone())),
765    );
766    services.extend(
767        builder
768            .code_index_providers
769            .iter()
770            .map(|service| ProvidedService::CodeIndexProvider(service.id())),
771    );
772    Ok(services)
773}
774
775fn validate_tool_contributors(
776    contributors: &[Arc<dyn crate::tools::ToolContributor>],
777) -> anyhow::Result<()> {
778    let mut registry = crate::tools::ToolRegistry::default();
779    for contributor in contributors {
780        contributor.contribute(&mut registry)?;
781    }
782    Ok(())
783}
784
785fn validate_capabilities(
786    manifests: &[ExtensionManifest],
787    granted: &BTreeMap<ExtensionId, BTreeSet<String>>,
788    denied: &BTreeMap<ExtensionId, BTreeMap<String, String>>,
789) -> anyhow::Result<BTreeMap<ExtensionId, Vec<CapabilityStatus>>> {
790    let mut statuses = BTreeMap::new();
791    for manifest in manifests {
792        let mut seen = BTreeSet::new();
793        let mut extension_statuses = Vec::new();
794        for request in &manifest.required_capabilities {
795            if !seen.insert(request.id.clone()) {
796                anyhow::bail!(
797                    "extension {} declares capability {} more than once",
798                    manifest.id,
799                    request.id
800                );
801            }
802            if let Some(reason) = denied
803                .get(&manifest.id)
804                .and_then(|denials| denials.get(&request.id))
805            {
806                anyhow::bail!(
807                    "extension {} requires denied capability {}: {}",
808                    manifest.id,
809                    request.id,
810                    reason
811                );
812            }
813            let decision = if granted
814                .get(&manifest.id)
815                .is_some_and(|grants| grants.contains(&request.id))
816            {
817                crate::capabilities::CapabilityDecision::Granted
818            } else {
819                crate::capabilities::CapabilityDecision::Requested
820            };
821            extension_statuses.push(CapabilityStatus {
822                id: request.id.clone(),
823                decision,
824                reason: request.reason.clone(),
825            });
826        }
827        statuses.insert(manifest.id.clone(), extension_statuses);
828    }
829    Ok(statuses)
830}
831
832fn service_label(service: &ProvidedService) -> String {
833    match service {
834        ProvidedService::InferenceEngine(id) => format!("InferenceEngine({id})"),
835        ProvidedService::InferenceRouter(id) => format!("InferenceRouter({id})"),
836        ProvidedService::ContextProvider(id) => format!("ContextProvider({id})"),
837        ProvidedService::ContextPlanner(id) => format!("ContextPlanner({id})"),
838        ProvidedService::ThreadStore(id) => format!("ThreadStore({id})"),
839        ProvidedService::CheckpointStore(id) => format!("CheckpointStore({id})"),
840        ProvidedService::MemoryStore(id) => format!("MemoryStore({id})"),
841        ProvidedService::KnowledgeStore(id) => format!("KnowledgeStore({id})"),
842        ProvidedService::EmbeddingProvider(id) => format!("EmbeddingProvider({id})"),
843        ProvidedService::MediaGenerator(id) => format!("MediaGenerator({id})"),
844        ProvidedService::ToolProvider(id) => format!("ToolProvider({id})"),
845        ProvidedService::SubagentDispatcher(id) => format!("SubagentDispatcher({id})"),
846        ProvidedService::PolicyContributor(id) => format!("PolicyContributor({id})"),
847        ProvidedService::EventSink(id) => format!("EventSink({id})"),
848        ProvidedService::ForkProvider(id) => format!("ForkProvider({id})"),
849        ProvidedService::TaskExecutor(id) => format!("TaskExecutor({id})"),
850        ProvidedService::NotificationSink(id) => format!("NotificationSink({id})"),
851        ProvidedService::InteractiveRegionHandler(id) => {
852            format!("InteractiveRegionHandler({id})")
853        }
854        ProvidedService::SpeechTranscriber(id) => format!("SpeechTranscriber({id})"),
855        ProvidedService::SpeechSynthesizer(id) => format!("SpeechSynthesizer({id})"),
856        ProvidedService::VersionControlProvider(id) => {
857            format!("VersionControlProvider({id})")
858        }
859        ProvidedService::RemoteRunnerProvider(id) => format!("RemoteRunnerProvider({id})"),
860        ProvidedService::StatusSegment(id) => format!("StatusSegment({id})"),
861        ProvidedService::PaletteSource(id) => format!("PaletteSource({id})"),
862        ProvidedService::CodeIndexProvider(id) => format!("CodeIndexProvider({id})"),
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use std::path::{Path, PathBuf};
869    use std::sync::Arc;
870
871    use crate::tui_status::{PaletteSourceDescriptor, StatusCell, StatusSegment, StatusStyle};
872    use crate::version_control::{
873        VcsCapabilities, VcsChangedContentPage, VcsChangedFile, VcsDetectionClaim, VcsError,
874        VcsListChangesRequest, VcsProvider, VcsReadChangedContentRequest, VcsStatus,
875        VcsStatusRequest, VcsWorkspace,
876    };
877
878    use super::*;
879
880    #[test]
881    fn provided_service_status_segment_round_trips_json() {
882        let service = ProvidedService::StatusSegment("mode".to_string());
883        let encoded = serde_json::to_value(&service).expect("serialize status segment service");
884        assert_eq!(encoded, serde_json::json!({ "StatusSegment": "mode" }));
885
886        let decoded = serde_json::from_value::<ProvidedService>(encoded)
887            .expect("deserialize status segment service");
888        assert_eq!(decoded, service);
889    }
890
891    #[test]
892    fn provided_service_inference_router_round_trips_json() {
893        let service = ProvidedService::InferenceRouter("adaptive".to_string());
894        let encoded = serde_json::to_value(&service).expect("serialize inference router service");
895        assert_eq!(
896            encoded,
897            serde_json::json!({ "InferenceRouter": "adaptive" })
898        );
899
900        let decoded = serde_json::from_value::<ProvidedService>(encoded)
901            .expect("deserialize inference router service");
902        assert_eq!(decoded, service);
903    }
904
905    #[test]
906    fn provided_service_palette_source_round_trips_json() {
907        let service = ProvidedService::PaletteSource("commands".to_string());
908        let encoded = serde_json::to_value(&service).expect("serialize palette source service");
909        assert_eq!(encoded, serde_json::json!({ "PaletteSource": "commands" }));
910
911        let decoded = serde_json::from_value::<ProvidedService>(encoded)
912            .expect("deserialize palette source service");
913        assert_eq!(decoded, service);
914    }
915
916    #[test]
917    fn provided_service_media_generator_round_trips_json() {
918        let service = ProvidedService::MediaGenerator("openai".to_string());
919        let encoded = serde_json::to_value(&service).expect("serialize media generator service");
920        assert_eq!(encoded, serde_json::json!({ "MediaGenerator": "openai" }));
921
922        let decoded = serde_json::from_value::<ProvidedService>(encoded)
923            .expect("deserialize media generator service");
924        assert_eq!(decoded, service);
925    }
926
927    #[test]
928    fn registering_media_generator_advertises_service_and_resolves_provider() {
929        struct FakeImageExtension;
930
931        struct FakeImageProvider;
932
933        #[async_trait::async_trait]
934        impl crate::media::MediaGeneratorProvider for FakeImageProvider {
935            fn provider_id(&self) -> &str {
936                "fake"
937            }
938
939            fn descriptor(&self) -> crate::media::MediaProviderDescriptor {
940                crate::media::MediaProviderDescriptor {
941                    id: "fake".to_string(),
942                    display_name: "Fake Image Provider".to_string(),
943                    supports_images: true,
944                    configured: true,
945                    ..crate::media::MediaProviderDescriptor::default()
946                }
947            }
948        }
949
950        impl RoderExtension for FakeImageExtension {
951            fn manifest(&self) -> ExtensionManifest {
952                ExtensionManifest {
953                    id: "fake-image-extension".to_string(),
954                    name: "Fake Image".to_string(),
955                    version: Version::new(0, 1, 0),
956                    api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
957                    description: None,
958                    provides: vec![ProvidedService::MediaGenerator("fake".to_string())],
959                    required_capabilities: Vec::new(),
960                }
961            }
962
963            fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
964                registry.media_generator_provider(Arc::new(FakeImageProvider));
965                Ok(())
966            }
967        }
968
969        let mut builder = ExtensionRegistryBuilder::new();
970        builder
971            .install(FakeImageExtension)
972            .expect("install image extension");
973        let registry = builder.build().expect("build registry");
974
975        assert!(
976            registry
977                .provided_services()
978                .contains(&ProvidedService::MediaGenerator("fake".to_string()))
979        );
980        let provider = registry.media_generator("fake").expect("resolve provider");
981        assert!(provider.descriptor().supports_images);
982        assert!(registry.media_generator("missing").is_none());
983    }
984
985    #[test]
986    fn provided_service_task_executor_round_trips_json() {
987        let service = ProvidedService::TaskExecutor("process".to_string());
988        let encoded = serde_json::to_value(&service).expect("serialize task executor service");
989        assert_eq!(encoded, serde_json::json!({ "TaskExecutor": "process" }));
990
991        let decoded = serde_json::from_value::<ProvidedService>(encoded)
992            .expect("deserialize task executor service");
993        assert_eq!(decoded, service);
994    }
995
996    #[test]
997    fn provided_service_code_index_provider_round_trips_json() {
998        let service = ProvidedService::CodeIndexProvider("local-code-index".to_string());
999        let encoded =
1000            serde_json::to_value(&service).expect("serialize code index provider service");
1001        assert_eq!(
1002            encoded,
1003            serde_json::json!({ "CodeIndexProvider": "local-code-index" })
1004        );
1005
1006        let decoded = serde_json::from_value::<ProvidedService>(encoded)
1007            .expect("deserialize code index provider service");
1008        assert_eq!(decoded, service);
1009    }
1010
1011    #[test]
1012    fn provided_service_notification_sink_round_trips_json() {
1013        let service = ProvidedService::NotificationSink("terminal-bell".to_string());
1014        let encoded = serde_json::to_value(&service).expect("serialize notification sink service");
1015        assert_eq!(
1016            encoded,
1017            serde_json::json!({ "NotificationSink": "terminal-bell" })
1018        );
1019
1020        let decoded = serde_json::from_value::<ProvidedService>(encoded)
1021            .expect("deserialize notification sink service");
1022        assert_eq!(decoded, service);
1023    }
1024
1025    #[test]
1026    fn provided_service_interactive_region_handler_round_trips_json() {
1027        let service = ProvidedService::InteractiveRegionHandler("links".to_string());
1028        let encoded =
1029            serde_json::to_value(&service).expect("serialize interactive region handler service");
1030        assert_eq!(
1031            encoded,
1032            serde_json::json!({ "InteractiveRegionHandler": "links" })
1033        );
1034
1035        let decoded = serde_json::from_value::<ProvidedService>(encoded)
1036            .expect("deserialize interactive region handler service");
1037        assert_eq!(decoded, service);
1038    }
1039
1040    #[test]
1041    fn provided_service_remote_runner_provider_round_trips_json() {
1042        let service = ProvidedService::RemoteRunnerProvider("unix-local".to_string());
1043        let encoded =
1044            serde_json::to_value(&service).expect("serialize remote runner provider service");
1045        assert_eq!(
1046            encoded,
1047            serde_json::json!({ "RemoteRunnerProvider": "unix-local" })
1048        );
1049
1050        let decoded = serde_json::from_value::<ProvidedService>(encoded)
1051            .expect("deserialize remote runner provider service");
1052        assert_eq!(decoded, service);
1053    }
1054
1055    #[test]
1056    fn provided_service_version_control_provider_round_trips_json() {
1057        let service = ProvidedService::VersionControlProvider("git".to_string());
1058        let encoded =
1059            serde_json::to_value(&service).expect("serialize version control provider service");
1060        assert_eq!(
1061            encoded,
1062            serde_json::json!({ "VersionControlProvider": "git" })
1063        );
1064
1065        let decoded = serde_json::from_value::<ProvidedService>(encoded)
1066            .expect("deserialize version control provider service");
1067        assert_eq!(decoded, service);
1068    }
1069
1070    #[test]
1071    fn registry_builder_records_status_segments() {
1072        let mut builder = ExtensionRegistryBuilder::new();
1073        builder.status_segment(StatusSegment::new("custom", 42, 6, |_| StatusCell {
1074            text: "ready".to_string(),
1075            style: StatusStyle::Accent,
1076            tooltip: None,
1077        }));
1078
1079        let registry = builder.build().expect("build registry");
1080        assert_eq!(registry.status_segments.len(), 1);
1081        assert_eq!(registry.status_segments[0].id, "custom");
1082        assert_eq!(registry.status_segments[0].priority, 42);
1083        assert_eq!(registry.status_segments[0].min_width, 6);
1084    }
1085
1086    #[test]
1087    fn registry_builder_records_palette_sources() {
1088        let mut builder = ExtensionRegistryBuilder::new();
1089        builder.palette_source(PaletteSourceDescriptor {
1090            id: "commands".to_string(),
1091            label: "Commands".to_string(),
1092            priority: 100,
1093        });
1094
1095        let registry = builder.build().expect("build registry");
1096        assert_eq!(registry.palette_sources.len(), 1);
1097        assert_eq!(registry.palette_sources[0].id, "commands");
1098        assert_eq!(registry.palette_sources[0].label, "Commands");
1099        assert_eq!(registry.palette_sources[0].priority, 100);
1100    }
1101
1102    #[test]
1103    fn registering_vcs_provider_advertises_service_and_builds_registry() {
1104        let mut builder = ExtensionRegistryBuilder::new();
1105        builder
1106            .install(FakeVcsExtension::new("git"))
1107            .expect("install vcs extension");
1108
1109        let registry = builder.build().expect("build registry");
1110
1111        assert!(
1112            registry
1113                .provided_services()
1114                .contains(&ProvidedService::VersionControlProvider("git".to_string()))
1115        );
1116        assert!(registry.version_control_provider("git").is_some());
1117    }
1118
1119    #[test]
1120    fn duplicate_vcs_provider_ids_fail_registry_validation() {
1121        let mut builder = ExtensionRegistryBuilder::new();
1122        builder.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
1123        builder.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
1124
1125        let error = match builder.build() {
1126            Ok(_) => panic!("duplicate provider should fail"),
1127            Err(error) => error,
1128        };
1129
1130        assert!(
1131            error
1132                .to_string()
1133                .contains("duplicate installed service VersionControlProvider(git)")
1134        );
1135    }
1136
1137    #[test]
1138    fn installing_an_undeclared_service_fails_install() {
1139        let mut builder = ExtensionRegistryBuilder::new();
1140
1141        let error = match builder.install(UndeclaredServiceExtension) {
1142            Ok(()) => panic!("undeclared service should fail install"),
1143            Err(error) => error,
1144        };
1145
1146        assert!(
1147            error
1148                .to_string()
1149                .contains("installed undeclared service VersionControlProvider(git)"),
1150            "unexpected error: {error}"
1151        );
1152        assert!(builder.manifests.is_empty());
1153    }
1154
1155    struct UndeclaredServiceExtension;
1156
1157    impl RoderExtension for UndeclaredServiceExtension {
1158        fn manifest(&self) -> ExtensionManifest {
1159            ExtensionManifest {
1160                id: "undeclared-service-extension".to_string(),
1161                name: "Undeclared Service".to_string(),
1162                version: Version::new(0, 1, 0),
1163                api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
1164                description: None,
1165                provides: Vec::new(),
1166                required_capabilities: Vec::new(),
1167            }
1168        }
1169
1170        fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
1171            registry.version_control_provider(Arc::new(FakeVcsProvider::new("git")));
1172            Ok(())
1173        }
1174    }
1175
1176    struct FakeVcsExtension {
1177        id: String,
1178    }
1179
1180    impl FakeVcsExtension {
1181        fn new(id: impl Into<String>) -> Self {
1182            Self { id: id.into() }
1183        }
1184    }
1185
1186    impl RoderExtension for FakeVcsExtension {
1187        fn manifest(&self) -> ExtensionManifest {
1188            ExtensionManifest {
1189                id: format!("{}-extension", self.id),
1190                name: "Fake VCS".to_string(),
1191                version: Version::new(0, 1, 0),
1192                api_version: SUPPORTED_EXTENSION_API_VERSION.to_string(),
1193                description: None,
1194                provides: vec![ProvidedService::VersionControlProvider(self.id.clone())],
1195                required_capabilities: Vec::new(),
1196            }
1197        }
1198
1199        fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
1200            registry.version_control_provider(Arc::new(FakeVcsProvider::new(self.id.clone())));
1201            Ok(())
1202        }
1203    }
1204
1205    struct FakeVcsProvider {
1206        id: String,
1207    }
1208
1209    impl FakeVcsProvider {
1210        fn new(id: impl Into<String>) -> Self {
1211            Self { id: id.into() }
1212        }
1213    }
1214
1215    #[async_trait::async_trait]
1216    impl VcsProvider for FakeVcsProvider {
1217        fn id(&self) -> crate::version_control::VcsProviderId {
1218            self.id.clone()
1219        }
1220
1221        fn display_name(&self) -> String {
1222            self.id.clone()
1223        }
1224
1225        async fn detect(
1226            &self,
1227            workspace_root: &Path,
1228        ) -> Result<Option<VcsDetectionClaim>, VcsError> {
1229            Ok(Some(VcsDetectionClaim {
1230                workspace: VcsWorkspace {
1231                    root: workspace_root.to_path_buf(),
1232                    id: None,
1233                },
1234                priority: 0,
1235                metadata: serde_json::Value::Null,
1236            }))
1237        }
1238
1239        async fn status(&self, request: VcsStatusRequest) -> Result<VcsStatus, VcsError> {
1240            Ok(VcsStatus {
1241                provider: crate::version_control::VcsProviderIdentity {
1242                    id: self.id.clone(),
1243                    display_name: self.id.clone(),
1244                },
1245                workspace: VcsWorkspace {
1246                    root: request.workspace_root,
1247                    id: None,
1248                },
1249                active_line: None,
1250                base: None,
1251                capabilities: VcsCapabilities::default(),
1252                changed_file_count: 0,
1253            })
1254        }
1255
1256        async fn list_changes(
1257            &self,
1258            _request: VcsListChangesRequest,
1259        ) -> Result<Vec<VcsChangedFile>, VcsError> {
1260            Ok(Vec::new())
1261        }
1262
1263        async fn read_changed_content(
1264            &self,
1265            request: VcsReadChangedContentRequest,
1266        ) -> Result<VcsChangedContentPage, VcsError> {
1267            Ok(VcsChangedContentPage {
1268                path: PathBuf::from(request.path),
1269                content: Some(String::new()),
1270                offset: request.offset,
1271                total_lines: 0,
1272                next_offset: None,
1273                binary: false,
1274            })
1275        }
1276    }
1277}