Skip to main content

systemprompt_models/profile/
services.rs

1//! Profile-level overrides for the services manifest tree.
2//!
3//! An empty [`ServicesProfileConfig::sources`] means the instance serves the
4//! services tree baked into the image at `paths.services`. Listing sources
5//! makes the tree a composition of independently-published bundles, fetched at
6//! boot in profile order.
7//!
8//! `deny_unknown_fields` rules out `serde(flatten)`, and `serde_yaml` renders
9//! an externally-tagged enum as a `!Variant` YAML tag rather than a nested map,
10//! so a source names its transport through optional `https:`/`oci:` sub-blocks
11//! with exactly-one enforced in validation. The YAML reads:
12//!
13//! ```yaml
14//! services:
15//!   sources:
16//!     - name: base
17//!       oci:
18//!         reference: ghcr.io/org/base@sha256:<64 hex>
19//!         verify:
20//!           ed25519_public_keys: ["<base64 32 bytes>"]
21//!     - name: sales-uk
22//!       https:
23//!         url: https://example.test/bundle.tar.gz
24//!         verify:
25//!           sha256: "<64 hex>"
26//! ```
27//!
28//! Copyright (c) systemprompt.io — Business Source License 1.1.
29//! See <https://systemprompt.io> for licensing details.
30
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
34#[serde(deny_unknown_fields)]
35pub struct ServicesProfileConfig {
36    #[serde(default)]
37    pub port_offset: u16,
38
39    #[serde(default)]
40    pub sources: Vec<ServicesSource>,
41
42    #[serde(default)]
43    pub cache_dir: Option<String>,
44
45    #[serde(default)]
46    pub on_fetch_failure: FetchFailurePolicy,
47}
48
49impl ServicesProfileConfig {
50    #[must_use]
51    pub const fn is_identity(&self) -> bool {
52        self.port_offset == 0 && self.sources.is_empty()
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
57#[serde(deny_unknown_fields)]
58pub struct ServicesSource {
59    pub name: String,
60
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub https: Option<HttpsServicesSource>,
63
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub oci: Option<OciServicesSource>,
66}
67
68impl ServicesSource {
69    #[must_use]
70    pub const fn verification(&self) -> Option<&BundleVerification> {
71        if let Some(https) = self.https.as_ref() {
72            return Some(&https.verify);
73        }
74        if let Some(oci) = self.oci.as_ref() {
75            return Some(&oci.verify);
76        }
77        None
78    }
79
80    #[must_use]
81    pub fn auth_secret(&self) -> Option<&str> {
82        self.https
83            .as_ref()
84            .and_then(|s| s.auth_secret.as_deref())
85            .or_else(|| self.oci.as_ref().and_then(|s| s.auth_secret.as_deref()))
86    }
87
88    #[must_use]
89    pub const fn is_exactly_one(&self) -> bool {
90        self.https.is_some() ^ self.oci.is_some()
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
95#[serde(deny_unknown_fields)]
96pub struct HttpsServicesSource {
97    pub url: String,
98
99    #[serde(default)]
100    pub auth_secret: Option<String>,
101
102    #[serde(default)]
103    pub verify: BundleVerification,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
107#[serde(deny_unknown_fields)]
108pub struct OciServicesSource {
109    pub reference: String,
110
111    #[serde(default)]
112    pub auth_secret: Option<String>,
113
114    #[serde(default)]
115    pub verify: BundleVerification,
116}
117
118#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
119#[serde(deny_unknown_fields)]
120pub struct BundleVerification {
121    #[serde(default)]
122    pub sha256: Option<String>,
123
124    #[serde(default)]
125    pub ed25519_public_keys: Vec<String>,
126}
127
128impl BundleVerification {
129    #[must_use]
130    pub const fn is_empty(&self) -> bool {
131        self.sha256.is_none() && self.ed25519_public_keys.is_empty()
132    }
133}
134
135#[derive(
136    Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
137)]
138#[serde(rename_all = "snake_case", deny_unknown_fields)]
139pub enum FetchFailurePolicy {
140    FailClosed,
141
142    #[default]
143    UseLastGood,
144
145    UseBundled,
146}