bamboo_engine/
ticket_token_budget.rs1use std::sync::Arc;
4
5use bamboo_domain::{Session, SessionKind, Storage};
6use bamboo_tickets::{Authority, Error, Principal, Result, TicketService};
7
8use crate::ticket_runtime::{self, TicketDispatchBinding};
9
10mod provider;
11
12#[derive(Clone)]
13struct SessionStamp {
14 id: String,
15 created_at: chrono::DateTime<chrono::Utc>,
16 parent: Option<String>,
17 root: String,
18 kind: SessionKind,
19}
20
21impl SessionStamp {
22 fn from_session(session: &Session) -> Self {
23 Self {
24 id: session.id.clone(),
25 created_at: session.created_at,
26 parent: session.parent_session_id.clone(),
27 root: session.root_session_id.clone(),
28 kind: session.kind,
29 }
30 }
31 fn matches(&self, session: &Session) -> bool {
32 self.id == session.id
33 && self.created_at == session.created_at
34 && self.parent == session.parent_session_id
35 && self.root == session.root_session_id
36 && self.kind == session.kind
37 }
38}
39
40pub struct NativeWorkTokenBudget {
43 service: Arc<TicketService>,
44 storage: Arc<dyn Storage>,
45 chain: Vec<SessionStamp>,
46 dispatch: Option<TicketDispatchBinding>,
47 binding: bamboo_tickets::ScopeBinding,
48}
49
50impl NativeWorkTokenBudget {
51 pub(crate) async fn capture(
52 service: Option<Arc<TicketService>>,
53 storage: Arc<dyn Storage>,
54 session: &Session,
55 ) -> Result<Option<Arc<Self>>> {
56 let Some(service) = service else {
57 if ticket_runtime::read_dispatch(session)?.is_some() {
58 return Err(Error::AuthorityUnavailable(
59 "Ticket token authority unavailable".into(),
60 ));
61 }
62 return Ok(None);
63 };
64 let mut chain = Vec::<SessionStamp>::new();
65 let mut id = session.id.clone();
66 let mut dispatch = None;
67 for _ in 0..32 {
68 if chain.iter().any(|stamp| stamp.id == id) {
69 return Err(Error::ScopeDenied("cyclic Native ancestry".into()));
70 }
71 let canonical = storage
72 .load_runtime_control_plane(&id)
73 .await?
74 .ok_or_else(|| Error::ScopeDenied("Native ancestor missing".into()))?;
75 if chain.is_empty() && !SessionStamp::from_session(session).matches(&canonical) {
76 return Err(Error::ScopeDenied(
77 "Native execution incarnation changed".into(),
78 ));
79 }
80 if dispatch.is_none() {
81 if let Some(actual) = ticket_runtime::read_dispatch(&canonical)? {
82 if canonical.id != ticket_runtime::ticket_child_id(&actual.dispatch_key) {
83 return Err(Error::ScopeDenied(
84 "Native budget binding is outside its canonical Ticket child".into(),
85 ));
86 }
87 dispatch = Some(actual);
88 }
89 }
90 chain.push(SessionStamp::from_session(&canonical));
91 if canonical.kind == SessionKind::Root {
92 let published = service.published()?.1;
93 if published.binding.supervisor_session_id != canonical.id {
94 return if dispatch.is_some() {
95 Err(Error::ScopeDenied(
96 "Ticket budget scope differs from Native ancestry".into(),
97 ))
98 } else {
99 Ok(None)
100 };
101 }
102 let binding = if published.execution_budget.is_some() || dispatch.is_some() {
107 ticket_runtime::verified_scope_binding_for_service(
108 storage.as_ref(),
109 &service,
110 &canonical.id,
111 )
112 .await?
113 .0
114 } else {
115 published.binding.clone()
116 };
117 if binding != published.binding
118 || dispatch.as_ref().is_some_and(|d| d.binding != binding)
119 {
120 return Err(Error::ScopeDenied("Ticket budget binding changed".into()));
121 }
122 return Ok(Some(Arc::new(Self {
123 service,
124 storage,
125 chain,
126 dispatch,
127 binding,
128 })));
129 }
130 if canonical.kind != SessionKind::Child {
131 return Err(Error::ScopeDenied(
132 "Native ancestry must use canonical Child sessions".into(),
133 ));
134 }
135 id = canonical
136 .parent_session_id
137 .ok_or_else(|| Error::ScopeDenied("Native parent missing".into()))?;
138 }
139 Err(Error::ResourceBlocked(
140 "Native budget ancestry exceeds bound".into(),
141 ))
142 }
143
144 async fn authority(&self) -> Result<Authority> {
145 let (binding, _) = ticket_runtime::verified_scope_binding_for_service(
146 self.storage.as_ref(),
147 &self.service,
148 &self.binding.supervisor_session_id,
149 )
150 .await?;
151 if binding != self.binding {
152 return Err(Error::ScopeDenied(
153 "Native budget Supervisor changed".into(),
154 ));
155 }
156 for stamp in &self.chain {
157 let canonical = self
158 .storage
159 .load_runtime_control_plane(&stamp.id)
160 .await?
161 .ok_or_else(|| Error::ScopeDenied("Native budget ancestor missing".into()))?;
162 if !stamp.matches(&canonical) {
163 return Err(Error::ScopeDenied("Native budget ancestry changed".into()));
164 }
165 if let Some(actual) = ticket_runtime::read_dispatch(&canonical)? {
166 if self.dispatch.as_ref() != Some(&actual) {
167 return Err(Error::ScopeDenied("Native budget dispatch changed".into()));
168 }
169 }
170 }
171 let dispatch = self.dispatch.as_ref().ok_or_else(|| {
172 Error::ResourceBlocked("configured scope requires an attributable Work dispatch".into())
173 })?;
174 ticket_runtime::require_dispatch_permission(&self.service, dispatch)?;
175 let snapshot = self.service.published()?.1;
176 let spec = &snapshot
177 .intents
178 .get(&dispatch.dispatch_key)
179 .ok_or_else(|| Error::ScopeDenied("Native budget dispatch missing".into()))?
180 .immutable_spec;
181 if !matches!(ticket_runtime::query_dispatch(self.storage.as_ref(), &self.service, &dispatch.dispatch_key, spec).await?,
182 ticket_runtime::DispatchObservation::Admitted { receipt } if dispatch.receipt.as_ref() == Some(&receipt))
183 {
184 return Err(Error::ScopeDenied(
185 "Native budget requires the original admitted attempt".into(),
186 ));
187 }
188 Ok(Authority::from_verified_host(binding, Principal::Runtime))
189 }
190}