Skip to main content

lean_ctx_activity/
identity.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The session identity a launching app hands to LeanCTX: the app sets
3//! [`SESSION_ENV`] in the environment of every agent session it starts
4//! (and, for the proxy, sends [`SESSION_HEADER`]), and LeanCTX attributes
5//! activity, session stats and proxy usage to that id. Attribution only:
6//! the id never grants anything.
7
8/// Environment variable naming the app's session (`LEAN_CTX_SESSION_ID`).
9pub const SESSION_ENV: &str = "LEAN_CTX_SESSION_ID";
10/// Request header naming the app's session at the proxy.
11pub const SESSION_HEADER: &str = "x-leanctx-session-id";
12/// Longest accepted id.
13pub const MAX_SESSION_ID_BYTES: usize = 128;
14
15/// `raw` as a session id: trimmed, 1–128 bytes of `[A-Za-z0-9._:-]`;
16/// anything else is no id (ignored, never an error).
17pub fn session_id(raw: &str) -> Option<String> {
18    let id = raw.trim();
19    let valid = !id.is_empty()
20        && id.len() <= MAX_SESSION_ID_BYTES
21        && id
22            .bytes()
23            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b':' | b'-'));
24    valid.then(|| id.to_owned())
25}
26
27/// Environment variable naming the app's session for people
28/// (`LEAN_CTX_SESSION_NAME`, e.g. "LeanCTX · Contracts"): shown on the bus
29/// presence, never used to attribute or authorize anything.
30pub const SESSION_NAME_ENV: &str = "LEAN_CTX_SESSION_NAME";
31/// Longest accepted session name, in characters.
32pub const MAX_SESSION_NAME_CHARS: usize = 128;
33
34/// `raw` as a session name: trimmed, 1–128 characters, no control
35/// characters; anything else is no name (ignored, never an error).
36pub fn session_name(raw: &str) -> Option<String> {
37    let name = raw.trim();
38    let valid = !name.is_empty()
39        && name.chars().count() <= MAX_SESSION_NAME_CHARS
40        && !name.chars().any(char::is_control);
41    valid.then(|| name.to_owned())
42}
43
44/// The app session name of this process ([`SESSION_NAME_ENV`]), if valid.
45pub fn session_name_from_env() -> Option<String> {
46    std::env::var(SESSION_NAME_ENV)
47        .ok()
48        .as_deref()
49        .and_then(session_name)
50}
51
52/// The app session id of this process ([`SESSION_ENV`]), if valid.
53pub fn session_id_from_env() -> Option<String> {
54    std::env::var(SESSION_ENV)
55        .ok()
56        .as_deref()
57        .and_then(session_id)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{session_id, session_name};
63
64    #[test]
65    fn only_bounded_plain_ids_are_accepted() {
66        assert_eq!(
67            session_id(" app-7f3a:1.x_y ").as_deref(),
68            Some("app-7f3a:1.x_y")
69        );
70        for bad in ["", "  ", "../etc", "a b", "a/b", "ü", &"a".repeat(129)] {
71            assert_eq!(session_id(bad), None, "{bad:?}");
72        }
73        assert!(session_id(&"a".repeat(128)).is_some());
74    }
75
76    #[test]
77    fn session_names_are_bounded_single_lines() {
78        assert_eq!(
79            session_name("  LeanCTX · Contracts ").as_deref(),
80            Some("LeanCTX · Contracts")
81        );
82        for bad in ["", "   ", "a\nb", "a\u{1b}[31m", &"ü".repeat(129)] {
83            assert_eq!(session_name(bad), None, "{bad:?}");
84        }
85        assert!(session_name(&"ü".repeat(128)).is_some());
86    }
87}