meerkat-mobkit 0.7.28

Companion orchestration platform for the Meerkat multi-agent runtime
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Single owner of the mapping between MobKit's public member aliases and
//! the mob-roster member ids handed to meerkat-mob.
//!
//! Meerkat 0.7 made the mob member comms name a fail-closed typed owner
//! (`meerkat_core::connection::MemberCommsName`): every component — including
//! the member id — must start with an ASCII letter or `_` and contain only
//! ASCII alphanumerics, `-`, or `_`. MobKit's identity-first surface mints
//! runtime-id-shaped member identities (`rt:{identity}:{generation}`, where
//! the durable identity itself conventionally contains `:`, e.g.
//! `review:singleton`), which 0.6.34 accepted (the comms name was an
//! unvalidated `format!`) and 0.7 rejects at spawn.
//!
//! The public alias space is unchanged: consoles, RPC surfaces, SDKs, and
//! persisted continuity records keep speaking `rt:review:singleton:0`. This
//! module encodes those aliases into comms-safe roster ids at the
//! mobkit→meerkat-mob boundary and decodes roster ids back to public aliases
//! at projection boundaries.
//!
//! Encoding contract:
//! - An alias that is already a valid comms-name component (and does not
//!   start with the reserved `mk--` marker) maps to itself, so plain
//!   definition-mob member names are untouched.
//! - Anything else maps to `mk--` + escaped body, where `_` → `__`,
//!   `:` → `_c`, and any other non-`[A-Za-z0-9-]` char → `_x{hex}_`.
//! - `decode(encode(s)) == s` for every alias, and `decode` is the identity
//!   on ids that were never encoded. The `mk--` prefix is a reserved
//!   namespace: user-chosen member names must not start with it (encode
//!   re-encodes such names so the round-trip still holds).
//!
//! This is a **public stability surface**: consumers that talk to raw
//! `MobHandle` APIs (which speak the encoded roster-id space on meerkat 0.7+)
//! must encode aliases on the way in and decode roster ids on the way out
//! using exactly this codec — there is no separate "correct" encoding. Use
//! [`mob_member_id`]/[`mob_member_id_str`] at the mobkit→meerkat-mob boundary
//! and [`runtime_alias_str`]/[`runtime_event_alias`] at projection boundaries.

use std::borrow::Cow;

/// Reserved marker prefix for encoded member ids.
const MARKER: &str = "mk--";

/// True when `s` is a valid meerkat 0.7 comms-name component
/// (mirrors `meerkat_core::connection::validate_member_comms_name_component`).
fn is_valid_comms_component(s: &str) -> bool {
    let mut chars = s.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    if !first.is_ascii_alphabetic() && first != '_' {
        return false;
    }
    chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}

fn escape_body(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 8);
    for c in s.chars() {
        match c {
            '_' => out.push_str("__"),
            ':' => out.push_str("_c"),
            c if c.is_ascii_alphanumeric() || c == '-' => out.push(c),
            c => {
                out.push_str("_x");
                out.push_str(&format!("{:x}", c as u32));
                out.push('_');
            }
        }
    }
    out
}

/// Decode an escaped body; `None` when the body is not a well-formed escape
/// production (such an id cannot have come from [`mob_member_id_str`]).
fn unescape_body(s: &str) -> Option<String> {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c != '_' {
            out.push(c);
            continue;
        }
        match chars.next()? {
            '_' => out.push('_'),
            'c' => out.push(':'),
            'x' => {
                let mut hex = String::new();
                loop {
                    match chars.next()? {
                        '_' => break,
                        h => hex.push(h),
                    }
                }
                let code = u32::from_str_radix(&hex, 16).ok()?;
                out.push(char::from_u32(code)?);
            }
            _ => return None,
        }
    }
    Some(out)
}

/// Map a public member alias (identity-first runtime id, durable identity, or
/// plain member name) to the comms-safe mob roster member id string.
pub fn mob_member_id_str(alias: &str) -> Cow<'_, str> {
    if is_valid_comms_component(alias) && !alias.starts_with(MARKER) {
        Cow::Borrowed(alias)
    } else {
        Cow::Owned(format!("{MARKER}{}", escape_body(alias)))
    }
}

