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
80impl<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 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 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 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
608fn 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}