Skip to main content

khive_runtime/
actor_identity.rs

1//! Shared actor-identity resolution (issue #567).
2//!
3//! Single source of truth for "who is the caller", consumed by the gate
4//! check, storage-token minting, comm attribution, and MCP strict-mode
5//! enforcement: those four sites must agree, so drift here is a silent
6//! trust-boundary bug.
7
8use khive_gate::ActorRef;
9
10/// Resolve an optional configured/request actor id string into the
11/// [`ActorRef`] used for gate checks, `NamespaceToken` minting, and comm
12/// attribution.
13///
14/// Preserves the pre-existing behavior at every call site: a non-empty
15/// string (after trimming) becomes `ActorRef::new("actor", id)` using the
16/// original (untrimmed) string; `None` or an all-whitespace string becomes
17/// `ActorRef::anonymous()`.
18pub fn resolve_actor(actor_id: Option<&str>) -> ActorRef {
19    match actor_id {
20        Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
21        _ => ActorRef::anonymous(),
22    }
23}
24
25/// True when `actor` is the backward-compatible anonymous/local fallback and
26/// therefore unsafe for multi-actor comm attribution or strict-mode serving.
27pub fn actor_is_unattributed(actor: &ActorRef) -> bool {
28    actor.id == "local"
29}
30
31/// Startup warning / strict-mode predicate for serving paths that load the
32/// `comm` pack: true when the resolved actor is unattributed AND `"comm"` is
33/// among the loaded packs.
34pub fn should_warn_unattributed_actor(actor_id: Option<&str>, loaded_packs: &[String]) -> bool {
35    let actor = resolve_actor(actor_id);
36    actor_is_unattributed(&actor) && loaded_packs.iter().any(|p| p == "comm")
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn resolve_actor_none_is_anonymous() {
45        let actor = resolve_actor(None);
46        assert_eq!(actor.id, "local");
47        assert!(actor_is_unattributed(&actor));
48    }
49
50    #[test]
51    fn resolve_actor_blank_is_anonymous() {
52        let actor = resolve_actor(Some("   "));
53        assert_eq!(actor.id, "local");
54        assert!(actor_is_unattributed(&actor));
55    }
56
57    #[test]
58    fn resolve_actor_configured_id_is_attributed() {
59        let actor = resolve_actor(Some("lambda:khive"));
60        assert_eq!(actor.kind, "actor");
61        assert_eq!(actor.id, "lambda:khive");
62        assert!(!actor_is_unattributed(&actor));
63    }
64
65    #[test]
66    fn resolve_actor_preserves_untrimmed_id() {
67        // Trim only decides emptiness; the id itself is stored verbatim.
68        let actor = resolve_actor(Some(" lambda:khive "));
69        assert_eq!(actor.id, " lambda:khive ");
70    }
71
72    #[test]
73    fn should_warn_unattributed_actor_fires_for_local_plus_comm() {
74        let packs = vec!["kg".to_string(), "comm".to_string()];
75        assert!(should_warn_unattributed_actor(None, &packs));
76        assert!(should_warn_unattributed_actor(Some("local"), &packs));
77        assert!(should_warn_unattributed_actor(Some("  "), &packs));
78    }
79
80    #[test]
81    fn should_warn_unattributed_actor_silent_without_comm_pack() {
82        let packs = vec!["kg".to_string(), "memory".to_string()];
83        assert!(!should_warn_unattributed_actor(None, &packs));
84    }
85
86    #[test]
87    fn should_warn_unattributed_actor_silent_when_attributed() {
88        let packs = vec!["kg".to_string(), "comm".to_string()];
89        assert!(!should_warn_unattributed_actor(
90            Some("lambda:khive"),
91            &packs
92        ));
93    }
94}