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:
ZacharyZhang-NY committed 2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 files changed
+1353757

No files matched your search

@@ -0,0 +1,354 @@
//! Leader-mode (`grok agent --leader stdio`) test harness.
//!
//! Spawns the real binary as a stdio client whose bridge elects a leader
//! subprocess hosting the actual sessions, speaks ACP over pipes, and
//! exposes lock-file helpers for leader-lifecycle assertions. Unix-only:
//! the leader transport is a unix socket.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use agent_client_protocol::{self as acp, Agent as _};
use kigi_acp_lib::LineBufferedRead;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::env::grok_binary;
use crate::mock_server::MockInferenceServer;
use crate::process::spawn_piped_with_stderr_capture;
/// Env var naming the binary that elects/hosts the leader in a two-binary
/// (version-skew) test. Falls back to [`grok_binary`]'s resolution.
pub const LEADER_BINARY_ENV: &str = "KIGI_BINARY_LEADER";
/// Env var naming the binary for the second (usually newer) client in a
/// two-binary test. Falls back to [`grok_binary`]'s resolution.
pub const CLIENT_BINARY_ENV: &str = "KIGI_BINARY_CLIENT";
fn role_binary(env_key: &str) -> PathBuf {
if let Ok(path) = std::env::var(env_key) {
let p = PathBuf::from(path);
assert!(p.exists(), "{env_key} does not exist: {}", p.display());
return p;
}
grok_binary()
}
/// Binary for the leader-electing side of a version-skew test
/// (`KIGI_BINARY_LEADER`, else the shared [`grok_binary`] resolution).
pub fn leader_binary() -> PathBuf {
role_binary(LEADER_BINARY_ENV)
}
/// Binary for the client side of a version-skew test (`KIGI_BINARY_CLIENT`,
/// else the shared [`grok_binary`] resolution).
pub fn client_binary() -> PathBuf {
role_binary(CLIENT_BINARY_ENV)
}
/// Capture for notifications + reconnect signals.
#[derive(Default)]
pub struct Capture {
chunks: std::sync::Mutex<Vec<String>>,
notification_count: AtomicU32,
reconnected_count: AtomicU32,
}
struct LeaderAcpClient {
capture: Arc<Capture>,
}
#[async_trait::async_trait(?Send)]
impl acp::Client for LeaderAcpClient {
async fn request_permission(
&self,
args: acp::RequestPermissionRequest,
) -> acp::Result<acp::RequestPermissionResponse> {
let outcome = args
.options
.iter()
.find(|o| o.kind == acp::PermissionOptionKind::AllowOnce)
.or(args.options.first())
.map(|o| {
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
o.option_id.clone(),
))
})
.unwrap_or(acp::RequestPermissionOutcome::Cancelled);
Ok(acp::RequestPermissionResponse::new(outcome))
}
async fn session_notification(&self, args: acp::SessionNotification) -> acp::Result<()> {
self.capture
.notification_count
.fetch_add(1, Ordering::SeqCst);
if let acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) =
args.update
&& let acp::ContentBlock::Text(t) = content
{
self.capture.chunks.lock().unwrap().push(t.text);
}
Ok(())
}
async fn ext_notification(&self, args: acp::ExtNotification) -> acp::Result<()> {
if &*args.method == "x.ai/leader_reconnected" {
self.capture
.reconnected_count
.fetch_add(1, Ordering::SeqCst);
}
Ok(())
}
}
/// A `grok agent --leader stdio` client subprocess speaking ACP over pipes.
/// The leader subprocess it elects hosts the actual sessions.
pub struct LeaderStdioClient {
pub conn: acp::ClientSideConnection,
// Exposed for PID assertions.
pub child: tokio::process::Child,
capture: Arc<Capture>,
stderr: Arc<std::sync::Mutex<Vec<u8>>>,
}
impl LeaderStdioClient {
pub async fn spawn(server: &MockInferenceServer, cwd: &Path, home: &Path) -> Self {
Self::spawn_with_binary(&grok_binary(), server, cwd, home).await
}
/// [`Self::spawn`] with an explicit binary, for two-binary version-skew
/// tests (pair with [`leader_binary`] / [`client_binary`]).
pub async fn spawn_with_binary(
binary: &Path,
server: &MockInferenceServer,
cwd: &Path,
home: &Path,
) -> Self {
let mut cmd = tokio::process::Command::new(binary);
cmd.args(["agent", "--leader", "stdio"])
.current_dir(cwd)
// Hermetic env: the developer's shell may export GROK_* vars
// (e.g. KIGI_LEADER_SOCKET pointing at a REAL leader on this
// machine). env_clear + explicit allowlist guarantees the test
// can never touch a leader outside its sandbox home.
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", home)
.env("KIGI_SHARE_DIR", home.join(".kigi"))
// Pin the socket inside the sandbox. The lock file is the
// sibling `.lock` (leader.sock -> leader.lock), and the spawned
// leader subprocess inherits/forwards this env var, so every
// (re-)elected leader binds the same sandboxed path.
.env("KIGI_LEADER_SOCKET", home.join(".kigi").join("leader.sock"))
.env("KIGI_CLI_CHAT_PROXY_BASE_URL", server.url())
.env("KIGI_XAI_API_BASE_URL", server.url())
.env("XAI_API_KEY", "test-key-for-ci")
.env("KIGI_TELEMETRY_ENABLED", "false")
.env("KIGI_FEEDBACK_ENABLED", "false")
.env("KIGI_TRACE_UPLOAD", "false")
.env("KIGI_INSTRUMENTATION", "disabled")
// Inherited by the spawned leader, whose stderr goes to
// ~/.kigi/leader.log — keep it chatty for diagnosis.
.env("RUST_LOG", "kigi_shell=debug");
let (mut child, stderr) = spawn_piped_with_stderr_capture(cmd);
let outgoing = child.stdin.take().unwrap().compat_write();
let incoming = child.stdout.take().unwrap().compat();
let capture = Arc::new(Capture::default());
let client = LeaderAcpClient {
capture: capture.clone(),
};
let incoming = LineBufferedRead::spawn_local(incoming);
let (conn, handle_io) = acp::ClientSideConnection::new(client, outgoing, incoming, |fut| {
tokio::task::spawn_local(fut);
});
tokio::task::spawn_local(handle_io);
Self {
conn,
child,
capture,
stderr,
}
}
pub fn stderr_text(&self) -> String {
String::from_utf8_lossy(&self.stderr.lock().unwrap()).into_owned()
}
pub fn captured_text(&self) -> String {
self.capture.chunks.lock().unwrap().join("")
}
pub async fn initialize(&self) -> acp::InitializeResponse {
let init = tokio::time::timeout(
Duration::from_secs(60),
self.conn.initialize(
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
.client_capabilities(
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapabilities::new())
.terminal(false),
)
.meta(
serde_json::json!({
"startupHints": {
"nonInteractive": true,
"skipGitStatus": true,
"skipProjectLayout": true
},
"clientType": "test-client",
"clientVersion": "0.0.0-test"
})
.as_object()
.cloned(),
),
),
)
.await
.unwrap_or_else(|_| panic!("initialize timed out\nstderr:\n{}", self.stderr_text()))
.expect("initialize failed");
let api_key_method = init
.auth_methods
.iter()
.find(|m| &*m.id().0 == "xai.api_key")
.expect("xai.api_key auth method");
self.conn
.authenticate(
acp::AuthenticateRequest::new(api_key_method.id().clone())
.meta(serde_json::json!({"headless": true}).as_object().cloned()),
)
.await
.expect("authenticate failed");
init
}
pub async fn create_session(&self, cwd: &Path) -> acp::SessionId {
self.create_session_inner(cwd, None).await
}
pub async fn create_session_with_model(&self, cwd: &Path, model_id: &str) -> acp::SessionId {
self.create_session_inner(
cwd,
serde_json::json!({ "modelId": model_id })
.as_object()
.cloned(),
)
.await
}
async fn create_session_inner(&self, cwd: &Path, meta: Option<acp::Meta>) -> acp::SessionId {
tokio::time::timeout(
Duration::from_secs(30),
self.conn.new_session(
acp::NewSessionRequest::new(cwd.to_path_buf())
.mcp_servers(vec![])
.meta(meta),
),
)
.await
.unwrap_or_else(|_| panic!("session/new timed out\nstderr:\n{}", self.stderr_text()))
.expect("session/new failed")
.session_id
}
pub async fn prompt(
&self,
session_id: &acp::SessionId,
text: &str,
) -> acp::Result<acp::PromptResponse> {
tokio::time::timeout(
Duration::from_secs(30),
self.conn.prompt(acp::PromptRequest::new(
session_id.clone(),
vec![acp::ContentBlock::Text(acp::TextContent::new(
text.to_string(),
))],
)),
)
.await
.unwrap_or_else(|_| panic!("prompt timed out\nstderr:\n{}", self.stderr_text()))
}
pub fn reconnected_count(&self) -> u32 {
self.capture.reconnected_count.load(Ordering::SeqCst)
}
pub fn notification_count(&self) -> u32 {
self.capture.notification_count.load(Ordering::SeqCst)
}
}
pub fn leader_lock_path(home: &Path) -> PathBuf {
home.join(".kigi").join("leader.lock")
}
pub fn read_leader_pid(home: &Path) -> Option<u32> {
std::fs::read_to_string(leader_lock_path(home))
.ok()?
.trim()
.parse()
.ok()
}
pub fn pid_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as i32, 0) == 0 }
}
/// Wait until the leader lock file contains a live PID, return it.
pub async fn wait_for_live_leader(home: &Path, timeout: Duration) -> Option<u32> {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
if let Some(pid) = read_leader_pid(home)
&& pid_alive(pid)
{
return Some(pid);
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
None
}
/// Wait until the leader lock file contains a live PID *different* from `old_pid`.
pub async fn wait_for_new_leader(home: &Path, old_pid: u32, timeout: Duration) -> Option<u32> {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
if let Some(pid) = read_leader_pid(home)
&& pid != old_pid
&& pid_alive(pid)
{
return Some(pid);
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
None
}
/// Wait for evidence that the bridge finished its reconnect replay.
///
/// The `x.ai/leader_reconnected` ext notification is dropped by the typed
/// `ClientSideConnection` (bare `x.ai/*` methods are rejected by the ACP
/// decoder), so we wait for the replayed `session/load` to emit session
/// notifications instead: the notification count rises above `baseline`.
pub async fn wait_for_replay_notifications(
client: &LeaderStdioClient,
baseline: u32,
timeout: Duration,
) -> bool {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
if client.reconnected_count() > 0 || client.notification_count() > baseline {
return true;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
false
}
pub fn leader_log(home: &Path) -> String {
std::fs::read_to_string(home.join(".kigi").join("leader.log")).unwrap_or_default()
}