/// Map a public member alias to a typed mob roster member id.
pub fn mob_member_id(alias: &str) -> meerkat_mob::ids::AgentIdentity {
    meerkat_mob::ids::AgentIdentity::from(mob_member_id_str(alias).as_ref())
}

/// Map a mob roster member id back to the public member alias. Identity on
/// ids that were never encoded.
pub fn runtime_alias_str(member_id: &str) -> Cow<'_, str> {
    match member_id.strip_prefix(MARKER) {
        Some(body) => match unescape_body(body) {
            Some(alias) => Cow::Owned(alias),
            // Not an encode production; treat as a literal member id.
            None => Cow::Borrowed(member_id),
        },
        None => Cow::Borrowed(member_id),
    }
}

/// Project a mob runtime id (`{roster_member_id}:{generation}`) into the
/// public alias space (`{alias}:{generation}`).
///
/// Agent events leave meerkat-mob keyed by [`AgentRuntimeId`]s built from
/// roster binding atoms; the member-id component is the comms-safe encoding,
/// so it must be decoded before any console/SDK projection — console replay
/// resolution, the `mobkit/events/subscribe` buffer, `/mob/events` SSE, and
/// per-agent ABAC view checks all speak the alias space.
pub fn runtime_event_alias(runtime_id: &meerkat_mob::ids::AgentRuntimeId) -> String {
    format!(
        "{}:{}",
        runtime_alias_str(runtime_id.identity.as_str()),
        runtime_id.generation.get()
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn plain_member_names_pass_through_unchanged() {
        for name in ["worker", "worker-one", "_internal", "Agent7"] {
            assert_eq!(mob_member_id_str(name), name);
            assert_eq!(runtime_alias_str(name), name);
        }
    }

    #[test]
    fn colon_aliases_round_trip_and_are_comms_safe() {
        for alias in [
            "rt:review:singleton:0",
            "rt:channel:C0SMOKEOB3:0",
            "agent:beta",
            "review:singleton",
            "rt:agent-x:0",
            "with_underscore:and:colons",
        ] {
            let encoded = mob_member_id_str(alias);
            assert!(
                is_valid_comms_component(&encoded),
                "{encoded:?} must satisfy meerkat 0.7 MemberCommsName"
            );
            assert_eq!(runtime_alias_str(&encoded), alias, "round trip of {alias}");
        }
    }

    #[test]
    fn non_ascii_and_punctuation_round_trip() {
        for alias in ["user@host", "a.b:c", "9starts-with-digit", "ünïcode:1"] {
            let encoded = mob_member_id_str(alias);
            assert!(is_valid_comms_component(&encoded), "{encoded:?}");
            assert_eq!(runtime_alias_str(&encoded), alias);
        }
    }

    #[test]
    fn reserved_marker_names_re_encode_so_round_trip_holds() {
        let alias = "mk--rt_creview";
        let encoded = mob_member_id_str(alias);
        assert_ne!(encoded, alias, "marker-prefixed names must be re-encoded");
        assert_eq!(runtime_alias_str(&encoded), alias);
    }

    #[test]
    fn runtime_event_alias_decodes_encoded_roster_member_ids() {
        use meerkat_mob::ids::{AgentIdentity, AgentRuntimeId, Generation};

        // Identity-first alias: the roster id is the comms-safe encoding and
        // must decode back to the public alias before any event projection.
        let encoded = mob_member_id_str("rt:review:singleton:0").into_owned();
        let runtime_id =
            AgentRuntimeId::new(AgentIdentity::from(encoded.as_str()), Generation::new(1));
        assert_eq!(runtime_event_alias(&runtime_id), "rt:review:singleton:0:1");

        // Plain member names pass through unchanged.
        let runtime_id = AgentRuntimeId::initial(AgentIdentity::from("worker-one"));
        assert_eq!(runtime_event_alias(&runtime_id), "worker-one:0");
    }

    #[test]
    fn encoding_is_injective_across_pass_through_and_encoded_forms() {
        let inputs = [
            "rt:review:singleton:0",
            "rt-review-singleton-0",
            "rt_creview_csingleton_c0",
            "mk--rt_creview_csingleton_c0",
        ];
        let mut seen = std::collections::BTreeSet::new();
        for input in inputs {
            assert!(
                seen.insert(mob_member_id_str(input).into_owned()),
                "collision on {input}"
            );
        }
    }

    // --- Defensive suite: map the whole id codec + reconciliation aliases ---

    /// The exact identities + runtime-id forms from the HomeCore deployment
    /// that surfaced the meerkat 0.7 comms-name regression, plus the escape
    /// productions. Every entry must encode to a comms-safe id and round-trip.
    fn defensive_corpus() -> Vec<String> {
        let mut v: Vec<String> = [
            // Plain definition-mob names (pass through unchanged).
            "worker",
            "worker-one",
            "_internal",
            "Agent7",
            "a",
            // HomeCore durable identities (colon-bearing).
            "identity:parent-1",
            "identity:parent-2",
            "identity:child-1",
            "identity:child-2",
            "domain:calendar",
            "domain:school",
            "domain:health",
            "domain:home",
            "domain:home-automation",
            "domain:finance",
            "domain:discovery",
            "family-group:main",
            "triage:main",
            "gate:main",
            // Runtime-id shaped aliases (`rt:{identity}:{generation}`).
            "rt:identity:parent-1:0",
            "rt:domain:home-automation:3",
            "rt:channel:C0SMOKEOB3:0",
            "rt:review:singleton:12",
            // Mixed punctuation / boundary shapes.
            "a:b_c",
            "with_underscore:and:colons",
            "user@host",
            "a.b:c",
            "9starts-with-digit",
            "::",
            ":",
            "-",
            "mk--collision",
        ]
        .into_iter()
        .map(String::from)
        .collect();
        // Non-ASCII, multi-byte, and control characters.
        v.push("ünïcode:1".to_string());
        v.push("emoji-😀:2".to_string());
        v.push("Ω≈ç:3".to_string());
        v.push("tab\tnl\n".to_string());
        v.push("\u{0}\u{1f}x".to_string());
        v.push("a b".to_string());
        v.push(String::new());
        v
    }

    #[test]
    fn homecore_corpus_round_trips_and_is_comms_safe() {
        for alias in defensive_corpus() {
            let encoded = mob_member_id_str(&alias).into_owned();
            assert!(
                is_valid_comms_component(&encoded),
                "encoded {encoded:?} (from {alias:?}) is not a valid comms component"
            );
            assert!(
                !encoded.contains([':', '/']),
                "encoded {encoded:?} leaks a routing separator"
            );
            assert_eq!(
                runtime_alias_str(&encoded),
                alias,
                "round trip of {alias:?}"
            );
        }
    }

    /// The load-bearing guard: the codec's output must be accepted by meerkat
    /// 0.7's own fail-closed comms-name validator — the exact check that
    /// rejected `rt:identity:parent-1:0` before this codec existed. If meerkat
    /// tightens `MemberCommsName` again, this test fails instead of HomeCore.
    #[test]
    fn encoded_ids_satisfy_meerkat_member_comms_name_validator() {
        use meerkat_core::connection::MemberCommsName;
        for alias in defensive_corpus() {
            let encoded = mob_member_id_str(&alias).into_owned();
            assert!(
                MemberCommsName::new("homecore-mob", "worker", encoded.clone()).is_ok(),
                "MemberCommsName::new rejected encoded id {encoded:?} (from alias {alias:?})"
            );
        }
    }

    /// Pin the on-the-wire encoding so an accidental codec change is caught.
    #[test]
    fn exact_wire_format_for_known_aliases() {
        let cases = [
            ("worker-one", "worker-one"), // already comms-safe -> pass through
            ("identity:parent-1", "mk--identity_cparent-1"),
            ("domain:home-automation", "mk--domain_chome-automation"),
            ("family-group:main", "mk--family-group_cmain"),
            ("rt:identity:parent-1:0", "mk--rt_cidentity_cparent-1_c0"),
            ("triage:main", "mk--triage_cmain"),
            ("a:b_c", "mk--a_cb__c"),
            ("a b", "mk--a_x20_b"),
            ("", "mk--"),
        ];
        for (alias, expected) in cases {
            assert_eq!(mob_member_id_str(alias), expected, "encode {alias:?}");
            assert_eq!(runtime_alias_str(expected), alias, "decode {expected:?}");
        }
    }

    /// Ids that begin with the marker but are not well-formed escape productions
    /// must decode to themselves (literal fallback) and never panic.
    #[test]
    fn malformed_encoded_bodies_decode_to_literal_without_panic() {
        for bad in [
            "mk--_",           // dangling underscore (no escape selector)
            "mk--_z",          // invalid escape selector
            "mk--_x",          // truncated hex escape (no terminator)
            "mk--_x_",         // empty hex body
            "mk--_xZZ_",       // non-hex digits
            "mk--_x110000_",   // code point above the Unicode max
            "mk--_xffffffff_", // parses as u32 but is not a valid char
            "mk--abc_",        // trailing dangling underscore after a valid run
        ] {
            assert_eq!(runtime_alias_str(bad), bad, "literal fallback for {bad:?}");
        }
    }

    /// Empty, single-character, and marker-adjacent inputs all round-trip.
    #[test]
    fn empty_and_boundary_strings_round_trip() {
        // Empty alias is not a valid comms component, so it becomes the bare
        // marker and decodes back to empty.
        assert_eq!(mob_member_id_str(""), "mk--");
        assert_eq!(runtime_alias_str("mk--"), "");
        for alias in [":", "_", "-", "::", "_x", "mk", "mk-", "mk--"] {
            let encoded = mob_member_id_str(alias).into_owned();
            assert!(
                is_valid_comms_component(&encoded),
                "{encoded:?} not comms-safe"
            );
            assert_eq!(runtime_alias_str(&encoded), alias, "round trip {alias:?}");
        }
    }

    /// Decode only applies inside the reserved marker namespace; a raw id that
    /// merely *looks* like an escape body must pass through untouched.
    #[test]
    fn decode_is_identity_on_unencoded_ids() {
        for id in [
            "worker",
            "worker-one",
            "_internal",
            "Agent7",
            "rt-review-singleton-0",
            "a_cb", // not marker-prefixed -> NOT decoded to "a:b"
        ] {
            assert_eq!(runtime_alias_str(id), id);
        }
    }

    #[test]
    fn runtime_event_alias_across_generations_and_forms() {
        use meerkat_mob::ids::{AgentIdentity, AgentRuntimeId, Generation};

        let encoded = mob_member_id_str("rt:identity:parent-1:0").into_owned();
        let rid = AgentRuntimeId::new(AgentIdentity::from(encoded.as_str()), Generation::new(7));
        assert_eq!(runtime_event_alias(&rid), "rt:identity:parent-1:0:7");

        let encoded = mob_member_id_str("domain:home-automation").into_owned();
        let rid = AgentRuntimeId::new(AgentIdentity::from(encoded.as_str()), Generation::new(1));
        assert_eq!(runtime_event_alias(&rid), "domain:home-automation:1");

        // Plain member name keeps its initial generation.
        let rid = AgentRuntimeId::initial(AgentIdentity::from("triage-main"));
        assert_eq!(runtime_event_alias(&rid), "triage-main:0");
    }

    /// Exhaustive total round-trip + comms-safety + injectivity over every
    /// string of length 0..=3 from a charset that exercises every escape branch.
    #[test]
    fn round_trip_is_total_and_injective_over_generated_corpus() {
        use meerkat_core::connection::MemberCommsName;
        use std::collections::BTreeMap;

        let charset = ['a', 'Z', '0', '-', '_', ':', '.', '@', ' ', 'ü', '😀'];
        let mut aliases: Vec<String> = vec![String::new()];
        let mut frontier = vec![String::new()];
        for _ in 0..3 {
            let mut next = Vec::new();
            for prefix in &frontier {
                for c in charset {
                    let mut s = prefix.clone();
                    s.push(c);
                    aliases.push(s.clone());
                    next.push(s);
                }
            }
            frontier = next;
        }

        let mut encoded_to_alias: BTreeMap<String, String> = BTreeMap::new();
        for alias in &aliases {
            let encoded = mob_member_id_str(alias).into_owned();
            assert!(
                MemberCommsName::new("m", "r", encoded.clone()).is_ok(),
                "encoded {encoded:?} (from {alias:?}) is not comms-safe"
            );
            assert_eq!(
                &runtime_alias_str(&encoded).into_owned(),
                alias,
                "round trip of {alias:?}"
            );
            if let Some(prev) = encoded_to_alias.insert(encoded.clone(), alias.clone()) {
                assert_eq!(
                    &prev, alias,
                    "collision: {prev:?} and {alias:?} both encode to {encoded:?}"
                );
            }
        }
    }
}