1use std::collections::{HashMap, HashSet, VecDeque};
6
7use async_trait::async_trait;
8use bamboo_agent_core::tools::ToolResult;
9use bamboo_domain::{ActorSession, Session, SessionKind, Storage};
10use serde_json::{json, Value};
11use sha2::{Digest, Sha256};
12
13use super::{assemble_session_tree, ChildSessionEntry, SessionTreeNode, MAX_CHILD_RESULT_BYTES};
14
15pub const MAX_TREE_CURSOR_BYTES: usize = 128;
16const PAGE_NODES: usize = 32;
17const DISPLAY_DEPTH: u32 = 4;
19const INDEX_NODE_CAP: usize = 5000;
20pub const INDEX_SNAPSHOT_CAP: usize = 50_000;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum OwnedTreeError {
24 InvalidLineage,
25 InvalidCursor,
26 ResultTooLarge,
27}
28
29#[async_trait]
30pub trait OwnedTreePort: Send + Sync {
31 async fn load(&self, id: &str) -> Result<Session, OwnedTreeError>;
32 async fn child_ids(&self, parent_id: &str) -> Result<Vec<String>, OwnedTreeError>;
33 async fn child_index(&self) -> Result<Option<Vec<(String, String)>>, OwnedTreeError> {
36 Ok(None)
37 }
38}
39
40#[async_trait]
43impl OwnedTreePort for bamboo_storage::SessionStoreV2 {
44 async fn load(&self, id: &str) -> Result<Session, OwnedTreeError> {
45 Storage::load_session(self, id)
46 .await
47 .map_err(|_| OwnedTreeError::InvalidLineage)?
48 .ok_or(OwnedTreeError::InvalidLineage)
49 }
50
51 async fn child_ids(&self, parent_id: &str) -> Result<Vec<String>, OwnedTreeError> {
52 Ok(self
53 .list_index_entries()
54 .await
55 .into_iter()
56 .filter(|entry| {
57 entry.kind == SessionKind::Child
58 && entry.parent_session_id.as_deref() == Some(parent_id)
59 })
60 .map(|entry| entry.id)
61 .collect())
62 }
63
64 async fn child_index(&self) -> Result<Option<Vec<(String, String)>>, OwnedTreeError> {
65 let entries = self.list_index_entries().await;
66 if entries.len() > INDEX_SNAPSHOT_CAP {
67 return Err(OwnedTreeError::InvalidLineage);
68 }
69 Ok(Some(
70 entries
71 .into_iter()
72 .filter(|entry| entry.kind == SessionKind::Child)
73 .filter_map(|entry| entry.parent_session_id.map(|parent| (parent, entry.id)))
74 .collect(),
75 ))
76 }
77}
78
79fn actor(session: &Session) -> Result<ActorSession, OwnedTreeError> {
80 if let (Some(typed), Some(legacy)) = (
83 session
84 .runtime_metadata
85 .as_ref()
86 .and_then(|metadata| metadata.project_id.as_ref()),
87 session.metadata.get("project_id"),
88 ) {
89 if typed != legacy {
90 return Err(OwnedTreeError::InvalidLineage);
91 }
92 }
93 ActorSession::from_session(session).map_err(|_| OwnedTreeError::InvalidLineage)
94}
95
96fn edge(
97 parent: &Session,
98 child: &Session,
99 root_id: &str,
100 project_id: Option<&str>,
101) -> Result<(), OwnedTreeError> {
102 let parent_actor = actor(parent)?;
103 let child_actor = actor(child)?;
104 if child.kind != SessionKind::Child
105 || child.id == parent.id
106 || child_actor.parent_actor_id.as_deref() != Some(parent.id.as_str())
107 || parent_actor.root_actor_id != root_id
108 || child_actor.root_actor_id != root_id
109 || parent_actor.project_id.as_deref() != project_id
110 || child_actor.project_id.as_deref() != project_id
111 || child_actor.spawn_depth != parent_actor.spawn_depth.saturating_add(1)
112 || child.created_at < parent.created_at
113 {
114 return Err(OwnedTreeError::InvalidLineage);
115 }
116 Ok(())
117}
118
119async fn lineage(
120 port: &dyn OwnedTreePort,
121 caller_id: &str,
122) -> Result<Vec<Session>, OwnedTreeError> {
123 let caller = port.load(caller_id).await?;
124 if caller.id != caller_id {
125 return Err(OwnedTreeError::InvalidLineage);
126 }
127 let caller_actor = actor(&caller)?;
128 let mut lineage = vec![caller];
129 let mut seen = HashSet::from([caller_id.to_owned()]);
130 while lineage
131 .last()
132 .is_some_and(|session| session.kind == SessionKind::Child)
133 {
134 let child = lineage.last().ok_or(OwnedTreeError::InvalidLineage)?;
135 let parent_id = child
136 .parent_session_id
137 .as_deref()
138 .ok_or(OwnedTreeError::InvalidLineage)?;
139 if !seen.insert(parent_id.to_owned()) || lineage.len() > INDEX_NODE_CAP {
140 return Err(OwnedTreeError::InvalidLineage);
141 }
142 let parent = port.load(parent_id).await?;
143 if parent.id != parent_id {
144 return Err(OwnedTreeError::InvalidLineage);
145 }
146 edge(
147 &parent,
148 child,
149 &caller_actor.root_actor_id,
150 caller_actor.project_id.as_deref(),
151 )?;
152 lineage.push(parent);
153 }
154 let root = lineage.last().ok_or(OwnedTreeError::InvalidLineage)?;
155 let root_actor = actor(root)?;
156 if root.kind != SessionKind::Root
157 || root.id != caller_actor.root_actor_id
158 || root_actor.project_id != caller_actor.project_id
159 || lineage.len() != caller_actor.spawn_depth as usize + 1
160 {
161 return Err(OwnedTreeError::InvalidLineage);
162 }
163 lineage.reverse();
164 Ok(lineage)
165}
166
167fn proof_session(proof: &mut Sha256, session: &Session) -> Result<(), OwnedTreeError> {
168 let bytes = serde_json::to_vec(&json!({
169 "actor": actor(session)?,
170 "updated_at": session.updated_at,
171 "title": session.title,
172 "last_run_status": session.last_run_status(),
173 }))
174 .map_err(|_| OwnedTreeError::InvalidLineage)?;
175 proof.update((bytes.len() as u64).to_le_bytes());
176 proof.update(bytes);
177 Ok(())
178}
179
180fn lineage_proof(lineage: &[Session]) -> Result<String, OwnedTreeError> {
181 let mut proof = Sha256::new();
182 for session in lineage {
183 proof_session(&mut proof, session)?;
184 }
185 Ok(hex::encode(proof.finalize()))
186}
187
188async fn verified_tree(
189 port: &dyn OwnedTreePort,
190 lineage: &[Session],
191) -> Result<(SessionTreeNode, String), OwnedTreeError> {
192 let caller = lineage.last().ok_or(OwnedTreeError::InvalidLineage)?;
193 let caller_actor = actor(caller)?;
194 let mut proof = Sha256::new();
195 for ancestor in lineage {
196 proof_session(&mut proof, ancestor)?;
197 }
198 let mut seen: HashSet<String> = lineage.iter().map(|session| session.id.clone()).collect();
199 let mut adjacency = HashMap::new();
200 let indexed = port.child_index().await?.map(|edges| {
201 let mut by_parent: HashMap<String, Vec<String>> = HashMap::new();
202 for (parent, child) in edges {
203 by_parent.entry(parent).or_default().push(child);
204 }
205 by_parent
206 });
207 let mut queue = VecDeque::from([(caller.clone(), 0_u32)]);
208 while let Some((parent, depth)) = queue.pop_front() {
209 if depth >= DISPLAY_DEPTH {
210 continue;
211 }
212 let mut ids = if let Some(indexed) = &indexed {
213 indexed.get(&parent.id).cloned().unwrap_or_default()
214 } else {
215 port.child_ids(&parent.id).await?
216 };
217 if ids.len() > INDEX_NODE_CAP {
218 return Err(OwnedTreeError::InvalidLineage);
219 }
220 ids.sort_unstable();
221 let mut children = Vec::new();
222 for id in ids {
223 if !seen.insert(id.clone()) || seen.len() > INDEX_NODE_CAP {
224 return Err(OwnedTreeError::InvalidLineage);
225 }
226 let child = port.load(&id).await?;
227 if child.id != id {
228 return Err(OwnedTreeError::InvalidLineage);
229 }
230 edge(
231 &parent,
232 &child,
233 &caller_actor.root_actor_id,
234 caller_actor.project_id.as_deref(),
235 )?;
236 proof_session(&mut proof, &child)?;
237 children.push(ChildSessionEntry {
238 child_session_id: child.id.clone(),
239 title: child.title.clone(),
240 pinned: child.pinned,
241 message_count: child.messages.len(),
242 updated_at: child.updated_at.to_rfc3339(),
243 last_run_status: child.last_run_status(),
244 last_run_error: child.last_run_error(),
245 });
246 queue.push_back((child, depth + 1));
247 }
248 adjacency.insert(parent.id, children);
249 }
250 let tree = assemble_session_tree(&caller.id, &caller.title, &adjacency, DISPLAY_DEPTH);
251 Ok((tree, hex::encode(proof.finalize())))
252}
253
254fn observed_status(raw: Option<&str>) -> &'static str {
255 match raw {
256 Some("created") => "created",
257 Some("pending") => "pending",
258 Some("queued") => "queued",
259 Some("running") => "running",
260 Some("running_in_background") => "running_in_background",
261 Some("already_running") => "already_running",
262 Some("completed") => "completed",
263 Some("error") => "error",
264 Some("timeout") => "timeout",
265 Some("cancelled") => "cancelled",
266 Some("message_delivered_live") => "message_delivered_live",
267 Some("message_queued") => "message_queued",
268 Some("activation_pending") => "activation_pending",
269 Some("activation_retry_required") => "activation_retry_required",
270 _ => "unknown",
271 }
272}
273
274fn cursor(offset: usize, digest: &str) -> String {
275 format!("tp1:{offset}:{digest}")
276}
277
278fn page(
279 caller: &Session,
280 tree: &SessionTreeNode,
281 raw_cursor: Option<&str>,
282 scope_digest: &str,
283) -> Result<Value, OwnedTreeError> {
284 let max_depth = DISPLAY_DEPTH;
285 let mut pending = vec![(tree, None)];
286 let mut nodes = Vec::new();
287 let mut depth_limited = false;
288 while let Some((node, parent)) = pending.pop() {
289 let title: String = node.title.chars().take(80).collect();
290 nodes.push(json!({"actor_id":node.session_id, "parent_actor_id":parent,
291 "title":title, "depth":node.depth,
292 "observed_status":observed_status(node.last_run_status.as_deref())}));
293 depth_limited |= node.depth >= max_depth;
294 let mut children = node.children.iter().collect::<Vec<_>>();
295 children.sort_unstable_by(|a, b| a.session_id.cmp(&b.session_id));
296 for child in children.into_iter().rev() {
297 pending.push((child, Some(node.session_id.as_str())));
298 }
299 }
300 let fingerprint = json!({"caller":caller.id, "birth":caller.created_at,
301 "project":caller.project_id_meta(), "metadata_version":caller.metadata_version,
302 "canonical_scope":scope_digest, "nodes":nodes});
303 let digest = hex::encode(Sha256::digest(
304 serde_json::to_vec(&fingerprint).map_err(|_| OwnedTreeError::InvalidLineage)?,
305 ));
306 let start = match raw_cursor {
307 None => 0,
308 Some(raw) => {
309 if raw.len() > MAX_TREE_CURSOR_BYTES {
310 return Err(OwnedTreeError::InvalidCursor);
311 }
312 let mut fields = raw.split(':');
313 let (Some("tp1"), Some(offset), Some(proof), None) =
314 (fields.next(), fields.next(), fields.next(), fields.next())
315 else {
316 return Err(OwnedTreeError::InvalidCursor);
317 };
318 if offset.starts_with('0') || proof != digest {
319 return Err(OwnedTreeError::InvalidCursor);
320 }
321 let offset = offset
322 .parse::<usize>()
323 .map_err(|_| OwnedTreeError::InvalidCursor)?;
324 if offset >= nodes.len() {
325 return Err(OwnedTreeError::InvalidCursor);
326 }
327 offset
328 }
329 };
330 let mut slice = Vec::new();
331 for node in nodes.iter().skip(start).take(PAGE_NODES) {
332 slice.push(node.clone());
333 let end = start + slice.len();
334 let next = (end < nodes.len()).then(|| cursor(end, &digest));
335 let candidate = json!({"actor_id":caller.id, "nodes":slice,
336 "truncated":depth_limited || next.is_some(), "next_cursor":next,
337 "observation":"Durable index tree; run statuses are observations, not activation leases."});
338 if serde_json::to_vec(&ToolResult::text(true, candidate.to_string()))
339 .map_or(true, |bytes| bytes.len() > MAX_CHILD_RESULT_BYTES)
340 {
341 slice.pop();
342 if slice.is_empty() {
343 return Err(OwnedTreeError::ResultTooLarge);
344 }
345 break;
346 }
347 }
348 let end = start + slice.len();
349 let next = (end < nodes.len()).then(|| cursor(end, &digest));
350 Ok(json!({"actor_id":caller.id, "nodes":slice,
351 "truncated":depth_limited || next.is_some(), "next_cursor":next,
352 "observation":"Durable index tree; run statuses are observations, not activation leases."}))
353}
354
355pub async fn inspect_owned_tree(
356 port: &dyn OwnedTreePort,
357 caller_id: &str,
358 raw_cursor: Option<&str>,
359) -> Result<Value, OwnedTreeError> {
360 let chain = lineage(port, caller_id).await?;
361 let caller = chain.last().ok_or(OwnedTreeError::InvalidLineage)?;
362 let (tree, scope_digest) = verified_tree(port, &chain).await?;
363 let page = page(caller, &tree, raw_cursor, &scope_digest)?;
364 let current = lineage(port, caller_id).await?;
365 if lineage_proof(¤t)? != lineage_proof(&chain)? {
366 return Err(OwnedTreeError::InvalidLineage);
367 }
368 Ok(page)
369}
370
371pub async fn load_owned_descendant(
375 port: &dyn OwnedTreePort,
376 caller_id: &str,
377 target_id: &str,
378) -> Result<Session, OwnedTreeError> {
379 let caller_chain = lineage(port, caller_id).await?;
380 let target_chain = lineage(port, target_id).await?;
381 if target_chain.len() <= caller_chain.len() {
382 return Err(OwnedTreeError::InvalidLineage);
383 }
384 let same_authority = |left: &Session, right: &Session| -> Result<bool, OwnedTreeError> {
385 let left = actor(left)?;
386 let right = actor(right)?;
387 Ok(left.actor_id == right.actor_id
388 && left.session_created_at == right.session_created_at
389 && left.parent_actor_id == right.parent_actor_id
390 && left.root_actor_id == right.root_actor_id
391 && left.project_id == right.project_id
392 && left.spawn_depth == right.spawn_depth)
393 };
394 for (caller, target_ancestor) in caller_chain.iter().zip(&target_chain) {
395 if !same_authority(caller, target_ancestor)? {
396 return Err(OwnedTreeError::InvalidLineage);
397 }
398 }
399 let current_chain = lineage(port, target_id).await?;
402 if current_chain.len() != target_chain.len() {
403 return Err(OwnedTreeError::InvalidLineage);
404 }
405 for (before, after) in target_chain.iter().zip(¤t_chain) {
406 if !same_authority(before, after)? {
407 return Err(OwnedTreeError::InvalidLineage);
408 }
409 }
410 target_chain
411 .into_iter()
412 .last()
413 .ok_or(OwnedTreeError::InvalidLineage)
414}
415
416#[cfg(test)]
417mod descendant_tests {
418 use super::*;
419 use chrono::{Duration, Utc};
420
421 struct Sessions(HashMap<String, Session>);
422
423 #[async_trait]
424 impl OwnedTreePort for Sessions {
425 async fn load(&self, id: &str) -> Result<Session, OwnedTreeError> {
426 self.0
427 .get(id)
428 .cloned()
429 .ok_or(OwnedTreeError::InvalidLineage)
430 }
431
432 async fn child_ids(&self, parent_id: &str) -> Result<Vec<String>, OwnedTreeError> {
433 Ok(self
434 .0
435 .values()
436 .filter(|session| session.parent_session_id.as_deref() == Some(parent_id))
437 .map(|session| session.id.clone())
438 .collect())
439 }
440 }
441
442 fn sessions() -> Sessions {
443 let now = Utc::now();
444 let mut root = Session::new("root", "model");
445 root.created_at = now;
446 let mut child = Session::new_child_of("child", &root, "model", "child");
447 child.created_at = now + Duration::seconds(1);
448 let mut grandchild = Session::new_child_of("grandchild", &child, "model", "grandchild");
449 grandchild.created_at = now + Duration::seconds(2);
450 let mut sibling = Session::new_child_of("sibling", &root, "model", "sibling");
451 sibling.created_at = now + Duration::seconds(1);
452 let mut outsider = Session::new("outsider", "model");
453 outsider.created_at = now;
454 Sessions(
455 [root, child, grandchild, sibling, outsider]
456 .into_iter()
457 .map(|session| (session.id.clone(), session))
458 .collect(),
459 )
460 }
461
462 #[tokio::test]
463 async fn root_and_ancestor_can_inspect_descendant_but_sibling_and_other_root_cannot() {
464 let store = sessions();
465 assert_eq!(
466 load_owned_descendant(&store, "root", "grandchild")
467 .await
468 .unwrap()
469 .id,
470 "grandchild"
471 );
472 assert_eq!(
473 load_owned_descendant(&store, "child", "grandchild")
474 .await
475 .unwrap()
476 .id,
477 "grandchild"
478 );
479 for caller in ["sibling", "outsider", "grandchild"] {
480 assert!(matches!(
481 load_owned_descendant(&store, caller, "grandchild").await,
482 Err(OwnedTreeError::InvalidLineage)
483 ));
484 }
485 }
486
487 #[tokio::test]
488 async fn inconsistent_ancestor_birth_fails_closed() {
489 let mut store = sessions();
490 let invalid_birth = store.0.get("root").unwrap().created_at - Duration::seconds(1);
491 store.0.get_mut("grandchild").unwrap().created_at = invalid_birth;
492 assert!(matches!(
493 load_owned_descendant(&store, "root", "grandchild").await,
494 Err(OwnedTreeError::InvalidLineage)
495 ));
496 }
497}