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,590 @@
#![allow(dead_code)] // Phase 1 internal helpers
use crate::permission::types::EditPolicy;
use kigi_paths::AbsPathBuf;
use kigi_tools::util::kigi_home::kigi_home;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PermissionState {
pub edit_policy: EditPolicy,
pub allow_bash_execute: bool,
pub allowed_bash_commands: HashSet<String>,
pub disallowed_bash_commands: HashSet<String>,
/// Domains the user has approved for `web_fetch`
/// during this session.
pub allowed_web_fetch_domains: HashSet<String>,
/// Exact MCP tool names (e.g. `"grok_com_notion__notion-fetch"`)
/// the user has granted "always allow" for. Lookup is exact.
pub allowed_mcp_tools: HashSet<String>,
/// MCP server prefixes (everything before the first `__`,
/// e.g. `"grok_com_notion"`) for which the user has granted
/// "always allow" to every tool. Lookup is "tool name starts with
/// `<prefix>__`".
pub allowed_mcp_servers: HashSet<String>,
}
fn state_dir_for_cwd(cwd: &AbsPathBuf) -> std::path::PathBuf {
kigi_config::sessions_cwd_dir(cwd.as_str())
}
fn sanitize_client_id(id: &str) -> String {
id.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}
fn state_file_path(dir: &std::path::Path, client_identifier: Option<&str>) -> std::path::PathBuf {
match client_identifier {
Some(id) => dir.join(format!("permission_{}.toml", sanitize_client_id(id))),
None => dir.join("permission.toml"),
}
}
async fn try_load_state(path: &std::path::Path) -> Option<PermissionState> {
match tokio::fs::read_to_string(path).await {
Ok(s) => Some(toml::from_str(&s).unwrap_or_default()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
tracing::warn!(?e, "failed reading permission state");
None
}
}
}
async fn load_state_from_dir(
dir: &std::path::Path,
client_identifier: Option<&str>,
) -> PermissionState {
if let Some(id) = client_identifier {
let per_client = state_file_path(dir, Some(id));
if let Some(state) = try_load_state(&per_client).await {
return state;
}
let shared = state_file_path(dir, None);
try_load_state(&shared).await.unwrap_or_default()
} else {
let path = state_file_path(dir, None);
try_load_state(&path).await.unwrap_or_default()
}
}
pub(crate) async fn load_state_from_disk(
cwd: &AbsPathBuf,
client_identifier: Option<&str>,
) -> PermissionState {
load_state_from_dir(&state_dir_for_cwd(cwd), client_identifier).await
}
async fn persist_state_to_dir(
dir: &std::path::Path,
state: &PermissionState,
client_identifier: Option<&str>,
) {
if let Err(e) = tokio::fs::create_dir_all(dir).await {
tracing::warn!(?e, "failed creating permission state directory");
return;
}
let path = state_file_path(dir, client_identifier);
match toml::to_string_pretty(state) {
Ok(s) => {
if let Err(e) = tokio::fs::write(&path, s).await {
tracing::warn!(?e, "failed writing permission state");
}
}
Err(e) => tracing::warn!(?e, "failed serializing permission state"),
}
}
pub(crate) async fn persist_state(
cwd: &AbsPathBuf,
state: &PermissionState,
client_identifier: Option<&str>,
) {
persist_state_to_dir(&state_dir_for_cwd(cwd), state, client_identifier).await
}
pub async fn cleanup_stale_permission_state(max_age: std::time::Duration) {
let sessions_dir = kigi_home().join("sessions");
let Ok(mut entries) = tokio::fs::read_dir(&sessions_dir).await else {
return;
};
while let Ok(Some(session_entry)) = entries.next_entry().await {
let Ok(ft) = session_entry.file_type().await else {
continue;
};
if !ft.is_dir() {
continue;
}
let session_dir = session_entry.path();
let Ok(mut files) = tokio::fs::read_dir(&session_dir).await else {
continue;
};
while let Ok(Some(file_entry)) = files.next_entry().await {
let path = file_entry.path();
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !file_name.starts_with("permission") || !file_name.ends_with(".toml") {
continue;
}
if let Ok(metadata) = tokio::fs::metadata(&path).await
&& let Ok(modified) = metadata.modified()
&& let Ok(age) = modified.elapsed()
&& age > max_age
{
tracing::debug!(path = %path.display(), "removing stale permission state");
let _ = tokio::fs::remove_file(&path).await;
}
}
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
// ── PermissionState serialization roundtrip tests ─────────────
#[test]
fn default_state_serialization() {
let state = PermissionState::default();
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert!(!restored.allow_bash_execute);
assert!(restored.allowed_bash_commands.is_empty());
assert!(restored.disallowed_bash_commands.is_empty());
}
#[test]
fn roundtrip_with_allowed_commands() {
let mut state = PermissionState::default();
state.allow_bash_execute = true;
state.allowed_bash_commands.insert("cargo test".to_string());
state
.allowed_bash_commands
.insert("npm run build".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert!(restored.allow_bash_execute);
assert!(restored.allowed_bash_commands.contains("cargo test"));
assert!(restored.allowed_bash_commands.contains("npm run build"));
assert_eq!(restored.allowed_bash_commands.len(), 2);
}
#[test]
fn roundtrip_with_disallowed_commands() {
let mut state = PermissionState::default();
state.disallowed_bash_commands.insert("rm -rf".to_string());
state
.disallowed_bash_commands
.insert("git push --force".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
let denied = &restored.disallowed_bash_commands;
assert!(denied.contains("rm -rf"));
assert!(denied.contains("git push --force"));
assert_eq!(denied.len(), 2);
}
#[test]
fn roundtrip_with_both_allowed_and_disallowed() {
// Simulate a real scenario: some commands explicitly allowed,
// others explicitly denied.
let mut state = PermissionState::default();
state.allow_bash_execute = false;
state.allowed_bash_commands.insert("cargo test".to_string());
state.allowed_bash_commands.insert("git status".to_string());
state
.disallowed_bash_commands
.insert("rm -rf /".to_string());
state.disallowed_bash_commands.insert("curl".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert!(!restored.allow_bash_execute);
assert_eq!(restored.allowed_bash_commands.len(), 2);
assert_eq!(restored.disallowed_bash_commands.len(), 2);
assert!(restored.allowed_bash_commands.contains("cargo test"));
assert!(restored.disallowed_bash_commands.contains("curl"));
}
#[test]
fn edit_policy_is_persisted() {
let mut state = PermissionState::default();
state.edit_policy = EditPolicy::Allow;
let toml_str = toml::to_string_pretty(&state).unwrap();
assert!(toml_str.contains("edit_policy"));
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.edit_policy, EditPolicy::Allow);
}
#[test]
fn edit_policy_reject_roundtrip() {
let mut state = PermissionState::default();
state.edit_policy = EditPolicy::Reject;
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.edit_policy, EditPolicy::Reject);
}
#[test]
fn missing_edit_policy_defaults_to_ask() {
let toml_str = r#"allow_bash_execute = false"#;
let state: PermissionState = toml::from_str(toml_str).unwrap();
assert_eq!(state.edit_policy, EditPolicy::Ask);
}
#[test]
fn deserialize_from_empty_toml() {
let state: PermissionState = toml::from_str("").unwrap();
assert!(!state.allow_bash_execute);
assert!(state.allowed_bash_commands.is_empty());
assert!(state.disallowed_bash_commands.is_empty());
}
#[test]
fn deserialize_partial_toml() {
// Only some fields present — others should default.
let toml_str = r#"allow_bash_execute = true"#;
let state: PermissionState = toml::from_str(toml_str).unwrap();
assert!(state.allow_bash_execute);
assert!(state.allowed_bash_commands.is_empty());
assert!(state.disallowed_bash_commands.is_empty());
}
#[test]
fn roundtrip_with_allowed_web_fetch_domains() {
let mut state = PermissionState::default();
state
.allowed_web_fetch_domains
.insert("stackoverflow.com".to_string());
state
.allowed_web_fetch_domains
.insert("custom.example.com".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.allowed_web_fetch_domains.len(), 2);
assert!(
restored
.allowed_web_fetch_domains
.contains("stackoverflow.com")
);
assert!(
restored
.allowed_web_fetch_domains
.contains("custom.example.com")
);
}
#[test]
fn roundtrip_with_allowed_mcp_tools() {
let mut state = PermissionState::default();
state
.allowed_mcp_tools
.insert("grok_com_notion__notion-fetch".to_string());
state
.allowed_mcp_tools
.insert("linear__list_issues".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.allowed_mcp_tools.len(), 2);
assert!(
restored
.allowed_mcp_tools
.contains("grok_com_notion__notion-fetch")
);
assert!(restored.allowed_mcp_tools.contains("linear__list_issues"));
assert!(restored.allowed_mcp_servers.is_empty());
}
#[test]
fn roundtrip_with_allowed_mcp_servers() {
let mut state = PermissionState::default();
state
.allowed_mcp_servers
.insert("grok_com_slack".to_string());
state.allowed_mcp_servers.insert("linear".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.allowed_mcp_servers.len(), 2);
assert!(restored.allowed_mcp_servers.contains("grok_com_slack"));
assert!(restored.allowed_mcp_servers.contains("linear"));
assert!(restored.allowed_mcp_tools.is_empty());
}
#[test]
fn roundtrip_with_both_mcp_sets() {
let mut state = PermissionState::default();
state.allowed_mcp_tools.insert("notion__fetch".to_string());
state.allowed_mcp_servers.insert("linear".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.allowed_mcp_tools.len(), 1);
assert_eq!(restored.allowed_mcp_servers.len(), 1);
assert!(restored.allowed_mcp_tools.contains("notion__fetch"));
assert!(restored.allowed_mcp_servers.contains("linear"));
}
#[test]
fn deserialize_old_state_without_mcp_fields() {
// A state file from a binary that predates this design has
// neither MCP field. #[serde(default)] should yield empty sets.
let toml_str = r#"
allow_bash_execute = true
allowed_bash_commands = ["cargo test"]
allowed_web_fetch_domains = ["github.com"]
"#;
let state: PermissionState = toml::from_str(toml_str).unwrap();
assert!(state.allow_bash_execute);
assert!(state.allowed_bash_commands.contains("cargo test"));
assert!(state.allowed_web_fetch_domains.contains("github.com"));
assert!(state.allowed_mcp_tools.is_empty());
assert!(state.allowed_mcp_servers.is_empty());
}
#[test]
fn deserialize_unknown_fields_tolerated() {
// PermissionState uses #[serde(default)] which provides defaults for
// missing fields. It does NOT use #[serde(deny_unknown_fields)], so
// unknown keys in TOML are silently ignored. This is important for
// forward compatibility: older versions of the binary should be able
// to read state files written by newer versions that may have added
// new fields.
let toml_str = r#"
allow_bash_execute = false
unknown_field = "should be ignored"
allowed_bash_commands = ["ls"]
"#;
let state: PermissionState = toml::from_str(toml_str).unwrap();
assert!(!state.allow_bash_execute);
assert!(state.allowed_bash_commands.contains("ls"));
assert!(state.disallowed_bash_commands.is_empty());
}
// ── Disk persistence roundtrip tests ─────────────────────────
#[tokio::test]
async fn persist_and_load_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let cwd_path = tmp.path().join("my-project");
std::fs::create_dir_all(&cwd_path).unwrap();
let _cwd = AbsPathBuf::new(cwd_path).unwrap();
let mut state = PermissionState::default();
state.allow_bash_execute = true;
state
.allowed_bash_commands
.insert("cargo build".to_string());
state.disallowed_bash_commands.insert("rm -rf".to_string());
// Override the state dir to use our temp dir.
// We can't easily override kigi_home(), so instead test
// the serialize/deserialize path directly with TOML.
let toml_str = toml::to_string_pretty(&state).unwrap();
let dir = tmp.path().join("sessions").join("test");
tokio::fs::create_dir_all(&dir).await.unwrap();
let path = dir.join("permission.toml");
tokio::fs::write(&path, &toml_str).await.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
let restored: PermissionState = toml::from_str(&content).unwrap();
assert!(restored.allow_bash_execute);
assert!(restored.allowed_bash_commands.contains("cargo build"));
assert!(restored.disallowed_bash_commands.contains("rm -rf"));
}
#[tokio::test]
async fn load_missing_file_returns_default() {
// Simulates load_state_from_disk behavior for a missing file.
let path = std::path::Path::new("/nonexistent/permission.toml");
let result = tokio::fs::read_to_string(path).await;
match result {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let state = PermissionState::default();
assert!(!state.allow_bash_execute);
}
_ => panic!("expected NotFound error"),
}
}
#[tokio::test]
async fn load_corrupt_file_returns_default() {
// Simulates load_state_from_disk behavior for corrupt TOML.
let corrupt = "this is not valid toml {{{{";
let state: PermissionState = toml::from_str(corrupt).unwrap_or_default();
assert!(!state.allow_bash_execute);
assert!(state.allowed_bash_commands.is_empty());
}
// ── Per-client state file path tests ──────────────────────────
#[test]
fn state_file_path_without_client_id() {
let tmp = tempfile::tempdir().unwrap();
let path = state_file_path(tmp.path(), None);
assert_eq!(path.file_name().unwrap(), "permission.toml");
}
#[test]
fn state_file_path_with_client_id() {
let tmp = tempfile::tempdir().unwrap();
let path = state_file_path(tmp.path(), Some("vscode-ext"));
assert_eq!(path.file_name().unwrap(), "permission_vscode-ext.toml");
}
#[test]
fn state_file_path_empty_client_id() {
let tmp = tempfile::tempdir().unwrap();
let path = state_file_path(tmp.path(), Some(""));
assert_eq!(path.file_name().unwrap(), "permission_.toml");
}
#[test]
fn state_file_path_sanitizes_path_separators() {
let tmp = tempfile::tempdir().unwrap();
let path = state_file_path(tmp.path(), Some("foo/bar"));
assert_eq!(path.file_name().unwrap(), "permission_foo_bar.toml");
let path = state_file_path(tmp.path(), Some("foo\\bar"));
assert_eq!(path.file_name().unwrap(), "permission_foo_bar.toml");
}
#[test]
fn sanitize_client_id_prevents_traversal() {
assert_eq!(sanitize_client_id("foo/../../attack"), "foo_______attack");
assert_eq!(sanitize_client_id("normal-id"), "normal-id");
assert_eq!(sanitize_client_id("has\0null"), "has_null");
assert_eq!(sanitize_client_id("back\\slash"), "back_slash");
}
#[tokio::test]
async fn try_load_state_missing_returns_none() {
let result = try_load_state(std::path::Path::new("/nonexistent/permission.toml")).await;
assert!(result.is_none());
}
#[tokio::test]
async fn try_load_state_valid_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("permission.toml");
tokio::fs::write(&path, "allow_bash_execute = true")
.await
.unwrap();
let state = try_load_state(&path).await.unwrap();
assert!(state.allow_bash_execute);
}
#[tokio::test]
async fn per_client_persist_and_load_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let mut state = PermissionState::default();
state.allow_bash_execute = true;
state.allowed_bash_commands.insert("cargo test".to_string());
persist_state_to_dir(dir, &state, Some("client_a")).await;
let loaded = load_state_from_dir(dir, Some("client_a")).await;
assert!(loaded.allow_bash_execute);
assert!(loaded.allowed_bash_commands.contains("cargo test"));
}
#[tokio::test]
async fn per_client_load_falls_back_to_shared() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let mut shared_state = PermissionState::default();
shared_state.allow_bash_execute = true;
shared_state
.allowed_bash_commands
.insert("cargo test".to_string());
persist_state_to_dir(dir, &shared_state, None).await;
let loaded = load_state_from_dir(dir, Some("new_client")).await;
assert!(loaded.allow_bash_execute);
assert!(loaded.allowed_bash_commands.contains("cargo test"));
}
#[tokio::test]
async fn per_client_file_takes_priority_over_shared() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let mut shared_state = PermissionState::default();
shared_state.allow_bash_execute = true;
persist_state_to_dir(dir, &shared_state, None).await;
let mut client_state = PermissionState::default();
client_state.allow_bash_execute = false;
client_state
.allowed_bash_commands
.insert("npm test".to_string());
persist_state_to_dir(dir, &client_state, Some("my-client")).await;
let loaded = load_state_from_dir(dir, Some("my-client")).await;
assert!(!loaded.allow_bash_execute);
assert!(loaded.allowed_bash_commands.contains("npm test"));
let shared_loaded = load_state_from_dir(dir, None).await;
assert!(shared_loaded.allow_bash_execute);
}
#[tokio::test]
async fn load_none_client_returns_default_when_no_file() {
let tmp = tempfile::tempdir().unwrap();
let loaded = load_state_from_dir(tmp.path(), None).await;
assert!(!loaded.allow_bash_execute);
assert!(loaded.allowed_bash_commands.is_empty());
}
#[tokio::test]
async fn per_client_isolation_between_clients() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let mut state_a = PermissionState::default();
state_a
.allowed_bash_commands
.insert("cargo test".to_string());
persist_state_to_dir(dir, &state_a, Some("client_a")).await;
let mut state_b = PermissionState::default();
state_b.allowed_bash_commands.insert("npm test".to_string());
persist_state_to_dir(dir, &state_b, Some("client_b")).await;
let loaded_a = load_state_from_dir(dir, Some("client_a")).await;
assert!(loaded_a.allowed_bash_commands.contains("cargo test"));
assert!(!loaded_a.allowed_bash_commands.contains("npm test"));
let loaded_b = load_state_from_dir(dir, Some("client_b")).await;
assert!(loaded_b.allowed_bash_commands.contains("npm test"));
assert!(!loaded_b.allowed_bash_commands.contains("cargo test"));
}
}