grok-build-upstream-mirror/crates/codegen/xai-grok-shell-base/src/util/mod.rs
grokkybara[bot] 6e38642082 Synced from monorepo
Synced from monorepo

Changes:
- Refresh tool search when the managed MCP catalog is re-fetched
- Prevent duplicate leader process spawn and startup hang from stale leaders
- Document marketplaces, plugins, and organization controls
- Stamp session ID on image generation direct-to-API requests
- Fix auto mode blocked documentation
- Auto mode considers recent user intent
- Expose deploy archive, taken-down, limit, and in-progress reasons on the chat API
- Fail-closed auth refresh contract for shell clients
- Emit a chat-supplied per-session turn index in turn hooks
- Show bash mode chrome in minimal mode
- Add metrics for true-noop and stationarity stops
- Include voice interim text on prompt submit
- Silently end turn on true-noop thrash
- Quiet copy toast when clipboard delivery is confirmed
- Fix session fork truncating at the wrong prompt in rewound sessions
- Make the idle "still running" watcher cue clickable to open the tasks pane
- Default web search model to grok-4.5
- Let plugin subagents inherit parent MCP servers
- Gate no-op end-turn reminder on system reminders
- Add gateway bridge lifecycle telemetry
- Allow editing finalized text while voice is open
- Relocate token carrier to turn-commit events and plumb per-turn origin context
- Raise workflow scratch quotas and make failed runs resumable
- Workflows overlay: auto-progress phases, live agent status, and drop budget meter

Source-Revision: 9b8d35b46d959c042ea9aa31cbbebbd1f0c5c527
2026-07-24 16:59:42 +00:00

