Skip to main content

liminal_server/services/site/
declared.rs

1//! ACCESS-015: construction on an exchange with no `[auth]`.
2//!
3//! An open exchange proves nothing about who a seat is and records what it
4//! declared, `open:<name>`. That holder already creates, deploys, holds, wakes
5//! and retires services (ACCESS-012); here it also acts on sites, and its own
6//! act stands where a person's approval stands on a gated exchange. The act
7//! goes through the same apply and commit as an act Lys answers outright, and
8//! the record names the holder as approver and answerable. Nothing reaches
9//! Lys: the effects that place and delegate there stand empty, and a builder
10//! is an open seat whose station is in the registry.
11
12use std::sync::Arc;
13
14use super::effects::SiteEffects;
15use super::{BuilderGrants, SiteAct, SiteChange, SiteError};
16use crate::agents::open_session::{OPEN_GRANT, OPEN_KIND};
17use crate::auth_lys::session::LysSession;
18use crate::server::connection::declared_holder::{ANONYMOUS, PREFIX};
19use crate::services::lifecycle::Lifecycle;
20
21/// The refusal an anonymous seat meets on every construction act.
22pub const NEEDS_A_NAME: &str = "construction_needs_a_name: an anonymous seat acts for no one; \
23                                liminal seat init --open <server> --name <name>";
24
25/// Whether `session` may build: a Lys agent, or an open seat with a name.
26#[must_use]
27pub fn builds(session: &dyn LysSession) -> bool {
28    match session.holder_kind() {
29        Some("agent") => true,
30        Some(OPEN_KIND) => session.holder() != ANONYMOUS,
31        _ => false,
32    }
33}
34
35/// The holder as a site's records name the one answerable for it: the person
36/// id for a Lys person, `open:<name>` for a named open seat.
37#[must_use]
38pub fn answerable(session: &dyn LysSession) -> Option<String> {
39    match session.holder_kind() {
40        Some("person") => Some(session.holder().to_owned()),
41        Some(OPEN_KIND) if session.holder() != ANONYMOUS => {
42            Some(format!("{PREFIX}{}", session.holder()))
43        }
44        _ => None,
45    }
46}
47
48/// The approval owner of an open exchange: every act is the declared
49/// holder's own and is applied at once.
50#[derive(Debug)]
51pub struct DeclaredActs;
52
53impl crate::server::connection::services::SiteDraftHolder for DeclaredActs {
54    fn hold(
55        &self,
56        lifecycle: &Lifecycle,
57        session: &dyn LysSession,
58        operation: &str,
59        target: &str,
60        change: SiteChange,
61        at_ms: u64,
62    ) -> Result<serde_json::Value, SiteError> {
63        if session.holder_kind() != Some(OPEN_KIND) {
64            return Err(SiteError::Invalid("construction holder kind"));
65        }
66        let holder = answerable(session).ok_or_else(|| SiteError::Refused(NEEDS_A_NAME.into()))?;
67        let act = SiteAct {
68            target: target.into(),
69            holder: holder.clone(),
70            responsible: holder,
71            change,
72        };
73        lifecycle.apply_declared(&act, operation, at_ms)
74    }
75}
76
77/// The effects of an open exchange: no placement, no grant, no revocation
78/// reaches Lys; a builder is an open seat with a station in the registry.
79pub struct OpenSiteEffects {
80    agents: Arc<crate::agents::runtime::Agents>,
81}
82
83impl OpenSiteEffects {
84    /// Effects over the open exchange's agent registry.
85    #[must_use]
86    pub const fn new(agents: Arc<crate::agents::runtime::Agents>) -> Self {
87        Self { agents }
88    }
89}
90
91impl SiteEffects for OpenSiteEffects {
92    fn agent_owner(&self, agent: &str) -> Result<String, SiteError> {
93        let record = self.agents.registry().record(agent).map_err(|_| {
94            SiteError::Refused(format!(
95                "builder {agent} has no station on this exchange; its seat listens to its \
96                 inbox once to place it"
97            ))
98        })?;
99        if !record.active() {
100            return Err(SiteError::Refused(format!("agent {agent} is retired")));
101        }
102        Ok(record.owner)
103    }
104
105    fn bind(
106        &self,
107        _draft: &str,
108        _site: &str,
109        agent: &str,
110        _at_ms: u64,
111    ) -> Result<BuilderGrants, SiteError> {
112        self.agent_owner(agent)?;
113        Ok(BuilderGrants {
114            plan: OPEN_GRANT.into(),
115            serve: OPEN_GRANT.into(),
116        })
117    }
118
119    fn revoke(&self, _draft: &str, _label: &str, _grant: &str) -> Result<(), SiteError> {
120        Ok(())
121    }
122
123    fn open(&self) -> bool {
124        true
125    }
126}
127
128#[cfg(test)]
129#[path = "declared_tests.rs"]
130mod tests;