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,208 @@
//! Black-box repro of rmcp's zero-backoff SSE reconnect loop (and of
//! `McpHttpClient` bounding it), against a fake MCP streamable-HTTP server
//! whose standing-GET behavior is the variable under test. Each GET the fake
//! server counts corresponds to one rmcp `WARN sse stream error: ...` line.
//!
//! Run with: cargo test -p kigi-mcp --test repro_sse_flood -- --nocapture
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use axum::Json;
use axum::body::Body;
use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use futures::StreamExt;
use serde_json::{Value, json};
use kigi_mcp::mcp_http_client::{McpHttpClient, WarnBudget};
use kigi_mcp::rmcp::ServiceExt;
use kigi_mcp::rmcp::transport::StreamableHttpClientTransport;
use kigi_mcp::rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
/// Gap letting the first body chunk flush before the abort, so the client
/// sees a *body* death (rmcp's zero-backoff flood path) rather than a failed
/// request (rmcp's policy-backed path).
const FLUSH_GAP: Duration = Duration::from_millis(5);
/// How the fake server ends the standing GET stream.
#[derive(Clone, Copy)]
enum GetBehavior {
/// 200, then the body is aborted mid-stream — the production failure
/// mode ("error decoding response body" -> the WARN flood path).
AbnormalBodyDeath,
/// 200, then the stream stays open (a working MCP server).
Healthy,
}
#[derive(Clone)]
struct ServerState {
behavior: GetBehavior,
gets: Arc<AtomicUsize>,
}
async fn handle_post(Json(req): Json<Value>) -> Response {
match req["method"].as_str() {
Some("initialize") => {
let result = json!({
"jsonrpc": "2.0",
"id": req["id"],
"result": {
"protocolVersion": req["params"]["protocolVersion"],
"capabilities": {},
"serverInfo": {"name": "fake", "version": "0.0.0"},
},
});
([("mcp-session-id", "fake-session-1")], Json(result)).into_response()
}
Some("tools/list") => {
let result = json!({
"jsonrpc": "2.0",
"id": req["id"],
"result": {"tools": [{"name": "echo", "inputSchema": {"type": "object"}}]},
});
Json(result).into_response()
}
// notifications/initialized and anything else.
_ => StatusCode::ACCEPTED.into_response(),
}
}
async fn handle_get(State(state): State<ServerState>) -> Response {
state.gets.fetch_add(1, Ordering::Relaxed);
let body = match state.behavior {
GetBehavior::AbnormalBodyDeath => Body::from_stream(
futures::stream::iter([Ok::<_, std::io::Error>(": partial\n".to_owned())]).chain(
futures::stream::once(async {
tokio::time::sleep(FLUSH_GAP).await;
Err(std::io::Error::other("body aborted mid-stream"))
}),
),
),
GetBehavior::Healthy => {
Body::from_stream(futures::stream::pending::<Result<String, std::io::Error>>())
}
};
([(header::CONTENT_TYPE, "text/event-stream")], body).into_response()
}
async fn spawn_fake_server(behavior: GetBehavior) -> (String, Arc<AtomicUsize>) {
let gets = Arc::new(AtomicUsize::new(0));
let app = axum::Router::new()
.route("/mcp", get(handle_get).post(handle_post))
.with_state(ServerState {
behavior,
gets: gets.clone(),
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("addr");
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
(format!("http://{addr}/mcp"), gets)
}
/// The flood: body death on the standing GET -> zero-backoff reconnect loop,
/// one WARN per GET (set RUST_LOG=rmcp=warn to see them).
#[tokio::test(flavor = "multi_thread")]
async fn repro_zero_backoff_reconnect_flood() {
let (url, gets) = spawn_fake_server(GetBehavior::AbnormalBodyDeath).await;
let transport = StreamableHttpClientTransport::from_uri(url.as_str());
let client = ().serve(transport).await.expect("handshake against fake server should succeed");
const OBSERVE: Duration = Duration::from_secs(3);
tokio::time::sleep(OBSERVE).await;
let n = gets.load(Ordering::Relaxed);
let _ = client.cancel().await;
eprintln!(
"[repro] {n} standing-GET reconnect attempts (= WARN log lines) in {OBSERVE:?} \
= {:.0}/sec",
n as f64 / OBSERVE.as_secs_f64()
);
// A backoff-respecting client would attempt ~3-5 in 3s; the bug produces
// hundreds-to-thousands.
assert!(
n > 20,
"expected a zero-backoff reconnect flood (>20 GETs in {OBSERVE:?}), got {n}; \
if this FAILS with a small n, the rmcp loop got fixed - delete mcp_http_client.rs"
);
}
/// The fix: same body-killing server, client wrapped in `McpHttpClient` —
/// the backoff schedule allows only a handful of reconnects.
#[tokio::test(flavor = "multi_thread")]
async fn throttled_client_bounds_the_flood() {
let (url, gets) = spawn_fake_server(GetBehavior::AbnormalBodyDeath).await;
let throttled = McpHttpClient::new(
reqwest::Client::default(),
"fake-server",
WarnBudget::default(),
);
let transport = StreamableHttpClientTransport::with_client(
throttled,
StreamableHttpClientTransportConfig::with_uri(url.as_str()),
);
let client = ().serve(transport).await.expect("handshake against fake server should succeed");
// Checkpoint: backoff must engage early (instant, instant, 0.5s => 3-4
// GETs by 1.2s; an unthrottled client would be in the hundreds).
tokio::time::sleep(Duration::from_millis(1200)).await;
let early = gets.load(Ordering::Relaxed);
assert!(
early <= 4,
"backoff must engage early; got {early} GETs by 1.2s"
);
const OBSERVE: Duration = Duration::from_secs(4);
tokio::time::sleep(OBSERVE - Duration::from_millis(1200)).await;
let n = gets.load(Ordering::Relaxed);
let _ = client.cancel().await;
eprintln!("[repro] {n} throttled reconnect attempts in {OBSERVE:?} (schedule allows ~5-6)");
assert!(
(3..=8).contains(&n),
"throttled reconnects should follow the backoff schedule (~5-6 in {OBSERVE:?}), got {n}"
);
}
/// A working server through the throttled client: handshake + tools/list
/// succeed and the standing GET opens exactly once — wrapper invisible.
#[tokio::test(flavor = "multi_thread")]
async fn throttled_client_does_not_affect_healthy_server() {
let (url, gets) = spawn_fake_server(GetBehavior::Healthy).await;
let throttled = McpHttpClient::new(
reqwest::Client::default(),
"fake-server",
WarnBudget::default(),
);
let client = ()
.serve(StreamableHttpClientTransport::with_client(
throttled,
StreamableHttpClientTransportConfig::with_uri(url.as_str()),
))
.await
.expect("handshake against healthy server should succeed");
let tools = client
.list_tools(Default::default())
.await
.expect("tools/list should succeed through the throttled client");
assert_eq!(tools.tools.len(), 1);
assert_eq!(tools.tools[0].name, "echo");
tokio::time::sleep(Duration::from_secs(3)).await;
let n = gets.load(Ordering::Relaxed);
let _ = client.cancel().await;
eprintln!("[repro] healthy server: {n} GET(s) in 3s, tools/list ok");
assert_eq!(
n, 1,
"healthy stream must be opened once and never reconnected"
);
}