Skip to main content

liminal_server/services/site/
records.rs

1//! Persisted construction shapes. Free text has no Debug representation.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::num::NonZeroUsize;
6
7/// Required positive operator bounds; reaching a bound refuses before mutation.
8#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct SiteLimits {
11    /// Limits active and retired construction records without evicting history.
12    pub sites: NonZeroUsize,
13    /// Limits subject schemas validated in one admission operation.
14    pub subjects: NonZeroUsize,
15    /// Limits each schema's compilation input before allocating a validator.
16    pub schema_bytes: NonZeroUsize,
17    /// Limits purpose, title, note and routing metadata individually.
18    pub text_bytes: NonZeroUsize,
19    /// Limits steps retained in one complete plan replacement.
20    pub steps: NonZeroUsize,
21    /// Limits historical transitions; a full history refuses new writes.
22    pub transitions: NonZeroUsize,
23    /// Limits prepared and completed drafts without evicting execution receipts.
24    pub drafts: NonZeroUsize,
25    /// Bounds the distinct authenticated people recorded on a draft approval.
26    pub approvers: NonZeroUsize,
27    /// Limits the whole atomic snapshot, including histories and prepared words.
28    pub snapshot_bytes: NonZeroUsize,
29    /// Limits one immutable request before parsing or holding a draft.
30    pub request_bytes: NonZeroUsize,
31}
32
33/// Operator bounds and delegation sources for the construction registry.
34#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct ConstructionConfig {
37    /// Positive construction byte and count limits, all explicitly configured.
38    pub limits: SiteLimits,
39    /// The two source grants held by this machine; no source is inferred.
40    pub grants: super::effects::GrantSources,
41}
42
43/// Exactly what the site promises on one served subject.
44#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct Contract {
47    /// Required input schema, exactly as supplied.
48    #[serde(default)]
49    pub input_schema_json: String,
50    /// Required output schema, exactly as supplied.
51    #[serde(default)]
52    pub output_schema_json: String,
53}
54
55/// Immutable contracts approved before binding any builder.
56#[derive(Clone, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct SiteSpec {
59    /// One service name segment.
60    pub name: String,
61    /// One sentence describing its work.
62    pub purpose: String,
63    /// Exact subject roots it will serve.
64    pub subjects: Vec<String>,
65    /// One validated pair for every subject.
66    #[serde(default)]
67    pub contracts: BTreeMap<String, Contract>,
68}
69
70/// The exact Lys grant ids, retained for deterministic revocation.
71#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct BuilderGrants {
74    /// The builder's plan-only grant on this service.
75    pub plan: String,
76    /// The builder's serve-only grant on this service.
77    pub serve: String,
78}
79
80/// The connector's authenticated approval, stored without free-text words.
81#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct Approval {
84    /// Exact Lys draft.
85    pub draft: String,
86    /// Digest of the immutable approved words.
87    pub digest: String,
88    /// The actual distinct approving people.
89    pub approved_by: Vec<String>,
90    /// The server's execution clock, in epoch milliseconds.
91    pub executed_at_ms: u64,
92    /// The durable execution outcome on this exact target transition.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub receipt: Option<lys_pass::drafts::Execution>,
95}
96impl From<crate::auth_lys::live::drafts::ApprovalTransition> for Approval {
97    fn from(value: crate::auth_lys::live::drafts::ApprovalTransition) -> Self {
98        Self {
99            draft: value.draft,
100            digest: value.digest,
101            approved_by: value.approved_by,
102            executed_at_ms: value.executed_at_ms,
103            receipt: None,
104        }
105    }
106}
107
108/// Deliberate construction events; no payload becomes executable code.
109#[derive(Clone, Serialize, Deserialize)]
110#[serde(
111    tag = "act",
112    content = "detail",
113    rename_all = "snake_case",
114    deny_unknown_fields
115)]
116pub enum SiteChange {
117    /// A contracts-first site, initially without a builder or template.
118    CreateSite(SiteSpec),
119    /// The named active Lys agent becomes its sole builder.
120    BindBuilder { agent: String },
121    /// Revokes both recorded builder grants.
122    UnbindBuilder,
123    /// Records the program exactly approved by the person.
124    CreateTemplate(super::super::templates::TemplateProposal),
125    /// Updates an existing template without changing read-only configuration.
126    UpdateTemplate(super::super::templates::TemplateProposal),
127    /// Retires a live template without removing its history.
128    RetireTemplate,
129    /// Removes the trial serve grant and leaves the site asleep.
130    Complete,
131}
132
133impl SiteChange {
134    /// The schema action used to hold this act.
135    #[must_use]
136    pub const fn action(&self) -> &'static str {
137        match self {
138            Self::CreateSite(_) => "create_site",
139            Self::BindBuilder { .. } => "bind_builder",
140            Self::UnbindBuilder => "unbind_builder",
141            Self::CreateTemplate(_) => "create",
142            Self::UpdateTemplate(_) => "update",
143            Self::RetireTemplate => "retire",
144            Self::Complete => "complete",
145        }
146    }
147    /// The resource kind named in the immutable draft.
148    #[must_use]
149    pub const fn kind(&self) -> &'static str {
150        match self {
151            Self::CreateSite(_) => "liminal.network",
152            Self::CreateTemplate(_) | Self::UpdateTemplate(_) | Self::RetireTemplate => {
153                "liminal.template"
154            }
155            _ => "liminal.service",
156        }
157    }
158}
159
160/// A prepared act whose actor is derived only from an admitted pass.
161#[derive(Clone, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct SiteAct {
164    /// The placed service id or template name, never a filesystem path.
165    pub target: String,
166    /// Original verified requester.
167    pub holder: String,
168    /// Responsible person from the same verified claims.
169    pub responsible: String,
170    /// The immutable approved event.
171    pub change: SiteChange,
172}
173
174impl SiteAct {
175    /// Binds immutable words to the signed requester before the draft is held.
176    ///
177    /// # Errors
178    /// Unsupported holder kind or missing responsible person.
179    pub fn prepare(
180        target: String,
181        change: SiteChange,
182        pass: &lys_pass::VerifiedPass,
183    ) -> Result<Self, super::SiteError> {
184        let holder = &pass.claims().holder;
185        let responsible = match holder.kind.as_str() {
186            "person" => holder.id.clone(),
187            "agent" => holder
188                .responsible
189                .clone()
190                .ok_or(super::SiteError::Invalid("responsible person"))?,
191            _ => return Err(super::SiteError::Invalid("construction holder kind")),
192        };
193        if responsible.is_empty() {
194            return Err(super::SiteError::Invalid("responsible person"));
195        }
196        Ok(Self {
197            target,
198            holder: holder.id.clone(),
199            responsible,
200            change,
201        })
202    }
203}
204
205impl SiteAct {
206    /// Creation asks the network before an unplaced site can have inherited rights.
207    #[must_use]
208    pub fn permission_id<'a>(&'a self, network: &'a str) -> &'a str {
209        if matches!(self.change, SiteChange::CreateSite(_)) {
210            network
211        } else {
212            &self.target
213        }
214    }
215}