Skip to main content

objectiveai_sdk/laboratories/
composite.rs

1//! The ASSISTANT-FACING composite laboratory id:
2//! `{machineID}/{base62(state)}/{base62(laboratoryID)}`.
3//!
4//! Laboratory ids are only unique per (machine, state), so the id an
5//! assistant quotes must carry the whole identity. The machine id is
6//! 64 lowercase hex chars ([`crate::machine::machine_id`]) — never a
7//! `/`; state and laboratory id are ARBITRARY strings (they may
8//! contain `/`), so both are base62-encoded (alphabet `0-9A-Za-z`,
9//! the `base62` crate's digit order) — after splitting on `/` there
10//! are always EXACTLY three unambiguous segments.
11//!
12//! The composite is a PRESENTATION at the assistant boundary only:
13//! the raw id stays raw on every other surface (DB rows, commands,
14//! HostIdentify, `ws://laboratory/{id}`, the `oail-<rawid>` tool
15//! prefix — tool names cannot carry a 64-hex machine id under
16//! provider name limits).
17//!
18//! The in-container `objectiveai-mcp-laboratory` binary keeps a local
19//! mirror of the DECODER (it deliberately does not depend on this
20//! crate) — keep the two in sync.
21
22use super::{ClientLaboratory, ClientLaboratoryType};
23
24/// The base62 digit alphabet, in the `base62` crate's default order.
25const ALPHABET: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
26
27/// Encode arbitrary bytes as base62 (big-endian big-integer base
28/// conversion). Leading 0x00 bytes are preserved as leading `'0'`
29/// digits; the empty string encodes to the empty string.
30fn base62_encode_str(s: &str) -> String {
31    let bytes = s.as_bytes();
32    let leading_zeros = bytes.iter().take_while(|b| **b == 0).count();
33    let mut digits: Vec<u8> = Vec::new();
34    let mut num: Vec<u8> = bytes[leading_zeros..].to_vec();
35    while !num.is_empty() {
36        // One long-division pass: num := num / 62, remainder → digit.
37        let mut quotient: Vec<u8> = Vec::with_capacity(num.len());
38        let mut remainder: u32 = 0;
39        for byte in &num {
40            let acc = remainder * 256 + *byte as u32;
41            let q = (acc / 62) as u8;
42            remainder = acc % 62;
43            if !(quotient.is_empty() && q == 0) {
44                quotient.push(q);
45            }
46        }
47        digits.push(ALPHABET[remainder as usize]);
48        num = quotient;
49    }
50    let mut out = String::with_capacity(leading_zeros + digits.len());
51    for _ in 0..leading_zeros {
52        out.push('0');
53    }
54    out.extend(digits.iter().rev().map(|d| *d as char));
55    out
56}
57
58/// Decode a [`base62_encode_str`] string back to the original. `None`
59/// on any character outside the alphabet or a byte sequence that is
60/// not valid UTF-8.
61fn base62_decode_str(s: &str) -> Option<String> {
62    let digits = s.as_bytes();
63    let leading_zeros = digits.iter().take_while(|d| **d == b'0').count();
64    let mut num: Vec<u8> = Vec::new();
65    for digit in &digits[leading_zeros..] {
66        let value = ALPHABET.iter().position(|a| a == digit)? as u32;
67        // num := num * 62 + value, over base-256 limbs.
68        let mut carry = value;
69        for byte in num.iter_mut().rev() {
70            let acc = *byte as u32 * 62 + carry;
71            *byte = (acc % 256) as u8;
72            carry = acc / 256;
73        }
74        while carry > 0 {
75            num.insert(0, (carry % 256) as u8);
76            carry /= 256;
77        }
78    }
79    // A '0' digit inside the number contributes nothing extra; only
80    // LEADING '0's map back to leading 0x00 bytes.
81    let mut bytes = vec![0u8; leading_zeros];
82    bytes.extend(num);
83    String::from_utf8(bytes).ok()
84}
85
86/// 32-bit FNV-1a — tiny, dependency-free, and trivially mirrored in
87/// the in-container binary. Only used to derive short display-safe
88/// server names; NOT a security or content-addressing hash.
89fn fnv1a32(bytes: &[u8]) -> u32 {
90    let mut hash: u32 = 0x811c9dc5;
91    for byte in bytes {
92        hash ^= *byte as u32;
93        hash = hash.wrapping_mul(0x0100_0193);
94    }
95    hash
96}
97
98/// A u32 as exactly 6 base62 digits (62^6 > 2^32), zero-padded — the
99/// fixed-width, `[0-9A-Za-z]`-only token server names are built from.
100fn base62_encode_u32(mut value: u32) -> String {
101    let mut digits = [b'0'; 6];
102    for slot in digits.iter_mut().rev() {
103        *slot = ALPHABET[(value % 62) as usize];
104        value /= 62;
105    }
106    String::from_utf8(digits.to_vec()).expect("alphabet is ASCII")
107}
108
109impl ClientLaboratory {
110    /// The laboratory's MCP SERVER NAME — `oail-<base62(fnv1a32(composite))>`,
111    /// a fixed 11-char `[0-9A-Za-z-]` token. Server names feed the
112    /// proxy's tool-name prefix (`<name>_Bash`), which is bound by
113    /// provider tool-name limits (length + charset) — so the name is a
114    /// HASH of the [composite id](Self::composite_id), never the raw
115    /// id (arbitrary length/charset) and never the composite itself
116    /// (the 64-hex machine id alone busts the limits). `None` when the
117    /// marker predates machine tracking (no composite to hash). The
118    /// in-container binary computes the same name from its
119    /// `OBJECTIVEAI_LABORATORY_ID` env — keep the mirrors in sync.
120    pub fn server_name(&self) -> Option<String> {
121        self.composite_id()
122            .map(|composite| format!("oail-{}", base62_encode_u32(fnv1a32(composite.as_bytes()))))
123    }
124
125    /// The assistant-facing composite id
126    /// `{machineID}/{base62(state)}/{base62(laboratoryID)}` — `None`
127    /// when the marker predates machine tracking (no pair to compose).
128    pub fn composite_id(&self) -> Option<String> {
129        let machine = self.machine.as_deref()?;
130        let machine_state = self.machine_state.as_deref()?;
131        Some(format!(
132            "{machine}/{}/{}",
133            base62_encode_str(machine_state),
134            base62_encode_str(&self.id)
135        ))
136    }
137
138    /// Parse a [`composite_id`](Self::composite_id) back into a
139    /// marker (with the pair always `Some`). `None` on anything that
140    /// is not EXACTLY three `/`-separated segments with a non-empty
141    /// machine id and base62-decodable state + laboratory id — safe
142    /// because the machine segment is hex and the encoded segments
143    /// are pure base62, so neither can contain a `/` of its own.
144    pub fn from_composite_id(s: &str) -> Option<Self> {
145        let mut parts = s.split('/');
146        let machine = parts.next()?;
147        let machine_state = parts.next()?;
148        let id = parts.next()?;
149        if parts.next().is_some() || machine.is_empty() {
150            return None;
151        }
152        Some(Self {
153            r#type: ClientLaboratoryType::Client,
154            id: base62_decode_str(id)?,
155            machine: Some(machine.to_string()),
156            machine_state: Some(base62_decode_str(machine_state)?),
157        })
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn marker(machine: &str, state: &str, id: &str) -> ClientLaboratory {
166        ClientLaboratory {
167            r#type: ClientLaboratoryType::Client,
168            id: id.to_string(),
169            machine: Some(machine.to_string()),
170            machine_state: Some(state.to_string()),
171        }
172    }
173
174    #[test]
175    fn roundtrips() {
176        for (state, id) in [
177            ("default", "my-lab"),
178            ("state/with/slashes", "id/with/slashes"),
179            ("under_score.dots", "0leading-zero-digit"),
180            ("ünïcodé 状態", "研究室/λ"),
181            ("", "empty-state"),
182            ("s", ""),
183            ("\u{0}\u{0}leading-nuls", "trailing-nul\u{0}"),
184        ] {
185            let original = marker("ab12", state, id);
186            let composite = original.composite_id().expect("pair present");
187            assert_eq!(
188                composite.matches('/').count(),
189                2,
190                "exactly three segments: {composite}"
191            );
192            let parsed = ClientLaboratory::from_composite_id(&composite)
193                .unwrap_or_else(|| panic!("parses: {composite}"));
194            assert_eq!(parsed, original, "roundtrip for {state:?}/{id:?}");
195        }
196    }
197
198    #[test]
199    fn server_name_is_fixed_width_and_name_safe() {
200        // Arbitrary-length/charset raw ids all hash to the same shape.
201        for (state, id) in [
202            ("default", "my-lab"),
203            ("state/with/slashes", &"x".repeat(500) as &str),
204            ("ünïcodé 状態", "研究室/λ"),
205        ] {
206            let lab = marker("3fa8", state, id);
207            let name = lab.server_name().expect("pair present");
208            assert_eq!(name.len(), "oail-".len() + 6, "fixed width: {name}");
209            assert!(name.starts_with("oail-"));
210            assert!(
211                name["oail-".len()..].bytes().all(|b| b.is_ascii_alphanumeric()),
212                "hash token is [0-9A-Za-z]: {name}"
213            );
214            // Deterministic.
215            assert_eq!(lab.server_name(), Some(name));
216        }
217        // Pair-less markers have no composite to hash.
218        let mut lab = marker("m", "s", "i");
219        lab.machine = None;
220        assert_eq!(lab.server_name(), None);
221    }
222
223    #[test]
224    fn pairless_marker_has_no_composite() {
225        let mut lab = marker("m", "s", "i");
226        lab.machine_state = None;
227        assert_eq!(lab.composite_id(), None);
228        lab.machine_state = Some("s".to_string());
229        lab.machine = None;
230        assert_eq!(lab.composite_id(), None);
231    }
232
233    #[test]
234    fn malformed_inputs_rejected() {
235        for bad in [
236            "",                    // no segments
237            "bare-id",             // one segment
238            "machine/only-two",    // two segments
239            "m/a/b/c",             // four segments
240            "/state62/id62",       // empty machine
241            "m/bad!chars/aa",      // outside the alphabet
242            "m/aa/bad chars",      // outside the alphabet
243        ] {
244            assert_eq!(
245                ClientLaboratory::from_composite_id(bad),
246                None,
247                "must reject {bad:?}"
248            );
249        }
250    }
251}