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).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,449 @@
//! Shared URL-opening and scheme validation utilities.
//!
//! Extracted from the `OpenSupergrokUrl` dispatch handler so that any
//! code path (keyboard navigation, mouse click, action dispatch) can
//! open a link safely without duplicating platform-specific logic.
use crate::terminal::hyperlinks::SchemeFilter;
/// Open a URL in the system's default browser/handler.
///
/// Spawns the platform-native opener (`open` on macOS, `xdg-open` on
/// Linux, `cmd /c start` on Windows) with fully detached stdio so it
/// cannot block the pager.
///
/// **Callers handling untrusted input** should call [`is_safe_to_open`]
/// first, or use [`open_url_if_safe`] which combines both steps.
pub fn open_url(url: &str) {
// Test seam: PTY e2e must observe the open without launching a real
// browser. When set, append the URL to the file and skip the OS opener.
if let Ok(path) = std::env::var("KIGI_TEST_OPEN_URL_FILE") {
use std::io::Write;
// Surface misconfiguration: a swallowed write leaves the PTY test
// failing with a generic timeout and no clue why.
if let Err(e) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.and_then(|mut f| writeln!(f, "{url}"))
{
tracing::warn!(error = %e, path, "KIGI_TEST_OPEN_URL_FILE write failed");
}
return;
}
#[cfg(target_os = "macos")]
let cmd = "open";
#[cfg(target_os = "windows")]
let cmd = "cmd";
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let cmd = "xdg-open";
let mut command = std::process::Command::new(cmd);
#[cfg(target_os = "windows")]
command.args(["/c", "start", ""]);
command
.arg(url)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
kigi_tools::util::detach_std_command(&mut command);
if let Err(e) = command.spawn() {
// Redact URL to avoid leaking sensitive query params to logs.
let redacted = url::Url::parse(url)
.map(|mut u| {
u.set_query(None);
u.set_fragment(None);
u.to_string()
})
.unwrap_or_else(|_| "<unparseable>".to_string());
tracing::warn!(url = %redacted, error = %e, "failed to open URL");
}
}
/// Build the `open`/`xdg-open` opener command (macOS / Linux / BSD).
///
/// The returned command is TTY-guarded via [`kigi_tty_utils::detach_std_command`]
/// (`setsid`/`setpgid`) so the spawned GUI helper and its children can't grab
/// the TUI's `/dev/tty`, with stdio fully redirected to null. Split from
/// [`open_path`] so it can be unit-tested without spawning. The path is a single
/// argument, never interpolated into a shell string. Windows uses
/// [`reveal_in_explorer`] instead.
#[cfg(not(target_os = "windows"))]
fn build_open_path_command(path: &std::path::Path) -> std::process::Command {
#[cfg(target_os = "macos")]
let mut command = std::process::Command::new("open");
#[cfg(not(target_os = "macos"))]
let mut command = std::process::Command::new("xdg-open");
command
.arg(path)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
kigi_tty_utils::detach_std_command(&mut command);
command
}
/// Reveal/open a local file in the OS file manager or default application.
///
/// Returns `true` on success. Takes a trusted filesystem path (no scheme
/// validation, unlike [`open_url`]).
///
/// - **Windows**: `explorer.exe /select,<path>` reveals + highlights the file
/// in Explorer. We deliberately avoid `cmd /c start`, whose `%VAR%`
/// expansion corrupts the percent-encoded session-directory segment in
/// imagine media paths (e.g. `…\C%3A%5CUsers…`).
/// - **macOS / Linux**: `open` / `xdg-open` open the file in its default app.
pub fn open_path(path: &std::path::Path) -> bool {
// Never launch a real GUI app in tests.
#[cfg(test)]
{
!path.as_os_str().is_empty()
}
#[cfg(all(not(test), target_os = "windows"))]
{
reveal_in_explorer(path)
}
#[cfg(all(not(test), not(target_os = "windows")))]
{
match build_open_path_command(path).spawn() {
Ok(_) => true,
Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "failed to open file natively");
false
}
}
}
}
/// Reveal `path` in a new Explorer window with the file selected.
///
/// Uses `raw_arg` so Explorer's required `/select,"<path>"` quoting is passed
/// verbatim — the default arg quoting wraps the whole token and breaks the
/// switch. Launched directly (not via `cmd`), so percent characters in the
/// path are not expanded by the shell. Session dirs embed a urlencoded cwd
/// segment (`C%3A%5CUsers…`); those `%` chars must reach Explorer intact.
///
/// Prefer the on-disk path as-is. When the file is missing, open the parent
/// folder (no `/select`) so the user lands near the media instead of Home.
#[cfg(all(not(test), target_os = "windows"))]
fn reveal_in_explorer(path: &std::path::Path) -> bool {
use std::os::windows::process::CommandExt;
// Prefer the real on-disk location (absolute). Fall back to parent when
// the file was deleted so Explorer does not dump the user in Home.
let target = if path.is_file() || path.is_dir() {
path.to_path_buf()
} else if let Some(parent) = path.parent().filter(|p| p.is_dir()) {
tracing::debug!(
path = %path.display(),
parent = %parent.display(),
"media path missing; opening parent folder in Explorer"
);
parent.to_path_buf()
} else {
path.to_path_buf()
};
let select_file = target.is_file();
let mut command = std::process::Command::new("explorer");
// Escape embedded double-quotes in the path so the `/select,"<path>"`
// quoting does not break. Windows file-system paths cannot legally contain
// `"`, but percent-decoded display paths or future user-chosen filenames
// could, so be defensive.
let escaped = target.display().to_string().replace('"', "\"\"");
if select_file {
command.raw_arg(format!("/select,\"{}\"", escaped));
} else {
// Open the folder itself (no /select) — works for dirs and as a
// fallback when we only have a parent path.
command.raw_arg(format!("\"{}\"", escaped));
}
command
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
kigi_tty_utils::detach_std_command(&mut command);
// explorer.exe returns exit code 1 even on success, so a successful spawn
// is the best signal we have.
match command.spawn() {
Ok(_) => true,
Err(e) => {
tracing::warn!(path = %target.display(), error = %e, "failed to reveal file in Explorer");
false
}
}
}
/// Check if a URL's scheme is safe to open.
///
/// Uses the `url` crate for robust scheme extraction. Falls back to
/// prefix matching for non-standard URLs that `url::Url::parse` rejects.
pub fn is_safe_to_open(url: &str, filter: SchemeFilter) -> bool {
let url = url.trim();
if let Ok(parsed) = url::Url::parse(url) {
return filter.allows(parsed.scheme());
}
// Fallback: check for scheme via "://" prefix, lowercasing for
// case-insensitive comparison (SchemeFilter matches lowercase literals).
if let Some((scheme, _)) = url.split_once("://") {
return filter.allows(&scheme.to_ascii_lowercase());
}
// Defensive: url::Url::parse handles well-formed mailto, but guard
// against edge cases where the parser rejects a mailto-like string.
if let Some((scheme, _)) = url.split_once(':')
&& scheme.eq_ignore_ascii_case("mailto")
{
return filter.allows(&scheme.to_ascii_lowercase());
}
false
}
/// Validate scheme and open a URL if permitted. Returns `true` if opened.
pub fn open_url_if_safe(url: &str, filter: SchemeFilter) -> bool {
if is_safe_to_open(url, filter) {
open_url(url);
true
} else {
tracing::debug!(url, "URL scheme not permitted");
false
}
}
/// Ensure `url` carries the given query parameter, returning the rewritten URL.
///
/// If the URL already contains a parameter with that name, its value is left
/// untouched (the caller upstream may have intentionally set one). On parse
/// failure, the original string is returned unchanged so this is safe to apply
/// to opener input from untrusted sources.
///
/// Used by the SuperGrok upsell flow to attribute clicks to `referrer=grok-build`,
/// matching the OAuth consent screen and x.ai/cli marketing links regardless of
/// what the remote settings `gate_url` value happens to be.
pub fn ensure_query_param(url: &str, key: &str, value: &str) -> String {
let Ok(mut parsed) = url::Url::parse(url) else {
return url.to_string();
};
let already_present = parsed.query_pairs().any(|(k, _)| k == key);
if already_present {
return parsed.to_string();
}
parsed.query_pairs_mut().append_pair(key, value);
parsed.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_path_command_passes_path_as_a_single_arg() {
// Path with spaces must be one argument, never shell-interpolated.
let path = std::path::Path::new("/tmp/grok session/image 1.jpg");
let command = build_open_path_command(path);
let args: Vec<_> = command.get_args().map(|a| a.to_os_string()).collect();
assert!(args.contains(&path.as_os_str().to_os_string()));
}
#[test]
fn standard_http_schemes_allowed() {
assert!(is_safe_to_open(
"http://example.com",
SchemeFilter::Standard
));
assert!(is_safe_to_open(
"https://example.com/path?q=1",
SchemeFilter::Standard
));
}
#[test]
fn mailto_allowed() {
assert!(is_safe_to_open(
"mailto:user@example.com",
SchemeFilter::Standard
));
}
#[test]
fn file_scheme_blocked_by_standard() {
// file:// removed from Standard to prevent local file / SSRF attacks.
assert!(!is_safe_to_open(
"file:///home/user/doc.pdf",
SchemeFilter::Standard
));
// But allowed under EditorExtended.
assert!(is_safe_to_open(
"file:///home/user/doc.pdf",
SchemeFilter::EditorExtended
));
}
#[test]
fn javascript_scheme_blocked() {
assert!(!is_safe_to_open(
"javascript:alert(1)",
SchemeFilter::Standard
));
}
#[test]
fn data_scheme_blocked() {
assert!(!is_safe_to_open(
"data:text/html,<h1>hi</h1>",
SchemeFilter::Standard
));
}
#[test]
fn empty_and_garbage_rejected() {
assert!(!is_safe_to_open("", SchemeFilter::Standard));
assert!(!is_safe_to_open("not-a-url", SchemeFilter::Standard));
assert!(!is_safe_to_open(
"://missing-scheme",
SchemeFilter::Standard
));
}
#[test]
fn editor_schemes_with_extended_filter() {
assert!(is_safe_to_open(
"vscode://file/path",
SchemeFilter::EditorExtended
));
assert!(is_safe_to_open(
"cursor://open",
SchemeFilter::EditorExtended
));
assert!(is_safe_to_open("idea://open", SchemeFilter::EditorExtended));
assert!(is_safe_to_open("zed://open", SchemeFilter::EditorExtended));
}
#[test]
fn editor_schemes_blocked_by_standard_filter() {
assert!(!is_safe_to_open(
"vscode://file/path",
SchemeFilter::Standard
));
assert!(!is_safe_to_open("cursor://open", SchemeFilter::Standard));
}
#[test]
fn scheme_case_sensitivity() {
// url::Url normalizes to lowercase
assert!(is_safe_to_open(
"HTTP://EXAMPLE.COM",
SchemeFilter::Standard
));
assert!(is_safe_to_open(
"HTTPS://EXAMPLE.COM",
SchemeFilter::Standard
));
}
#[test]
fn url_with_fragment_and_query() {
assert!(is_safe_to_open(
"https://example.com/page?key=val#section",
SchemeFilter::Standard
));
}
#[test]
fn ftp_scheme_blocked() {
assert!(!is_safe_to_open(
"ftp://files.example.com/pub",
SchemeFilter::Standard
));
}
#[test]
fn fallback_colon_slash_slash_path() {
// A custom scheme that url::Url may reject but has ://
assert!(!is_safe_to_open(
"custom://something",
SchemeFilter::Standard
));
}
#[test]
fn non_mailto_colon_without_slashes_rejected() {
assert!(!is_safe_to_open("tel:+1234567890", SchemeFilter::Standard));
}
#[test]
fn whitespace_trimmed_before_parse() {
assert!(is_safe_to_open(
" https://example.com ",
SchemeFilter::Standard
));
assert!(is_safe_to_open(
"\thttps://example.com\n",
SchemeFilter::Standard
));
}
#[test]
fn ensure_query_param_appends_when_missing() {
let out = ensure_query_param("https://grok.com/supergrok", "referrer", "grok-build");
assert_eq!(out, "https://grok.com/supergrok?referrer=grok-build");
}
#[test]
fn ensure_query_param_preserves_existing_value() {
let out = ensure_query_param(
"https://grok.com/supergrok?referrer=other",
"referrer",
"grok-build",
);
assert_eq!(out, "https://grok.com/supergrok?referrer=other");
}
#[test]
fn ensure_query_param_keeps_other_query_pairs() {
let out = ensure_query_param(
"https://grok.com/supergrok?heavy=1",
"referrer",
"grok-build",
);
assert_eq!(
out,
"https://grok.com/supergrok?heavy=1&referrer=grok-build"
);
}
#[test]
fn ensure_query_param_preserves_fragment() {
// The current remote settings value uses a hash fragment for client-side
// routing (`grok.com/#supergrok`); we still want the referrer attached.
let out = ensure_query_param("https://grok.com/#supergrok", "referrer", "grok-build");
assert_eq!(out, "https://grok.com/?referrer=grok-build#supergrok");
}
#[test]
fn ensure_query_param_returns_unchanged_on_parse_failure() {
let out = ensure_query_param("not a url", "referrer", "grok-build");
assert_eq!(out, "not a url");
}
#[test]
fn ensure_query_param_url_encodes_value() {
let out = ensure_query_param("https://grok.com/supergrok", "referrer", "grok build");
assert_eq!(out, "https://grok.com/supergrok?referrer=grok+build");
}
#[test]
fn fallback_scheme_case_insensitive() {
// Uppercase scheme that url::Url::parse rejects triggers fallback path;
// the fallback must lowercase before matching SchemeFilter.
assert!(!is_safe_to_open(
"CUSTOM://something",
SchemeFilter::Standard
));
// Ensure mailto fallback is case-insensitive too.
assert!(is_safe_to_open(
"MAILTO:user@example.com",
SchemeFilter::Standard
));
}
}