bamboo_domain/session/
root_mode_transition.rs1use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use super::Session;
7
8pub const ROOT_MODE_OPERATION_HISTORY_LIMIT: usize = 8;
9
10pub 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 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 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}