Skip to main content

weft_core/app/
resolve.rs

1use crate::app::config::AppManifest;
2use crate::app::packages::PackageIndex;
3use crate::app::policy::{AppProfile, CorePolicy};
4use crate::app::registry::CapabilityRegistry;
5use crate::app::state::{
6    AppBindingResolution, ResolvedApp, ResolvedAppSources, ResolvedAppStatus,
7};
8use crate::package::DiscoveredPackage;
9use anyhow::{anyhow, Result};
10use std::collections::{HashMap, HashSet};
11
12const WEFT_CLAW_REQUIRED_REAL_PACKAGES: &[&str] = &[
13    "prompt-system",
14    "workflow-orchestrator",
15    "tool-runtime-core",
16    "tool-shell",
17    "tool-files",
18    "tool-web",
19    "tool-git",
20];
21
22fn requires_real_package_source(package_name: &str) -> bool {
23    WEFT_CLAW_REQUIRED_REAL_PACKAGES.contains(&package_name)
24}
25
26#[derive(Default)]
27pub struct ResolveCandidateContext {
28    provider_candidates: ProviderCandidateSet,
29    candidate_grouping: ResolveCandidateGroupingSet,
30}
31
32#[derive(Debug, Clone)]
33pub struct ServiceOriginCandidatePayload {
34    pub provider: String,
35    pub candidates: Vec<ServiceOriginCandidatePayloadEntry>,
36    pub closure_metadata: ResolveCandidateGrouping,
37}
38
39#[derive(Debug, Clone)]
40pub struct ServiceContractCandidatePayload {
41    pub provider: String,
42    pub candidates: Vec<String>,
43    pub closure: ServiceContractCandidateClosure,
44    pub provenance: Option<ServiceContractCandidateProvenance>,
45}
46
47#[derive(Debug, Clone)]
48pub struct ServiceContractCandidateClosure {
49    pub id: Option<String>,
50    pub candidates: Vec<String>,
51    pub rationale: Option<String>,
52}
53
54#[derive(Debug, Clone)]
55pub struct ServiceContractCandidateProvenance {
56    pub package_name: String,
57    pub package_kind: String,
58    pub runtime_provider: String,
59    pub current_source: String,
60    pub source_kind: String,
61}
62
63#[derive(Debug, Clone)]
64pub struct ServiceOriginCandidatePayloadFixture {
65    pub provider: String,
66    pub candidates: Vec<String>,
67    pub closure_candidates: Vec<String>,
68    pub closure_id: Option<String>,
69    pub closure_rationale: Option<String>,
70    pub provenance: Option<PackageCandidateProvenance>,
71}
72
73#[derive(Debug, Clone)]
74pub struct ServiceOriginCandidatePayloadEntry {
75    pub candidate: String,
76    pub provenance: Option<PackageCandidateProvenance>,
77}
78
79impl ServiceOriginCandidatePayload {
80    pub fn from_service_contract_payload(payload: &[ServiceContractCandidatePayload]) -> Vec<Self> {
81        payload
82            .iter()
83            .filter_map(|service_payload| {
84                let provider = service_payload.provider.trim();
85                if provider.is_empty() {
86                    return None;
87                }
88
89                let mut closure_candidates: Vec<String> = service_payload
90                    .closure
91                    .candidates
92                    .iter()
93                    .filter_map(|candidate| {
94                        let candidate = candidate.trim();
95                        if candidate.is_empty() {
96                            None
97                        } else {
98                            Some(candidate.to_string())
99                        }
100                    })
101                    .collect();
102
103                if !closure_candidates.contains(&provider.to_string()) {
104                    closure_candidates.push(provider.to_string());
105                }
106
107                let mut candidates = Vec::new();
108                for candidate in &service_payload.candidates {
109                    let candidate = candidate.trim();
110                    if candidate.is_empty() {
111                        continue;
112                    }
113
114                    candidates.push(ServiceOriginCandidatePayloadEntry {
115                        candidate: candidate.to_string(),
116                        provenance: service_payload.provenance.as_ref().map(|provenance| {
117                            PackageCandidateProvenance {
118                                package_name: provenance.package_name.clone(),
119                                package_kind: provenance.package_kind.clone(),
120                                runtime_provider: provenance.runtime_provider.clone(),
121                                current_source: provenance.current_source.clone(),
122                                source_kind: provenance.source_kind.clone(),
123                            }
124                        }),
125                    });
126                }
127
128                Some(Self {
129                    provider: provider.to_string(),
130                    candidates,
131                    closure_metadata: ResolveCandidateGrouping {
132                        id: service_payload
133                            .closure
134                            .id
135                            .as_ref()
136                            .and_then(|id| {
137                                let id = id.trim();
138                                if id.is_empty() {
139                                    None
140                                } else {
141                                    Some(id.to_string())
142                                }
143                            })
144                            .unwrap_or_else(|| format!("service-contract:{}:closure", provider)),
145                        source: ResolveCandidateGroupingSource::Service,
146                        kind: ResolveCandidateGroupingKind::Closure,
147                        candidates: closure_candidates,
148                        rationale: Some(
149                            service_payload
150                                .closure
151                                .rationale
152                                .as_ref()
153                                .and_then(|rationale| {
154                                    let rationale = rationale.trim();
155                                    if rationale.is_empty() {
156                                        None
157                                    } else {
158                                        Some(rationale.to_string())
159                                    }
160                                })
161                                .unwrap_or_else(|| "service-candidate-contract".to_string()),
162                        ),
163                    },
164                })
165            })
166            .collect()
167    }
168
169    pub fn from_fixtures(fixtures: &[ServiceOriginCandidatePayloadFixture]) -> Vec<Self> {
170        fixtures
171            .iter()
172            .filter_map(|fixture| {
173                let provider = fixture.provider.trim().to_string();
174                if provider.is_empty() {
175                    return None;
176                }
177
178                let mut closure_candidates: Vec<String> = fixture
179                    .closure_candidates
180                    .iter()
181                    .filter_map(|candidate| {
182                        let candidate = candidate.trim();
183                        if candidate.is_empty() {
184                            None
185                        } else {
186                            Some(candidate.to_string())
187                        }
188                    })
189                    .collect();
190
191                if !closure_candidates
192                    .iter()
193                    .any(|candidate| candidate == &provider)
194                {
195                    closure_candidates.push(provider.clone());
196                }
197
198                let candidates = fixture
199                    .candidates
200                    .iter()
201                    .filter_map(|candidate| {
202                        let candidate = candidate.trim();
203                        if candidate.is_empty() {
204                            None
205                        } else {
206                            Some(ServiceOriginCandidatePayloadEntry {
207                                candidate: candidate.to_string(),
208                                provenance: fixture.provenance.clone(),
209                            })
210                        }
211                    })
212                    .collect();
213
214                let closure_id = fixture
215                    .closure_id
216                    .as_ref()
217                    .and_then(|closure_id| {
218                        let closure_id = closure_id.trim();
219                        if closure_id.is_empty() {
220                            None
221                        } else {
222                            Some(closure_id.to_string())
223                        }
224                    })
225                    .unwrap_or_else(|| format!("service-fixture:{}:closure", provider));
226
227                let closure_rationale = fixture
228                    .closure_rationale
229                    .as_ref()
230                    .and_then(|rationale| {
231                        let rationale = rationale.trim();
232                        if rationale.is_empty() {
233                            None
234                        } else {
235                            Some(rationale.to_string())
236                        }
237                    })
238                    .unwrap_or_else(|| "service-candidate-fixture".to_string());
239
240                Some(Self {
241                    provider,
242                    candidates,
243                    closure_metadata: ResolveCandidateGrouping {
244                        id: closure_id,
245                        source: ResolveCandidateGroupingSource::Service,
246                        kind: ResolveCandidateGroupingKind::Closure,
247                        candidates: closure_candidates,
248                        rationale: Some(closure_rationale),
249                    },
250                })
251            })
252            .collect()
253    }
254
255    pub fn from_package_index(package_index: &PackageIndex) -> Vec<Self> {
256        package_index
257            .package_sources
258            .iter()
259            .filter(|package| package.kind == "service")
260            .filter_map(|package| {
261                let runtime_provider = package.runtime_provider_name();
262                if runtime_provider.trim().is_empty() {
263                    return None;
264                }
265
266                let service_package_name = package.name.trim();
267                let mut candidates = Vec::new();
268                if !service_package_name.is_empty() && service_package_name != runtime_provider {
269                    candidates.push(ServiceOriginCandidatePayloadEntry {
270                        candidate: service_package_name.to_string(),
271                        provenance: Some(PackageCandidateProvenance {
272                            package_name: package.name.clone(),
273                            package_kind: package.package_kind.clone(),
274                            runtime_provider: package.runtime_provider.clone(),
275                            current_source: package.current_source.clone(),
276                            source_kind: package.kind.clone(),
277                        }),
278                    });
279                }
280
281                let mut closure_candidates = vec![runtime_provider.clone()];
282                if !service_package_name.is_empty()
283                    && !closure_candidates.contains(&service_package_name.to_string())
284                {
285                    closure_candidates.push(service_package_name.to_string());
286                }
287
288                Some(Self {
289                    provider: runtime_provider,
290                    candidates,
291                    closure_metadata: ResolveCandidateGrouping {
292                        id: format!("service:{}:closure", package.name),
293                        source: ResolveCandidateGroupingSource::Service,
294                        kind: ResolveCandidateGroupingKind::Closure,
295                        candidates: closure_candidates,
296                        rationale: Some("service-candidate-synthesis".into()),
297                    },
298                })
299            })
300            .collect()
301    }
302}
303pub struct ResolveInputCoordinator<'a> {
304    package_index: &'a PackageIndex,
305    resolve_candidate_context: ResolveCandidateContext,
306}
307
308pub struct ResolveInputCoordinatorBuilder<'a> {
309    package_index: &'a PackageIndex,
310    resolve_candidate_context: ResolveCandidateContext,
311}
312
313impl<'a> ResolveInputCoordinator<'a> {
314    pub fn from_package_index(
315        package_index: &'a PackageIndex,
316    ) -> ResolveInputCoordinatorBuilder<'a> {
317        ResolveInputCoordinatorBuilder {
318            package_index,
319            resolve_candidate_context: ResolveCandidateContext::from_package_index(package_index),
320        }
321    }
322
323    pub fn package_index(&self) -> &'a PackageIndex {
324        self.package_index
325    }
326
327    pub fn resolve_candidate_context(&self) -> &ResolveCandidateContext {
328        &self.resolve_candidate_context
329    }
330}
331
332impl<'a> ResolveInputCoordinatorBuilder<'a> {
333    pub fn with_service_origin_candidates(
334        mut self,
335        adapter: &dyn ServiceOriginCandidateAdapter,
336        payload: &[ServiceOriginCandidatePayload],
337    ) -> Self {
338        self.resolve_candidate_context = self
339            .resolve_candidate_context
340            .with_service_origin_candidates(adapter, payload);
341        self
342    }
343
344    pub fn with_synthesized_service_origin_candidates(self) -> Self {
345        let payload = ServiceOriginCandidatePayload::from_package_index(self.package_index);
346        self.with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
347    }
348
349    pub fn build(self) -> ResolveInputCoordinator<'a> {
350        ResolveInputCoordinator {
351            package_index: self.package_index,
352            resolve_candidate_context: self.resolve_candidate_context,
353        }
354    }
355}
356
357pub trait ServiceOriginCandidateAdapter {
358    fn synthesize(
359        &self,
360        resolve_candidate_context: &mut ResolveCandidateContext,
361        payload: &[ServiceOriginCandidatePayload],
362    );
363}
364
365pub struct SynthesizedServiceOriginCandidateAdapter;
366
367impl ServiceOriginCandidateAdapter for SynthesizedServiceOriginCandidateAdapter {
368    fn synthesize(
369        &self,
370        resolve_candidate_context: &mut ResolveCandidateContext,
371        payload: &[ServiceOriginCandidatePayload],
372    ) {
373        for entry in payload {
374            let runtime_provider = entry.provider.trim();
375            if runtime_provider.is_empty() {
376                continue;
377            }
378
379            for candidate in &entry.candidates {
380                if candidate.candidate.trim().is_empty() {
381                    continue;
382                }
383
384                resolve_candidate_context
385                    .provider_candidates
386                    .add_candidate_entry(
387                        runtime_provider,
388                        ResolveCandidateEntry {
389                            candidate: candidate.candidate.clone(),
390                            provenance: candidate
391                                .provenance
392                                .clone()
393                                .map(ResolveCandidateProvenance::PackageIndex)
394                                .unwrap_or(ResolveCandidateProvenance::External),
395                        },
396                    );
397            }
398
399            resolve_candidate_context
400                .candidate_grouping
401                .add_grouping(runtime_provider, entry.closure_metadata.clone());
402        }
403    }
404}
405
406#[derive(Debug, Clone)]
407pub enum ResolveCandidateGroupingSource {
408    Service,
409    Local,
410    Index,
411}
412
413#[derive(Debug, Clone)]
414pub enum ResolveCandidateGroupingKind {
415    Closure,
416    RecommendationSet,
417}
418
419#[derive(Debug, Clone)]
420pub struct ResolveCandidateGrouping {
421    pub id: String,
422    pub source: ResolveCandidateGroupingSource,
423    pub kind: ResolveCandidateGroupingKind,
424    pub candidates: Vec<String>,
425    pub rationale: Option<String>,
426}
427
428#[derive(Default)]
429pub struct ResolveCandidateGroupingSet {
430    groups: HashMap<String, Vec<ResolveCandidateGrouping>>,
431}
432
433impl ResolveCandidateGroupingSet {
434    fn add_grouping(&mut self, provider: impl Into<String>, grouping: ResolveCandidateGrouping) {
435        let groups = self.groups.entry(provider.into()).or_default();
436        groups.push(grouping);
437    }
438
439    pub fn groups_for(&self, provider: &str) -> &[ResolveCandidateGrouping] {
440        self.groups.get(provider).map_or(&[], Vec::as_slice)
441    }
442}
443
444#[derive(Default)]
445pub struct ProviderCandidateSet {
446    entries: HashMap<String, Vec<ResolveCandidateEntry>>,
447}
448
449#[derive(Debug, Clone)]
450pub struct ResolveCandidateEntry {
451    pub candidate: String,
452    pub provenance: ResolveCandidateProvenance,
453}
454
455#[derive(Debug, Clone, PartialEq)]
456pub struct PackageCandidateProvenance {
457    pub package_name: String,
458    pub package_kind: String,
459    pub runtime_provider: String,
460    pub current_source: String,
461    pub source_kind: String,
462}
463
464#[derive(Debug, Clone)]
465pub enum ResolveCandidateProvenance {
466    External,
467    PackageIndex(PackageCandidateProvenance),
468}
469
470impl ProviderCandidateSet {
471    fn add_candidate_entry(&mut self, provider: impl Into<String>, entry: ResolveCandidateEntry) {
472        let entries = self.entries.entry(provider.into()).or_default();
473        entries.push(entry);
474    }
475
476    pub fn add_candidates<S>(
477        &mut self,
478        provider: impl Into<String>,
479        candidates: impl IntoIterator<Item = S>,
480    ) where
481        S: Into<String>,
482    {
483        let entries = self.entries.entry(provider.into()).or_default();
484        for candidate in candidates {
485            entries.push(ResolveCandidateEntry {
486                candidate: candidate.into(),
487                provenance: ResolveCandidateProvenance::External,
488            });
489        }
490    }
491
492    fn candidates_for(&self, provider: &str) -> &[ResolveCandidateEntry] {
493        self.entries
494            .get(provider)
495            .map_or(&[], std::vec::Vec::as_slice)
496    }
497}
498
499impl ResolveCandidateContext {
500    pub fn candidates_for<'a>(&'a self, provider: &'a str) -> &'a [ResolveCandidateEntry] {
501        self.provider_candidates
502            .entries
503            .get(provider)
504            .map_or(&[], Vec::as_slice)
505    }
506
507    pub fn add_candidates<S>(
508        &mut self,
509        provider: impl Into<String>,
510        candidates: impl IntoIterator<Item = S>,
511    ) where
512        S: Into<String>,
513    {
514        self.provider_candidates
515            .add_candidates(provider, candidates);
516    }
517
518    pub fn add_closure_metadata(
519        &mut self,
520        provider: impl Into<String>,
521        id: impl Into<String>,
522        source: ResolveCandidateGroupingSource,
523        candidates: impl IntoIterator<Item = impl Into<String>>,
524    ) {
525        self.candidate_grouping.add_grouping(
526            provider,
527            ResolveCandidateGrouping {
528                id: id.into(),
529                source,
530                kind: ResolveCandidateGroupingKind::Closure,
531                candidates: candidates.into_iter().map(Into::into).collect(),
532                rationale: None,
533            },
534        );
535    }
536
537    pub fn add_recommendation_metadata(
538        &mut self,
539        provider: impl Into<String>,
540        id: impl Into<String>,
541        source: ResolveCandidateGroupingSource,
542        candidates: impl IntoIterator<Item = impl Into<String>>,
543        rationale: Option<String>,
544    ) {
545        self.candidate_grouping.add_grouping(
546            provider,
547            ResolveCandidateGrouping {
548                id: id.into(),
549                source,
550                kind: ResolveCandidateGroupingKind::RecommendationSet,
551                candidates: candidates.into_iter().map(Into::into).collect(),
552                rationale,
553            },
554        );
555    }
556
557    pub fn grouping_for<'a>(&'a self, provider: &'a str) -> &'a [ResolveCandidateGrouping] {
558        self.candidate_grouping.groups_for(provider)
559    }
560
561    pub fn with_service_origin_candidates(
562        mut self,
563        adapter: &dyn ServiceOriginCandidateAdapter,
564        payload: &[ServiceOriginCandidatePayload],
565    ) -> Self {
566        adapter.synthesize(&mut self, payload);
567        self
568    }
569
570    pub fn from_package_index_with_service_candidates(package_index: &PackageIndex) -> Self {
571        Self::from_package_index_with_service_origin_adapter_and_payload(
572            package_index,
573            &SynthesizedServiceOriginCandidateAdapter,
574            &ServiceOriginCandidatePayload::from_package_index(package_index),
575        )
576    }
577
578    pub fn from_package_index_with_service_origin_adapter_and_payload(
579        package_index: &PackageIndex,
580        adapter: &dyn ServiceOriginCandidateAdapter,
581        payload: &[ServiceOriginCandidatePayload],
582    ) -> Self {
583        Self::from_package_index(package_index).with_service_origin_candidates(adapter, payload)
584    }
585
586    pub fn from_package_index_with_service_origin_adapter(
587        package_index: &PackageIndex,
588        adapter: &dyn ServiceOriginCandidateAdapter,
589    ) -> Self {
590        Self::from_package_index(package_index).with_service_origin_candidates(
591            adapter,
592            &ServiceOriginCandidatePayload::from_package_index(package_index),
593        )
594    }
595
596    pub fn from_package_index(package_index: &PackageIndex) -> Self {
597        let mut context = Self::default();
598        for package in &package_index.package_sources {
599            let runtime_provider = package.runtime_provider_name();
600            if runtime_provider != package.name {
601                context.provider_candidates.add_candidate_entry(
602                    package.name.clone(),
603                    ResolveCandidateEntry {
604                        candidate: runtime_provider.clone(),
605                        provenance: ResolveCandidateProvenance::PackageIndex(
606                            PackageCandidateProvenance {
607                                package_name: package.name.clone(),
608                                package_kind: package.package_kind.clone(),
609                                runtime_provider: package.runtime_provider.clone(),
610                                current_source: package.current_source.clone(),
611                                source_kind: package.kind.clone(),
612                            },
613                        ),
614                    },
615                );
616
617                context.candidate_grouping.add_grouping(
618                    package.name.clone(),
619                    ResolveCandidateGrouping {
620                        id: format!("index:{}:closure", package.name),
621                        source: ResolveCandidateGroupingSource::Index,
622                        kind: ResolveCandidateGroupingKind::Closure,
623                        candidates: vec![runtime_provider.clone()],
624                        rationale: Some("package-index-runtime-provider".into()),
625                    },
626                );
627            }
628        }
629        context
630    }
631}
632
633fn expand_manifest_capabilities(manifest: &AppManifest) -> Vec<String> {
634    let mut ordered = Vec::new();
635    let mut seen = HashSet::new();
636
637    for capability in &manifest.requires.capabilities {
638        if seen.insert(capability.clone()) {
639            ordered.push(capability.clone());
640        }
641    }
642
643    for feature in &manifest.features.default_enabled {
644        if let Some(binding) = manifest.feature_bindings.get(feature) {
645            for capability in &binding.requires {
646                if seen.insert(capability.clone()) {
647                    ordered.push(capability.clone());
648                }
649            }
650        }
651    }
652
653    ordered
654}
655
656fn provider_candidates_from_package_index<'a>(
657    provider: &'a str,
658    package_index: Option<&'a PackageIndex>,
659) -> Vec<String> {
660    let mut candidates = vec![provider.to_string()];
661    if let Some(runtime_provider) = package_index
662        .and_then(|index| index.get(provider))
663        .map(|pkg| pkg.runtime_provider_name())
664        .filter(|runtime_provider| runtime_provider != provider)
665    {
666        candidates.push(runtime_provider);
667    }
668    candidates
669}
670
671fn provider_candidates<'a>(
672    provider: &'a str,
673    package_index: Option<&'a PackageIndex>,
674    resolve_candidate_context: &ResolveCandidateContext,
675) -> Vec<String> {
676    let mut candidates = provider_candidates_from_package_index(provider, package_index);
677
678    for candidate in resolve_candidate_context
679        .provider_candidates
680        .candidates_for(provider)
681    {
682        if !candidates.contains(&candidate.candidate) {
683            candidates.push(candidate.candidate.clone());
684        }
685    }
686
687    candidates
688}
689
690pub fn resolve_app_manifest(
691    manifest: &AppManifest,
692    packages: &[DiscoveredPackage],
693) -> Result<ResolvedApp> {
694    resolve_app_manifest_with_policy(manifest, packages, None, None, None, None)
695}
696
697pub fn resolve_product_package_declaration(
698    declaration: &AppManifest,
699    packages: &[DiscoveredPackage],
700) -> Result<ResolvedApp> {
701    resolve_app_manifest(declaration, packages)
702}
703
704pub fn resolve_app_manifest_with_policy(
705    manifest: &AppManifest,
706    packages: &[DiscoveredPackage],
707    profile: Option<AppProfile>,
708    policy: Option<&CorePolicy>,
709    core_capabilities: Option<&CapabilityRegistry>,
710    package_index: Option<&PackageIndex>,
711) -> Result<ResolvedApp> {
712    resolve_app_manifest_with_policy_and_candidate_context(
713        manifest,
714        packages,
715        profile,
716        policy,
717        core_capabilities,
718        package_index,
719        &ResolveCandidateContext::default(),
720    )
721}
722
723pub fn resolve_app_manifest_with_policy_and_candidate_context(
724    manifest: &AppManifest,
725    packages: &[DiscoveredPackage],
726    profile: Option<AppProfile>,
727    policy: Option<&CorePolicy>,
728    core_capabilities: Option<&CapabilityRegistry>,
729    package_index: Option<&PackageIndex>,
730    resolve_candidate_context: &ResolveCandidateContext,
731) -> Result<ResolvedApp> {
732    let mut bindings = Vec::new();
733    let mut errors = Vec::new();
734    let flattened_bindings = manifest.flattened_bindings();
735    let expanded_capabilities = expand_manifest_capabilities(manifest);
736
737    for capability in &expanded_capabilities {
738        if let (Some(prof), Some(pol)) = (profile, policy) {
739            let decision = pol.check(capability, prof);
740            if !decision.allowed {
741                errors.push(format!(
742                    "Policy blocks capability '{}': {}",
743                    capability, decision.reason
744                ));
745                continue;
746            }
747        }
748
749        let binding = flattened_bindings
750            .get(capability)
751            .ok_or_else(|| anyhow!("Missing binding for capability '{}'", capability))?;
752
753        let provider_candidates =
754            provider_candidates(&binding.provider, package_index, resolve_candidate_context);
755
756        let matched = packages.iter().find(|package| {
757            provider_candidates.contains(&package.manifest.package_info.name)
758                && package
759                    .manifest
760                    .resolved_provides()
761                    .iter()
762                    .any(|provided| provided == capability)
763        });
764
765        let matched_core = binding.provider == "core"
766            && core_capabilities
767                .and_then(|registry| registry.get(capability))
768                .map(|entry| {
769                    entry
770                        .providers
771                        .iter()
772                        .any(|provider| provider.provider == "core" && provider.runtime == "core")
773                })
774                .unwrap_or(false);
775
776        let matched_index_package = package_index
777            .and_then(|index| index.get(&binding.provider))
778            .map(|package| {
779                !requires_real_package_source(&package.name)
780                    && package.kind == "metadata"
781                    && (package
782                        .provides
783                        .iter()
784                        .any(|provided| provided == capability)
785                        || package.name == binding.provider
786                        || package.runtime_provider_name() == binding.provider)
787            })
788            .unwrap_or(false);
789
790        if matched.is_none() && !matched_core && !matched_index_package {
791            return Err(anyhow!(
792                "Provider '{}' for capability '{}' is not available",
793                binding.provider,
794                capability
795            ));
796        }
797
798        bindings.push(AppBindingResolution {
799            capability: capability.clone(),
800            provider: binding.provider.clone(),
801            mutable: binding.mutable,
802            source: "declaration-default".into(),
803        });
804    }
805
806    let status = if errors.is_empty() {
807        ResolvedAppStatus::Resolved
808    } else {
809        ResolvedAppStatus::Unresolved
810    };
811
812    Ok(ResolvedApp {
813        name: manifest.app.name.clone(),
814        version: manifest.app.version.clone(),
815        display_name: manifest.app.display_name.clone(),
816        description: manifest.app.description.clone(),
817        capabilities: expanded_capabilities,
818        enabled_features: manifest.features.default_enabled.clone(),
819        bindings,
820        validation_checks: manifest.validation.checks.clone(),
821        config_path: None,
822        status,
823        errors,
824        sources: ResolvedAppSources::default(),
825    })
826}
827
828pub fn resolve_product_package_declaration_with_policy(
829    declaration: &AppManifest,
830    packages: &[DiscoveredPackage],
831    profile: Option<AppProfile>,
832    policy: Option<&CorePolicy>,
833    core_capabilities: Option<&CapabilityRegistry>,
834    package_index: Option<&PackageIndex>,
835) -> Result<ResolvedApp> {
836    resolve_app_manifest_with_policy(
837        declaration,
838        packages,
839        profile,
840        policy,
841        core_capabilities,
842        package_index,
843    )
844}
845
846pub fn resolve_product_package_declaration_with_policy_and_candidate_context(
847    declaration: &AppManifest,
848    packages: &[DiscoveredPackage],
849    profile: Option<AppProfile>,
850    policy: Option<&CorePolicy>,
851    core_capabilities: Option<&CapabilityRegistry>,
852    package_index: Option<&PackageIndex>,
853    resolve_candidate_context: &ResolveCandidateContext,
854) -> Result<ResolvedApp> {
855    resolve_app_manifest_with_policy_and_candidate_context(
856        declaration,
857        packages,
858        profile,
859        policy,
860        core_capabilities,
861        package_index,
862        resolve_candidate_context,
863    )
864}
865
866#[cfg(test)]
867mod tests {
868    use super::{
869        resolve_app_manifest_with_policy, resolve_app_manifest_with_policy_and_candidate_context,
870        PackageCandidateProvenance, ResolveCandidateContext, ResolveCandidateGroupingKind,
871        ResolveCandidateGroupingSource, ResolveCandidateProvenance, ResolveInputCoordinator,
872        ServiceContractCandidateClosure, ServiceContractCandidatePayload,
873        ServiceContractCandidateProvenance, ServiceOriginCandidatePayload,
874        ServiceOriginCandidatePayloadFixture, SynthesizedServiceOriginCandidateAdapter,
875    };
876    use crate::app::{CapabilityRegistry, PackageIndex, PackageSource, ResolvedAppStatus};
877    use crate::package::{DiscoveredPackage, PackageRuntime};
878
879    fn package_source(
880        name: &str,
881        kind: &str,
882        current_source: &str,
883        runtime_provider: &str,
884    ) -> PackageSource {
885        PackageSource {
886            name: name.into(),
887            kind: kind.into(),
888            package_kind: String::new(),
889            runtime_provider: runtime_provider.into(),
890            current_source: current_source.into(),
891            trusted: true,
892            signature: "builtin:official".into(),
893            source_authority: String::new(),
894            source_public_keys: vec![],
895            provides: vec![],
896            requires: vec![],
897        }
898    }
899
900    fn manifest_with_agent_runtime_dependency() -> crate::app::config::AppManifest {
901        toml::from_str(
902            r#"
903        [app]
904        name = "contract-matrix-app"
905        version = "0.1.0"
906        display_name = "Contract Matrix App"
907        description = "test"
908
909        [requires]
910        capabilities = ["agent.runtime"]
911
912        [bindings.agent.runtime]
913        provider = "agent-runtime"
914        mutable = false
915        "#,
916        )
917        .expect("manifest parses")
918    }
919
920    fn proxy_packages() -> Vec<DiscoveredPackage> {
921        vec![DiscoveredPackage {
922            manifest: toml::from_str(
923                r#"
924            [package_info]
925            name = "agent-runtime-proxy"
926            version = "0.1.0"
927            description = "agent runtime proxy"
928            entry = "package.wasm"
929
930            [capability]
931            provides = ["agent.runtime"]
932            "#,
933            )
934            .expect("agent runtime proxy manifest parses"),
935            dir: std::path::PathBuf::from("agent-runtime-proxy"),
936            entry_path: None,
937            runtime: PackageRuntime::Wasm,
938        }]
939    }
940
941    fn manifest_package_index_with_service_proxy() -> PackageIndex {
942        PackageIndex {
943            version: 1,
944            revision: "test-rev".into(),
945            source_url: "local://packages".into(),
946            package_sources: vec![PackageSource {
947                name: "agent-runtime-proxy".into(),
948                kind: "service".into(),
949                package_kind: "provider".into(),
950                runtime_provider: "agent-runtime".into(),
951                current_source: "services/agent-runtime-proxy".into(),
952                trusted: true,
953                signature: "service-proxy".into(),
954                source_authority: String::new(),
955                source_public_keys: vec![],
956                provides: vec!["agent.runtime".into()],
957                requires: vec![],
958            }],
959        }
960    }
961
962    #[test]
963    fn ui_metadata_prefers_existing_package_ui_declaration() {
964        let repo_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
965        let product_dir = repo_root.join("packages").join("weft-claw");
966        let manifest = crate::app::load_product_package_declaration(&product_dir)
967            .expect("product package declaration loads");
968        let package_index = PackageIndex {
969            version: 1,
970            revision: "test-rev".into(),
971            source_url: "local://packages".into(),
972            package_sources: vec![
973                PackageSource {
974                    ..package_source("agent-runtime", "wasm", "packages/official/agent-core", "agent-runtime")
975                },
976                PackageSource {
977                    ..package_source("skills-runtime", "wasm", "packages/official/skills", "skills-runtime")
978                },
979                PackageSource {
980                    ..package_source("memory-store", "wasm", "packages/official/memory", "memory-store")
981                },
982                PackageSource {
983                    ..package_source("channel-core", "wasm", "packages/official/channels", "channel-core")
984                },
985                PackageSource {
986                    ..package_source(
987                        "workflow-orchestrator",
988                        "wasm",
989                        "packages/official/workflow-orchestrator",
990                        "workflow-orchestrator",
991                    )
992                },
993                PackageSource {
994                    ..package_source(
995                        "team-runtime",
996                        "wasm",
997                        "packages/official/team-runtime",
998                        "team-runtime",
999                    )
1000                },
1001                PackageSource {
1002                    ..package_source(
1003                        "team-task-board",
1004                        "wasm",
1005                        "packages/official/team-task-board",
1006                        "team-task-board",
1007                    )
1008                },
1009                PackageSource {
1010                    ..package_source(
1011                        "workflow-template-devteam",
1012                        "wasm",
1013                        "packages/official/workflow-template-devteam",
1014                        "workflow-template-devteam",
1015                    )
1016                },
1017                PackageSource {
1018                    name: "weft-claw-ui".into(),
1019                    kind: "embedded".into(),
1020                    package_kind: "provider".into(),
1021                    runtime_provider: "weft-claw-ui".into(),
1022                    current_source: "packages/installed/weft-claw".into(),
1023                    trusted: true,
1024                    signature: "ed25519:/RckOFqgx1tk+3jNYC+h2ZH96/drE8WO1wLqyDXp9hg=:W5R67MWOB0WbCerqoefO7toDvZ5POEQ7NtLCLZGhi4Vgv/h5eF6SjfCShELaxa3OEki6KI7iTnRvMNPDKmPZBw==".into(),
1025                    source_authority: "local-installed".into(),
1026                    source_public_keys: vec!["/RckOFqgx1tk+3jNYC+h2ZH96/drE8WO1wLqyDXp9hg=".into()],
1027                    provides: vec!["ui.surface".into()],
1028                    requires: vec![],
1029                },
1030              ],
1031          };
1032        let packages = vec![
1033            DiscoveredPackage {
1034                manifest: toml::from_str(
1035                    r#"
1036[package_info]
1037name = "agent-runtime"
1038version = "0.1.0"
1039description = "agent"
1040entry = "package.wasm"
1041
1042[capability]
1043provides = ["agent.runtime", "team.delegate"]
1044"#,
1045                )
1046                .expect("agent-core manifest parses"),
1047                dir: repo_root
1048                    .join("packages")
1049                    .join("official")
1050                    .join("agent-core"),
1051                entry_path: None,
1052                runtime: PackageRuntime::Wasm,
1053            },
1054            DiscoveredPackage {
1055                manifest: toml::from_str(
1056                    r#"
1057[package_info]
1058name = "skills-runtime"
1059version = "0.1.0"
1060description = "skills"
1061entry = "package.wasm"
1062
1063[capability]
1064provides = ["ext.skills"]
1065"#,
1066                )
1067                .expect("skills manifest parses"),
1068                dir: repo_root.join("packages").join("official").join("skills"),
1069                entry_path: None,
1070                runtime: PackageRuntime::Wasm,
1071            },
1072            DiscoveredPackage {
1073                manifest: toml::from_str(
1074                    r#"
1075[package_info]
1076name = "memory-store"
1077version = "0.1.0"
1078description = "memory"
1079entry = "package.wasm"
1080
1081[capability]
1082provides = ["memory.store"]
1083"#,
1084                )
1085                .expect("memory manifest parses"),
1086                dir: repo_root.join("packages").join("official").join("memory"),
1087                entry_path: None,
1088                runtime: PackageRuntime::Wasm,
1089            },
1090            DiscoveredPackage {
1091                manifest: toml::from_str(
1092                    r#"
1093[package_info]
1094name = "channel-core"
1095version = "0.1.0"
1096description = "channels"
1097entry = "package.wasm"
1098
1099[capability]
1100provides = ["channel.bridge"]
1101"#,
1102                )
1103                .expect("channels manifest parses"),
1104                dir: repo_root.join("packages").join("official").join("channels"),
1105                entry_path: None,
1106                runtime: PackageRuntime::Wasm,
1107            },
1108            DiscoveredPackage {
1109                manifest: toml::from_str(
1110                    r#"
1111[package_info]
1112name = "workflow-orchestrator"
1113version = "0.1.0"
1114description = "workflow"
1115entry = "package.wasm"
1116
1117[capability]
1118provides = ["workflow.orchestration"]
1119"#,
1120                )
1121                .expect("workflow-orchestrator manifest parses"),
1122                dir: repo_root
1123                    .join("packages")
1124                    .join("official")
1125                    .join("workflow-orchestrator"),
1126                entry_path: None,
1127                runtime: PackageRuntime::Wasm,
1128            },
1129            DiscoveredPackage {
1130                manifest: toml::from_str(
1131                    r#"
1132[package_info]
1133name = "team-runtime"
1134version = "0.1.0"
1135description = "team runtime"
1136entry = "package.wasm"
1137
1138[capability]
1139provides = ["team.runtime", "team.role.catalog", "team.context.shared"]
1140"#,
1141                )
1142                .expect("team-runtime manifest parses"),
1143                dir: repo_root
1144                    .join("packages")
1145                    .join("official")
1146                    .join("team-runtime"),
1147                entry_path: None,
1148                runtime: PackageRuntime::Wasm,
1149            },
1150            DiscoveredPackage {
1151                manifest: toml::from_str(
1152                    r#"
1153[package_info]
1154name = "team-task-board"
1155version = "0.1.0"
1156description = "team task board"
1157entry = "package.wasm"
1158
1159[capability]
1160provides = ["team.taskboard", "team.handoff"]
1161"#,
1162                )
1163                .expect("team-task-board manifest parses"),
1164                dir: repo_root
1165                    .join("packages")
1166                    .join("official")
1167                    .join("team-task-board"),
1168                entry_path: None,
1169                runtime: PackageRuntime::Wasm,
1170            },
1171            DiscoveredPackage {
1172                manifest: toml::from_str(
1173                    r#"
1174[package_info]
1175name = "workflow-template-devteam"
1176version = "0.1.0"
1177description = "devteam workflow template"
1178entry = "package.wasm"
1179
1180[capability]
1181provides = ["workflow.template.devteam"]
1182"#,
1183                )
1184                .expect("workflow-template-devteam manifest parses"),
1185                dir: repo_root
1186                    .join("packages")
1187                    .join("official")
1188                    .join("workflow-template-devteam"),
1189                entry_path: None,
1190                runtime: PackageRuntime::Wasm,
1191            },
1192            DiscoveredPackage {
1193                manifest: toml::from_str(
1194                    r#"
1195[package_info]
1196name = "mcp-client"
1197version = "0.1.0"
1198description = "mcp"
1199entry = "package.wasm"
1200
1201[capability]
1202provides = ["ext.mcp"]
1203"#,
1204                )
1205                .expect("mcp-client manifest parses"),
1206                dir: repo_root
1207                    .join("packages")
1208                    .join("official")
1209                    .join("mcp-client"),
1210                entry_path: None,
1211                runtime: PackageRuntime::Wasm,
1212            },
1213            DiscoveredPackage {
1214                manifest: toml::from_str(
1215                    r#"
1216[package_info]
1217name = "session-events"
1218version = "0.1.0"
1219description = "session events"
1220entry = "package.wasm"
1221
1222[capability]
1223provides = ["session.events"]
1224"#,
1225                )
1226                .expect("session-events manifest parses"),
1227                dir: repo_root
1228                    .join("packages")
1229                    .join("official")
1230                    .join("session-events"),
1231                entry_path: None,
1232                runtime: PackageRuntime::Wasm,
1233            },
1234            DiscoveredPackage {
1235                manifest: toml::from_str(
1236                    r#"
1237[package_info]
1238name = "prompt-system"
1239version = "0.1.0"
1240description = "prompt system"
1241entry = "package.wasm"
1242
1243[capability]
1244provides = ["prompt.system"]
1245"#,
1246                )
1247                .expect("prompt-system manifest parses"),
1248                dir: repo_root
1249                    .join("packages")
1250                    .join("official")
1251                    .join("prompt-system"),
1252                entry_path: None,
1253                runtime: PackageRuntime::Wasm,
1254            },
1255            DiscoveredPackage {
1256                manifest: toml::from_str(
1257                    r#"
1258[package_info]
1259name = "weft-claw"
1260version = "0.1.0"
1261description = "weft claw runtime"
1262entry = "package.wasm"
1263
1264[capability]
1265provides = ["weft_claw.turn", "ui.surface"]
1266"#,
1267                )
1268                .expect("weft-claw manifest parses"),
1269                dir: repo_root
1270                    .join("packages")
1271                    .join("official")
1272                    .join("weft-claw"),
1273                entry_path: None,
1274                runtime: PackageRuntime::Wasm,
1275            },
1276            DiscoveredPackage {
1277                manifest: toml::from_str(
1278                    r#"
1279[package_info]
1280name = "weft-claw-ui"
1281version = "0.1.0"
1282description = "Weft Claw UI shell"
1283
1284[ui]
1285title = "Weft Claw"
1286icon = "robot"
1287mode = "native:ai-director"
1288
1289[capability]
1290provides = ["ui.surface"]
1291"#,
1292                )
1293                .expect("weft-claw ui manifest parses"),
1294                dir: repo_root
1295                    .join("packages")
1296                    .join("installed")
1297                    .join("weft-claw"),
1298                entry_path: None,
1299                runtime: PackageRuntime::Wasm,
1300            },
1301        ];
1302
1303        let mut registry = CapabilityRegistry::new();
1304        registry.insert(
1305            "core.execution".into(),
1306            crate::app::CapabilityRegistryEntry {
1307                capability: "core.execution".into(),
1308                providers: vec![crate::app::CapabilityProviderRecord {
1309                    provider: "core".into(),
1310                    runtime: "core".into(),
1311                    priority: 0,
1312                }],
1313                bindings: vec![],
1314            },
1315        );
1316
1317        let resolved = resolve_app_manifest_with_policy(
1318            &manifest,
1319            &packages,
1320            None,
1321            None,
1322            Some(&registry),
1323            Some(&package_index),
1324        )
1325        .expect("app resolves");
1326
1327        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
1328
1329        let matched_package = packages
1330            .iter()
1331            .find(|package| package.manifest.package_info.name == "weft-claw")
1332            .expect("weft-claw runtime package discovered");
1333        assert!(matches!(matched_package.runtime, PackageRuntime::Wasm));
1334    }
1335
1336    #[test]
1337    fn resolve_with_candidate_context_can_resolve_aliased_provider() {
1338        let manifest: crate::app::config::AppManifest = toml::from_str(
1339            r#"
1340        [app]
1341        name = "alias-app"
1342        version = "0.1.0"
1343        display_name = "Alias App"
1344        description = "test"
1345
1346        [requires]
1347        capabilities = ["agent.runtime"]
1348
1349        [bindings.agent.runtime]
1350        provider = "agent-runtime-alias"
1351        mutable = false
1352        "#,
1353        )
1354        .expect("manifest parses");
1355
1356        let packages = vec![DiscoveredPackage {
1357            manifest: toml::from_str(
1358                r#"
1359            [package_info]
1360            name = "agent-runtime"
1361            version = "0.1.0"
1362            description = "agent"
1363            entry = "package.wasm"
1364
1365            [capability]
1366            provides = ["agent.runtime"]
1367            "#,
1368            )
1369            .expect("agent manifest parses"),
1370            dir: std::path::PathBuf::from("agent-runtime"),
1371            entry_path: None,
1372            runtime: PackageRuntime::Wasm,
1373        }];
1374
1375        let err = resolve_app_manifest_with_policy(&manifest, &packages, None, None, None, None)
1376            .expect_err("missing alias should fail without external candidates");
1377
1378        assert!(err.to_string().contains(
1379            "Provider 'agent-runtime-alias' for capability 'agent.runtime' is not available"
1380        ));
1381
1382        let mut resolve_candidate_context = ResolveCandidateContext::default();
1383        resolve_candidate_context.add_candidates("agent-runtime-alias", vec!["agent-runtime"]);
1384
1385        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
1386            &manifest,
1387            &packages,
1388            None,
1389            None,
1390            None,
1391            None,
1392            &resolve_candidate_context,
1393        )
1394        .expect("external candidate source should resolve alias provider");
1395
1396        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
1397        assert_eq!(resolved.bindings[0].capability, "agent.runtime");
1398        assert_eq!(resolved.bindings[0].provider, "agent-runtime-alias");
1399        assert_eq!(resolved.bindings.len(), 1);
1400    }
1401
1402    #[test]
1403    fn candidate_context_can_store_grouping_metadata() {
1404        let mut context = ResolveCandidateContext::default();
1405
1406        context.add_closure_metadata(
1407            "agent-runtime-alias",
1408            "closure-1",
1409            ResolveCandidateGroupingSource::Local,
1410            vec!["agent-runtime", "agent-runtime-v2"],
1411        );
1412        context.add_recommendation_metadata(
1413            "agent-runtime-alias",
1414            "recommendation-1",
1415            ResolveCandidateGroupingSource::Service,
1416            vec!["agent-runtime", "agent-runtime-pro"],
1417            Some("preferred runtime set".to_string()),
1418        );
1419
1420        let groups = context.grouping_for("agent-runtime-alias");
1421        assert_eq!(groups.len(), 2);
1422
1423        let closure = groups
1424            .iter()
1425            .find(|group| matches!(group.kind, ResolveCandidateGroupingKind::Closure))
1426            .expect("closure grouping should exist");
1427        assert_eq!(closure.id, "closure-1");
1428        assert_eq!(closure.candidates.len(), 2);
1429
1430        let recommendation = groups
1431            .iter()
1432            .find(|group| matches!(group.kind, ResolveCandidateGroupingKind::RecommendationSet))
1433            .expect("recommendation grouping should exist");
1434        assert_eq!(
1435            recommendation.rationale,
1436            Some("preferred runtime set".to_string())
1437        );
1438    }
1439
1440    #[test]
1441    fn service_origin_can_supply_synthesized_candidates() {
1442        let manifest: crate::app::config::AppManifest = toml::from_str(
1443            r#"
1444        [app]
1445        name = "service-origin-app"
1446        version = "0.1.0"
1447        display_name = "Service-Origin App"
1448        description = "test"
1449
1450        [requires]
1451        capabilities = ["agent.runtime"]
1452
1453        [bindings.agent.runtime]
1454        provider = "agent-runtime"
1455        mutable = false
1456        "#,
1457        )
1458        .expect("manifest parses");
1459
1460        let packages = vec![DiscoveredPackage {
1461            manifest: toml::from_str(
1462                r#"
1463            [package_info]
1464            name = "agent-runtime-proxy"
1465            version = "0.1.0"
1466            description = "agent proxy"
1467            entry = "package.wasm"
1468
1469            [capability]
1470            provides = ["agent.runtime"]
1471            "#,
1472            )
1473            .expect("agent runtime proxy manifest parses"),
1474            dir: std::path::PathBuf::from("agent-runtime-proxy"),
1475            entry_path: None,
1476            runtime: PackageRuntime::Wasm,
1477        }];
1478
1479        let package_index = PackageIndex {
1480            version: 1,
1481            revision: "test-rev".into(),
1482            source_url: "local://packages".into(),
1483            package_sources: vec![PackageSource {
1484                name: "agent-runtime-proxy".into(),
1485                kind: "service".into(),
1486                package_kind: "provider".into(),
1487                runtime_provider: "agent-runtime".into(),
1488                current_source: "services/agent-runtime-proxy".into(),
1489                trusted: true,
1490                signature: "service-proxy".into(),
1491                source_authority: String::new(),
1492                source_public_keys: vec![],
1493                provides: vec!["agent.runtime".into()],
1494                requires: vec![],
1495            }],
1496        };
1497
1498        let err = resolve_app_manifest_with_policy_and_candidate_context(
1499            &manifest,
1500            &packages,
1501            None,
1502            None,
1503            None,
1504            None,
1505            &ResolveCandidateContext::from_package_index(&package_index),
1506        )
1507        .expect_err("missing synthesized service candidates should fail without helper");
1508
1509        assert!(err
1510            .to_string()
1511            .contains("Provider 'agent-runtime' for capability 'agent.runtime' is not available"));
1512
1513        let resolve_candidate_context =
1514            ResolveCandidateContext::from_package_index_with_service_candidates(&package_index);
1515
1516        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
1517            &manifest,
1518            &packages,
1519            None,
1520            None,
1521            None,
1522            None,
1523            &resolve_candidate_context,
1524        )
1525        .expect("service-origin candidates should resolve provider alias");
1526
1527        let groups = resolve_candidate_context.grouping_for("agent-runtime");
1528        assert_eq!(groups.len(), 1);
1529        assert!(matches!(
1530            groups[0].source,
1531            ResolveCandidateGroupingSource::Service
1532        ));
1533
1534        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
1535        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
1536    }
1537
1538    #[test]
1539    fn resolve_input_coordinator_prepares_service_candidates() {
1540        use crate::app::ResolveCandidateContext;
1541
1542        let package_index = PackageIndex {
1543            version: 1,
1544            revision: "test-rev".into(),
1545            source_url: "local://packages".into(),
1546            package_sources: vec![PackageSource {
1547                name: "agent-runtime-proxy".into(),
1548                kind: "service".into(),
1549                package_kind: "provider".into(),
1550                runtime_provider: "agent-runtime".into(),
1551                current_source: "services/agent-runtime-proxy".into(),
1552                trusted: true,
1553                signature: "service-proxy".into(),
1554                source_authority: String::new(),
1555                source_public_keys: vec![],
1556                provides: vec!["agent.runtime".into()],
1557                requires: vec![],
1558            }],
1559        };
1560
1561        let coordinator = ResolveInputCoordinator::from_package_index(&package_index)
1562            .with_synthesized_service_origin_candidates()
1563            .build();
1564
1565        let groups = coordinator
1566            .resolve_candidate_context()
1567            .grouping_for("agent-runtime");
1568
1569        let mut has_service_closure = false;
1570        for group in groups {
1571            if matches!(group.source, ResolveCandidateGroupingSource::Service)
1572                && matches!(group.kind, ResolveCandidateGroupingKind::Closure)
1573                && group
1574                    .candidates
1575                    .iter()
1576                    .any(|candidate| candidate == "agent-runtime")
1577            {
1578                has_service_closure = true;
1579                break;
1580            }
1581        }
1582        assert!(has_service_closure);
1583
1584        let base_context = ResolveCandidateContext::from_package_index(&package_index);
1585        let base_groupings = base_context.grouping_for("agent-runtime");
1586        assert_eq!(base_groupings.len(), 0);
1587    }
1588
1589    #[test]
1590    fn service_origin_fixture_payload_maps_multiple_dto_shapes() {
1591        let fixture_payload = vec![
1592            ServiceOriginCandidatePayloadFixture {
1593                provider: "  agent-runtime  ".to_string(),
1594                candidates: vec![
1595                    "agent-runtime-proxy".to_string(),
1596                    "  agent-runtime  ".to_string(),
1597                    "   ".to_string(),
1598                ],
1599                closure_candidates: vec![
1600                    "  ".to_string(),
1601                    "agent-runtime".to_string(),
1602                    "agent-runtime-proxy".to_string(),
1603                ],
1604                closure_id: Some("   ".to_string()),
1605                closure_rationale: Some("   ".to_string()),
1606                provenance: Some(PackageCandidateProvenance {
1607                    package_name: "agent-runtime-proxy".into(),
1608                    package_kind: "provider".into(),
1609                    runtime_provider: "agent-runtime".into(),
1610                    current_source: "services/agent-runtime-proxy".into(),
1611                    source_kind: "service".into(),
1612                }),
1613            },
1614            ServiceOriginCandidatePayloadFixture {
1615                provider: "  ".to_string(),
1616                candidates: vec!["should-not-appear".to_string()],
1617                closure_candidates: vec!["agent-runtime".to_string()],
1618                closure_id: None,
1619                closure_rationale: None,
1620                provenance: None,
1621            },
1622            ServiceOriginCandidatePayloadFixture {
1623                provider: "memory-provider".to_string(),
1624                candidates: vec!["memory-provider".to_string()],
1625                closure_candidates: vec!["memory-provider".to_string()],
1626                closure_id: Some("memory-provider:closure".to_string()),
1627                closure_rationale: Some("provider overlap test".to_string()),
1628                provenance: None,
1629            },
1630        ];
1631
1632        let payload = ServiceOriginCandidatePayload::from_fixtures(&fixture_payload);
1633
1634        assert_eq!(payload.len(), 2);
1635
1636        let runtime_payload = payload
1637            .iter()
1638            .find(|entry| entry.provider == "agent-runtime");
1639        let runtime_payload = runtime_payload.expect("runtime payload maps");
1640
1641        assert_eq!(
1642            runtime_payload.candidates[0].candidate,
1643            "agent-runtime-proxy"
1644        );
1645        assert_eq!(runtime_payload.candidates[1].candidate, "agent-runtime");
1646        assert_eq!(
1647            runtime_payload.closure_metadata.id,
1648            "service-fixture:agent-runtime:closure"
1649        );
1650        assert_eq!(
1651            runtime_payload.closure_metadata.rationale,
1652            Some("service-candidate-fixture".to_string())
1653        );
1654        assert_eq!(
1655            runtime_payload.closure_metadata.candidates,
1656            vec![
1657                "agent-runtime".to_string(),
1658                "agent-runtime-proxy".to_string()
1659            ]
1660        );
1661        assert!(matches!(
1662            runtime_payload.candidates[0]
1663                .provenance
1664                .as_ref()
1665                .expect("provenance maps")
1666                .package_name
1667                .as_str(),
1668            "agent-runtime-proxy"
1669        ));
1670
1671        let mut context = ResolveCandidateContext::default();
1672        context = context
1673            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
1674
1675        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
1676            &manifest_with_agent_runtime_dependency(),
1677            &proxy_packages(),
1678            None,
1679            None,
1680            None,
1681            None,
1682            &context,
1683        )
1684        .expect("fixture mapper payload should drive resolution");
1685
1686        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
1687        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
1688        assert_eq!(
1689            context.candidates_for("agent-runtime").len(),
1690            2,
1691            "fixture entry should include overlap and non-empty candidate values"
1692        );
1693        assert!(matches!(
1694            context.candidates_for("agent-runtime")[1].provenance,
1695            ResolveCandidateProvenance::PackageIndex(_)
1696        ));
1697    }
1698
1699    #[test]
1700    fn service_origin_fixture_payload_trims_and_defaults_optional_fields() {
1701        let fixture_payload = vec![
1702            ServiceOriginCandidatePayloadFixture {
1703                provider: "  memory-provider  ".to_string(),
1704                candidates: vec![
1705                    "  ".to_string(),
1706                    "memory-provider".to_string(),
1707                    "memory-provider-proxy".to_string(),
1708                ],
1709                closure_candidates: vec!["  ".to_string()],
1710                closure_id: None,
1711                closure_rationale: Some("  ".to_string()),
1712                provenance: Some(PackageCandidateProvenance {
1713                    package_name: "memory-provider-proxy".into(),
1714                    package_kind: "provider".into(),
1715                    runtime_provider: "memory-provider".into(),
1716                    current_source: "services/memory-provider-proxy".into(),
1717                    source_kind: "service".into(),
1718                }),
1719            },
1720            ServiceOriginCandidatePayloadFixture {
1721                provider: "   ".to_string(),
1722                candidates: vec!["ignore-me".to_string()],
1723                closure_candidates: vec!["memory-provider".to_string()],
1724                closure_id: Some("memory-provider:closure".to_string()),
1725                closure_rationale: Some("unused".to_string()),
1726                provenance: None,
1727            },
1728        ];
1729
1730        let payload = ServiceOriginCandidatePayload::from_fixtures(&fixture_payload);
1731
1732        assert_eq!(payload.len(), 1);
1733
1734        let memory_payload = &payload[0];
1735        assert_eq!(memory_payload.provider, "memory-provider");
1736        assert_eq!(
1737            memory_payload.closure_metadata.id,
1738            "service-fixture:memory-provider:closure"
1739        );
1740        assert_eq!(
1741            memory_payload.closure_metadata.candidates,
1742            vec!["memory-provider".to_string()]
1743        );
1744        assert_eq!(
1745            memory_payload.closure_metadata.rationale,
1746            Some("service-candidate-fixture".to_string())
1747        );
1748        assert_eq!(memory_payload.candidates.len(), 2);
1749        assert_eq!(memory_payload.candidates[0].candidate, "memory-provider");
1750        assert_eq!(
1751            memory_payload.candidates[1].candidate,
1752            "memory-provider-proxy"
1753        );
1754    }
1755
1756    #[test]
1757    fn service_contract_payload_handles_optional_closure_fields_and_provenance() {
1758        let contract_payload = vec![
1759            ServiceContractCandidatePayload {
1760                provider: "  memory-provider  ".to_string(),
1761                candidates: vec![
1762                    "memory-provider-proxy".to_string(),
1763                    "memory-provider".to_string(),
1764                ],
1765                closure: ServiceContractCandidateClosure {
1766                    id: None,
1767                    candidates: vec!["   ".to_string(), "memory-provider".to_string()],
1768                    rationale: Some("  ".to_string()),
1769                },
1770                provenance: Some(ServiceContractCandidateProvenance {
1771                    package_name: "memory-provider-proxy".into(),
1772                    package_kind: "provider".into(),
1773                    runtime_provider: "memory-provider".into(),
1774                    current_source: "services/memory-provider-proxy".into(),
1775                    source_kind: "service".into(),
1776                }),
1777            },
1778            ServiceContractCandidatePayload {
1779                provider: "  \tagent-runtime\t  ".to_string(),
1780                candidates: vec!["   ".to_string(), "agent-runtime-proxy".to_string()],
1781                closure: ServiceContractCandidateClosure {
1782                    id: Some("\t".to_string()),
1783                    candidates: vec!["agent-runtime".to_string()],
1784                    rationale: None,
1785                },
1786                provenance: None,
1787            },
1788        ];
1789
1790        let payload =
1791            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
1792
1793        assert_eq!(payload.len(), 2);
1794
1795        let memory_payload = payload
1796            .iter()
1797            .find(|entry| entry.provider == "memory-provider")
1798            .expect("memory provider should map");
1799        assert_eq!(
1800            memory_payload.closure_metadata.id,
1801            "service-contract:memory-provider:closure"
1802        );
1803        assert_eq!(memory_payload.candidates.len(), 2);
1804        assert_eq!(
1805            memory_payload.candidates[0].candidate,
1806            "memory-provider-proxy"
1807        );
1808        assert_eq!(memory_payload.candidates[1].candidate, "memory-provider");
1809        for candidate in &memory_payload.candidates {
1810            let provenance = candidate
1811                .provenance
1812                .as_ref()
1813                .expect("provenance should be propagated");
1814            assert_eq!(provenance.package_name, "memory-provider-proxy");
1815            assert_eq!(provenance.package_kind, "provider");
1816            assert_eq!(provenance.runtime_provider, "memory-provider");
1817            assert_eq!(provenance.current_source, "services/memory-provider-proxy");
1818            assert_eq!(provenance.source_kind, "service");
1819        }
1820        assert_eq!(
1821            memory_payload.closure_metadata.candidates,
1822            vec!["memory-provider".to_string()]
1823        );
1824        assert_eq!(
1825            memory_payload.closure_metadata.rationale,
1826            Some("service-candidate-contract".to_string())
1827        );
1828
1829        let runtime_payload = payload
1830            .iter()
1831            .find(|entry| entry.provider == "agent-runtime")
1832            .expect("agent runtime should map");
1833        assert_eq!(
1834            runtime_payload.closure_metadata.id,
1835            "service-contract:agent-runtime:closure"
1836        );
1837        assert_eq!(runtime_payload.candidates.len(), 1);
1838        assert_eq!(
1839            runtime_payload.candidates[0].candidate,
1840            "agent-runtime-proxy"
1841        );
1842        assert!(runtime_payload.candidates[0].provenance.is_none());
1843
1844        let context = ResolveCandidateContext::default()
1845            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
1846
1847        assert!(matches!(
1848            context.candidates_for("memory-provider")[0].provenance,
1849            ResolveCandidateProvenance::PackageIndex(_)
1850        ));
1851        assert!(matches!(
1852            context.candidates_for("agent-runtime")[0].provenance,
1853            ResolveCandidateProvenance::External
1854        ));
1855
1856        let package_index = manifest_package_index_with_service_proxy();
1857        let coordinator = ResolveInputCoordinator::from_package_index(&package_index)
1858            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
1859            .build();
1860
1861        let groups = coordinator
1862            .resolve_candidate_context()
1863            .grouping_for("memory-provider");
1864        assert_eq!(groups.len(), 1);
1865        assert_eq!(groups[0].id, "service-contract:memory-provider:closure");
1866        assert_eq!(groups[0].candidates, vec!["memory-provider".to_string()]);
1867    }
1868
1869    #[test]
1870    fn service_origin_fixture_payload_handles_optional_closure_fields_and_overlap() {
1871        let fixture_payload = vec![ServiceOriginCandidatePayloadFixture {
1872            provider: "  memory-provider  ".to_string(),
1873            candidates: vec![
1874                "memory-provider".to_string(),
1875                "memory-provider-proxy".to_string(),
1876                "   ".to_string(),
1877            ],
1878            closure_candidates: vec!["  ".to_string()],
1879            closure_id: None,
1880            closure_rationale: None,
1881            provenance: Some(PackageCandidateProvenance {
1882                package_name: "memory-provider-proxy".into(),
1883                package_kind: "provider".into(),
1884                runtime_provider: "memory-provider".into(),
1885                current_source: "services/memory-provider-proxy".into(),
1886                source_kind: "service".into(),
1887            }),
1888        }];
1889
1890        let payload = ServiceOriginCandidatePayload::from_fixtures(&fixture_payload);
1891
1892        assert_eq!(payload.len(), 1);
1893
1894        let memory_payload = &payload[0];
1895        assert_eq!(memory_payload.provider, "memory-provider");
1896        assert_eq!(
1897            memory_payload.closure_metadata.id,
1898            "service-fixture:memory-provider:closure"
1899        );
1900        assert_eq!(
1901            memory_payload.closure_metadata.rationale,
1902            Some("service-candidate-fixture".to_string())
1903        );
1904        assert_eq!(
1905            memory_payload.closure_metadata.candidates,
1906            vec!["memory-provider".to_string()]
1907        );
1908        assert_eq!(memory_payload.candidates[0].candidate, "memory-provider");
1909        assert_eq!(
1910            memory_payload.candidates[1].candidate,
1911            "memory-provider-proxy"
1912        );
1913        assert_eq!(memory_payload.candidates.len(), 2);
1914
1915        let context = ResolveCandidateContext::default()
1916            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
1917
1918        let candidates = context.candidates_for("memory-provider");
1919        assert_eq!(candidates.len(), 2);
1920        assert_eq!(candidates[0].candidate, "memory-provider");
1921        assert_eq!(candidates[1].candidate, "memory-provider-proxy");
1922        assert!(matches!(
1923            candidates[0].provenance,
1924            ResolveCandidateProvenance::PackageIndex(_)
1925        ));
1926    }
1927
1928    #[test]
1929    fn service_contract_payload_maps_defaults_and_drives_resolution() {
1930        let contract_payload = vec![
1931            ServiceContractCandidatePayload {
1932                provider: " agent-runtime ".to_string(),
1933                candidates: vec![
1934                    " agent-runtime-proxy ".to_string(),
1935                    "   ".to_string(),
1936                    "agent-runtime".to_string(),
1937                ],
1938                closure: ServiceContractCandidateClosure {
1939                    id: Some("  ".to_string()),
1940                    candidates: vec!["  ".to_string(), " agent-runtime ".to_string()],
1941                    rationale: Some("   ".to_string()),
1942                },
1943                provenance: None,
1944            },
1945            ServiceContractCandidatePayload {
1946                provider: "  ".to_string(),
1947                candidates: vec!["agent-runtime-proxy".to_string()],
1948                closure: ServiceContractCandidateClosure {
1949                    id: None,
1950                    candidates: vec!["agent-runtime".to_string()],
1951                    rationale: None,
1952                },
1953                provenance: Some(ServiceContractCandidateProvenance {
1954                    package_name: "agent-runtime-proxy".into(),
1955                    package_kind: "provider".into(),
1956                    runtime_provider: "agent-runtime".into(),
1957                    current_source: "services/agent-runtime-proxy".into(),
1958                    source_kind: "service".into(),
1959                }),
1960            },
1961        ];
1962
1963        let payload =
1964            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
1965
1966        assert_eq!(payload.len(), 1);
1967        let mapped = &payload[0];
1968
1969        assert_eq!(mapped.provider, "agent-runtime");
1970        assert_eq!(
1971            mapped.closure_metadata.id,
1972            "service-contract:agent-runtime:closure"
1973        );
1974        assert_eq!(
1975            mapped.closure_metadata.rationale,
1976            Some("service-candidate-contract".to_string())
1977        );
1978        assert_eq!(mapped.candidates[0].candidate, "agent-runtime-proxy");
1979        assert_eq!(mapped.candidates[1].candidate, "agent-runtime");
1980
1981        let mut context = ResolveCandidateContext::default();
1982        context = context
1983            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
1984
1985        assert!(matches!(
1986            context.candidates_for("agent-runtime")[0].provenance,
1987            ResolveCandidateProvenance::External
1988        ));
1989
1990        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
1991            &manifest_with_agent_runtime_dependency(),
1992            &proxy_packages(),
1993            None,
1994            None,
1995            None,
1996            Some(&manifest_package_index_with_service_proxy()),
1997            &context,
1998        )
1999        .expect("contract compatibility payload should resolve");
2000
2001        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2002        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
2003
2004        let proxy_index = manifest_package_index_with_service_proxy();
2005        let coordinator = ResolveInputCoordinator::from_package_index(&proxy_index)
2006            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
2007            .build();
2008
2009        let groups = coordinator
2010            .resolve_candidate_context()
2011            .grouping_for("agent-runtime");
2012        assert_eq!(groups.len(), 1);
2013        assert_eq!(groups[0].id, "service-contract:agent-runtime:closure");
2014        assert_eq!(
2015            groups[0].rationale,
2016            Some("service-candidate-contract".to_string())
2017        );
2018    }
2019
2020    #[test]
2021    fn from_service_contract_payload_trims_provider_and_skips_empty_values() {
2022        let contract_payload = vec![
2023            ServiceContractCandidatePayload {
2024                provider: "  agent-runtime  ".to_string(),
2025                candidates: vec![
2026                    "   ".to_string(),
2027                    "agent-runtime-proxy".to_string(),
2028                    "\t".to_string(),
2029                    "agent-runtime".to_string(),
2030                ],
2031                closure: ServiceContractCandidateClosure {
2032                    id: None,
2033                    candidates: vec!["  ".to_string(), "agent-runtime".to_string()],
2034                    rationale: None,
2035                },
2036                provenance: None,
2037            },
2038            ServiceContractCandidatePayload {
2039                provider: "   ".to_string(),
2040                candidates: vec!["agent-runtime-proxy".to_string()],
2041                closure: ServiceContractCandidateClosure {
2042                    id: None,
2043                    candidates: vec!["agent-runtime".to_string()],
2044                    rationale: None,
2045                },
2046                provenance: None,
2047            },
2048        ];
2049
2050        let payload =
2051            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
2052
2053        assert_eq!(payload.len(), 1);
2054        assert_eq!(payload[0].provider, "agent-runtime");
2055        assert_eq!(payload[0].candidates.len(), 2);
2056        assert_eq!(payload[0].candidates[0].candidate, "agent-runtime-proxy");
2057        assert_eq!(payload[0].candidates[1].candidate, "agent-runtime");
2058        assert_eq!(
2059            payload[0].closure_metadata.candidates,
2060            vec!["agent-runtime".to_string()]
2061        );
2062        assert_eq!(
2063            payload[0].closure_metadata.id,
2064            "service-contract:agent-runtime:closure"
2065        );
2066        assert_eq!(
2067            payload[0].closure_metadata.rationale,
2068            Some("service-candidate-contract".to_string())
2069        );
2070    }
2071
2072    #[test]
2073    fn from_service_contract_payload_injects_provider_into_closure_without_duplicates() {
2074        let contract_payload = vec![ServiceContractCandidatePayload {
2075            provider: "agent-runtime".to_string(),
2076            candidates: vec![
2077                "agent-runtime".to_string(),
2078                "agent-runtime-proxy".to_string(),
2079            ],
2080            closure: ServiceContractCandidateClosure {
2081                id: Some("custom-closure-id".to_string()),
2082                candidates: vec!["agent-runtime".to_string()],
2083                rationale: Some("service contract overlap".to_string()),
2084            },
2085            provenance: None,
2086        }];
2087
2088        let payload =
2089            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
2090
2091        assert_eq!(payload.len(), 1);
2092        assert_eq!(payload[0].provider, "agent-runtime");
2093        assert_eq!(payload[0].candidates[0].candidate, "agent-runtime");
2094        assert_eq!(payload[0].candidates[1].candidate, "agent-runtime-proxy");
2095        assert_eq!(
2096            payload[0].closure_metadata.candidates,
2097            vec!["agent-runtime".to_string()]
2098        );
2099        assert_eq!(payload[0].closure_metadata.id, "custom-closure-id");
2100        assert_eq!(
2101            payload[0].closure_metadata.rationale,
2102            Some("service contract overlap".to_string())
2103        );
2104    }
2105
2106    #[test]
2107    fn from_service_contract_payload_falls_back_when_closure_fields_are_blank() {
2108        let contract_payload = vec![ServiceContractCandidatePayload {
2109            provider: "agent-runtime".to_string(),
2110            candidates: vec!["agent-runtime-proxy".to_string()],
2111            closure: ServiceContractCandidateClosure {
2112                id: Some("   ".to_string()),
2113                candidates: vec!["agent-runtime".to_string()],
2114                rationale: Some("   ".to_string()),
2115            },
2116            provenance: None,
2117        }];
2118
2119        let payload =
2120            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
2121
2122        assert_eq!(payload.len(), 1);
2123        assert_eq!(
2124            payload[0].closure_metadata.id,
2125            "service-contract:agent-runtime:closure"
2126        );
2127        assert_eq!(
2128            payload[0].closure_metadata.rationale,
2129            Some("service-candidate-contract".to_string())
2130        );
2131    }
2132
2133    #[test]
2134    fn service_contract_payload_provenance_option_drives_coordinator_resolution_path() {
2135        let manifest: crate::app::AppManifest = toml::from_str(
2136            r#"
2137        [app]
2138        name = "contract-provenance-app"
2139        version = "0.1.0"
2140        display_name = "Contract Provenance App"
2141        description = "test"
2142
2143        [requires]
2144        capabilities = ["agent.runtime"]
2145
2146        [bindings.agent.runtime]
2147        provider = "agent-runtime"
2148        mutable = false
2149        "#,
2150        )
2151        .expect("manifest parses");
2152
2153        let contract_payload = vec![
2154            ServiceContractCandidatePayload {
2155                provider: "agent-runtime".to_string(),
2156                candidates: vec!["agent-runtime-proxy".to_string()],
2157                closure: ServiceContractCandidateClosure {
2158                    id: None,
2159                    candidates: vec!["agent-runtime".to_string()],
2160                    rationale: Some("contract provenance path".to_string()),
2161                },
2162                provenance: Some(ServiceContractCandidateProvenance {
2163                    package_name: "agent-runtime-proxy".into(),
2164                    package_kind: "provider".into(),
2165                    runtime_provider: "agent-runtime".into(),
2166                    current_source: "services/agent-runtime-proxy".into(),
2167                    source_kind: "service".into(),
2168                }),
2169            },
2170            ServiceContractCandidatePayload {
2171                provider: "agent-runtime-alt".to_string(),
2172                candidates: vec!["agent-runtime-alt".to_string()],
2173                closure: ServiceContractCandidateClosure {
2174                    id: None,
2175                    candidates: vec!["agent-runtime-alt".to_string()],
2176                    rationale: None,
2177                },
2178                provenance: None,
2179            },
2180        ];
2181
2182        let payload =
2183            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
2184
2185        let mapped_with_provenance = payload
2186            .iter()
2187            .find(|entry| entry.provider == "agent-runtime")
2188            .expect("mapped contract payload contains agent-runtime")
2189            .candidates[0]
2190            .provenance
2191            .as_ref()
2192            .expect("provenance is preserved");
2193
2194        assert_eq!(mapped_with_provenance.package_name, "agent-runtime-proxy");
2195        assert_eq!(mapped_with_provenance.package_kind, "provider");
2196        assert_eq!(mapped_with_provenance.runtime_provider, "agent-runtime");
2197
2198        let mapped_without_provenance = payload
2199            .iter()
2200            .find(|entry| entry.provider == "agent-runtime-alt")
2201            .expect("mapped contract payload contains agent-runtime-alt")
2202            .candidates[0]
2203            .provenance
2204            .as_ref();
2205
2206        assert!(mapped_without_provenance.is_none());
2207
2208        let package_index = manifest_package_index_with_service_proxy();
2209        let coordinator = ResolveInputCoordinator::from_package_index(&package_index)
2210            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
2211            .build();
2212
2213        let contract_candidates = coordinator
2214            .resolve_candidate_context()
2215            .candidates_for("agent-runtime");
2216        assert_eq!(contract_candidates.len(), 1);
2217        assert_eq!(contract_candidates[0].candidate, "agent-runtime-proxy");
2218        let contract_candidate_provenance = match &contract_candidates[0].provenance {
2219            ResolveCandidateProvenance::PackageIndex(provenance) => provenance,
2220            ResolveCandidateProvenance::External => {
2221                panic!("contract provenance should be converted to package index provenance")
2222            }
2223        };
2224        assert_eq!(
2225            contract_candidate_provenance.package_name,
2226            "agent-runtime-proxy"
2227        );
2228
2229        let alt_candidates = coordinator
2230            .resolve_candidate_context()
2231            .candidates_for("agent-runtime-alt");
2232        assert_eq!(alt_candidates.len(), 1);
2233        assert_eq!(alt_candidates[0].candidate, "agent-runtime-alt");
2234        assert!(matches!(
2235            alt_candidates[0].provenance,
2236            ResolveCandidateProvenance::External
2237        ));
2238
2239        let groups = coordinator
2240            .resolve_candidate_context()
2241            .grouping_for("agent-runtime");
2242        assert_eq!(groups.len(), 1);
2243        assert_eq!(groups[0].id, "service-contract:agent-runtime:closure");
2244        assert_eq!(
2245            groups[0].rationale,
2246            Some("contract provenance path".to_string())
2247        );
2248
2249        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
2250            &manifest,
2251            &proxy_packages(),
2252            None,
2253            None,
2254            None,
2255            Some(&package_index),
2256            coordinator.resolve_candidate_context(),
2257        )
2258        .expect("contract provenance payload should resolve via coordinator path");
2259
2260        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2261        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
2262    }
2263
2264    #[test]
2265    fn fixture_service_origin_payload_drives_coordinator_path() {
2266        let manifest: crate::app::AppManifest = toml::from_str(
2267            r#"
2268        [app]
2269        name = "fixture-service-origin-app"
2270        version = "0.1.0"
2271        display_name = "Fixture Service Origin App"
2272        description = "test"
2273
2274        [requires]
2275        capabilities = ["agent.runtime"]
2276
2277        [bindings.agent.runtime]
2278        provider = "agent-runtime"
2279        mutable = false
2280        "#,
2281        )
2282        .expect("manifest parses");
2283
2284        let packages = vec![DiscoveredPackage {
2285            manifest: toml::from_str(
2286                r#"
2287            [package_info]
2288            name = "agent-runtime-proxy"
2289            version = "0.1.0"
2290            description = "agent runtime proxy"
2291            entry = "package.wasm"
2292
2293            [capability]
2294            provides = ["agent.runtime"]
2295            "#,
2296            )
2297            .expect("agent runtime proxy manifest parses"),
2298            dir: std::path::PathBuf::from("agent-runtime-proxy"),
2299            entry_path: None,
2300            runtime: PackageRuntime::Wasm,
2301        }];
2302
2303        let err = resolve_app_manifest_with_policy_and_candidate_context(
2304            &manifest,
2305            &packages,
2306            None,
2307            None,
2308            None,
2309            None,
2310            &ResolveCandidateContext::default(),
2311        )
2312        .expect_err("missing fixture candidates should fail");
2313
2314        assert!(err
2315            .to_string()
2316            .contains("Provider 'agent-runtime' for capability 'agent.runtime' is not available"));
2317
2318        let fixture_payload = vec![ServiceOriginCandidatePayloadFixture {
2319            provider: "agent-runtime".to_string(),
2320            candidates: vec!["agent-runtime-proxy".to_string()],
2321            closure_candidates: vec!["agent-runtime".to_string()],
2322            closure_id: Some("fixture:agent-runtime:closure".to_string()),
2323            closure_rationale: Some("fixture-based service origin".to_string()),
2324            provenance: None,
2325        }];
2326
2327        let payload = ServiceOriginCandidatePayload::from_fixtures(&fixture_payload);
2328        let mut resolve_candidate_context = ResolveCandidateContext::default();
2329        resolve_candidate_context = resolve_candidate_context
2330            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
2331
2332        let fixture_package_index = PackageIndex {
2333            version: 1,
2334            revision: "test-rev".into(),
2335            source_url: "local://packages".into(),
2336            package_sources: vec![],
2337        };
2338
2339        let coordinator = ResolveInputCoordinator::from_package_index(&fixture_package_index)
2340            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
2341            .build();
2342
2343        let resolved_with_coordinator = resolve_app_manifest_with_policy_and_candidate_context(
2344            &manifest,
2345            &packages,
2346            None,
2347            None,
2348            None,
2349            None,
2350            coordinator.resolve_candidate_context(),
2351        )
2352        .expect("fixture payload should resolve via coordinator path");
2353
2354        assert_eq!(
2355            resolved_with_coordinator.status,
2356            ResolvedAppStatus::Resolved
2357        );
2358        assert_eq!(
2359            resolved_with_coordinator.bindings[0].provider,
2360            "agent-runtime"
2361        );
2362
2363        let candidates = resolve_candidate_context.candidates_for("agent-runtime");
2364        assert_eq!(candidates.len(), 1);
2365        assert_eq!(candidates[0].candidate, "agent-runtime-proxy");
2366
2367        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
2368            &manifest,
2369            &packages,
2370            None,
2371            None,
2372            None,
2373            None,
2374            &resolve_candidate_context,
2375        )
2376        .expect("fixture payload should resolve via direct context path");
2377
2378        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2379        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
2380
2381        let groups = coordinator
2382            .resolve_candidate_context()
2383            .grouping_for("agent-runtime");
2384        assert_eq!(groups.len(), 1);
2385        assert!(matches!(
2386            groups[0].source,
2387            ResolveCandidateGroupingSource::Service
2388        ));
2389        assert_eq!(groups[0].id, "fixture:agent-runtime:closure");
2390        assert_eq!(groups[0].candidates, vec!["agent-runtime".to_string()]);
2391        assert_eq!(
2392            groups[0].rationale,
2393            Some("fixture-based service origin".to_string())
2394        );
2395    }
2396
2397    #[test]
2398    fn service_contract_payload_drives_resolve_input_path() {
2399        let manifest: crate::app::AppManifest = toml::from_str(
2400            r#"
2401        [app]
2402        name = "contract-service-origin-app"
2403        version = "0.1.0"
2404        display_name = "Contract Service Origin App"
2405        description = "test"
2406
2407        [requires]
2408        capabilities = ["agent.runtime"]
2409
2410        [bindings.agent.runtime]
2411        provider = "agent-runtime"
2412        mutable = false
2413        "#,
2414        )
2415        .expect("manifest parses");
2416
2417        let packages = vec![DiscoveredPackage {
2418            manifest: toml::from_str(
2419                r#"
2420            [package_info]
2421            name = "agent-runtime-proxy"
2422            version = "0.1.0"
2423            description = "agent runtime proxy"
2424            entry = "package.wasm"
2425
2426            [capability]
2427            provides = ["agent.runtime"]
2428            "#,
2429            )
2430            .expect("agent runtime proxy manifest parses"),
2431            dir: std::path::PathBuf::from("agent-runtime-proxy"),
2432            entry_path: None,
2433            runtime: PackageRuntime::Wasm,
2434        }];
2435
2436        let package_index = PackageIndex {
2437            version: 1,
2438            revision: "test-rev".into(),
2439            source_url: "local://packages".into(),
2440            package_sources: vec![PackageSource {
2441                name: "agent-runtime-proxy".into(),
2442                kind: "service".into(),
2443                package_kind: "provider".into(),
2444                runtime_provider: "agent-runtime".into(),
2445                current_source: "services/agent-runtime-proxy".into(),
2446                trusted: true,
2447                signature: "service-proxy".into(),
2448                source_authority: String::new(),
2449                source_public_keys: vec![],
2450                provides: vec!["agent.runtime".into()],
2451                requires: vec![],
2452            }],
2453        };
2454
2455        let contract_payload = vec![ServiceContractCandidatePayload {
2456            provider: "agent-runtime".into(),
2457            candidates: vec!["agent-runtime-proxy".into()],
2458            closure: ServiceContractCandidateClosure {
2459                id: Some("service-contract:agent-runtime:closure".into()),
2460                candidates: vec!["agent-runtime".into()],
2461                rationale: Some("contract-driven service origin".into()),
2462            },
2463            provenance: Some(ServiceContractCandidateProvenance {
2464                package_name: "agent-runtime-proxy".into(),
2465                package_kind: "provider".into(),
2466                runtime_provider: "agent-runtime".into(),
2467                current_source: "services/agent-runtime-proxy".into(),
2468                source_kind: "service".into(),
2469            }),
2470        }];
2471
2472        let payload =
2473            ServiceOriginCandidatePayload::from_service_contract_payload(&contract_payload);
2474        let resolve_candidate_context = ResolveCandidateContext::default()
2475            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload);
2476
2477        let coordinator = ResolveInputCoordinator::from_package_index(&package_index)
2478            .with_service_origin_candidates(&SynthesizedServiceOriginCandidateAdapter, &payload)
2479            .build();
2480
2481        let resolved = resolve_app_manifest_with_policy_and_candidate_context(
2482            &manifest,
2483            &packages,
2484            None,
2485            None,
2486            None,
2487            Some(&package_index),
2488            coordinator.resolve_candidate_context(),
2489        )
2490        .expect("contract payload should resolve alias provider via coordinator path");
2491
2492        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2493        assert_eq!(resolved.bindings[0].provider, "agent-runtime");
2494
2495        let candidates = resolve_candidate_context.candidates_for("agent-runtime");
2496        assert_eq!(candidates.len(), 1);
2497        assert_eq!(candidates[0].candidate, "agent-runtime-proxy");
2498
2499        let groups = coordinator
2500            .resolve_candidate_context()
2501            .grouping_for("agent-runtime");
2502        assert_eq!(groups.len(), 1);
2503        assert!(matches!(
2504            groups[0].source,
2505            ResolveCandidateGroupingSource::Service
2506        ));
2507        assert_eq!(groups[0].id, "service-contract:agent-runtime:closure");
2508        assert_eq!(groups[0].candidates, vec!["agent-runtime".to_string()]);
2509        assert_eq!(
2510            groups[0].rationale,
2511            Some("contract-driven service origin".to_string())
2512        );
2513
2514        let resolved_direct = resolve_app_manifest_with_policy_and_candidate_context(
2515            &manifest,
2516            &packages,
2517            None,
2518            None,
2519            None,
2520            Some(&package_index),
2521            &resolve_candidate_context,
2522        )
2523        .expect("contract payload should resolve alias provider via direct context path");
2524
2525        assert_eq!(resolved_direct.status, ResolvedAppStatus::Resolved);
2526        assert_eq!(resolved_direct.bindings[0].provider, "agent-runtime");
2527    }
2528
2529    #[test]
2530    fn metadata_provider_is_available_from_package_index_without_package_runtime() {
2531        let manifest: crate::app::config::AppManifest = toml::from_str(
2532            r#"
2533[app]
2534name = "metadata-app"
2535version = "0.1.0"
2536display_name = "Metadata App"
2537description = "test"
2538
2539[requires]
2540capabilities = ["ui.surface"]
2541
2542[bindings.ui.surface]
2543provider = "metadata-ui"
2544mutable = false
2545"#,
2546        )
2547        .expect("manifest parses");
2548
2549        let package_index = PackageIndex {
2550            version: 1,
2551            revision: "test-rev".into(),
2552            source_url: "local://packages".into(),
2553            package_sources: vec![PackageSource {
2554                name: "metadata-ui".into(),
2555                kind: "metadata".into(),
2556                package_kind: "provider".into(),
2557                runtime_provider: "metadata-app".into(),
2558                current_source: "apps/metadata-app".into(),
2559                trusted: true,
2560                signature: "builtin:app".into(),
2561                source_authority: "official-apps".into(),
2562                source_public_keys: vec![],
2563                provides: vec!["ui.surface".into()],
2564                requires: vec![],
2565            }],
2566        };
2567
2568        let resolved = resolve_app_manifest_with_policy(
2569            &manifest,
2570            &[],
2571            None,
2572            None,
2573            None,
2574            Some(&package_index),
2575        )
2576        .expect("metadata-backed app resolves");
2577
2578        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2579        assert_eq!(resolved.bindings.len(), 1);
2580        assert_eq!(resolved.bindings[0].capability, "ui.surface");
2581        assert_eq!(resolved.bindings[0].provider, "metadata-ui");
2582    }
2583
2584    #[test]
2585    fn weft_claw_required_provider_rejects_metadata_only_package_index_entry() {
2586        let manifest: crate::app::config::AppManifest = toml::from_str(
2587            r#"
2588[app]
2589name = "weft-claw"
2590version = "0.1.0"
2591display_name = "Weft Claw"
2592description = "test"
2593
2594[requires]
2595capabilities = ["prompt.system"]
2596
2597[bindings.prompt.system]
2598provider = "prompt-system"
2599mutable = false
2600"#,
2601        )
2602        .expect("manifest parses");
2603
2604        let package_index = PackageIndex {
2605            version: 1,
2606            revision: "test-rev".into(),
2607            source_url: "local://packages".into(),
2608            package_sources: vec![PackageSource {
2609                name: "prompt-system".into(),
2610                kind: "metadata".into(),
2611                package_kind: "foundation".into(),
2612                runtime_provider: "agent-core".into(),
2613                current_source: "packages/weft-claw".into(),
2614                trusted: true,
2615                signature: "builtin:product-package".into(),
2616                source_authority: "product-package-instance".into(),
2617                source_public_keys: vec![],
2618                provides: vec!["prompt.system".into()],
2619                requires: vec![],
2620            }],
2621        };
2622
2623        let error = resolve_app_manifest_with_policy(
2624            &manifest,
2625            &[],
2626            None,
2627            None,
2628            None,
2629            Some(&package_index),
2630        )
2631        .expect_err("weft-claw required packages must not resolve from metadata-only entries");
2632
2633        assert!(error
2634            .to_string()
2635            .contains("Provider 'prompt-system' for capability 'prompt.system' is not available"));
2636    }
2637
2638    #[test]
2639    fn weft_claw_required_provider_resolves_from_real_package_source() {
2640        let manifest: crate::app::config::AppManifest = toml::from_str(
2641            r#"
2642[app]
2643name = "weft-claw"
2644version = "0.1.0"
2645display_name = "Weft Claw"
2646description = "test"
2647
2648[requires]
2649capabilities = ["prompt.system"]
2650
2651[bindings.prompt.system]
2652provider = "prompt-system"
2653mutable = false
2654"#,
2655        )
2656        .expect("manifest parses");
2657
2658        let package_index = PackageIndex {
2659            version: 1,
2660            revision: "test-rev".into(),
2661            source_url: "local://packages".into(),
2662            package_sources: vec![PackageSource {
2663                name: "prompt-system".into(),
2664                kind: "wasm".into(),
2665                package_kind: "foundation".into(),
2666                runtime_provider: "prompt-system".into(),
2667                current_source: "packages/official/prompt-system".into(),
2668                trusted: true,
2669                signature: "builtin:official".into(),
2670                source_authority: "official".into(),
2671                source_public_keys: vec![],
2672                provides: vec!["prompt.system".into()],
2673                requires: vec![],
2674            }],
2675        };
2676
2677        let packages = vec![DiscoveredPackage {
2678            manifest: toml::from_str(
2679                r#"
2680[package_info]
2681name = "prompt-system"
2682version = "0.1.0"
2683description = "prompt system"
2684entry = "package.wasm"
2685
2686[identity]
2687name = "prompt-system"
2688version = "0.1.0"
2689description = "prompt system"
2690
2691[package]
2692entry = "package.wasm"
2693runtime = "wasm"
2694api_version = "v1"
2695provides = ["prompt.system"]
2696
2697[capability]
2698provides = ["prompt.system"]
2699"#,
2700            )
2701            .expect("prompt-system manifest parses"),
2702            dir: std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2703                .join("..")
2704                .join("packages")
2705                .join("official")
2706                .join("prompt-system"),
2707            entry_path: None,
2708            runtime: PackageRuntime::Wasm,
2709        }];
2710
2711        let resolved = resolve_app_manifest_with_policy(
2712            &manifest,
2713            &packages,
2714            None,
2715            None,
2716            None,
2717            Some(&package_index),
2718        )
2719        .expect("real package-backed app resolves");
2720
2721        assert_eq!(resolved.status, ResolvedAppStatus::Resolved);
2722        assert_eq!(resolved.bindings.len(), 1);
2723        assert_eq!(resolved.bindings[0].provider, "prompt-system");
2724    }
2725}