Skip to main content

bamboo_engine/external_agents/
actor_event_stream.rs

1//! The deliberately small public projection of a Directory-fenced Actor event.
2//! A browser only learns that its authorized Actor view may have changed. It
3//! never receives a worker frame, an AgentEvent body, or placement authority.
4
5use serde::Serialize;
6use sha2::{Digest, Sha256};
7use std::fmt::Write;
8
9use super::actor_event_router::{ActorEventClass, ActorEventEnvelope};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum PublicActorEventClass {
14    Lifecycle,
15    Semantic,
16    Snapshot,
17    Ephemeral,
18}
19
20/// Host-admitted order within an activation. This is only for the process-local
21/// change hub; the browser receives an opaque event ID instead of these lease
22/// and execution coordinates.
23#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
24pub struct ActorEventSourceOrder {
25    pub lease_epoch: u64,
26    pub execution_epoch: u64,
27    pub sequence: u64,
28}
29
30/// Safe metadata copied from the host-admitted envelope. The opaque event ID
31/// preserves duplicate identity without exposing internal lease/epoch fields.
32#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
33pub struct PublicActorEvent {
34    pub actor_id: String,
35    pub root_actor_id: String,
36    pub parent_actor_id: Option<String>,
37    pub activation_id: String,
38    pub attempt: u64,
39    pub event_id: String,
40    pub class: PublicActorEventClass,
41    #[serde(skip)]
42    pub source_order: ActorEventSourceOrder,
43}
44
45pub(super) fn opaque_event_id(identity: &str) -> String {
46    let digest = Sha256::digest(identity.as_bytes());
47    let mut event_id = String::with_capacity(68);
48    event_id.push_str("ae1-");
49    for byte in digest {
50        write!(&mut event_id, "{byte:02x}").expect("String formatting cannot fail");
51    }
52    event_id
53}
54
55impl From<&ActorEventEnvelope> for PublicActorEvent {
56    fn from(envelope: &ActorEventEnvelope) -> Self {
57        let event_id = opaque_event_id(&envelope.event_id);
58        let class = match envelope.class {
59            ActorEventClass::Lifecycle => PublicActorEventClass::Lifecycle,
60            ActorEventClass::Semantic => PublicActorEventClass::Semantic,
61            ActorEventClass::Snapshot => PublicActorEventClass::Snapshot,
62            ActorEventClass::Ephemeral => PublicActorEventClass::Ephemeral,
63        };
64        Self {
65            actor_id: envelope.actor_id.clone(),
66            root_actor_id: envelope.root_actor_id.clone(),
67            parent_actor_id: envelope.parent_actor_id.clone(),
68            activation_id: envelope.activation_id.clone(),
69            attempt: envelope.attempt,
70            event_id,
71            class,
72            source_order: ActorEventSourceOrder {
73                lease_epoch: envelope.lease_epoch,
74                execution_epoch: envelope.execution_epoch,
75                sequence: envelope.sequence,
76            },
77        }
78    }
79}
80
81/// Synchronous and nonblocking: the actor frame pump must never wait for a
82/// browser. Implementations may drop an event; consumers recover via snapshot.
83pub trait ActorEventObserver: Send + Sync {
84    fn publish(&self, event: PublicActorEvent);
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use bamboo_agent_core::AgentEvent;
91
92    #[test]
93    fn public_projection_hashes_internal_identity_and_excludes_content() {
94        let envelope = ActorEventEnvelope {
95            actor_id: "child".into(),
96            parent_actor_id: Some("root".into()),
97            root_actor_id: "root".into(),
98            activation_id: "safe-activation".into(),
99            attempt: 1,
100            lease_epoch: 7,
101            execution_epoch: 9,
102            sequence: 3,
103            event_id: "safe-activation:7:9:3".into(),
104            class: ActorEventClass::Semantic,
105            payload: AgentEvent::Token {
106                content: "private-token".into(),
107            },
108        };
109        let public = PublicActorEvent::from(&envelope);
110        assert_eq!(public.event_id.len(), 68);
111        assert!(public.event_id.starts_with("ae1-"));
112        assert_eq!(public.source_order.sequence, 3);
113        assert!(public.event_id[4..]
114            .bytes()
115            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)));
116        let json = serde_json::to_string(&public).unwrap();
117        for private in [
118            "private-token",
119            "project_id",
120            "lease_epoch",
121            "execution_epoch",
122            "source_order",
123        ] {
124            assert!(!json.contains(private));
125        }
126    }
127}