Skip to main content

appcore_contracts/deployment/
config.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: config.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 13:18:47 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use super::*;
12use crate::deployment::manifest::validate_setting;
13
14/// Schema version written by [`DeploymentManifestV1`].
15pub const DEPLOYMENT_MANIFEST_VERSION: u16 = 1;
16
17/// Installation-owned watchdog settings.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(default)]
20pub struct DeploymentWatchdogConfig {
21    enabled: bool,
22    check_interval_ms: u64,
23    stall_timeout_ms: u64,
24}
25
26impl DeploymentWatchdogConfig {
27    /// Creates explicit watchdog settings.
28    pub fn new(
29        enabled: bool,
30        check_interval_ms: u64,
31        stall_timeout_ms: u64,
32    ) -> ContractResult<Self> {
33        let config = Self {
34            enabled,
35            check_interval_ms,
36            stall_timeout_ms,
37        };
38        config.validate()?;
39        Ok(config)
40    }
41
42    /// Reports whether watchdog health affects Runtime health.
43    pub fn is_enabled(self) -> bool {
44        self.enabled
45    }
46
47    /// Returns the independent watchdog evaluation interval.
48    pub fn check_interval_ms(self) -> u64 {
49        self.check_interval_ms
50    }
51
52    /// Returns the maximum interval without completed reconciliation.
53    pub fn stall_timeout_ms(self) -> u64 {
54        self.stall_timeout_ms
55    }
56
57    pub(super) fn validate(self) -> ContractResult<()> {
58        if self.check_interval_ms == 0 || self.stall_timeout_ms == 0 {
59            return Err(ContractError::InvalidValue {
60                field: "supervisor.watchdog",
61                reason: "watchdog intervals must be greater than zero",
62            });
63        }
64        if self.enabled && self.stall_timeout_ms <= self.check_interval_ms {
65            return Err(ContractError::InvalidValue {
66                field: "supervisor.watchdog.stall_timeout_ms",
67                reason: "must exceed check_interval_ms",
68            });
69        }
70        Ok(())
71    }
72}
73
74impl Default for DeploymentWatchdogConfig {
75    fn default() -> Self {
76        Self {
77            enabled: true,
78            check_interval_ms: 1_000,
79            stall_timeout_ms: 15_000,
80        }
81    }
82}
83
84/// Installation-owned supervisor settings.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
86#[serde(default)]
87pub struct DeploymentSupervisorConfig {
88    watchdog: DeploymentWatchdogConfig,
89}
90
91impl DeploymentSupervisorConfig {
92    /// Creates supervisor settings from watchdog policy.
93    pub fn new(watchdog: DeploymentWatchdogConfig) -> Self {
94        Self { watchdog }
95    }
96
97    /// Returns watchdog settings.
98    pub fn watchdog(self) -> DeploymentWatchdogConfig {
99        self.watchdog
100    }
101
102    pub(super) fn validate(self) -> ContractResult<()> {
103        self.watchdog.validate()
104    }
105}
106
107/// Reference to a secret held outside a manifest.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(try_from = "String", into = "String")]
110pub struct SecretRef(String);
111
112impl SecretRef {
113    /// Creates a secret reference such as `env:APPCORE_TOKEN` or `vault:key-name`.
114    pub fn new(reference: impl Into<String>) -> ContractResult<Self> {
115        let reference = reference.into();
116        validate_text("secret_ref", &reference, 512)?;
117        let Some((scheme, target)) = reference.split_once(':') else {
118            return Err(ContractError::InvalidValue {
119                field: "secret_ref",
120                reason: "a provider scheme is required",
121            });
122        };
123        if !matches!(scheme, "env" | "file" | "vault" | "provider") || target.trim().is_empty() {
124            return Err(ContractError::InvalidValue {
125                field: "secret_ref",
126                reason: "unsupported or empty secret reference",
127            });
128        }
129        Ok(Self(reference))
130    }
131
132    /// Returns the non-secret reference string.
133    pub fn as_str(&self) -> &str {
134        &self.0
135    }
136}
137
138impl TryFrom<String> for SecretRef {
139    type Error = ContractError;
140
141    fn try_from(value: String) -> Result<Self, Self::Error> {
142        Self::new(value)
143    }
144}
145
146impl From<SecretRef> for String {
147    fn from(value: SecretRef) -> Self {
148        value.0
149    }
150}
151
152/// Provider selection and its non-sensitive installation settings.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct ProviderConfig {
155    provider_id: ProviderId,
156    endpoint: Option<String>,
157    settings: BTreeMap<String, String>,
158    secret_refs: BTreeMap<String, SecretRef>,
159}
160
161impl ProviderConfig {
162    /// Selects a provider without implementation-specific settings.
163    pub fn new(provider_id: ProviderId) -> Self {
164        Self {
165            provider_id,
166            endpoint: None,
167            settings: BTreeMap::new(),
168            secret_refs: BTreeMap::new(),
169        }
170    }
171
172    /// Adds an optional provider endpoint.
173    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> ContractResult<Self> {
174        let endpoint = endpoint.into();
175        validate_text("provider.endpoint", &endpoint, 2_048)?;
176        self.endpoint = Some(endpoint);
177        Ok(self)
178    }
179
180    /// Adds a non-sensitive provider setting.
181    pub fn with_setting(
182        mut self,
183        key: impl Into<String>,
184        value: impl Into<String>,
185    ) -> ContractResult<Self> {
186        let key = key.into();
187        let value = value.into();
188        validate_setting(&key, &value)?;
189        self.settings.insert(key, value);
190        Ok(self)
191    }
192
193    /// Adds a named reference to externally managed secret material.
194    pub fn with_secret_ref(
195        mut self,
196        name: impl Into<String>,
197        secret: SecretRef,
198    ) -> ContractResult<Self> {
199        let name = name.into();
200        validate_text("provider.secret_ref.name", &name, 128)?;
201        self.secret_refs.insert(name, secret);
202        Ok(self)
203    }
204
205    /// Returns the selected provider identity.
206    pub fn provider_id(&self) -> &ProviderId {
207        &self.provider_id
208    }
209
210    /// Returns the optional provider endpoint.
211    pub fn endpoint(&self) -> Option<&str> {
212        self.endpoint.as_deref()
213    }
214
215    /// Returns non-sensitive provider settings.
216    pub fn settings(&self) -> &BTreeMap<String, String> {
217        &self.settings
218    }
219
220    /// Returns provider secret references.
221    pub fn secret_refs(&self) -> &BTreeMap<String, SecretRef> {
222        &self.secret_refs
223    }
224
225    pub(super) fn validate(&self) -> ContractResult<()> {
226        if let Some(endpoint) = &self.endpoint {
227            validate_text("provider.endpoint", endpoint, 2_048)?;
228        }
229        for (key, value) in &self.settings {
230            validate_setting(key, value)?;
231        }
232        for name in self.secret_refs.keys() {
233            validate_text("provider.secret_ref.name", name, 128)?;
234        }
235        Ok(())
236    }
237}
238
239/// TLS settings represented only by external certificate references.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct TlsConfig {
242    enabled: bool,
243    certificate: Option<SecretRef>,
244    private_key: Option<SecretRef>,
245}
246
247impl TlsConfig {
248    /// Disables TLS at this adapter boundary.
249    pub fn disabled() -> Self {
250        Self {
251            enabled: false,
252            certificate: None,
253            private_key: None,
254        }
255    }
256
257    /// Enables TLS using certificate and private-key references.
258    pub fn enabled(certificate: SecretRef, private_key: SecretRef) -> Self {
259        Self {
260            enabled: true,
261            certificate: Some(certificate),
262            private_key: Some(private_key),
263        }
264    }
265
266    /// Reports whether TLS is enabled.
267    pub fn is_enabled(&self) -> bool {
268        self.enabled
269    }
270
271    /// Returns the certificate reference.
272    pub fn certificate(&self) -> Option<&SecretRef> {
273        self.certificate.as_ref()
274    }
275
276    /// Returns the private-key reference.
277    pub fn private_key(&self) -> Option<&SecretRef> {
278        self.private_key.as_ref()
279    }
280
281    pub(super) fn validate(&self) -> ContractResult<()> {
282        if self.enabled && (self.certificate.is_none() || self.private_key.is_none()) {
283            return Err(ContractError::InvalidValue {
284                field: "network.tls",
285                reason: "certificate and private-key references are required",
286            });
287        }
288        if !self.enabled && (self.certificate.is_some() || self.private_key.is_some()) {
289            return Err(ContractError::InvalidValue {
290                field: "network.tls",
291                reason: "disabled TLS must not retain key material references",
292            });
293        }
294        Ok(())
295    }
296}
297
298/// Network adapters selected for one installation.
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct NetworkConfig {
301    listen_addresses: Vec<String>,
302    peer_transport: ProviderId,
303    command_transport: ProviderId,
304    tls: TlsConfig,
305}
306
307impl NetworkConfig {
308    /// Creates network configuration using explicit transport providers.
309    pub fn new(peer_transport: ProviderId, command_transport: ProviderId) -> Self {
310        Self {
311            listen_addresses: Vec::new(),
312            peer_transport,
313            command_transport,
314            tls: TlsConfig::disabled(),
315        }
316    }
317
318    /// Adds a listen address owned by this installation.
319    pub fn with_listen_address(mut self, address: impl Into<String>) -> ContractResult<Self> {
320        let address = address.into();
321        validate_text("network.listen_address", &address, 2_048)?;
322        self.listen_addresses.push(address);
323        Ok(self)
324    }
325
326    /// Replaces TLS settings.
327    pub fn with_tls(mut self, tls: TlsConfig) -> ContractResult<Self> {
328        tls.validate()?;
329        self.tls = tls;
330        Ok(self)
331    }
332
333    /// Returns listen addresses.
334    pub fn listen_addresses(&self) -> &[String] {
335        &self.listen_addresses
336    }
337
338    /// Returns the peer transport provider.
339    pub fn peer_transport(&self) -> &ProviderId {
340        &self.peer_transport
341    }
342
343    /// Returns the command transport provider.
344    pub fn command_transport(&self) -> &ProviderId {
345        &self.command_transport
346    }
347
348    /// Returns TLS settings.
349    pub fn tls(&self) -> &TlsConfig {
350        &self.tls
351    }
352
353    pub(super) fn validate(&self) -> ContractResult<()> {
354        for address in &self.listen_addresses {
355            validate_text("network.listen_address", address, 2_048)?;
356        }
357        self.tls.validate()
358    }
359}
360
361/// Installation volume mounted for an application or runtime adapter.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct VolumeMount {
364    name: String,
365    source: String,
366    target: String,
367    read_only: bool,
368}
369
370impl VolumeMount {
371    /// Creates a named volume mount.
372    pub fn new(
373        name: impl Into<String>,
374        source: impl Into<String>,
375        target: impl Into<String>,
376        read_only: bool,
377    ) -> ContractResult<Self> {
378        let mount = Self {
379            name: name.into(),
380            source: source.into(),
381            target: target.into(),
382            read_only,
383        };
384        mount.validate()?;
385        Ok(mount)
386    }
387
388    /// Returns the logical volume name.
389    pub fn name(&self) -> &str {
390        &self.name
391    }
392
393    /// Returns the installation-owned source.
394    pub fn source(&self) -> &str {
395        &self.source
396    }
397
398    /// Returns the mount target exposed to the application.
399    pub fn target(&self) -> &str {
400        &self.target
401    }
402
403    /// Reports whether the mount is read-only.
404    pub fn is_read_only(&self) -> bool {
405        self.read_only
406    }
407
408    pub(super) fn validate(&self) -> ContractResult<()> {
409        validate_text("volume.name", &self.name, 128)?;
410        validate_text("volume.source", &self.source, 2_048)?;
411        validate_text("volume.target", &self.target, 2_048)
412    }
413}
414
415/// Environment variable value or external secret binding.
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
418pub enum EnvironmentBinding {
419    /// Non-sensitive literal configuration value.
420    Literal(String),
421    /// Reference to externally managed secret material.
422    Secret(SecretRef),
423}