255 lines
9.4 KiB
Rust
255 lines
9.4 KiB
Rust
//! Agent identity helpers — short label + hive-qualified long name +
|
|
//! human display names for the hive and swarm. Full env var surface +
|
|
//! domain-vs-name distinction documented in
|
|
//! `docs/conventions.md::Hive identity (label + domain + display names)`.
|
|
|
|
use std::env;
|
|
|
|
/// Short, hive-local agent label. Read from `HIVE_LABEL`; falls back to an
|
|
/// empty string when the env var is missing, so callers downstream can decide
|
|
/// how to surface "unknown agent" rather than getting a panic from this
|
|
/// module.
|
|
#[must_use]
|
|
pub fn label() -> String {
|
|
env::var("HIVE_LABEL").unwrap_or_default()
|
|
}
|
|
|
|
/// `env::var(key)` reduced to `Some(value)` only when the var is set and
|
|
/// non-empty — the shared shape of the hive/swarm display-name lookups below.
|
|
fn non_empty_env(key: &str) -> Option<String> {
|
|
env::var(key).ok().filter(|s| !s.is_empty())
|
|
}
|
|
|
|
/// The hive's canonical DNS domain when set, otherwise None. Single-hive
|
|
/// deployments where `HYPERHIVE_HIVE_DOMAIN` is unset return None — callers
|
|
/// then degrade gracefully to the short label.
|
|
#[must_use]
|
|
pub fn hive_domain() -> Option<String> {
|
|
non_empty_env("HYPERHIVE_HIVE_DOMAIN")
|
|
}
|
|
|
|
/// Human display name of this hive (e.g. `pr1ma`). Distinct from
|
|
/// [`hive_domain`] — the domain is the machine-readable DNS address;
|
|
/// this is the prose label humans use in conversation. Returns None
|
|
/// when the host-side `services.hyperhive.hiveName` option is unset,
|
|
/// in which case callers fall back to the domain or the short label
|
|
/// at their discretion.
|
|
#[must_use]
|
|
pub fn hive_name() -> Option<String> {
|
|
non_empty_env("HYPERHIVE_HIVE_NAME")
|
|
}
|
|
|
|
/// Human display name of the wider swarm this hive belongs to (e.g.
|
|
/// `constellat1on`). Federated hives at different DNS domains can
|
|
/// share a swarm name. Returns None when the host-side
|
|
/// `services.hyperhive.swarmName` option is unset.
|
|
#[must_use]
|
|
pub fn swarm_name() -> Option<String> {
|
|
non_empty_env("HYPERHIVE_SWARM_NAME")
|
|
}
|
|
|
|
/// Hive-qualified agent identity. When the hive domain is configured, returns
|
|
/// `${label}@${domain}` (e.g. `iris@darkest.space`); when not, returns just
|
|
/// the short label so callers can render a single string regardless of
|
|
/// deployment shape. Callers that want to know whether the result is
|
|
/// qualified should check [`hive_domain`] directly.
|
|
#[must_use]
|
|
pub fn qualified_label() -> String {
|
|
qualify(&label())
|
|
}
|
|
|
|
/// Apply hive qualification to an arbitrary agent label (e.g. a peer name
|
|
/// from the broker). Mirrors [`qualified_label`] but lets a caller qualify
|
|
/// names it didn't read from the env. Returns `${label}@${domain}` when the
|
|
/// hive domain is set, else just `label`.
|
|
#[must_use]
|
|
pub fn qualify(label: &str) -> String {
|
|
match hive_domain() {
|
|
Some(domain) if !label.is_empty() => format!("{label}@{domain}"),
|
|
_ => label.to_owned(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Mutex;
|
|
|
|
/// cargo's test runner parallelises by default, so a `with_env`
|
|
/// helper that mutates process-wide env vars races between tests in
|
|
/// this module. Serialise on a module-scope mutex so each `with_env`
|
|
/// call holds the lock for its set / run / restore window. Cheap
|
|
/// (each test body is microseconds) and avoids pulling in
|
|
/// `serial_test` for just one module.
|
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
/// Helper: run `f` with a clean env, restoring previous values on exit.
|
|
/// Acquires `ENV_LOCK` first so concurrent tests don't race the env-var
|
|
/// state. If a previous test panicked while holding the lock the
|
|
/// mutex would be poisoned — we use `lock().unwrap_or_else(|e| e.into_inner())`
|
|
/// to recover so a single test failure doesn't cascade through the
|
|
/// whole module.
|
|
fn with_env<F: FnOnce()>(label: Option<&str>, domain: Option<&str>, f: F) {
|
|
with_full_env(label, domain, None, None, f);
|
|
}
|
|
|
|
/// Extended form of [`with_env`] covering the display-name env vars
|
|
/// (hive name + swarm name) alongside label + domain. Same SAFETY
|
|
/// contract — serialised on `ENV_LOCK`, restore in scope.
|
|
fn with_full_env<F: FnOnce()>(
|
|
label: Option<&str>,
|
|
domain: Option<&str>,
|
|
hive_name: Option<&str>,
|
|
swarm_name: Option<&str>,
|
|
f: F,
|
|
) {
|
|
let _guard = ENV_LOCK
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let prev_label = env::var("HIVE_LABEL").ok();
|
|
let prev_domain = env::var("HYPERHIVE_HIVE_DOMAIN").ok();
|
|
let prev_hive_name = env::var("HYPERHIVE_HIVE_NAME").ok();
|
|
let prev_swarm_name = env::var("HYPERHIVE_SWARM_NAME").ok();
|
|
// SAFETY: serialised by ENV_LOCK above; restore in the same scope.
|
|
unsafe {
|
|
match label {
|
|
Some(v) => env::set_var("HIVE_LABEL", v),
|
|
None => env::remove_var("HIVE_LABEL"),
|
|
}
|
|
match domain {
|
|
Some(v) => env::set_var("HYPERHIVE_HIVE_DOMAIN", v),
|
|
None => env::remove_var("HYPERHIVE_HIVE_DOMAIN"),
|
|
}
|
|
match hive_name {
|
|
Some(v) => env::set_var("HYPERHIVE_HIVE_NAME", v),
|
|
None => env::remove_var("HYPERHIVE_HIVE_NAME"),
|
|
}
|
|
match swarm_name {
|
|
Some(v) => env::set_var("HYPERHIVE_SWARM_NAME", v),
|
|
None => env::remove_var("HYPERHIVE_SWARM_NAME"),
|
|
}
|
|
}
|
|
f();
|
|
unsafe {
|
|
match prev_label {
|
|
Some(v) => env::set_var("HIVE_LABEL", v),
|
|
None => env::remove_var("HIVE_LABEL"),
|
|
}
|
|
match prev_domain {
|
|
Some(v) => env::set_var("HYPERHIVE_HIVE_DOMAIN", v),
|
|
None => env::remove_var("HYPERHIVE_HIVE_DOMAIN"),
|
|
}
|
|
match prev_hive_name {
|
|
Some(v) => env::set_var("HYPERHIVE_HIVE_NAME", v),
|
|
None => env::remove_var("HYPERHIVE_HIVE_NAME"),
|
|
}
|
|
match prev_swarm_name {
|
|
Some(v) => env::set_var("HYPERHIVE_SWARM_NAME", v),
|
|
None => env::remove_var("HYPERHIVE_SWARM_NAME"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn qualified_label_with_domain_set() {
|
|
with_env(Some("iris"), Some("darkest.space"), || {
|
|
assert_eq!(qualified_label(), "iris@darkest.space");
|
|
assert_eq!(hive_domain().as_deref(), Some("darkest.space"));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn qualified_label_falls_back_to_short_when_domain_unset() {
|
|
with_env(Some("iris"), None, || {
|
|
assert_eq!(qualified_label(), "iris");
|
|
assert!(hive_domain().is_none());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn qualified_label_falls_back_to_short_when_domain_empty() {
|
|
// Empty string is treated the same as unset — a misconfigured
|
|
// module shouldn't surface `iris@` (no domain) to the operator.
|
|
with_env(Some("iris"), Some(""), || {
|
|
assert_eq!(qualified_label(), "iris");
|
|
assert!(hive_domain().is_none());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn qualify_takes_arbitrary_label() {
|
|
with_env(Some("iris"), Some("darkest.space"), || {
|
|
// Local label gets local hive applied; useful for rendering
|
|
// a peer's name when the caller knows it's hive-local.
|
|
assert_eq!(qualify("damocles"), "damocles@darkest.space");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn qualify_empty_label_stays_empty() {
|
|
with_env(Some("iris"), Some("darkest.space"), || {
|
|
assert_eq!(qualify(""), "");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn label_returns_empty_when_unset() {
|
|
with_env(None, None, || {
|
|
assert_eq!(label(), "");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn hive_name_returns_some_when_env_set() {
|
|
with_full_env(Some("iris"), None, Some("pr1ma"), None, || {
|
|
assert_eq!(hive_name().as_deref(), Some("pr1ma"));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn hive_name_returns_none_when_env_unset_or_empty() {
|
|
with_full_env(Some("iris"), None, None, None, || {
|
|
assert!(hive_name().is_none());
|
|
});
|
|
with_full_env(Some("iris"), None, Some(""), None, || {
|
|
assert!(hive_name().is_none(), "empty string treated as unset");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swarm_name_returns_some_when_env_set() {
|
|
with_full_env(Some("iris"), None, None, Some("constellat1on"), || {
|
|
assert_eq!(swarm_name().as_deref(), Some("constellat1on"));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn swarm_name_returns_none_when_env_unset_or_empty() {
|
|
with_full_env(Some("iris"), None, None, None, || {
|
|
assert!(swarm_name().is_none());
|
|
});
|
|
with_full_env(Some("iris"), None, None, Some(""), || {
|
|
assert!(swarm_name().is_none(), "empty string treated as unset");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn name_accessors_independent_from_domain() {
|
|
// hive_name + swarm_name surface without HYPERHIVE_HIVE_DOMAIN
|
|
// being set — the names are display labels, not derived from
|
|
// the DNS domain.
|
|
with_full_env(
|
|
Some("iris"),
|
|
None,
|
|
Some("pr1ma"),
|
|
Some("constellat1on"),
|
|
|| {
|
|
assert!(hive_domain().is_none());
|
|
assert_eq!(hive_name().as_deref(), Some("pr1ma"));
|
|
assert_eq!(swarm_name().as_deref(), Some("constellat1on"));
|
|
// qualified_label still degrades to short label without domain.
|
|
assert_eq!(qualified_label(), "iris");
|
|
},
|
|
);
|
|
}
|
|
}
|