Skip to main content

bamboo_domain/session/
root_mode_transition.rs

1//! Terminal, bounded host-owned Root mode operations.
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use super::Session;
7
8pub const ROOT_MODE_OPERATION_HISTORY_LIMIT: usize = 8;
9
10/// Bind the operation identity to its durable CAS epoch. An old operation ID
11/// cannot be reused with a newer epoch after its bounded receipt is evicted.
12pub fn root_mode_operation_id_matches_epoch(value: &str, expected_epoch: u64) -> bool {
13    let Some((epoch, uuid)) = value.split_once(':') else {
14        return false;
15    };
16    epoch == expected_epoch.to_string()
17        && uuid::Uuid::parse_str(uuid).is_ok_and(|parsed| parsed.to_string() == uuid)
18}
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum RootModeOperationAction {
22    Select,
23    Recover,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct RootModeOperationRequest {
28    pub session_id: String,
29    pub operation_id: String,
30    pub birth_token: String,
31    pub expected_epoch: u64,
32    pub requested_enabled: bool,
33    pub action: RootModeOperationAction,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum RootModeOperationDecision {
38    Terminal(RootModeOperationReceipt),
39    /// A later terminal epoch already prevents this request from committing.
40    FencedBySuccessor {
41        operation_id: String,
42        expected_epoch: u64,
43        current_epoch: u64,
44        current_enabled: bool,
45        current_tool_revision: u64,
46    },
47    NotFound,
48    NotRoot,
49    BirthMismatch,
50    OperationConflict,
51    FutureEpoch,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum RootModeOperationOutcome {
57    Committed,
58    Fenced,
59    RejectedIncompatible,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
63pub struct RootModeOperationReceipt {
64    pub operation_id: String,
65    pub expected_epoch: u64,
66    pub resulting_epoch: u64,
67    pub requested_enabled: bool,
68    pub enabled_at_completion: bool,
69    pub tool_authority_revision: u64,
70    pub outcome: RootModeOperationOutcome,
71}
72
73impl Session {
74    /// Opaque lifetime token, so a deleted Root ID cannot reuse an old CAS.
75    pub fn root_mode_birth_token(&self) -> String {
76        let mut digest = Sha256::new();
77        digest.update(b"bamboo.root-mode-birth.v1\0");
78        digest.update(self.id.as_bytes());
79        digest.update(b"\0");
80        digest.update(self.created_at.to_rfc3339().as_bytes());
81        digest.update(b"\0");
82        digest.update(
83            serde_json::to_vec(&self.authority_identity)
84                .expect("Session authority identity is serializable"),
85        );
86        hex::encode(digest.finalize())
87    }
88
89    pub fn root_mode_operation(&self, operation_id: &str) -> Option<&RootModeOperationReceipt> {
90        self.root_mode_operations
91            .iter()
92            .find(|receipt| receipt.operation_id == operation_id)
93    }
94
95    pub fn record_root_mode_operation(
96        &mut self,
97        mut receipt: RootModeOperationReceipt,
98    ) -> Result<(), super::RootToolAuthorityError> {
99        if self.kind != super::SessionKind::Root || self.parent_session_id.is_some() {
100            return Err(super::RootToolAuthorityError::NotRoot);
101        }
102        let next = self
103            .root_mode_transition_epoch
104            .checked_add(1)
105            .ok_or(super::RootToolAuthorityError::RevisionOverflow)?;
106        receipt.expected_epoch = self.root_mode_transition_epoch;
107        receipt.resulting_epoch = next;
108        receipt.enabled_at_completion = self.root_orchestration_only_enabled();
109        receipt.tool_authority_revision = self.root_tool_authority_revision;
110        self.root_mode_transition_epoch = next;
111        self.root_mode_operations.push(receipt);
112        self.updated_at = chrono::Utc::now();
113        if self.root_mode_operations.len() > ROOT_MODE_OPERATION_HISTORY_LIMIT {
114            self.root_mode_operations.remove(0);
115        }
116        Ok(())
117    }
118}