Files
Kigi-CLI/crates/codegen/kigi-shell-base/src/util/mod.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

334 lines
13 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 kigi_home;
pub mod secure_file;
pub mod tips;
pub mod uname;
pub use kigi_shared::clipboard;
pub use kigi_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 subscription coding-API URLs (the compiled production endpoint;
/// deliberately NOT the env-overridable [`kigi_env::coding_api_base_url`] so a
/// runtime override can't widen this trust set).
pub fn is_cli_chat_proxy_url(url: &str) -> bool {
matches_trusted_base_url(url, kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url)
}
/// True for URLs the idle model-metadata refresh may re-fetch from: the
/// *effective* subscription coding endpoint (the `KIGI_CODE_BASE_URL`
/// override when set, else the compiled production endpoint), plus loopback
/// hosts (local dev proxies and test mocks). Unlike [`is_cli_chat_proxy_url`]
/// this honours the env override and loopback, so use it only to gate traffic
/// that already flows to the session's configured base URL (the refresh
/// re-fetches from the same host the session samples against); it must never
/// widen a security trust set.
pub fn is_effective_coding_endpoint_url(url: &str) -> bool {
if is_cli_chat_proxy_url(url) {
return true;
}
if matches_trusted_base_url(url, &kigi_env::coding_api_base_url()) {
return true;
}
reqwest::Url::parse(url)
.ok()
.is_some_and(|u| match u.host() {
Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
Some(url::Host::Domain(host)) => host == "localhost",
None => false,
})
}
/// True for first-party xAI endpoints (`*.x.ai`, cli-chat-proxy, and optional
/// non-production first-party 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`).
pub fn is_first_party_xai_url(url: &str) -> bool {
if is_cli_chat_proxy_url(url) {
return true;
}
reqwest::Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_owned()))
.is_some_and(|host| host == "x.ai" || host.ends_with(".x.ai"))
}
/// 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
}
/// Terminate a process by PID. Idempotent: already-dead is `Ok`.
///
/// - Unix: `SIGTERM` via `nix::sys::signal::kill`; ESRCH maps to `Ok`.
/// - Windows: `OpenProcess(PROCESS_TERMINATE)` + `TerminateProcess`;
/// ERROR_INVALID_PARAMETER (Windows' "no such process") maps to `Ok`.
pub fn kill_process_by_pid(pid: u32) -> std::io::Result<()> {
#[cfg(unix)]
{
use nix::errno::Errno;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
Ok(()) | Err(Errno::ESRCH) => Ok(()),
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
}
}
#[cfg(windows)]
{
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());
kigi_tty_utils::detach_std_command(&mut cmd);
cmd.status().is_ok_and(|s| s.success())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_cli_chat_proxy_url_accepts_proxy_subpath() {
assert!(is_cli_chat_proxy_url(
"https://api.kimi.com/coding/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://api.kimi.com.evil.example/coding/v1"
));
}
#[test]
fn test_is_effective_coding_endpoint_url_accepts_prod_and_loopback() {
assert!(is_effective_coding_endpoint_url(
"https://api.kimi.com/coding/v1"
));
assert!(is_effective_coding_endpoint_url("http://127.0.0.1:8080/v1"));
assert!(is_effective_coding_endpoint_url("http://localhost:8080/v1"));
assert!(is_effective_coding_endpoint_url("http://[::1]:8080/v1"));
}
#[test]
fn test_is_effective_coding_endpoint_url_rejects_remote_third_party() {
assert!(!is_effective_coding_endpoint_url("https://api.x.ai/v1"));
assert!(!is_effective_coding_endpoint_url(
"https://localhost.evil.example/v1"
));
}
#[test]
fn test_is_cli_chat_proxy_url_rejects_v11_prefix_confusion() {
assert!(!is_cli_chat_proxy_url(
"https://api.kimi.com/coding/v11/chat/completions"
));
}
#[test]
fn test_is_first_party_xai_url() {
assert!(is_first_party_xai_url("https://api.x.ai/v1"));
assert!(is_first_party_xai_url(
"https://api.x.ai/v1/chat/completions"
));
assert!(is_first_party_xai_url("https://x.ai"));
assert!(is_first_party_xai_url(
"https://api.kimi.com/coding/v1/chat/completions"
));
assert!(!is_first_party_xai_url("https://api.openai.com/v1"));
assert!(!is_first_party_xai_url("https://api.anthropic.com/v1"));
assert!(!is_first_party_xai_url(
"https://generativelanguage.googleapis.com"
));
assert!(!is_first_party_xai_url("https://api.x.ai.evil.example/v1"));
assert!(!is_first_party_xai_url("https://evil-x.ai.attacker.com/v1"));
assert!(!is_first_party_xai_url("https://prefixx.ai/v1"));
assert!(!is_first_party_xai_url("not-a-url"));
assert!(!is_first_party_xai_url(""));
}
#[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));
}
}