Skip to main content

meerkat_core/
runtime_bootstrap.rs

1//! Surface/runtime bootstrap contracts shared across interfaces.
2//!
3//! This module defines how runtimes resolve realm identity and filesystem roots
4//! without relying on ambient process state by default.
5
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use uuid::Uuid;
9
10use crate::connection::{IdentityError, RealmId};
11
12/// Canonical state sharing locator.
13///
14/// Wave-c C-12 / C-1 follow-up: `realm_id: String` retyped to
15/// `realm: RealmId` to match the typed-atom rename C-1 did on
16/// `AuthBindingRef`. Consumers must use `self.realm.as_str()` where a
17/// `&str` is required (path construction, logging, wire projection).
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct RealmLocator {
20    pub state_root: PathBuf,
21    pub realm: RealmId,
22}
23
24/// Realm selection mode.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "snake_case", tag = "kind")]
27pub enum RealmSelection {
28    Explicit { realm_id: String },
29    Isolated,
30    WorkspaceDerived { root: PathBuf },
31}
32
33/// Realm/runtime settings.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(default)]
36pub struct RealmConfig {
37    pub selection: RealmSelection,
38    pub instance_id: Option<String>,
39    /// String hint (e.g. "sqlite", "jsonl"), interpreted by surface/store layers.
40    pub backend_hint: Option<String>,
41    /// Root directory containing all realm directories.
42    pub state_root: Option<PathBuf>,
43}
44
45impl Default for RealmConfig {
46    fn default() -> Self {
47        Self {
48            selection: RealmSelection::Isolated,
49            instance_id: None,
50            backend_hint: None,
51            state_root: None,
52        }
53    }
54}
55
56/// Filesystem convention settings.
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
58#[serde(default)]
59pub struct ContextConfig {
60    pub context_root: Option<PathBuf>,
61    pub user_config_root: Option<PathBuf>,
62}
63
64/// Top-level runtime bootstrap payload.
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
66#[serde(default)]
67pub struct RuntimeBootstrap {
68    pub realm: RealmConfig,
69    pub context: ContextConfig,
70}
71
72/// Errors resolving runtime bootstrap.
73#[derive(Debug, thiserror::Error)]
74pub enum RuntimeBootstrapError {
75    #[error("`--realm` and `--isolated` cannot be used together")]
76    ConflictingSelection,
77    #[error("invalid explicit realm id: {0}")]
78    InvalidRealmId(String),
79}
80
81/// Default global state root shared across surfaces.
82pub fn default_state_root() -> PathBuf {
83    dirs::data_dir()
84        .unwrap_or_else(|| PathBuf::from("."))
85        .join("meerkat")
86        .join("realms")
87}
88
89impl RealmConfig {
90    /// Build selection from common CLI inputs, with a provided default mode.
91    pub fn selection_from_inputs(
92        realm: Option<String>,
93        isolated: bool,
94        default: RealmSelection,
95    ) -> Result<RealmSelection, RuntimeBootstrapError> {
96        if realm.is_some() && isolated {
97            return Err(RuntimeBootstrapError::ConflictingSelection);
98        }
99        if let Some(realm_id) = realm {
100            validate_explicit_realm_id(&realm_id)?;
101            return Ok(RealmSelection::Explicit { realm_id });
102        }
103        if isolated {
104            return Ok(RealmSelection::Isolated);
105        }
106        Ok(default)
107    }
108
109    /// Resolve a concrete `(state_root, realm)` locator.
110    pub fn resolve_locator(&self) -> Result<RealmLocator, RuntimeBootstrapError> {
111        let state_root = self.state_root.clone().unwrap_or_else(default_state_root);
112        let realm_raw = match &self.selection {
113            RealmSelection::Explicit { realm_id } => realm_id.clone(),
114            RealmSelection::Isolated => generate_realm_id(),
115            RealmSelection::WorkspaceDerived { root } => derive_workspace_realm_id(root),
116        };
117        let realm = RealmId::parse(&realm_raw).map_err(|source| match source {
118            IdentityError::Empty => RuntimeBootstrapError::InvalidRealmId(realm_raw.clone()),
119            IdentityError::InvalidChar(_) => {
120                RuntimeBootstrapError::InvalidRealmId(realm_raw.clone())
121            }
122        })?;
123        Ok(RealmLocator { state_root, realm })
124    }
125}
126
127pub fn validate_explicit_realm_id(realm_id: &str) -> Result<(), RuntimeBootstrapError> {
128    if realm_id.is_empty()
129        || realm_id.len() > 64
130        || realm_id.contains(':')
131        || realm_id.chars().any(char::is_whitespace)
132    {
133        return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
134    }
135    let mut chars = realm_id.chars();
136    let first = chars
137        .next()
138        .ok_or_else(|| RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()))?;
139    if !first.is_ascii_alphanumeric() {
140        return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
141    }
142    if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
143        return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
144    }
145    // Reserve UUID-looking IDs for session ids, preventing locator ambiguity.
146    if Uuid::parse_str(realm_id).is_ok() {
147        return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
148    }
149    Ok(())
150}
151
152pub fn generate_realm_id() -> String {
153    format!("realm-{}", crate::time_compat::new_uuid_v7())
154}
155
156pub fn derive_workspace_realm_id(path: &Path) -> String {
157    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
158    let key = canonical.to_string_lossy();
159    format!("ws-{}", fnv1a64_hex(&key))
160}
161
162pub fn fnv1a64_hex(input: &str) -> String {
163    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
164    const PRIME: u64 = 0x0100_0000_01b3;
165    let mut hash = OFFSET;
166    for b in input.as_bytes() {
167        hash ^= u64::from(*b);
168        hash = hash.wrapping_mul(PRIME);
169    }
170    format!("{hash:016x}")
171}
172
173#[cfg(test)]
174#[allow(clippy::expect_used)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn selection_conflict_is_rejected() {
180        let result = RealmConfig::selection_from_inputs(
181            Some("team".to_string()),
182            true,
183            RealmSelection::Isolated,
184        );
185        assert!(matches!(
186            result,
187            Err(RuntimeBootstrapError::ConflictingSelection)
188        ));
189    }
190
191    #[test]
192    fn explicit_realm_id_validation() {
193        assert!(validate_explicit_realm_id("team-alpha_1").is_ok());
194        assert!(validate_explicit_realm_id("bad:name").is_err());
195        assert!(validate_explicit_realm_id("").is_err());
196        assert!(validate_explicit_realm_id("550e8400-e29b-41d4-a716-446655440000").is_err());
197    }
198
199    #[test]
200    fn workspace_selection_is_deterministic() {
201        let root = PathBuf::from(".");
202        let cfg = RealmConfig {
203            selection: RealmSelection::WorkspaceDerived { root },
204            ..RealmConfig::default()
205        };
206        let a = cfg.resolve_locator().map(|locator| locator.realm);
207        let b = cfg.resolve_locator().map(|locator| locator.realm);
208        assert!(a.is_ok());
209        assert_eq!(a.ok(), b.ok());
210    }
211}