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}