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(
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 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 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 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
611fn 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}