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(®istry),
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}