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