Skip to main content

gate4agent_wake/grok_leader/
pipe.rs

1//! Grok home, the leader socket path, and the Windows pipe name.
2//!
3//! The hash must match `xai-grok-shell`'s `pipe_leaf_name`: SipHash-1-3 with
4//! the keys shipped in that file, then `Path::hash`. The keys are not a
5//! secret. They exist so the name does not move when Rust changes
6//! `DefaultHasher`. `Path::hash` itself is still std's component hash, so
7//! this name matches a grok binary built with a compatible std.
8
9use std::ffi::OsStr;
10use std::path::{Path, PathBuf};
11
12use thiserror::Error;
13
14/// Fixed SipHash-1-3 keys from grok `leader/transport.rs`. Do not change.
15const PIPE_KEY_0: u64 = 0x6772_6f6b_6c65_6164;
16const PIPE_KEY_1: u64 = 0x6572_5f70_6970_6521;
17
18#[derive(Debug, Error)]
19pub enum PushError {
20    #[error("leader-absent")]
21    LeaderAbsent,
22    #[error("leader-not-ready")]
23    LeaderNotReady,
24    #[error("leader-protocol: {0}")]
25    Protocol(String),
26    #[error("session-load-failed: {0}")]
27    SessionLoadFailed(String),
28    #[error("prompt-failed: {0}")]
29    PromptFailed(String),
30    #[error("frame-too-large")]
31    FrameTooLarge,
32}
33
34/// `$GROK_HOME` verbatim when set, otherwise `<home>/.grok` with the home
35/// canonicalized through `dunce` (no `\\?\` prefix). Matches grok's
36/// `grok_home_in`.
37pub fn grok_home() -> Option<PathBuf> {
38    if let Some(env) = std::env::var_os("GROK_HOME").filter(|value| !value.is_empty()) {
39        return Some(PathBuf::from(env));
40    }
41    let home = std::env::var_os("USERPROFILE")
42        .or_else(|| std::env::var_os("HOME"))
43        .filter(|value| !value.is_empty())?;
44    let home = PathBuf::from(home);
45    Some(dunce::canonicalize(&home).unwrap_or(home).join(".grok"))
46}
47
48/// `GROK_LEADER_SOCKET` when set, otherwise `<grok_home>/leader.sock`.
49/// A non-default relay suffix (`leader-<hash>.sock`) is not derived here:
50/// `DefaultHasher` is not stable. Set the env var for that socket.
51pub fn leader_socket(grok_home: &Path, override_socket: Option<&OsStr>) -> PathBuf {
52    if let Some(over) = override_socket.filter(|value| !value.is_empty()) {
53        return PathBuf::from(over);
54    }
55    grok_home.join("leader.sock")
56}
57
58pub fn leader_pipe_name(sock: &Path) -> String {
59    use std::hash::{Hash, Hasher};
60
61    use siphasher::sip::SipHasher13;
62
63    let mut hasher = SipHasher13::new_with_keys(PIPE_KEY_0, PIPE_KEY_1);
64    sock.hash(&mut hasher);
65    format!("grok-leader-{:016x}", hasher.finish())
66}
67
68#[cfg(windows)]
69pub fn leader_pipe_os_path(sock: &Path) -> PathBuf {
70    PathBuf::from(format!(r"\\.\pipe\{}", leader_pipe_name(sock)))
71}
72
73/// Non-connecting probe. On Windows `WaitNamedPipeW` does not take a server
74/// instance. A connecting open would be accepted as a phantom client.
75pub fn leader_is_listening(sock: &Path) -> bool {
76    #[cfg(windows)]
77    {
78        windows_pipe_ready(&leader_pipe_os_path(sock))
79    }
80    #[cfg(not(windows))]
81    {
82        unix_socket_ready(sock)
83    }
84}
85
86#[cfg(windows)]
87fn windows_pipe_ready(pipe: &Path) -> bool {
88    use std::os::windows::ffi::OsStrExt;
89
90    #[link(name = "kernel32")]
91    extern "system" {
92        fn WaitNamedPipeW(name: *const u16, timeout_ms: u32) -> i32;
93        fn GetLastError() -> u32;
94    }
95
96    const ERROR_FILE_NOT_FOUND: u32 = 2;
97    let mut wide: Vec<u16> = pipe.as_os_str().encode_wide().collect();
98    wide.push(0);
99    let ready = unsafe { WaitNamedPipeW(wide.as_ptr(), 1) };
100    if ready != 0 {
101        return true;
102    }
103    let err = unsafe { GetLastError() };
104    err != ERROR_FILE_NOT_FOUND
105}
106
107#[cfg(not(windows))]
108fn unix_socket_ready(sock: &Path) -> bool {
109    use std::os::unix::fs::FileTypeExt;
110    std::fs::metadata(sock).is_ok_and(|meta| meta.file_type().is_socket())
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn pipe_name_is_stable_and_shaped() {
119        let sock = Path::new(r"C:\grok\leader.sock");
120        let once = leader_pipe_name(sock);
121        let twice = leader_pipe_name(sock);
122        assert_eq!(once, twice);
123        assert!(once.starts_with("grok-leader-"));
124        assert_eq!(once.len(), "grok-leader-".len() + 16);
125        assert!(once.bytes().skip("grok-leader-".len()).all(|byte| byte.is_ascii_hexdigit()));
126    }
127
128    #[test]
129    fn a_different_path_hashes_differently() {
130        let home = leader_pipe_name(Path::new(r"C:\grok\leader.sock"));
131        let other = leader_pipe_name(Path::new(r"C:\grok\leader-other.sock"));
132        assert_ne!(home, other);
133    }
134
135    #[test]
136    fn socket_override_replaces_the_default() {
137        let home = Path::new(r"C:\grok");
138        assert_eq!(leader_socket(home, None), home.join("leader.sock"));
139        assert_eq!(
140            leader_socket(home, Some(OsStr::new(r"D:\custom.sock"))),
141            PathBuf::from(r"D:\custom.sock")
142        );
143        assert_eq!(leader_socket(home, Some(OsStr::new(""))), home.join("leader.sock"));
144    }
145}