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,300 @@
//! Numeric ↔ string error-code mapping.
//!
//! Receivers SHOULD switch on `data.code` (the snake_case string) rather
//! than the numeric JSON-RPC `error.code`. The numeric is the JSON-RPC
//! envelope code; the string is the Grok stable identifier.
//!
//! Implemented as a `&'static [(i32, &'static str)]` table; the table is
//! a small fixed set so a linear scan is faster than any
//! `HashMap`/`OnceLock`-shaped alternative.
use serde::{Deserialize, Serialize};
use crate::error_wire::ToolErrorWire;
/// `(numeric_code, string_code)` pairs. Both columns are unique.
pub const ERROR_CODES: &[(i32, &str)] = &[
(-32700, "parse_error"),
(-32600, "invalid_request"),
(-32601, "method_not_found"),
(-32602, "invalid_params"),
(-32603, "internal_error"),
(-32605, "unsupported_protocol_version"),
(-32001, "timeout"),
(-32002, "unauthorized"),
(-32003, "forbidden"),
(-32004, "connection_lost"),
(-32005, "tool_server_gone"),
(-32006, "session_not_found"),
(-32008, "session_draining"),
(-32011, "tool_not_found"),
(-32012, "tool_already_registered"),
(-32013, "tool_unavailable"),
(-32014, "stale_generation"),
(-32015, "duplicate_client_name"),
(-32016, "tool_busy"),
(-32017, "notification_schema_violation"),
(-32018, "frame_too_large"),
(-32019, "schema_unknown_kind"),
(-32020, "behavior_version_unsupported"),
(-32021, "server_id_in_use"),
(-32022, "invalid_description"),
(-32023, "render_limited"),
(-32024, "terminal_error"),
(-32099, "rate_limited"),
];
/// Returns `None` for strings not in the table. Receivers should fall
/// back to `-32603 internal_error` for unknown strings.
pub fn numeric_for(code_str: &str) -> Option<i32> {
ERROR_CODES
.iter()
.find_map(|(n, s)| (*s == code_str).then_some(*n))
}
/// Returns `None` for codes not in the table.
pub fn string_for(code: i32) -> Option<&'static str> {
ERROR_CODES
.iter()
.find_map(|(n, s)| (*n == code).then_some(*s))
}
/// Numeric code most-appropriate for a [`ToolErrorWire`] variant.
/// `Custom` always maps to `-32603 internal_error` since its `code`
/// string is not in the table by definition.
pub fn from_tool_error_wire(err: &ToolErrorWire) -> i32 {
match err {
ToolErrorWire::ToolNotFound { .. } => -32011,
ToolErrorWire::SessionMismatch => -32600,
ToolErrorWire::PermissionDenied { .. } => -32003,
ToolErrorWire::TransportClosed { .. } => -32004,
ToolErrorWire::Timeout { .. } => -32001,
ToolErrorWire::Cancelled { .. } => -32603,
ToolErrorWire::InvalidArguments { .. } => -32602,
ToolErrorWire::Execution { .. } => -32603,
ToolErrorWire::UnsupportedProtocolVersion { .. } => -32605,
ToolErrorWire::PayloadTooLarge { .. } => -32018,
ToolErrorWire::BehaviorVersionUnsupported { .. } => -32020,
ToolErrorWire::Internal { .. } => -32603,
ToolErrorWire::RenderLimited { .. } => -32023,
ToolErrorWire::TerminalError { .. } => -32024,
ToolErrorWire::Custom { .. } => -32603,
}
}
/// Stable identifier for "this session's workspace (tool) server is gone;
/// re-provision and retry", used as both the [`ToolErrorWire::Custom`] subcode
/// and the `details["code"]` value. Reusing `Custom` (not a new variant) keeps
/// the frame deserializable on older peers.
pub const WORKSPACE_UNAVAILABLE_SUBCODE: &str = "workspace_unavailable";
/// Generic, tenant-data-free message paired with the workspace-gone error.
pub const WORKSPACE_UNAVAILABLE_MESSAGE: &str = "workspace server gone; re-provision and retry";
/// JSON-RPC envelope code paired with the workspace-unavailable error. Shares
/// the canonical `tool_server_gone` numeric; recognizers key on `data.subcode`,
/// not this companion.
pub const WORKSPACE_UNAVAILABLE_JSONRPC_CODE: i32 = -32005;
/// Why the workspace (tool) server went away. `Unknown` absorbs values a newer
/// peer may add, so the typed parse never fails across independently-deployed
/// hub/SDK versions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceGoneReason {
IdleTimeout,
Disconnect,
Shutdown,
/// No owner has bound a tool-server for the session yet (an attach-time
/// miss), as opposed to a workspace that was bound and then lost.
NotBound,
/// Target hub liveness key absent (origin reaper or forward-time check).
InstanceGone,
#[serde(other)]
Unknown,
}
/// When, relative to the failing tool call, the loss was observed. `Unknown`
/// absorbs values a newer peer may add.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceGonePhase {
InFlightCancelled,
RouteMissing,
/// Observed while resolving a `session_attach_server` request.
Attach,
#[serde(other)]
Unknown,
}
/// Structured payload placed in the wire `details` object. `code` mirrors the
/// `Custom` subcode (the `ToolError::custom` convention), so it survives a
/// `Wire → ToolError → Wire` round-trip and is the field recognizers read.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceUnavailableDetails {
pub code: String,
pub reason: WorkspaceGoneReason,
pub phase: WorkspaceGonePhase,
pub retryable: bool,
}
/// Build the recognizable "workspace gone" error as a [`ToolErrorWire::Custom`].
pub fn workspace_unavailable_wire(
reason: WorkspaceGoneReason,
phase: WorkspaceGonePhase,
) -> ToolErrorWire {
let details = serde_json::to_value(WorkspaceUnavailableDetails {
code: WORKSPACE_UNAVAILABLE_SUBCODE.to_owned(),
reason,
phase,
retryable: true,
});
// This plain struct serializes infallibly; a missing `details` would make
// the error unrecognizable, so guard the invariant in debug builds.
debug_assert!(details.is_ok(), "workspace details must serialize");
ToolErrorWire::Custom {
subcode: WORKSPACE_UNAVAILABLE_SUBCODE.to_owned(),
message: WORKSPACE_UNAVAILABLE_MESSAGE.to_owned(),
details: details.ok(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const REASONS: [WorkspaceGoneReason; 5] = [
WorkspaceGoneReason::IdleTimeout,
WorkspaceGoneReason::Disconnect,
WorkspaceGoneReason::Shutdown,
WorkspaceGoneReason::NotBound,
WorkspaceGoneReason::InstanceGone,
];
const PHASES: [WorkspaceGonePhase; 3] = [
WorkspaceGonePhase::InFlightCancelled,
WorkspaceGonePhase::RouteMissing,
WorkspaceGonePhase::Attach,
];
// Exhaustive-match helpers pin the exact snake_case wire strings; adding a
// variant forces an update here.
fn reason_wire(r: WorkspaceGoneReason) -> &'static str {
match r {
WorkspaceGoneReason::IdleTimeout => "idle_timeout",
WorkspaceGoneReason::Disconnect => "disconnect",
WorkspaceGoneReason::Shutdown => "shutdown",
WorkspaceGoneReason::NotBound => "not_bound",
WorkspaceGoneReason::InstanceGone => "instance_gone",
WorkspaceGoneReason::Unknown => "unknown",
}
}
fn phase_wire(p: WorkspaceGonePhase) -> &'static str {
match p {
WorkspaceGonePhase::InFlightCancelled => "in_flight_cancelled",
WorkspaceGonePhase::RouteMissing => "route_missing",
WorkspaceGonePhase::Attach => "attach",
WorkspaceGonePhase::Unknown => "unknown",
}
}
#[test]
fn builder_emits_custom_with_code_in_details_for_every_reason_and_phase() {
for reason in REASONS {
for phase in PHASES {
let v = serde_json::to_value(workspace_unavailable_wire(reason, phase)).unwrap();
assert_eq!(v["code"], json!("custom"), "outer discriminator");
assert_eq!(v["subcode"], json!(WORKSPACE_UNAVAILABLE_SUBCODE));
// details.code mirrors the subcode (round-trip identity).
assert_eq!(v["details"]["code"], json!(WORKSPACE_UNAVAILABLE_SUBCODE));
assert_eq!(v["details"]["reason"], json!(reason_wire(reason)));
assert_eq!(v["details"]["phase"], json!(phase_wire(phase)));
assert_eq!(v["details"]["retryable"], json!(true));
}
}
}
#[test]
fn builder_uses_the_pinned_generic_message() {
let ToolErrorWire::Custom { message, .. } = workspace_unavailable_wire(
WorkspaceGoneReason::IdleTimeout,
WorkspaceGonePhase::RouteMissing,
) else {
panic!("expected Custom variant");
};
// Exact, tenant-data-free contract.
assert_eq!(message, WORKSPACE_UNAVAILABLE_MESSAGE);
}
#[test]
fn unknown_reason_and_phase_deserialize_to_unknown() {
// Independently-deployed peers may emit reason/phase values this build
// does not know; the typed parse must absorb them, not fail.
let details: WorkspaceUnavailableDetails = serde_json::from_value(json!({
"code": WORKSPACE_UNAVAILABLE_SUBCODE,
"reason": "reason_from_a_newer_hub",
"phase": "phase_from_a_newer_hub",
"retryable": true,
}))
.expect("typed parse tolerates unknown enum values");
assert_eq!(details.reason, WorkspaceGoneReason::Unknown);
assert_eq!(details.phase, WorkspaceGonePhase::Unknown);
}
#[test]
fn unknown_reason_serializes_and_round_trips() {
// The route-missing classifier emits `Unknown` ("cause not observed"),
// so — despite `Unknown` being the `#[serde(other)]` deserialize
// catch-all — it must serialize to a stable `"unknown"` label and parse
// back, both in the wire payload and as the bare enum.
assert_eq!(
serde_json::to_value(WorkspaceGoneReason::Unknown).unwrap(),
json!("unknown"),
);
let wire = workspace_unavailable_wire(
WorkspaceGoneReason::Unknown,
WorkspaceGonePhase::RouteMissing,
);
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["details"]["reason"], json!("unknown"));
let parsed: WorkspaceUnavailableDetails =
serde_json::from_value(v["details"].clone()).expect("details round-trip");
assert_eq!(parsed.reason, WorkspaceGoneReason::Unknown);
}
#[test]
fn custom_variant_tolerates_unknown_future_details_shape() {
// An unknown subcode + richer future details must still deserialize rather than failing the frame.
let future = json!({
"code": "custom",
"subcode": "some_future_subcode",
"message": "from a newer peer",
"details": {
"code": "some_future_subcode",
"extra_new_field": {"nested": [1, 2, 3]},
},
});
let wire: ToolErrorWire =
serde_json::from_value(future).expect("custom variant deserializes");
let ToolErrorWire::Custom {
subcode, details, ..
} = &wire
else {
panic!("expected Custom variant");
};
assert_eq!(subcode, "some_future_subcode");
assert!(
details
.as_ref()
.and_then(|d| d.get("extra_new_field"))
.is_some(),
"unknown details fields are preserved",
);
// Re-serialization preserves the unknown fields.
let reser = serde_json::to_value(&wire).unwrap();
assert_eq!(
reser["details"]["extra_new_field"]["nested"],
json!([1, 2, 3])
);
}
}