415 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pub mod changelog;
pub mod event_id;
pub mod grok_home;
pub mod secure_file;
pub mod tips;
pub mod uname;
pub use xai_grok_shared::clipboard;
pub use xai_grok_shared::stderr::{stderr_lock, with_locked_stderr};
/// Generate a pseudo-random f64 in [0.0, 1.0).
///
/// Uses `RandomState::new()` which is OS-seeded (via `getrandom`) on each
/// instantiation, producing a unique hasher state per call. A fixed sentinel
/// is hashed to extract the random bits — the entropy comes entirely from
/// the OS-seeded `RandomState`, not from any clock source.
///
/// # Precision
/// The result uses all 53 bits of `f64` mantissa for a uniform distribution
/// over `[0.0, 1.0)`. We shift the 64-bit hash right by 11 bits to get a
/// 53-bit integer, then divide by `2^53`. This avoids the subtle bias that
/// occurs when casting a full `u64` to `f64` (which has only 52 bits of
/// mantissa, causing multiple `u64` values to map to the same `f64` for
/// values > 2^52).
///
/// Not cryptographically secure — suitable for sampling and feature
/// rollouts, not for security-sensitive randomness.
pub fn random_f64() -> f64 {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
let random_state = RandomState::new();
let mut hasher = random_state.build_hasher();
hasher.write_u64(0x517cc1b727220a95);
(hasher.finish() >> 11) as f64 / (1u64 << 53) as f64
}
/// Probabilistic sampling. Returns `true` with probability `rate` (0.01.0).
pub fn probabilistic_sample(rate: f64) -> bool {
random_f64() < rate
}
fn matches_trusted_base_url(candidate: &str, trusted_base: &str) -> bool {
let Ok(candidate) = reqwest::Url::parse(candidate) else {
return false;
};
let Ok(trusted) = reqwest::Url::parse(trusted_base) else {
return false;
};
let trusted_path = trusted.path();
let candidate_path = candidate.path();
let path_matches = candidate_path == trusted_path
|| candidate_path
.strip_prefix(trusted_path)
.is_some_and(|suffix| suffix.starts_with('/'));
candidate.scheme() == trusted.scheme()
&& candidate.host_str() == trusted.host_str()
&& candidate.port_or_known_default() == trusted.port_or_known_default()
&& path_matches
}
/// True for cli-chat-proxy URLs (production, plus local-dev hosts when the
/// optional non-production feature is enabled). When that feature is on,
/// runtime env overrides can extend this trust set. Loopback is always
/// accepted (unit tests and local mock servers on arbitrary ports).
pub fn is_cli_chat_proxy_url(url: &str) -> bool {
if matches_trusted_base_url(url, crate::env::PROD_CLI_CHAT_PROXY_BASE_URL) {
return true;
}
if let Ok(u) = reqwest::Url::parse(url)
&& let Some(h) = u.host_str()
&& (h == "localhost" || h == "127.0.0.1" || h == "::1")
{
return true;
}
false
}
/// True for xAI-operated endpoints (`*.x.ai`, cli-chat-proxy, and optional
/// non-production xAI hosts when that feature is enabled).
/// `disable_api_key_auth` refuses keys only for these; other hosts are BYOK and
/// exempt. Safe against invalid URLs and suffix attacks (`evil-x.ai.example`).
///
/// Scheme-agnostic so credential *refusal* fails closed. To decide where to
/// *attach* a credential, use [`is_xai_api_bearer_url`].
pub fn is_xai_api_url(url: &str) -> bool {
is_xai_api_url_impl(url, false)
}
/// Like [`is_xai_api_url`], but requires `https` on every arm, so a
/// session bearer is never attached to a cleartext endpoint, including loopback
/// (a co-located process could otherwise read a token sent to `http://localhost`).
pub fn is_xai_api_bearer_url(url: &str) -> bool {
is_xai_api_url_impl(url, true)
}
fn is_xai_api_url_impl(url: &str, require_https: bool) -> bool {
if require_https {
let Ok(parsed) = reqwest::Url::parse(url) else {
return false;
};
if parsed.scheme() != "https" {
return false;
}
if is_loopback_host(&parsed) {
return false;
}
}
if is_cli_chat_proxy_url(url) {
return true;
}
reqwest::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(str::to_owned))
.is_some_and(|host| host == "x.ai" || host.ends_with(".x.ai"))
}
fn is_loopback_host(parsed: &reqwest::Url) -> bool {
match parsed.host() {
Some(url::Host::Domain(host)) => host == "localhost",
Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
None => false,
}
}
/// Truncate a string to at most `max_chars` characters.
/// Slices at char boundaries so multi-byte UTF-8 never panics.
pub fn truncate(s: &str, max_chars: usize) -> &str {
if s.len() <= max_chars {
return s;
}
let end = s
.char_indices()
.nth(max_chars)
.map(|(i, _)| i)
.unwrap_or(s.len());
&s[..end]
}
/// Check if a process is still alive.
///
/// - Unix: `kill(pid, 0)` via `nix`. True if the process exists (even
/// under a different UID); false only on ESRCH.
/// - Windows: `OpenProcess(SYNCHRONIZE)` + `WaitForSingleObject(0)`. True
/// while running; false on exit, absence, or open failure.
#[cfg(unix)]
pub fn is_process_alive(pid: u32) -> bool {
use nix::errno::Errno;
use nix::sys::signal::kill;
use nix::unistd::Pid;
match kill(Pid::from_raw(pid as i32), None) {
Ok(()) => true,
Err(Errno::ESRCH) => false,
Err(_) => true,
}
}
#[cfg(windows)]
pub fn is_process_alive(pid: u32) -> bool {
use windows::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};
let Ok(handle) = (unsafe { OpenProcess(PROCESS_SYNCHRONIZE, false, pid) }) else {
return false;
};
let wait_result = unsafe { WaitForSingleObject(handle, 0) };
let _ = unsafe { CloseHandle(handle) };
wait_result == WAIT_TIMEOUT
}
/// Which termination signal to send. On Windows both map to `TerminateProcess`
/// (already forceful), so the distinction only matters on Unix.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KillSignal {
/// Graceful `SIGTERM` (Unix) — the process may catch and drain.
Term,
/// Forceful `SIGKILL` (Unix) — unblockable escalation.
Kill,
}
/// Terminate a process by PID with `SIGTERM`. Idempotent: already-dead is `Ok`.
pub fn kill_process_by_pid(pid: u32) -> std::io::Result<()> {
kill_process_with_signal(pid, KillSignal::Term)
}
/// Terminate a process by PID with a chosen signal. Idempotent: already-dead is `Ok`.
///
/// - Unix: `SIGTERM`/`SIGKILL` via `nix::sys::signal::kill`; ESRCH maps to `Ok`.
/// - Windows: `OpenProcess(PROCESS_TERMINATE)` + `TerminateProcess` (already
/// forceful, so `signal` is ignored); ERROR_INVALID_PARAMETER maps to `Ok`.
pub fn kill_process_with_signal(pid: u32, signal: KillSignal) -> std::io::Result<()> {
#[cfg(unix)]
{
use nix::errno::Errno;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
let sig = match signal {
KillSignal::Term => Signal::SIGTERM,
KillSignal::Kill => Signal::SIGKILL,
};
match kill(Pid::from_raw(pid as i32), sig) {
Ok(()) | Err(Errno::ESRCH) => Ok(()),
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
}
}
#[cfg(windows)]
{
let _ = signal;
use windows::Win32::Foundation::{CloseHandle, ERROR_INVALID_PARAMETER};
use windows::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess};
use windows::core::HRESULT;
let no_such_process = HRESULT::from_win32(ERROR_INVALID_PARAMETER.0);
let handle = match unsafe { OpenProcess(PROCESS_TERMINATE, false, pid) } {
Ok(h) => h,
Err(e) if e.code() == no_such_process => return Ok(()),
Err(e) => {
return Err(std::io::Error::other(format!("OpenProcess({pid}): {e}")));
}
};
let terminate = unsafe { TerminateProcess(handle, 0) };
let _ = unsafe { CloseHandle(handle) };
terminate.map_err(|e| std::io::Error::other(format!("TerminateProcess({pid}): {e}")))
}
}
/// True if `pid` is a grok process; pairs with [`kill_process_by_pid`] to avoid killing a recycled PID.
/// Best-effort on macOS/BSD (liveness-only via `kill -0`), exact on Linux (/proc cmdline) and Windows (image path).
pub fn is_grok_process(pid: u32) -> bool {
#[cfg(target_os = "linux")]
{
let cmdline_path = format!("/proc/{pid}/cmdline");
match std::fs::read(&cmdline_path) {
Ok(data) => String::from_utf8_lossy(&data).contains("grok"),
Err(_) => false,
}
}
#[cfg(windows)]
{
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
};
use windows::core::PWSTR;
let Ok(handle) = (unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) })
else {
return false;
};
let mut buf: Vec<u16> = vec![0; 1024];
let mut size: u32 = buf.len() as u32;
let result = unsafe {
QueryFullProcessImageNameW(
handle,
PROCESS_NAME_WIN32,
PWSTR(buf.as_mut_ptr()),
&mut size,
)
};
let _ = unsafe { CloseHandle(handle) };
if result.is_err() {
return false;
}
String::from_utf16_lossy(&buf[..size as usize])
.to_ascii_lowercase()
.contains("grok")
}
#[cfg(all(not(target_os = "linux"), not(windows)))]
{
let mut cmd = std::process::Command::new("kill");
cmd.args(["-0", &pid.to_string()])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
xai_tty_utils::detach_std_command(&mut cmd);
cmd.status().is_ok_and(|s| s.success())
}
}
/// Stricter [`is_grok_process`] for the auto-kill zombie path: on macOS/BSD it
/// name-matches via `ps` (not liveness-only), so eviction never SIGKILLs a
/// recycled PID now owned by an unrelated process. Linux/Windows already match
/// exactly, so this delegates there. Use the permissive [`is_grok_process`] for
/// operator-driven `grok leaders kill`.
pub fn is_grok_process_strict(pid: u32) -> bool {
#[cfg(all(not(target_os = "linux"), not(windows)))]
{
let mut cmd = std::process::Command::new("ps");
cmd.args(["-p", &pid.to_string(), "-o", "comm="])
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
xai_tty_utils::detach_std_command(&mut cmd);
match cmd.output() {
Ok(out) if out.status.success() => {
let comm = String::from_utf8_lossy(&out.stdout);
comm.lines()
.next()
.map(str::trim)
.filter(|line| !line.is_empty())
.and_then(|line| std::path::Path::new(line).file_name())
.and_then(|name| name.to_str())
.is_some_and(|name| name.to_ascii_lowercase().contains("grok"))
}
_ => false,
}
}
#[cfg(any(target_os = "linux", windows))]
{
is_grok_process(pid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_cli_chat_proxy_url_accepts_proxy_subpath() {
assert!(is_cli_chat_proxy_url(
"https://cli-chat-proxy.grok.com/v1/chat/completions"
));
}
#[test]
fn test_is_cli_chat_proxy_url_rejects_public_api() {
assert!(!is_cli_chat_proxy_url("https://api.x.ai/v1"));
}
#[test]
fn test_is_cli_chat_proxy_url_rejects_spoofed_hostname() {
assert!(!is_cli_chat_proxy_url(
"https://cli-chat-proxy.grok.com.evil.example/v1"
));
}
#[test]
fn test_is_cli_chat_proxy_url_rejects_v11_prefix_confusion() {
assert!(!is_cli_chat_proxy_url(
"https://cli-chat-proxy.grok.com/v11/chat/completions"
));
}
#[test]
fn test_is_xai_api_url() {
assert!(is_xai_api_url("https://api.x.ai/v1"));
assert!(is_xai_api_url("https://api.x.ai/v1/chat/completions"));
assert!(is_xai_api_url("https://x.ai"));
assert!(is_xai_api_url(
"https://cli-chat-proxy.grok.com/v1/chat/completions"
));
assert!(!is_xai_api_url("https://api.openai.com/v1"));
assert!(!is_xai_api_url("https://api.anthropic.com/v1"));
assert!(!is_xai_api_url("https://generativelanguage.googleapis.com"));
assert!(!is_xai_api_url("https://api.x.ai.evil.example/v1"));
assert!(!is_xai_api_url("https://evil-x.ai.attacker.com/v1"));
assert!(!is_xai_api_url("https://prefixx.ai/v1"));
assert!(!is_xai_api_url("not-a-url"));
assert!(!is_xai_api_url(""));
assert!(is_xai_api_url("http://api.x.ai/v1"));
assert!(is_xai_api_url("http://localhost:11434/v1"));
}
#[test]
fn test_is_xai_api_bearer_url() {
assert!(is_xai_api_bearer_url("https://api.x.ai/v1"));
assert!(!is_xai_api_bearer_url("http://api.x.ai/v1"));
assert!(!is_xai_api_bearer_url("http://localhost:11434/v1"));
{
assert!(!is_xai_api_bearer_url("https://localhost:11434/v1"));
assert!(!is_xai_api_bearer_url("https://127.0.0.2:11434/v1"));
assert!(!is_xai_api_bearer_url("https://[::1]:11434/v1"));
}
assert!(is_xai_api_bearer_url("https://API.X.AI/v1"));
assert!(!is_xai_api_bearer_url(
"https://api.x.ai@attacker.example/v1"
));
assert!(!is_xai_api_bearer_url("https://х.ai/v1"));
}
#[test]
fn test_truncate() {
assert_eq!(truncate("hello", 5), "hello");
assert_eq!(truncate("hello world", 5), "hello");
assert_eq!(truncate("abc🎉🎉def", 5), "abc🎉🎉");
}
#[test]
fn is_process_alive_current_process() {
assert!(is_process_alive(std::process::id()));
}
#[test]
fn is_process_alive_dead_pid() {
assert!(!is_process_alive(4_000_000_000));
}
#[cfg(unix)]
#[test]
fn is_process_alive_init_process() {
assert!(is_process_alive(1));
}
#[test]
fn kill_process_by_pid_already_dead_is_ok() {
assert!(kill_process_by_pid(4_000_000_000).is_ok());
}
#[cfg(unix)]
#[test]
fn kill_process_by_pid_terminates_live_child() {
let mut child = std::process::Command::new("sleep")
.arg("60")
.spawn()
.expect("spawn sleep");
let pid = child.id();
kill_process_by_pid(pid).expect("kill should succeed");
let status = child.wait().expect("wait child");
assert!(
!status.success(),
"sleep was terminated, not exited cleanly"
);
}
#[test]
fn is_grok_process_self_true_impossible_pid_false() {
assert!(is_grok_process(std::process::id()));
assert!(!is_grok_process(u32::MAX));
}
#[test]
fn is_grok_process_strict_self_true_impossible_pid_false() {
assert!(is_grok_process_strict(std::process::id()));
assert!(!is_grok_process_strict(u32::MAX));
}
#[cfg(unix)]
#[test]
fn kill_process_with_signal_sigkill_terminates_live_child() {
let mut child = std::process::Command::new("sleep")
.arg("60")
.spawn()
.expect("spawn sleep");
let pid = child.id();
kill_process_with_signal(pid, KillSignal::Kill).expect("sigkill should succeed");
let status = child.wait().expect("wait child");
assert!(!status.success(), "sleep was killed, not exited cleanly");
}
}