Skip to main content

bamboo_engine/session_app/
supervisor.rs

1//! Host-facing Supervisor identity bootstrap over the canonical Storage port.
2
3use std::{collections::BTreeSet, io, sync::Arc};
4
5use bamboo_domain::{
6    ProjectId, Storage, SupervisorBootstrapReceipt, SupervisorLinkObservation,
7    SupervisorManagementMutation, SupervisorManagementReceipt, SupervisorManagementRequest,
8    SupervisorReference, SupervisorScopeObservation,
9};
10
11/// Trusted host service; not a model tool or an automatic Session startup hook.
12/// Storage owns atomic publication and identity validation. This service never
13/// creates an ordinary Session, changes a cache, or launches a run.
14#[derive(Clone)]
15pub struct SupervisorSessionService {
16    storage: Arc<dyn Storage>,
17}
18
19impl SupervisorSessionService {
20    pub fn new(storage: Arc<dyn Storage>) -> Self {
21        Self { storage }
22    }
23
24    /// Get the stable identity, using `initial_model` only on first creation.
25    pub async fn get_or_create_default(
26        &self,
27        initial_model: &str,
28    ) -> io::Result<SupervisorBootstrapReceipt> {
29        self.storage
30            .get_or_create_default_supervisor(initial_model)
31            .await
32    }
33
34    /// Read the bounded host scope and current CAS revision. Defaults to empty.
35    pub async fn inspect_scope(
36        &self,
37        supervisor: &SupervisorReference,
38    ) -> io::Result<SupervisorScopeObservation> {
39        self.storage.inspect_supervisor_scope(supervisor).await
40    }
41
42    /// Replace trusted Project scope. Removal disables associated links;
43    /// subsequent regrant never silently enables those tombstones.
44    pub async fn configure_project_scope(
45        &self,
46        supervisor: &SupervisorReference,
47        expected_state_revision: u64,
48        allowed_projects: BTreeSet<ProjectId>,
49    ) -> io::Result<SupervisorManagementReceipt> {
50        self.mutate(
51            supervisor,
52            expected_state_revision,
53            SupervisorManagementMutation::ConfigureProjectScope { allowed_projects },
54        )
55        .await
56    }
57
58    /// Attach/revalidate an existing Ordinary Root in configured Project scope.
59    /// Storage captures its exact current birth and metadata revision atomically.
60    pub async fn attach(
61        &self,
62        supervisor: &SupervisorReference,
63        expected_state_revision: u64,
64        target_session_id: &str,
65    ) -> io::Result<SupervisorManagementReceipt> {
66        self.mutate(
67            supervisor,
68            expected_state_revision,
69            SupervisorManagementMutation::Attach {
70                target_session_id: target_session_id.to_string(),
71            },
72        )
73        .await
74    }
75
76    /// Disable a stored link even if the target is now absent or damaged.
77    pub async fn detach(
78        &self,
79        supervisor: &SupervisorReference,
80        expected_state_revision: u64,
81        target_session_id: &str,
82    ) -> io::Result<SupervisorManagementReceipt> {
83        self.mutate(
84            supervisor,
85            expected_state_revision,
86            SupervisorManagementMutation::Detach {
87                target_session_id: target_session_id.to_string(),
88            },
89        )
90        .await
91    }
92
93    /// Strict single-link observation. A later command must validate again at
94    /// durable admission; this returned value carries no retained lock/grant.
95    pub async fn inspect_link(
96        &self,
97        supervisor: &SupervisorReference,
98        target_session_id: &str,
99    ) -> io::Result<SupervisorLinkObservation> {
100        self.storage
101            .inspect_supervisor_link(supervisor, target_session_id)
102            .await
103    }
104
105    async fn mutate(
106        &self,
107        supervisor: &SupervisorReference,
108        expected_state_revision: u64,
109        mutation: SupervisorManagementMutation,
110    ) -> io::Result<SupervisorManagementReceipt> {
111        self.storage
112            .mutate_supervisor_management(&SupervisorManagementRequest {
113                supervisor: supervisor.clone(),
114                expected_state_revision,
115                mutation,
116            })
117            .await
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use bamboo_domain::Session;
125
126    struct UnsupportedStore;
127
128    #[async_trait::async_trait]
129    impl Storage for UnsupportedStore {
130        async fn save_session(&self, _: &Session) -> io::Result<()> {
131            panic!("bootstrap must not fall back to ordinary save")
132        }
133        async fn load_session(&self, _: &str) -> io::Result<Option<Session>> {
134            panic!("authority must not fall back to ordinary load")
135        }
136        async fn delete_session(&self, _: &str) -> io::Result<bool> {
137            panic!("bootstrap must not delete sessions")
138        }
139    }
140
141    #[tokio::test]
142    async fn unsupported_authority_ports_fail_without_ordinary_fallback() {
143        let storage: Arc<dyn Storage> = Arc::new(UnsupportedStore);
144        let service = SupervisorSessionService::new(storage.clone());
145        assert_eq!(
146            service
147                .get_or_create_default("model")
148                .await
149                .unwrap_err()
150                .kind(),
151            io::ErrorKind::Unsupported
152        );
153        assert_eq!(
154            storage
155                .load_root_authority("root")
156                .await
157                .unwrap_err()
158                .kind(),
159            io::ErrorKind::Unsupported
160        );
161        let supervisor = SupervisorReference {
162            session_id: bamboo_domain::DEFAULT_SUPERVISOR_SESSION_ID.into(),
163            incarnation_id: uuid::Uuid::new_v4(),
164        };
165        assert_eq!(
166            service.inspect_scope(&supervisor).await.unwrap_err().kind(),
167            io::ErrorKind::Unsupported
168        );
169        assert_eq!(
170            service
171                .configure_project_scope(&supervisor, 0, BTreeSet::new())
172                .await
173                .unwrap_err()
174                .kind(),
175            io::ErrorKind::Unsupported
176        );
177        assert_eq!(
178            service
179                .attach(&supervisor, 0, "target")
180                .await
181                .unwrap_err()
182                .kind(),
183            io::ErrorKind::Unsupported
184        );
185        assert_eq!(
186            service
187                .detach(&supervisor, 0, "target")
188                .await
189                .unwrap_err()
190                .kind(),
191            io::ErrorKind::Unsupported
192        );
193        assert_eq!(
194            service
195                .inspect_link(&supervisor, "target")
196                .await
197                .unwrap_err()
198                .kind(),
199            io::ErrorKind::Unsupported
200        );
201    }
202}