Skip to main content

harn_vm/observability/
execution_scope.rs

1//! Ambient EXECUTION SCOPE — an immutable owner token minted once per Harn
2//! program run and readable, like [`crate::observability::request_id`], from
3//! hostlib builtins on the synchronous dispatch stack.
4//!
5//! Unlike `request_id` (which a *host* pushes only at served ingress and which
6//! is `None` for standalone `harn run`/`harn test`), the execution scope is
7//! established by the VM itself at the top-level program boundary
8//! (`Vm::execute_scoped`), so it is ALWAYS present during real execution and is
9//! never a shared sentinel. That is exactly what lets the verdict issuance
10//! authority bind a proof-of-execution receipt to the specific run that
11//! PRODUCED the evidence: `run_test` captures the active scope when it records a
12//! real execution, and `harness.verdict.issue` mints a positive verdict only
13//! when the current active scope EQUALS that captured owner — so an old green
14//! handle cannot bless a later, different run (the cross-run replay class).
15//!
16//! The scope is a stack so nested program executions restore the outer owner on
17//! return; the innermost entry wins for [`current_execution_scope`]. When no
18//! scope is active (there is no owning execution), issuance FAILS CLOSED — it
19//! never falls back to a default owner.
20
21use std::cell::RefCell;
22use std::fmt;
23use std::str::FromStr;
24use std::sync::Arc;
25
26/// Stable prefix for VM-owned execution identities.
27pub const EXECUTION_ID_PREFIX: &str = "hxe-";
28
29/// Harn-owned identity for one top-level execution tree.
30///
31/// Construction is deliberately closed: values are either minted here or
32/// parsed through the canonical UUIDv7 validator. Wire formats may carry a
33/// string, but runtime authority never carries an unchecked one.
34#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct ExecutionId(Arc<str>);
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
38#[error("invalid Harn execution identity")]
39pub struct InvalidExecutionId;
40
41impl ExecutionId {
42    /// Mint a fresh UUIDv7 identity. UUIDv7 preserves useful creation-time
43    /// ordering while remaining unique across processes and hosts.
44    pub fn mint() -> Self {
45        Self(Arc::from(format!(
46            "{EXECUTION_ID_PREFIX}{}",
47            uuid::Uuid::now_v7()
48        )))
49    }
50
51    /// Parse an identity at a trust boundary, accepting only the canonical
52    /// lowercase, hyphenated UUIDv7 representation owned by Harn.
53    pub fn parse(candidate: &str) -> Result<Self, InvalidExecutionId> {
54        let raw = candidate
55            .strip_prefix(EXECUTION_ID_PREFIX)
56            .ok_or(InvalidExecutionId)?;
57        let value = uuid::Uuid::parse_str(raw).map_err(|_| InvalidExecutionId)?;
58        if raw != value.hyphenated().to_string()
59            || value.get_version_num() != 7
60            || value.get_variant() != uuid::Variant::RFC4122
61        {
62            return Err(InvalidExecutionId);
63        }
64        Ok(Self(Arc::from(candidate)))
65    }
66
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70}
71
72impl fmt::Debug for ExecutionId {
73    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74        formatter
75            .debug_tuple("ExecutionId")
76            .field(&self.as_str())
77            .finish()
78    }
79}
80
81impl fmt::Display for ExecutionId {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(self.as_str())
84    }
85}
86
87impl AsRef<str> for ExecutionId {
88    fn as_ref(&self) -> &str {
89        self.as_str()
90    }
91}
92
93impl std::ops::Deref for ExecutionId {
94    type Target = str;
95
96    fn deref(&self) -> &Self::Target {
97        self.as_str()
98    }
99}
100
101impl FromStr for ExecutionId {
102    type Err = InvalidExecutionId;
103
104    fn from_str(candidate: &str) -> Result<Self, Self::Err> {
105        Self::parse(candidate)
106    }
107}
108
109impl serde::Serialize for ExecutionId {
110    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
111    where
112        S: serde::Serializer,
113    {
114        serializer.serialize_str(self.as_str())
115    }
116}
117
118impl<'de> serde::Deserialize<'de> for ExecutionId {
119    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120    where
121        D: serde::Deserializer<'de>,
122    {
123        let candidate = <String as serde::Deserialize>::deserialize(deserializer)?;
124        Self::parse(&candidate).map_err(serde::de::Error::custom)
125    }
126}
127
128thread_local! {
129    static ACTIVE_EXECUTION_SCOPE_STACK: RefCell<Vec<ExecutionId>> = const { RefCell::new(Vec::new()) };
130}
131
132/// Mint a fresh, durable execution id. UUIDv7 keeps identifiers unique across
133/// processes and hosts while preserving useful creation-time ordering.
134pub fn mint_execution_scope() -> ExecutionId {
135    ExecutionId::mint()
136}
137
138/// RAII guard returned by [`enter_execution_scope`]. Popping the stack on drop
139/// keeps the ambient balanced even when the enclosed program run panics or
140/// returns an error.
141#[must_use = "dropping the guard immediately pops the execution scope"]
142pub struct ExecutionScopeGuard {
143    _private: (),
144}
145
146impl Drop for ExecutionScopeGuard {
147    fn drop(&mut self) {
148        ACTIVE_EXECUTION_SCOPE_STACK.with(|stack| {
149            stack.borrow_mut().pop();
150        });
151    }
152}
153
154/// Push `scope` onto the ambient stack for the lifetime of the returned guard.
155/// The innermost entry wins for [`current_execution_scope`].
156pub fn enter_execution_scope(scope: ExecutionId) -> ExecutionScopeGuard {
157    ACTIVE_EXECUTION_SCOPE_STACK.with(|stack| stack.borrow_mut().push(scope));
158    ExecutionScopeGuard { _private: () }
159}
160
161/// Currently-active execution scope, or `None` when no owning program run is
162/// active on this task. Verdict issuance treats `None` as fail-closed.
163pub fn current_execution_scope() -> Option<ExecutionId> {
164    ACTIVE_EXECUTION_SCOPE_STACK.with(|stack| stack.borrow().last().cloned())
165}
166
167/// Replace the whole ambient stack, returning the previous one. Used by the
168/// orchestration ambient-scope machinery to carry the owner across
169/// `spawn_local` fan-out boundaries (which plain thread-locals do not cross),
170/// mirroring how the current-session stack is propagated.
171pub(crate) fn swap_execution_scope_stack(replacement: Vec<ExecutionId>) -> Vec<ExecutionId> {
172    ACTIVE_EXECUTION_SCOPE_STACK
173        .with(|stack| std::mem::replace(&mut *stack.borrow_mut(), replacement))
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn current_returns_none_when_nothing_pushed() {
182        // A fresh thread has no owning execution.
183        std::thread::spawn(|| {
184            assert_eq!(current_execution_scope(), None);
185        })
186        .join()
187        .unwrap();
188    }
189
190    #[test]
191    fn guard_pops_on_drop_and_inner_shadows_outer() {
192        std::thread::spawn(|| {
193            let outer = mint_execution_scope();
194            let inner = mint_execution_scope();
195            assert_ne!(outer, inner);
196            let _o = enter_execution_scope(outer.clone());
197            assert_eq!(current_execution_scope().as_deref(), Some(&*outer));
198            {
199                let _i = enter_execution_scope(inner.clone());
200                assert_eq!(current_execution_scope().as_deref(), Some(&*inner));
201            }
202            assert_eq!(current_execution_scope().as_deref(), Some(&*outer));
203        })
204        .join()
205        .unwrap();
206    }
207
208    #[test]
209    fn parse_and_serde_reject_noncanonical_or_non_v7_ids() {
210        let minted = ExecutionId::mint();
211        assert_eq!(ExecutionId::parse(minted.as_str()), Ok(minted));
212
213        let valid = "hxe-019c13e0-8080-7000-8000-000000000001";
214        assert_eq!(ExecutionId::parse(valid).unwrap().as_str(), valid);
215        assert_eq!(
216            serde_json::to_string(&ExecutionId::parse(valid).unwrap()).unwrap(),
217            format!("\"{valid}\"")
218        );
219        assert_eq!(
220            serde_json::from_str::<ExecutionId>(&format!("\"{valid}\""))
221                .unwrap()
222                .as_str(),
223            valid
224        );
225
226        for invalid in [
227            "cloud-run-id",
228            "hxe-019C13E0-8080-7000-8000-000000000001",
229            "hxe-019c13e0-8080-4000-8000-000000000001",
230        ] {
231            assert_eq!(ExecutionId::parse(invalid), Err(InvalidExecutionId));
232            assert!(serde_json::from_str::<ExecutionId>(&format!("\"{invalid}\"")).is_err());
233        }
234    }
235}