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

Changes:
- Temporarily disable session share link creation in the TUI
- Do not approve plan on empty Enter from the revise prompt
- Expose chat product Skills via ACP available_commands_update
- Return immediately from a blocking wait on an already-completed ACP task
- Split headless pager module for clearer structure
- Stop git worktree prune from removing user registrations on resume
- Use compaction sampler tokenizer for item token counts
- Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE
- Cancel all session subagents when the user stops
- Let the session persistence actor exit when its session ends
- Make fullscreen terminal resize much cheaper on long sessions
- Report honestly from kill_task when an ACP task does not exist
- Hide /usage for external-auth deployments
- Forward the history-load trailer’s computer_reason to the client
- Remove ineffective no-op tool reminder
- Declare slash-command screen-mode support in one place
- Keep settings enum picker on the committed value until Enter
- Reap a PTY’s full process tree
- Stream tool calls from headless mode over ACP
- Bridge gateway task lifecycle to ACP for chat session background tasks
- Don’t warn about truncated history on a suppressed replay
- Fit full-replace summarizer input and recover on context-length errors
- Stop dropping agents over an unrecognized frontmatter color
- Add /undo as a slash alias for /rewind
- Harden sleep/wake token-refresh paths against forced re-login
- Add session/list ACP method
- Give each sampling backend its own conversion module
- Treat an unenrolled child process as a lint error
- Suppress the cancelled marker on send-now wake turns
- Stop tearing down Roslyn on every edit, and read C# diagnostics

Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
2026-07-30 19:07:40 +00:00

426 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
}
/// Production cli-chat-proxy base only (compiled-in constant).
///
/// Unlike [`is_cli_chat_proxy_url`], this rejects loopback and staging/dev hosts.
/// Used for security-sensitive remote kill-switches that must not become env
/// toggles via `GROK_CLI_CHAT_PROXY_BASE_URL` (or similar) pointing at an
/// attacker-controlled origin.
pub fn is_prod_cli_chat_proxy_url(url: &str) -> bool {
matches_trusted_base_url(url, crate::env::PROD_CLI_CHAT_PROXY_BASE_URL)
}
/// 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() {
#[allow(clippy::disallowed_methods)]
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() {
#[allow(clippy::disallowed_methods)]
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");
}
}