Skip to main content

trustee_core/
lib.rs

1//! Trustee Core — shared types, session state, and workflow logic.
2//!
3//! This crate is the shared foundation used by both `trustee-tui` (terminal UI)
4//! and the upcoming `trustee-api` (HTTP/WebSocket server). It contains:
5//!
6//! - **Types**: enums and structs that model the agent's state machine
7//! - **Session**: the core `Session` struct (agent state without UI concerns)
8//! - **Workflow**: command execution, handoff, and message handling
9//! - **Config**: auto-handoff and color parsing from TOML
10
11pub mod config;
12pub mod session;
13pub mod sessions;
14pub mod types;
15
16/// Compute the per-user directory name for `~/.trustee/users/{hash}/`.
17///
18/// Single definition of the user→hash mapping: SHA-256 of `key`, first 8
19/// bytes interpreted big-endian, formatted as 16 lowercase hex chars.
20///
21/// Two input domains feed this one function:
22/// - **Web/API path**: the authenticated `user_key` (JWT `sub` claim, or
23///   `dev:email` in dev mode) — see `trustee-api`'s `ServerState`.
24/// - **CLI path**: the OS username (`$USER` / `$USERNAME`, falling back to
25///   `"unknown"`) — see the `trustee` binary's `compute_user_hash`.
26///
27/// The two domains intentionally produce different hashes for the same
28/// directory tree; consolidating the *algorithm* here guarantees they cannot
29/// drift apart.
30pub fn user_hash(key: &str) -> String {
31    use sha2::{Digest, Sha256};
32    let mut hasher = Sha256::new();
33    hasher.update(key.as_bytes());
34    let hash_bytes = hasher.finalize();
35    format!(
36        "{:016x}",
37        u64::from_be_bytes(
38            hash_bytes[..8]
39                .try_into()
40                .expect("SHA-256 digest is ≥ 8 bytes")
41        )
42    )
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    /// Known vector: SHA-256("test") =
50    /// 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08,
51    /// so the first 8 bytes big-endian format to the first 16 hex chars.
52    #[test]
53    fn user_hash_known_vector() {
54        assert_eq!(user_hash("test"), "9f86d081884c7d65");
55    }
56
57    #[test]
58    fn user_hash_is_deterministic() {
59        assert_eq!(
60            user_hash("farzan@example.com"),
61            user_hash("farzan@example.com")
62        );
63    }
64
65    #[test]
66    fn user_hash_is_hex16_and_input_sensitive() {
67        let a = user_hash("alice");
68        let b = user_hash("bob");
69        assert_eq!(a.len(), 16);
70        assert_eq!(b.len(), 16);
71        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
72        assert!(b.chars().all(|c| c.is_ascii_hexdigit()));
73        assert_ne!(a, b);
74    }
75
76    #[test]
77    fn user_hash_matches_full_sha256_prefix() {
78        // Cross-check against an independently computed digest
79        // (sha256sum of "trustee"): first 16 hex chars of the full digest.
80        use sha2::{Digest, Sha256};
81        let full = Sha256::digest("trustee");
82        let expected = full[..8]
83            .iter()
84            .map(|b| format!("{:02x}", b))
85            .collect::<String>();
86        assert_eq!(user_hash("trustee"), expected);
87    }
88}