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