Skip to main content

appcore_contracts/
runtime.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: runtime.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/21 23:21:21 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 16:07:49 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Runtime-produced manifest contract.
12
13use crate::identifiers::{is_sensitive_key, validate_text};
14use crate::{
15    BuildId, CapabilityId, ContractError, ContractResult, CoreId, CoreProfile, FeatureId, NodeId,
16    ProviderId, RuntimeMode,
17};
18use serde::{Deserialize, Serialize};
19use std::collections::{BTreeMap, BTreeSet};
20
21/// Schema version written by [`RuntimeManifestV1`].
22pub const RUNTIME_MANIFEST_VERSION: u16 = 1;
23
24/// Coarse health state produced by a runtime.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum RuntimeHealthStatus {
28    /// Runtime is operating normally.
29    Healthy,
30    /// Runtime remains available with reduced guarantees.
31    Degraded,
32    /// Runtime is unable to serve its declared contract.
33    Unhealthy,
34}
35
36/// Observable health snapshot without application data or secrets.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct RuntimeHealth {
39    status: RuntimeHealthStatus,
40    checked_at_ms: u64,
41    details: BTreeMap<String, String>,
42}
43
44impl RuntimeHealth {
45    /// Creates an empty runtime health snapshot.
46    pub fn new(status: RuntimeHealthStatus, checked_at_ms: u64) -> Self {
47        Self {
48            status,
49            checked_at_ms,
50            details: BTreeMap::new(),
51        }
52    }
53
54    /// Adds a non-sensitive diagnostic detail.
55    pub fn with_detail(
56        mut self,
57        key: impl Into<String>,
58        value: impl Into<String>,
59    ) -> ContractResult<Self> {
60        let key = key.into();
61        let value = value.into();
62        validate_health_detail(&key, &value)?;
63        self.details.insert(key, value);
64        Ok(self)
65    }
66
67    /// Returns the health status.
68    pub fn status(&self) -> RuntimeHealthStatus {
69        self.status
70    }
71
72    /// Returns the snapshot timestamp in Unix milliseconds.
73    pub fn checked_at_ms(&self) -> u64 {
74        self.checked_at_ms
75    }
76
77    /// Returns non-sensitive health details.
78    pub fn details(&self) -> &BTreeMap<String, String> {
79        &self.details
80    }
81
82    fn validate(&self) -> ContractResult<()> {
83        for (key, value) in &self.details {
84            validate_health_detail(key, value)?;
85        }
86        Ok(())
87    }
88}
89
90/// Operational state relevant to routing and scheduling decisions.
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum RuntimeOperationalMode {
94    /// Runtime is initializing local infrastructure.
95    Starting,
96    /// Runtime is discovering cluster peers.
97    Discovering,
98    /// Runtime is synchronizing state.
99    Syncing,
100    /// Runtime accepts read-only work.
101    ReadOnly,
102    /// Runtime accepts reads and writes.
103    #[default]
104    ReadWrite,
105    /// Runtime remains partially available.
106    Degraded,
107    /// Runtime is detached from required cluster coordination.
108    Isolated,
109}
110
111impl RuntimeOperationalMode {
112    /// Reports whether local queries are allowed.
113    pub fn allows_local_queries(self) -> bool {
114        matches!(
115            self,
116            Self::ReadOnly | Self::ReadWrite | Self::Degraded | Self::Isolated
117        )
118    }
119
120    /// Reports whether writes are allowed.
121    pub fn allows_writes(self) -> bool {
122        matches!(self, Self::ReadWrite)
123    }
124
125    /// Returns the stable serialized label.
126    pub fn as_str(self) -> &'static str {
127        match self {
128            Self::Starting => "starting",
129            Self::Discovering => "discovering",
130            Self::Syncing => "syncing",
131            Self::ReadOnly => "read_only",
132            Self::ReadWrite => "read_write",
133            Self::Degraded => "degraded",
134            Self::Isolated => "isolated",
135        }
136    }
137}
138
139impl TryFrom<&str> for RuntimeOperationalMode {
140    type Error = ContractError;
141
142    fn try_from(value: &str) -> Result<Self, Self::Error> {
143        match value {
144            "starting" => Ok(Self::Starting),
145            "discovering" => Ok(Self::Discovering),
146            "syncing" => Ok(Self::Syncing),
147            "read_only" => Ok(Self::ReadOnly),
148            "read_write" => Ok(Self::ReadWrite),
149            "readonly" | "readwrite" => Err(ContractError::InvalidValue {
150                field: "operational_mode",
151                reason: "NO MORE SUPPORTED PLEASE UPDATE",
152            }),
153            "degraded" => Ok(Self::Degraded),
154            "isolated" => Ok(Self::Isolated),
155            _ => Err(ContractError::InvalidValue {
156                field: "operational_mode",
157                reason: "unsupported operational mode",
158            }),
159        }
160    }
161}
162
163/// Runtime-owned description of a running host.
164///
165/// Application identity and application metadata are deliberately absent.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(try_from = "RuntimeManifestData")]
168pub struct RuntimeManifestV1 {
169    manifest_version: u16,
170    runtime_version: String,
171    protocol_version: String,
172    build_id: BuildId,
173    features: BTreeSet<FeatureId>,
174    node_id: NodeId,
175    core_id: CoreId,
176    mode: RuntimeMode,
177    platform: String,
178    architecture: String,
179    storage_backend: ProviderId,
180    health: RuntimeHealth,
181    operational_mode: RuntimeOperationalMode,
182    loaded_capabilities: BTreeSet<CapabilityId>,
183    core_profile: CoreProfile,
184}
185
186#[derive(Deserialize)]
187struct RuntimeManifestData {
188    manifest_version: u16,
189    runtime_version: String,
190    protocol_version: String,
191    build_id: BuildId,
192    features: BTreeSet<FeatureId>,
193    node_id: NodeId,
194    core_id: CoreId,
195    mode: RuntimeMode,
196    platform: String,
197    architecture: String,
198    storage_backend: ProviderId,
199    health: RuntimeHealth,
200    operational_mode: RuntimeOperationalMode,
201    loaded_capabilities: BTreeSet<CapabilityId>,
202    core_profile: CoreProfile,
203}
204
205impl RuntimeManifestV1 {
206    /// Creates a runtime-produced manifest from runtime and node facts.
207    #[allow(clippy::too_many_arguments)]
208    pub fn new(
209        runtime_version: impl Into<String>,
210        protocol_version: impl Into<String>,
211        build_id: BuildId,
212        node_id: NodeId,
213        core_id: CoreId,
214        mode: RuntimeMode,
215        platform: impl Into<String>,
216        architecture: impl Into<String>,
217        storage_backend: ProviderId,
218        health: RuntimeHealth,
219        core_profile: CoreProfile,
220    ) -> ContractResult<Self> {
221        let manifest = Self {
222            manifest_version: RUNTIME_MANIFEST_VERSION,
223            runtime_version: runtime_version.into(),
224            protocol_version: protocol_version.into(),
225            build_id,
226            features: BTreeSet::new(),
227            node_id,
228            core_id,
229            mode,
230            platform: platform.into(),
231            architecture: architecture.into(),
232            storage_backend,
233            health,
234            operational_mode: RuntimeOperationalMode::Starting,
235            loaded_capabilities: BTreeSet::new(),
236            core_profile,
237        };
238        manifest.validate()?;
239        Ok(manifest)
240    }
241
242    /// Adds a runtime feature.
243    pub fn with_feature(mut self, feature: FeatureId) -> Self {
244        self.features.insert(feature);
245        self
246    }
247
248    /// Adds a capability loaded by the current host.
249    pub fn with_loaded_capability(mut self, capability: CapabilityId) -> ContractResult<Self> {
250        if !self.core_profile.capabilities().contains(&capability) {
251            return Err(ContractError::InvalidValue {
252                field: "loaded_capabilities",
253                reason: "capability is not declared by the core profile",
254            });
255        }
256        self.loaded_capabilities.insert(capability);
257        Ok(self)
258    }
259
260    /// Replaces observable health.
261    pub fn with_health(mut self, health: RuntimeHealth) -> ContractResult<Self> {
262        health.validate()?;
263        self.health = health;
264        Ok(self)
265    }
266
267    /// Replaces the current operational mode.
268    pub fn with_operational_mode(mut self, mode: RuntimeOperationalMode) -> Self {
269        self.operational_mode = mode;
270        self
271    }
272
273    /// Returns the manifest schema version.
274    pub fn manifest_version(&self) -> u16 {
275        self.manifest_version
276    }
277
278    /// Returns the AppCore runtime version.
279    pub fn runtime_version(&self) -> &str {
280        &self.runtime_version
281    }
282
283    /// Returns the distributed protocol version.
284    pub fn protocol_version(&self) -> &str {
285        &self.protocol_version
286    }
287
288    /// Returns the immutable runtime build identity.
289    pub fn build_id(&self) -> &BuildId {
290        &self.build_id
291    }
292
293    /// Returns enabled runtime features.
294    pub fn features(&self) -> &BTreeSet<FeatureId> {
295        &self.features
296    }
297
298    /// Returns the host node identity.
299    pub fn node_id(&self) -> &NodeId {
300        &self.node_id
301    }
302
303    /// Returns the executable core identity.
304    pub fn core_id(&self) -> &CoreId {
305        &self.core_id
306    }
307
308    /// Returns the explicit standalone or cluster mode.
309    pub fn mode(&self) -> RuntimeMode {
310        self.mode
311    }
312
313    /// Returns the host platform.
314    pub fn platform(&self) -> &str {
315        &self.platform
316    }
317
318    /// Returns the host architecture.
319    pub fn architecture(&self) -> &str {
320        &self.architecture
321    }
322
323    /// Returns the active storage backend identity.
324    pub fn storage_backend(&self) -> &ProviderId {
325        &self.storage_backend
326    }
327
328    /// Returns observable runtime health.
329    pub fn health(&self) -> &RuntimeHealth {
330        &self.health
331    }
332
333    /// Returns the current operational mode.
334    pub fn operational_mode(&self) -> RuntimeOperationalMode {
335        self.operational_mode
336    }
337
338    /// Returns capabilities loaded by this runtime.
339    pub fn loaded_capabilities(&self) -> &BTreeSet<CapabilityId> {
340        &self.loaded_capabilities
341    }
342
343    /// Returns the scheduler-facing core profile.
344    pub fn core_profile(&self) -> &CoreProfile {
345        &self.core_profile
346    }
347
348    /// Validates runtime facts and cross-field capability declarations.
349    pub fn validate(&self) -> ContractResult<()> {
350        if self.manifest_version != RUNTIME_MANIFEST_VERSION {
351            return Err(ContractError::InvalidValue {
352                field: "manifest_version",
353                reason: "unsupported runtime manifest version",
354            });
355        }
356        validate_text("runtime_version", &self.runtime_version, 64)?;
357        validate_text("protocol_version", &self.protocol_version, 64)?;
358        validate_text("platform", &self.platform, 128)?;
359        validate_text("architecture", &self.architecture, 128)?;
360        self.health.validate()?;
361        self.core_profile.validate()?;
362        if !self
363            .loaded_capabilities
364            .is_subset(self.core_profile.capabilities())
365        {
366            return Err(ContractError::InvalidValue {
367                field: "loaded_capabilities",
368                reason: "loaded capabilities must be declared by the core profile",
369            });
370        }
371        Ok(())
372    }
373}
374
375impl TryFrom<RuntimeManifestData> for RuntimeManifestV1 {
376    type Error = ContractError;
377
378    fn try_from(data: RuntimeManifestData) -> Result<Self, Self::Error> {
379        let manifest = Self {
380            manifest_version: data.manifest_version,
381            runtime_version: data.runtime_version,
382            protocol_version: data.protocol_version,
383            build_id: data.build_id,
384            features: data.features,
385            node_id: data.node_id,
386            core_id: data.core_id,
387            mode: data.mode,
388            platform: data.platform,
389            architecture: data.architecture,
390            storage_backend: data.storage_backend,
391            health: data.health,
392            operational_mode: data.operational_mode,
393            loaded_capabilities: data.loaded_capabilities,
394            core_profile: data.core_profile,
395        };
396        manifest.validate()?;
397        Ok(manifest)
398    }
399}
400
401fn validate_health_detail(key: &str, value: &str) -> ContractResult<()> {
402    validate_text("health.detail.key", key, 128)?;
403    validate_text("health.detail.value", value, 2_048)?;
404    if is_sensitive_key(key) {
405        return Err(ContractError::SecretValue {
406            field: format!("health.details.{key}"),
407        });
408    }
409    Ok(())
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::{
416        CoreRole, LeadershipMode, LeadershipRequirement, ResourceProfile, SchedulingProfile,
417        ServiceId, WorkloadClass,
418    };
419
420    fn profile() -> CoreProfile {
421        let service = ServiceId::new("document.extract").unwrap();
422        CoreProfile::new(
423            CoreRole::Compute,
424            service.clone(),
425            [CapabilityId::new("document.extract").unwrap()],
426            LeadershipRequirement::new(service, LeadershipMode::Required, 30_000).unwrap(),
427            ResourceProfile::new(Some(8), Some(16_000_000_000), 1),
428            SchedulingProfile::new(10, 5, 4, WorkloadClass::Compute).unwrap(),
429        )
430        .unwrap()
431    }
432
433    fn manifest() -> RuntimeManifestV1 {
434        RuntimeManifestV1::new(
435            "0.6.1",
436            "1",
437            BuildId::new("build-123").unwrap(),
438            NodeId::new("node-a").unwrap(),
439            CoreId::new("core-a").unwrap(),
440            RuntimeMode::Cluster,
441            "linux",
442            "aarch64",
443            ProviderId::new("file").unwrap(),
444            RuntimeHealth::new(RuntimeHealthStatus::Healthy, 100),
445            profile(),
446        )
447        .unwrap()
448        .with_loaded_capability(CapabilityId::new("document.extract").unwrap())
449        .unwrap()
450    }
451
452    #[test]
453    fn runtime_manifest_contains_runtime_facts_only() {
454        let encoded = serde_json::to_value(manifest()).unwrap();
455        assert!(encoded.get("application_id").is_none());
456        assert!(encoded.get("vendor").is_none());
457        assert_eq!(encoded["mode"], "cluster");
458    }
459
460    #[test]
461    fn runtime_manifest_round_trip_revalidates_capabilities() {
462        let manifest = manifest();
463        let encoded = serde_json::to_string(&manifest).unwrap();
464        let decoded: RuntimeManifestV1 = serde_json::from_str(&encoded).unwrap();
465        assert_eq!(manifest, decoded);
466    }
467
468    #[test]
469    fn runtime_manifest_matches_v1_fixture() {
470        let expected: serde_json::Value =
471            serde_json::from_str(include_str!("fixtures/runtime-manifest-v1.json")).unwrap();
472        assert_eq!(serde_json::to_value(manifest()).unwrap(), expected);
473        let decoded: RuntimeManifestV1 = serde_json::from_value(expected).unwrap();
474        assert_eq!(decoded, manifest());
475    }
476
477    #[test]
478    fn health_rejects_sensitive_details() {
479        assert!(RuntimeHealth::new(RuntimeHealthStatus::Healthy, 0)
480            .with_detail("access_token", "raw")
481            .is_err());
482    }
483}