Skip to main content

roder_api/
extension.rs

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