Skip to main content

aura_core/
context.rs

1//! Execution context for effectful operations.
2//!
3//! This is the canonical `EffectContext` type used across Aura.
4//!
5//! Design intent:
6//! - `AuthorityContext` (in aura-agent) is *identity scope* (who am I, known contexts).
7//! - `EffectContext` is *operation scope* (which authority/context/session is this call in).
8//! - Protocol/session-specific context lives in session-type runtimes and should not be
9//!   merged into long-lived identity containers.
10
11use crate::effects::ExecutionMode;
12use crate::hash::hash;
13use crate::types::identifiers::{AuthorityId, ContextId, SessionId};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fmt;
17
18/// Operation-scoped session identity for `EffectContext`.
19///
20/// This is intentionally distinct from long-lived Aura `SessionId` values used for
21/// durable domain state and from runtime choreography session identities.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct OperationSessionId(SessionId);
25
26impl OperationSessionId {
27    /// Wrap one raw Aura session identifier as an operation-scoped session identity.
28    #[must_use]
29    pub fn new(session_id: SessionId) -> Self {
30        Self(session_id)
31    }
32
33    /// Borrow the underlying Aura session identifier.
34    #[must_use]
35    pub fn raw(self) -> SessionId {
36        self.0
37    }
38}
39
40impl fmt::Display for OperationSessionId {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        self.0.fmt(f)
43    }
44}
45
46impl From<SessionId> for OperationSessionId {
47    fn from(value: SessionId) -> Self {
48        Self::new(value)
49    }
50}
51
52impl From<OperationSessionId> for SessionId {
53    fn from(value: OperationSessionId) -> Self {
54        value.raw()
55    }
56}
57
58/// Operation-scoped context threaded through effectful calls.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct EffectContext {
61    authority_id: AuthorityId,
62    context_id: ContextId,
63    session_id: OperationSessionId,
64    execution_mode: ExecutionMode,
65    metadata: HashMap<String, String>,
66}
67
68/// Lightweight snapshot of operation context for handlers that don't need metadata.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ContextSnapshot {
71    authority_id: AuthorityId,
72    context_id: ContextId,
73    session_id: OperationSessionId,
74    execution_mode: ExecutionMode,
75}
76
77fn derive_session_id(
78    authority_id: AuthorityId,
79    context_id: ContextId,
80    execution_mode: ExecutionMode,
81) -> OperationSessionId {
82    let mut material = Vec::with_capacity(1 + 32 + 32 + 9);
83    material.extend_from_slice(b"aura-session");
84    material.extend_from_slice(&authority_id.to_bytes());
85    material.extend_from_slice(&context_id.to_bytes());
86    match execution_mode {
87        ExecutionMode::Testing => material.push(0),
88        ExecutionMode::Production => material.push(1),
89        ExecutionMode::Simulation { seed } => {
90            material.push(2);
91            material.extend_from_slice(&seed.to_le_bytes());
92        }
93    }
94    OperationSessionId::new(SessionId::new_from_entropy(hash(&material)))
95}
96
97impl EffectContext {
98    /// Create a new context for an operation scoped to a specific authority and context.
99    ///
100    /// A fresh `SessionId` is allocated to represent the operation/session boundary.
101    pub fn new(
102        authority_id: AuthorityId,
103        context_id: ContextId,
104        execution_mode: ExecutionMode,
105    ) -> Self {
106        Self {
107            authority_id,
108            context_id,
109            session_id: derive_session_id(authority_id, context_id, execution_mode),
110            execution_mode,
111            metadata: HashMap::new(),
112        }
113    }
114
115    /// Convenience for creating a context when only an authority is known.
116    ///
117    /// This derives a deterministic `ContextId` from the authority for callers that need a
118    /// stable default context. Prefer `new(...)` with an explicit `ContextId` when possible.
119    #[must_use]
120    pub fn with_authority(authority_id: AuthorityId) -> Self {
121        let context_id = ContextId::new_from_entropy(hash(&authority_id.to_bytes()));
122        Self::new(authority_id, context_id, ExecutionMode::Production)
123    }
124
125    /// Authority performing the operation.
126    pub fn authority_id(&self) -> AuthorityId {
127        self.authority_id
128    }
129
130    /// Relational context in which the operation executes.
131    pub fn context_id(&self) -> ContextId {
132        self.context_id
133    }
134
135    /// Operation/session identifier.
136    pub fn session_id(&self) -> OperationSessionId {
137        self.session_id
138    }
139
140    /// Execution mode controlling handler selection.
141    pub fn execution_mode(&self) -> ExecutionMode {
142        self.execution_mode
143    }
144
145    /// Set metadata for diagnostics/telemetry.
146    pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
147        self.metadata.insert(key.into(), value.into());
148    }
149
150    /// Get metadata by key.
151    pub fn get_metadata(&self, key: &str) -> Option<&String> {
152        self.metadata.get(key)
153    }
154
155    /// Get all metadata.
156    pub fn metadata(&self) -> &HashMap<String, String> {
157        &self.metadata
158    }
159
160    /// Create a lightweight snapshot without metadata.
161    pub fn snapshot(&self) -> ContextSnapshot {
162        ContextSnapshot {
163            authority_id: self.authority_id,
164            context_id: self.context_id,
165            session_id: self.session_id,
166            execution_mode: self.execution_mode,
167        }
168    }
169
170    /// Create a child context in the same authority, with a new `ContextId`.
171    ///
172    /// A new `SessionId` is allocated to avoid accidentally smuggling operation boundaries.
173    pub fn create_child(&self, context_id: ContextId) -> Self {
174        Self {
175            authority_id: self.authority_id,
176            context_id,
177            session_id: derive_session_id(self.authority_id, context_id, self.execution_mode),
178            execution_mode: self.execution_mode,
179            metadata: self.metadata.clone(),
180        }
181    }
182}
183
184impl ContextSnapshot {
185    /// Create a new snapshot with a fresh session id.
186    pub fn new(
187        authority_id: AuthorityId,
188        context_id: ContextId,
189        execution_mode: ExecutionMode,
190    ) -> Self {
191        Self {
192            authority_id,
193            context_id,
194            session_id: derive_session_id(authority_id, context_id, execution_mode),
195            execution_mode,
196        }
197    }
198    /// Authority performing the operation.
199    pub fn authority_id(&self) -> AuthorityId {
200        self.authority_id
201    }
202
203    /// Relational context in which the operation executes.
204    pub fn context_id(&self) -> ContextId {
205        self.context_id
206    }
207
208    /// Operation/session identifier.
209    pub fn session_id(&self) -> OperationSessionId {
210        self.session_id
211    }
212
213    /// Execution mode controlling handler selection.
214    pub fn execution_mode(&self) -> ExecutionMode {
215        self.execution_mode
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn operation_session_id_round_trips_raw_session_id() {
225        let raw = SessionId::new_from_entropy([7; 32]);
226        let operation = OperationSessionId::new(raw);
227        assert_eq!(operation.raw(), raw);
228    }
229
230    #[test]
231    fn effect_context_uses_operation_session_id_boundary() {
232        let authority_id = AuthorityId::new_from_entropy([1; 32]);
233        let context_id = ContextId::new_from_entropy([2; 32]);
234        let context = EffectContext::new(authority_id, context_id, ExecutionMode::Testing);
235
236        let operation_session = context.session_id();
237        let raw_session: SessionId = operation_session.into();
238
239        assert_eq!(operation_session.raw(), raw_session);
240    }
241}