1use 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
21pub const RUNTIME_MANIFEST_VERSION: u16 = 1;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum RuntimeHealthStatus {
28 Healthy,
30 Degraded,
32 Unhealthy,
34}
35
36#[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 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 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 pub fn status(&self) -> RuntimeHealthStatus {
69 self.status
70 }
71
72 pub fn checked_at_ms(&self) -> u64 {
74 self.checked_at_ms
75 }
76
77 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum RuntimeOperationalMode {
94 Starting,
96 Discovering,
98 Syncing,
100 ReadOnly,
102 #[default]
104 ReadWrite,
105 Degraded,
107 Isolated,
109}
110
111impl RuntimeOperationalMode {
112 pub fn allows_local_queries(self) -> bool {
114 matches!(
115 self,
116 Self::ReadOnly | Self::ReadWrite | Self::Degraded | Self::Isolated
117 )
118 }
119
120 pub fn allows_writes(self) -> bool {
122 matches!(self, Self::ReadWrite)
123 }
124
125 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#[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 #[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 pub fn with_feature(mut self, feature: FeatureId) -> Self {
244 self.features.insert(feature);
245 self
246 }
247
248 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 pub fn with_health(mut self, health: RuntimeHealth) -> ContractResult<Self> {
262 health.validate()?;
263 self.health = health;
264 Ok(self)
265 }
266
267 pub fn with_operational_mode(mut self, mode: RuntimeOperationalMode) -> Self {
269 self.operational_mode = mode;
270 self
271 }
272
273 pub fn manifest_version(&self) -> u16 {
275 self.manifest_version
276 }
277
278 pub fn runtime_version(&self) -> &str {
280 &self.runtime_version
281 }
282
283 pub fn protocol_version(&self) -> &str {
285 &self.protocol_version
286 }
287
288 pub fn build_id(&self) -> &BuildId {
290 &self.build_id
291 }
292
293 pub fn features(&self) -> &BTreeSet<FeatureId> {
295 &self.features
296 }
297
298 pub fn node_id(&self) -> &NodeId {
300 &self.node_id
301 }
302
303 pub fn core_id(&self) -> &CoreId {
305 &self.core_id
306 }
307
308 pub fn mode(&self) -> RuntimeMode {
310 self.mode
311 }
312
313 pub fn platform(&self) -> &str {
315 &self.platform
316 }
317
318 pub fn architecture(&self) -> &str {
320 &self.architecture
321 }
322
323 pub fn storage_backend(&self) -> &ProviderId {
325 &self.storage_backend
326 }
327
328 pub fn health(&self) -> &RuntimeHealth {
330 &self.health
331 }
332
333 pub fn operational_mode(&self) -> RuntimeOperationalMode {
335 self.operational_mode
336 }
337
338 pub fn loaded_capabilities(&self) -> &BTreeSet<CapabilityId> {
340 &self.loaded_capabilities
341 }
342
343 pub fn core_profile(&self) -> &CoreProfile {
345 &self.core_profile
346 }
347
348 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}