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,366 @@
//! Tool taxonomy — the harness-independent vocabulary, identity, and canonical
//! `_meta` envelope.
//!
//! Depends only on `ToolKind`/`ToolNamespace` + `serde`/`serde_json` (no
//! `ToolInput`, proto, or runtime). A future `xai-tool-taxonomy` leaf crate
//! would need those two (dependency-free) enums moved here too — coherence ties
//! the inherent impls to the enum definitions. The `ToolInput`-coupled
//! projection lives in [`crate::normalization`].
use crate::types::tool::{ToolKind, ToolNamespace};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
/// Canonical input field names — the one vocabulary every harness normalizes
/// onto. Emit canonical keys through these so the wire contract has one source.
pub mod field {
pub const PATH: &str = "path";
pub const OFFSET: &str = "offset";
pub const LIMIT: &str = "limit";
pub const COMMAND: &str = "command";
pub const DESCRIPTION: &str = "description";
pub const CWD: &str = "cwd";
pub const DIRECTORY: &str = "directory";
pub const PATTERN: &str = "pattern";
}
/// The single `_meta` key holding the canonical tool identity as one nested
/// object (mirroring `x.ai/mcp_tool`). Consumers deserialize it into
/// [`CanonicalToolMeta`].
pub const TOOL_META_KEY: &str = "x.ai/tool";
/// Version of the canonical tool `_meta` contract. Bump on any breaking change
/// to keys or value shapes so consumers can adapt.
pub const TOOL_META_VERSION: u32 = 1;
impl ToolKind {
/// Unified, harness-independent display label for this semantic kind. A pure
/// function of the kind, so equivalent tools across toolsets share it
/// (`read_file` and `Read` → `Read`; `run_terminal_cmd` and `Shell` →
/// `Run Command`). Display only; the model's tool name is `name` in
/// `x.ai/tool`. Exhaustive, so a new `ToolKind` must add a label to compile.
pub fn presentation_name(self) -> &'static str {
match self {
ToolKind::Read => "Read",
ToolKind::Edit => "Edit",
ToolKind::Delete => "Delete",
ToolKind::Write => "Write",
ToolKind::Move => "Move",
ToolKind::ListDir => "List Files",
ToolKind::List => "List Files",
ToolKind::Search => "Search",
ToolKind::Lsp => "Code Intelligence",
ToolKind::Execute => "Run Command",
ToolKind::Plan => "Plan",
ToolKind::WebSearch => "Web Search",
ToolKind::WebFetch => "Web Fetch",
ToolKind::BackgroundTaskAction => "Background Task",
ToolKind::WaitTasksAction => "Wait for Tasks",
ToolKind::KillTaskAction => "Kill Task",
ToolKind::Skill => "Skill",
ToolKind::MemorySearch => "Memory Search",
ToolKind::MemoryGet => "Memory Read",
ToolKind::Task => "Subagent",
ToolKind::EnterPlan => "Enter Plan Mode",
ToolKind::ExitPlan => "Exit Plan Mode",
ToolKind::AskUser => "Ask User",
ToolKind::ImageGen => "Generate Image",
ToolKind::VideoGen => "Generate Video",
ToolKind::ImageToVideo => "Generate Video",
ToolKind::ReferenceToVideo => "Generate Video",
ToolKind::DeployApp => "Deploy App",
ToolKind::SearchTool => "Search Tools",
ToolKind::UseTool => "Use Tool",
ToolKind::Monitor => "Monitor",
ToolKind::GoalUpdate => "Update Goal",
ToolKind::Other => "Tool",
}
}
/// Whether this kind only reads (no workspace or external mutation) by
/// default. The kind-level default for `ToolMetadata::is_read_only`, which
/// individual tools may override. Exhaustive (no `_`) so a new kind must
/// classify itself rather than silently defaulting to "mutating".
pub fn is_read_only(self) -> bool {
match self {
ToolKind::Read
| ToolKind::Search
| ToolKind::Lsp
| ToolKind::ListDir
| ToolKind::List
| ToolKind::MemorySearch
| ToolKind::MemoryGet
| ToolKind::WebSearch
| ToolKind::WebFetch
| ToolKind::EnterPlan
| ToolKind::ExitPlan
| ToolKind::AskUser => true,
ToolKind::Edit
| ToolKind::Delete
| ToolKind::Write
| ToolKind::Move
| ToolKind::Execute
| ToolKind::Plan
| ToolKind::BackgroundTaskAction
| ToolKind::WaitTasksAction
| ToolKind::KillTaskAction
| ToolKind::Skill
| ToolKind::Task
| ToolKind::ImageGen
| ToolKind::VideoGen
| ToolKind::ImageToVideo
| ToolKind::ReferenceToVideo
| ToolKind::DeployApp
| ToolKind::SearchTool
| ToolKind::UseTool
| ToolKind::Monitor
| ToolKind::GoalUpdate
| ToolKind::Other => false,
}
}
}
impl schemars::JsonSchema for ToolKind {
fn schema_name() -> Cow<'static, str> {
"ToolKind".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
use strum::IntoEnumIterator;
let known = Self::iter()
.filter_map(|k| serde_json::to_value(k).ok())
.filter_map(|v| v.as_str().map(|s| format!("`{s}`")))
.collect::<Vec<_>>()
.join(", ");
schemars::json_schema!(
{ "type" : "string", "description" :
format!("Categorizes what a tool does at a high level. Open set — consumers must \
tolerate unknown values (Rust deserializes them to `other` via \
`#[serde(other)]`). Known values: {known}."),
}
)
}
}
/// Canonical identity for a tool call, resolved from a tool's registered
/// metadata by its client-facing wire name.
///
/// Harness-independent. `tool_kind` is the authoritative `metadata.kind()`.
#[derive(Debug, Clone, Copy)]
pub struct ToolIdentity {
pub tool_kind: ToolKind,
pub namespace: ToolNamespace,
pub presentation_name: &'static str,
pub read_only: bool,
}
/// The canonical tool-identity envelope, attached to a tool-call event `_meta`
/// as one nested object under [`TOOL_META_KEY`].
///
/// ```json
/// "x.ai/tool": {
/// "version": 1,
/// "name": "read_file",
/// "kind": "read",
/// "namespace": "grok_build",
/// "label": "Read",
/// "read_only": true,
/// "input": { "path": "..." }
/// }
/// ```
///
/// Consumer contract:
/// - **`label`** is the cross-harness grouping/display key: equivalent tools
/// share it (grok `read_file` → `"Read"`).
/// - **`kind`** is a finer discriminator (`metadata.kind()`), *not* guaranteed
/// equal for equivalent ops across harnesses (listing is `list` in one
/// toolset, `list_dir` in another); prefer `label` to join, tolerate unknowns.
/// - **`name`** is the harness-specific model-facing name; for diagnostics.
/// For harness-initiated events (e.g. the `bash_mode` marker), `raw_input`
/// is not guaranteed to match `name`'s schema.
/// - **`input`** is a canonical *projection*, not a mirror: cross-harness keys
/// only, so some raw fields are intentionally dropped (e.g. grep flags,
/// `replace_all`), and bulky payload
/// fields (edit `old_string`/`new_string`, full write contents) are never
/// projected — read them from `raw_input`. It is omitted entirely
/// when no stable shape exists (MCP / dynamic / out-of-scope). When a field or
/// the whole dict is absent, fall back to `raw_input` on this or an earlier
/// update for the same `tool_call_id` (some updates, e.g. a parse failure,
/// carry neither and rely on the merge below).
/// - **Lifecycle:** updates for one call share a `tool_call_id` — merge across
/// them (last write wins); `input` may arrive on a later update.
/// - **Versioning:** additive changes (new object fields, new `kind` / `label`
/// values) don't bump `version`. Unknown `kind` degrades to `"other"`;
/// `namespace` is a closed enum (no `other` sink), so a new toolset fails
/// strict typed deserialization of the whole envelope — intentional, to force
/// typed consumers with exhaustive matches to update. Out-of-tree consumers
/// should read `namespace` loosely (as a string) and, on any `x.ai/tool`
/// parse failure, treat it as absent and fall back to `raw_input` + the ACP
/// `kind`. `version` bumps only on removal or meaning change.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CanonicalToolMeta {
pub version: u32,
pub name: String,
pub kind: ToolKind,
pub namespace: ToolNamespace,
pub label: Cow<'static, str>,
pub read_only: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<serde_json::Value>,
}
impl CanonicalToolMeta {
/// Build from a resolved identity + already-projected `input`. `ToolInput`-free
/// so the type stays a leaf (projection lives in `normalization`).
pub fn new(
name: impl Into<String>,
identity: &ToolIdentity,
input: Option<serde_json::Value>,
) -> Self {
Self {
version: TOOL_META_VERSION,
name: name.into(),
kind: identity.tool_kind,
namespace: identity.namespace,
label: Cow::Borrowed(identity.presentation_name),
read_only: identity.read_only,
input,
}
}
/// Attach under [`TOOL_META_KEY`], preserving existing `_meta` keys
/// (`bash_mode`, `backend`, `x.ai/mcp_tool`, …).
pub fn merge_into(&self, existing: Option<serde_json::Value>) -> serde_json::Value {
debug_assert!(
matches!(existing, None | Some(serde_json::Value::Object(_))),
"_meta is always absent or an object"
);
let mut map = match existing {
Some(serde_json::Value::Object(m)) => m,
Some(other) => return other,
None => serde_json::Map::new(),
};
let value = serde_json::to_value(self).expect("CanonicalToolMeta serializes");
map.insert(TOOL_META_KEY.to_string(), value);
serde_json::Value::Object(map)
}
}
/// The published JSON Schema (draft-07) for the [`CanonicalToolMeta`] wire
/// envelope (`schema/tool_meta.schema.json`). Non-Rust consumers codegen from
/// it; kept in sync with the type by `tool_meta_schema_is_up_to_date`.
pub fn tool_meta_json_schema_str() -> &'static str {
include_str!("../schema/tool_meta.schema.json")
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(kind: ToolKind) -> ToolIdentity {
ToolIdentity {
tool_kind: kind,
namespace: ToolNamespace::GrokBuild,
presentation_name: kind.presentation_name(),
read_only: kind.is_read_only(),
}
}
#[test]
fn is_read_only_classifies_kinds() {
assert!(ToolKind::Read.is_read_only());
assert!(ToolKind::Search.is_read_only());
assert!(ToolKind::List.is_read_only());
assert!(!ToolKind::Edit.is_read_only());
assert!(!ToolKind::Execute.is_read_only());
assert!(!ToolKind::Delete.is_read_only());
}
#[test]
fn namespace_round_trips_snake_case_with_pascal_aliases() {
use strum::IntoEnumIterator;
fn wire_and_pascal(ns: ToolNamespace) -> (&'static str, &'static str) {
match ns {
ToolNamespace::GrokBuild => ("grok_build", "GrokBuild"),
ToolNamespace::GrokBuildConcise => ("grok_build_concise", "GrokBuildConcise"),
ToolNamespace::GrokBuildHashline => ("grok_build_hashline", "GrokBuildHashline"),
ToolNamespace::Codex => ("codex", "Codex"),
ToolNamespace::OpenCode => ("opencode", "OpenCode"),
ToolNamespace::MCP => ("mcp", "MCP"),
}
}
for ns in ToolNamespace::iter() {
let (snake, pascal) = wire_and_pascal(ns);
assert_eq!(serde_json::to_value(ns).unwrap(), serde_json::json!(snake));
assert_eq!(
serde_json::from_value::<ToolNamespace>(serde_json::json!(snake)).unwrap(),
ns
);
assert_eq!(
serde_json::from_value::<ToolNamespace>(serde_json::json!(pascal)).unwrap(),
ns
);
}
}
#[test]
fn unknown_kind_degrades_to_other() {
let k: ToolKind = serde_json::from_value(serde_json::json!("teleport")).unwrap();
assert_eq!(k, ToolKind::Other);
}
/// The published `kind` schema must stay an open string (codegen'd
/// consumers would otherwise hard-fail on new kinds, contradicting the
/// `#[serde(other)]` contract above). `namespace` stays intentionally
/// closed — see the versioning contract on [`CanonicalToolMeta`].
#[test]
fn kind_schema_is_open_string_namespace_stays_closed() {
let kind = serde_json::to_value(schemars::schema_for!(ToolKind)).unwrap();
assert_eq!(kind["type"], "string");
assert!(kind.get("enum").is_none(), "kind must not be a closed enum");
assert!(
kind["description"].as_str().unwrap().contains("`read`"),
"known values must be listed in the description"
);
let ns = serde_json::to_value(schemars::schema_for!(ToolNamespace)).unwrap();
assert!(ns.get("enum").is_some(), "namespace is a closed enum");
}
#[test]
fn canonical_meta_wire_shape_round_trips() {
let meta = CanonicalToolMeta::new(
"read_file",
&identity(ToolKind::Read),
Some(serde_json::json!({ "path" : "/a" })),
);
let t = serde_json::to_value(&meta).unwrap();
assert_eq!(t["version"], serde_json::json!(TOOL_META_VERSION));
assert_eq!(t["name"], "read_file");
assert_eq!(t["kind"], "read");
assert_eq!(t["namespace"], "grok_build");
assert_eq!(t["label"], "Read");
assert_eq!(t["read_only"], true);
assert_eq!(t["input"]["path"], "/a");
assert_eq!(
serde_json::from_value::<CanonicalToolMeta>(t).unwrap(),
meta
);
}
/// The checked-in schema (the artifact non-Rust consumers codegen from) must
/// track the type. Regenerate with `UPDATE_TOOL_META_SCHEMA=1`.
#[test]
fn tool_meta_schema_is_up_to_date() {
let generator = schemars::generate::SchemaSettings::draft07().into_generator();
let schema = serde_json::to_value(generator.into_root_schema_for::<CanonicalToolMeta>())
.expect("schema serializes");
let generated = format!("{}\n", serde_json::to_string_pretty(&schema).unwrap());
if std::env::var("UPDATE_TOOL_META_SCHEMA").is_ok() {
std::fs::write(
concat!(env!("CARGO_MANIFEST_DIR"), "/schema/tool_meta.schema.json"),
&generated,
)
.unwrap();
return;
}
let mut expected: serde_json::Value =
serde_json::from_str(tool_meta_json_schema_str()).expect("checked-in schema parses");
if let Some(values) = expected["definitions"]["ToolNamespace"]["enum"].as_array_mut() {
values.retain(|v| v != "cursor");
}
let expected = format!("{}\n", serde_json::to_string_pretty(&expected).unwrap());
assert_eq!(
generated, expected,
"tool_meta.schema.json is stale; regenerate with UPDATE_TOOL_META_SCHEMA=1"
);
}
#[test]
fn merge_into_nests_under_one_key_and_preserves_existing() {
let meta = CanonicalToolMeta::new("run_terminal_cmd", &identity(ToolKind::Execute), None);
let merged = meta.merge_into(Some(serde_json::json!({ "bash_mode" : true })));
let o = merged.as_object().unwrap();
assert_eq!(o["bash_mode"], true, "existing meta must be preserved");
let t = &o[TOOL_META_KEY];
assert_eq!(t["kind"], "execute");
assert!(t.get("input").is_none(), "absent input omitted");
}
}