Skip to main content

appcore_contracts/
application.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: application.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/21 23:21:21 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 11:51:10 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Application-owned manifest contract.
12
13use crate::identifiers::{is_sensitive_key, looks_like_local_path, looks_like_url, validate_text};
14use crate::{
15    ApplicationDependency, ApplicationId, CapabilityClass, CapabilityDeclaration, ContractError,
16    ContractResult, FeatureId, HealthRequirements, JobPolicy, LeadershipMode,
17    LeadershipRequirement, ModuleDeclaration, RuntimeRequirements, SchedulerRequirements,
18    ServiceId, StorageDurability, StorageRequirements, UpdatePolicy,
19};
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, BTreeSet};
22
23/// Schema version written by [`ApplicationManifestV1`].
24pub const APPLICATION_MANIFEST_VERSION: u16 = 1;
25
26/// Contract published by an application and consumed by any compatible runtime.
27///
28/// It contains application intent and requirements only. Provider selection,
29/// installation paths and secrets belong to the deployment manifest.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(try_from = "ApplicationManifestData")]
32pub struct ApplicationManifestV1 {
33    manifest_version: u16,
34    application_id: ApplicationId,
35    application_version: String,
36    display_name: String,
37    vendor: String,
38    service_id: ServiceId,
39    runtime: RuntimeRequirements,
40    capabilities: Vec<CapabilityDeclaration>,
41    leadership: Vec<LeadershipRequirement>,
42    jobs: JobPolicy,
43    dependencies: Vec<ApplicationDependency>,
44    storage: StorageRequirements,
45    scheduler: SchedulerRequirements,
46    health: HealthRequirements,
47    update: UpdatePolicy,
48    modules: Vec<ModuleDeclaration>,
49    feature_flags: BTreeMap<FeatureId, bool>,
50    metadata: BTreeMap<String, String>,
51}
52
53#[derive(Deserialize)]
54struct ApplicationManifestData {
55    manifest_version: u16,
56    application_id: ApplicationId,
57    application_version: String,
58    display_name: String,
59    vendor: String,
60    service_id: ServiceId,
61    runtime: RuntimeRequirements,
62    capabilities: Vec<CapabilityDeclaration>,
63    leadership: Vec<LeadershipRequirement>,
64    jobs: JobPolicy,
65    dependencies: Vec<ApplicationDependency>,
66    storage: StorageRequirements,
67    scheduler: SchedulerRequirements,
68    health: HealthRequirements,
69    update: UpdatePolicy,
70    modules: Vec<ModuleDeclaration>,
71    feature_flags: BTreeMap<FeatureId, bool>,
72    metadata: BTreeMap<String, String>,
73}
74
75impl ApplicationManifestV1 {
76    /// Creates a minimal application manifest with conservative local defaults.
77    pub fn new(
78        application_id: ApplicationId,
79        application_version: impl Into<String>,
80        display_name: impl Into<String>,
81        vendor: impl Into<String>,
82        service_id: ServiceId,
83        runtime: RuntimeRequirements,
84    ) -> ContractResult<Self> {
85        let manifest = Self {
86            manifest_version: APPLICATION_MANIFEST_VERSION,
87            application_id,
88            application_version: application_version.into(),
89            display_name: display_name.into(),
90            vendor: vendor.into(),
91            service_id,
92            runtime,
93            capabilities: Vec::new(),
94            leadership: Vec::new(),
95            jobs: JobPolicy::disabled(),
96            dependencies: Vec::new(),
97            storage: StorageRequirements::new(StorageDurability::Local, 0, false),
98            scheduler: SchedulerRequirements::new(false, 0)?,
99            health: HealthRequirements::new(30_000, 10_000, 3)?,
100            update: UpdatePolicy::new("stable", false)?,
101            modules: Vec::new(),
102            feature_flags: BTreeMap::new(),
103            metadata: BTreeMap::new(),
104        };
105        manifest.validate()?;
106        Ok(manifest)
107    }
108
109    /// Adds a capability declaration.
110    pub fn with_capability(mut self, capability: CapabilityDeclaration) -> ContractResult<Self> {
111        if self
112            .capabilities
113            .iter()
114            .any(|existing| existing.id() == capability.id())
115        {
116            return Err(ContractError::Duplicate {
117                field: "capability",
118                value: capability.id().to_string(),
119            });
120        }
121        self.capabilities.push(capability);
122        self.validate()?;
123        Ok(self)
124    }
125
126    /// Adds a service-scoped leadership requirement.
127    pub fn with_leadership(mut self, requirement: LeadershipRequirement) -> ContractResult<Self> {
128        if self
129            .leadership
130            .iter()
131            .any(|existing| existing.service_id() == requirement.service_id())
132        {
133            return Err(ContractError::Duplicate {
134                field: "leadership.service_id",
135                value: requirement.service_id().to_string(),
136            });
137        }
138        self.leadership.push(requirement);
139        self.validate()?;
140        Ok(self)
141    }
142
143    /// Replaces the job policy.
144    pub fn with_job_policy(mut self, policy: JobPolicy) -> ContractResult<Self> {
145        self.jobs = policy;
146        self.validate()?;
147        Ok(self)
148    }
149
150    /// Adds an application dependency.
151    pub fn with_dependency(mut self, dependency: ApplicationDependency) -> ContractResult<Self> {
152        if self
153            .dependencies
154            .iter()
155            .any(|existing| existing.application_id() == dependency.application_id())
156        {
157            return Err(ContractError::Duplicate {
158                field: "dependency.application_id",
159                value: dependency.application_id().to_string(),
160            });
161        }
162        self.dependencies.push(dependency);
163        self.validate()?;
164        Ok(self)
165    }
166
167    /// Replaces provider-independent storage requirements.
168    pub fn with_storage_requirements(mut self, requirements: StorageRequirements) -> Self {
169        self.storage = requirements;
170        self
171    }
172
173    /// Replaces scheduler requirements.
174    pub fn with_scheduler_requirements(
175        mut self,
176        requirements: SchedulerRequirements,
177    ) -> ContractResult<Self> {
178        self.scheduler = requirements;
179        self.validate()?;
180        Ok(self)
181    }
182
183    /// Replaces health requirements.
184    pub fn with_health_requirements(
185        mut self,
186        requirements: HealthRequirements,
187    ) -> ContractResult<Self> {
188        self.health = requirements;
189        self.validate()?;
190        Ok(self)
191    }
192
193    /// Replaces application update preferences.
194    pub fn with_update_policy(mut self, policy: UpdatePolicy) -> ContractResult<Self> {
195        self.update = policy;
196        self.validate()?;
197        Ok(self)
198    }
199
200    /// Adds an application module.
201    pub fn with_module(mut self, module: ModuleDeclaration) -> ContractResult<Self> {
202        if self
203            .modules
204            .iter()
205            .any(|existing| existing.id() == module.id())
206        {
207            return Err(ContractError::Duplicate {
208                field: "module",
209                value: module.id().to_string(),
210            });
211        }
212        self.modules.push(module);
213        self.validate()?;
214        Ok(self)
215    }
216
217    /// Sets one application feature flag.
218    pub fn with_feature_flag(mut self, feature: FeatureId, enabled: bool) -> Self {
219        self.feature_flags.insert(feature, enabled);
220        self
221    }
222
223    /// Adds non-sensitive, portable application metadata.
224    pub fn with_metadata(
225        mut self,
226        key: impl Into<String>,
227        value: impl Into<String>,
228    ) -> ContractResult<Self> {
229        let key = key.into();
230        let value = value.into();
231        validate_application_metadata(&key, &value)?;
232        self.metadata.insert(key, value);
233        Ok(self)
234    }
235
236    /// Returns the manifest schema version.
237    pub fn manifest_version(&self) -> u16 {
238        self.manifest_version
239    }
240
241    /// Returns the stable application identity.
242    pub fn application_id(&self) -> &ApplicationId {
243        &self.application_id
244    }
245
246    /// Returns the application version.
247    pub fn application_version(&self) -> &str {
248        &self.application_version
249    }
250
251    /// Returns the human-readable application name.
252    pub fn display_name(&self) -> &str {
253        &self.display_name
254    }
255
256    /// Returns the application vendor.
257    pub fn vendor(&self) -> &str {
258        &self.vendor
259    }
260
261    /// Returns the primary service identity.
262    pub fn service_id(&self) -> &ServiceId {
263        &self.service_id
264    }
265
266    /// Returns runtime compatibility requirements.
267    pub fn runtime_requirements(&self) -> &RuntimeRequirements {
268        &self.runtime
269    }
270
271    /// Returns declared capabilities.
272    pub fn capabilities(&self) -> &[CapabilityDeclaration] {
273        &self.capabilities
274    }
275
276    /// Returns service-scoped leadership requirements.
277    pub fn leadership(&self) -> &[LeadershipRequirement] {
278        &self.leadership
279    }
280
281    /// Returns the job policy.
282    pub fn job_policy(&self) -> &JobPolicy {
283        &self.jobs
284    }
285
286    /// Returns application dependencies.
287    pub fn dependencies(&self) -> &[ApplicationDependency] {
288        &self.dependencies
289    }
290
291    /// Returns storage requirements.
292    pub fn storage_requirements(&self) -> &StorageRequirements {
293        &self.storage
294    }
295
296    /// Returns scheduler requirements.
297    pub fn scheduler_requirements(&self) -> &SchedulerRequirements {
298        &self.scheduler
299    }
300
301    /// Returns health requirements.
302    pub fn health_requirements(&self) -> &HealthRequirements {
303        &self.health
304    }
305
306    /// Returns update preferences.
307    pub fn update_policy(&self) -> &UpdatePolicy {
308        &self.update
309    }
310
311    /// Returns application modules.
312    pub fn modules(&self) -> &[ModuleDeclaration] {
313        &self.modules
314    }
315
316    /// Returns application feature flags.
317    pub fn feature_flags(&self) -> &BTreeMap<FeatureId, bool> {
318        &self.feature_flags
319    }
320
321    /// Returns non-sensitive portable metadata.
322    pub fn metadata(&self) -> &BTreeMap<String, String> {
323        &self.metadata
324    }
325
326    /// Validates the complete manifest and all cross-field invariants.
327    pub fn validate(&self) -> ContractResult<()> {
328        if self.manifest_version != APPLICATION_MANIFEST_VERSION {
329            return Err(ContractError::InvalidValue {
330                field: "manifest_version",
331                reason: "unsupported application manifest version",
332            });
333        }
334        validate_text("application_version", &self.application_version, 64)?;
335        validate_text("display_name", &self.display_name, 256)?;
336        validate_text("vendor", &self.vendor, 256)?;
337        self.runtime.validate()?;
338        self.jobs.validate()?;
339        self.scheduler.validate()?;
340        self.health.validate()?;
341        self.update.validate()?;
342
343        ensure_unique(
344            "capability",
345            self.capabilities.iter().map(|item| item.id().as_str()),
346        )?;
347        for capability in &self.capabilities {
348            capability.validate()?;
349            reject_reserved_capability_namespace(capability.id().as_str())?;
350            if capability.class() != CapabilityClass::Functional {
351                return Err(ContractError::InvalidValue {
352                    field: "capabilities.class",
353                    reason: "application manifests may declare only functional capabilities",
354                });
355            }
356        }
357        ensure_unique(
358            "leadership.service_id",
359            self.leadership
360                .iter()
361                .map(|item| item.service_id().as_str()),
362        )?;
363        for leadership in &self.leadership {
364            leadership.validate()?;
365        }
366        if self
367            .capabilities
368            .iter()
369            .any(CapabilityDeclaration::requires_leader)
370            && !self.leadership.iter().any(|requirement| {
371                requirement.service_id() == &self.service_id
372                    && requirement.mode() != LeadershipMode::Disabled
373            })
374        {
375            return Err(ContractError::InvalidValue {
376                field: "capabilities.requires_leader",
377                reason: "primary service leadership must be enabled",
378            });
379        }
380        ensure_unique(
381            "dependency.application_id",
382            self.dependencies
383                .iter()
384                .map(|item| item.application_id().as_str()),
385        )?;
386        for dependency in &self.dependencies {
387            dependency.validate()?;
388        }
389        ensure_unique("module", self.modules.iter().map(|item| item.id().as_str()))?;
390        for module in &self.modules {
391            module.validate()?;
392        }
393        for (key, value) in &self.metadata {
394            validate_application_metadata(key, value)?;
395        }
396        Ok(())
397    }
398}
399
400fn reject_reserved_capability_namespace(capability: &str) -> ContractResult<()> {
401    const RESERVED_PREFIXES: [&str; 3] = ["appcore.", "runtime.", "infrastructure."];
402    let normalized = capability.to_ascii_lowercase();
403    if RESERVED_PREFIXES
404        .iter()
405        .any(|prefix| normalized.starts_with(prefix))
406    {
407        return Err(ContractError::InvalidValue {
408            field: "capabilities.id",
409            reason: "application capability uses a reserved Runtime namespace",
410        });
411    }
412    Ok(())
413}
414
415impl TryFrom<ApplicationManifestData> for ApplicationManifestV1 {
416    type Error = ContractError;
417
418    fn try_from(data: ApplicationManifestData) -> Result<Self, Self::Error> {
419        let manifest = Self {
420            manifest_version: data.manifest_version,
421            application_id: data.application_id,
422            application_version: data.application_version,
423            display_name: data.display_name,
424            vendor: data.vendor,
425            service_id: data.service_id,
426            runtime: data.runtime,
427            capabilities: data.capabilities,
428            leadership: data.leadership,
429            jobs: data.jobs,
430            dependencies: data.dependencies,
431            storage: data.storage,
432            scheduler: data.scheduler,
433            health: data.health,
434            update: data.update,
435            modules: data.modules,
436            feature_flags: data.feature_flags,
437            metadata: data.metadata,
438        };
439        manifest.validate()?;
440        Ok(manifest)
441    }
442}
443
444fn ensure_unique<'a>(
445    field: &'static str,
446    values: impl IntoIterator<Item = &'a str>,
447) -> ContractResult<()> {
448    let mut seen = BTreeSet::new();
449    for value in values {
450        if !seen.insert(value) {
451            return Err(ContractError::Duplicate {
452                field,
453                value: value.to_string(),
454            });
455        }
456    }
457    Ok(())
458}
459
460fn validate_application_metadata(key: &str, value: &str) -> ContractResult<()> {
461    validate_text("metadata.key", key, 128)?;
462    validate_text("metadata.value", value, 2_048)?;
463    if is_sensitive_key(key) {
464        return Err(ContractError::SecretValue {
465            field: format!("metadata.{key}"),
466        });
467    }
468    if key.to_ascii_lowercase().contains("path") || looks_like_local_path(value) {
469        return Err(ContractError::LocalPath {
470            field: format!("metadata.{key}"),
471        });
472    }
473    let is_location_key = key.split(['.', '_', '-']).any(|part| {
474        matches!(
475            part.to_ascii_lowercase().as_str(),
476            "url" | "uri" | "endpoint"
477        )
478    });
479    if is_location_key || looks_like_url(value) {
480        return Err(ContractError::InvalidValue {
481            field: "metadata",
482            reason: "installation-specific URLs belong to the deployment manifest",
483        });
484    }
485    Ok(())
486}
487
488#[cfg(test)]
489#[path = "application/tests.rs"]
490mod tests;