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,464 @@
//! The managed-config cloud-cache subsystem: the sync marker, serving identity,
//! staleness (timer + hard), and the fail-closed enforcement gate that combines
//! the signed-cache verdict with the best-effort marker.
//!
//! The marker is **unsigned** and user-writable — a refresh hint, not a tamper
//! control; real tamper resistance is [`crate::signed_policy`] plus the
//! OS-protected layers (root-owned `/etc/kigi`, MDM).
use std::path::Path;
use crate::paths::user_kigi_home;
/// Sync marker; staleness keys on this, not mtimes.
const MANAGED_CONFIG_CACHE_FILE: &str = "managed_config_cache.json";
/// The on-disk marker: unsigned, detects only deletion / identity change, not
/// in-place edits (see the module doc).
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct ManagedConfigCache {
/// Unix seconds of the last successful fetch.
synced_at: Option<u64>,
/// Team id, or the deploy-key path's server `deployment_id` (reported via
/// [`managed_deployment_id`]; identity is `key_fingerprint`).
principal: Option<String>,
/// Artifacts this sync served, so staleness spots a later deletion; `default` false so pre-upgrade markers don't over-claim.
#[serde(default)]
had_managed_config: bool,
#[serde(default)]
had_requirements: bool,
/// Deploy-key fingerprint (never the raw key) — the deploy-key identity (see [`ServingIdentity`]); `None` on the team path.
#[serde(default)]
key_fingerprint: Option<String>,
/// Served opt-in (`fail_closed = true`); `default` false so a pre-upgrade or un-opted marker never fails closed.
#[serde(default)]
fail_closed: bool,
}
/// What the cache is bound to (one value, so a (team, key) combo can't form). The
/// deploy-key fingerprint is the only identity verifiable offline (no key→deployment_id map without the network).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServingIdentity {
Team(String),
DeploymentKey { fingerprint: String },
None,
}
/// Whether to refetch for `identity`: no marker, past the timer, different identity, or a served artifact now missing.
/// Best-effort — callers continue without managed config on failure.
pub fn is_managed_config_stale_for(identity: &ServingIdentity) -> bool {
managed_config_stale_at(user_kigi_home().as_deref(), identity)
}
/// Fields a successful sync records. A struct (destructured without `..`) so a new field is
/// a compile error at every writer — three adjacent positional bools would silently transpose.
pub struct SyncMarker<'a> {
pub principal: Option<&'a str>,
pub had_managed_config: bool,
pub had_requirements: bool,
pub key_fingerprint: Option<&'a str>,
pub fail_closed: bool,
}
/// Record a successful sync (best-effort; called even for a config-less principal so it doesn't refetch every tick).
pub fn mark_managed_config_synced(marker: SyncMarker<'_>) {
if let Some(home) = user_kigi_home() {
mark_managed_config_synced_at(&home, marker);
}
}
/// Server-side GrokBuildDeployment UUID from the last deploy-key managed-config
/// sync, bound to the key that synced it: returns the marker's `principal` only
/// when the marker's `key_fingerprint` equals `key_fingerprint`, so a rotated or
/// removed key never reports the previous deployment's id. Team-path syncs store
/// a team id and no fingerprint, so they never match.
pub fn managed_deployment_id(key_fingerprint: &str) -> Option<String> {
managed_deployment_id_at(user_kigi_home()?.as_path(), key_fingerprint)
}
fn managed_deployment_id_at(home: &Path, key_fingerprint: &str) -> Option<String> {
if key_fingerprint.trim().is_empty() {
return None;
}
let cache = read_managed_config_cache(home)?;
if cache.key_fingerprint.as_deref() != Some(key_fingerprint) {
return None;
}
cache.principal.filter(|p| !p.trim().is_empty())
}
fn mark_managed_config_synced_at(home: &Path, marker: SyncMarker<'_>) {
let SyncMarker {
principal,
had_managed_config,
had_requirements,
key_fingerprint,
fail_closed,
} = marker;
let synced_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.ok();
let cache = ManagedConfigCache {
synced_at,
principal: principal.map(str::to_owned),
// What THIS sync served, not on-disk presence — a confirmed switch already evicted any prior files.
had_managed_config,
had_requirements,
key_fingerprint: key_fingerprint.map(str::to_owned),
fail_closed,
};
match serde_json::to_string(&cache) {
Ok(json) => write_marker_atomically(home, &json),
Err(e) => tracing::warn!("failed to serialize managed config cache: {e}"),
}
}
/// Atomic write of the marker; best-effort (failure is logged, never surfaced).
fn write_marker_atomically(home: &Path, json: &str) {
if let Err(e) =
crate::fs_atomic::write_atomically(&home.join(MANAGED_CONFIG_CACHE_FILE), json, None)
{
tracing::warn!("failed to write managed config cache: {e}");
}
}
/// The sync marker, or `None` if absent / unreadable / corrupt. Allow-on-unreadable:
/// a read blip or torn write mustn't lock out a managed user. Unreadable/corrupt are
/// logged (a corruption-to-disarm isn't silent) and self-heal on the next sync.
fn read_managed_config_cache(home: &Path) -> Option<ManagedConfigCache> {
let json = match std::fs::read_to_string(home.join(MANAGED_CONFIG_CACHE_FILE)) {
Ok(json) => json,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => {
tracing::warn!("managed config cache unreadable; treating as no marker: {e}");
return None;
}
};
match serde_json::from_str(&json) {
Ok(cache) => Some(cache),
Err(e) => {
tracing::warn!(
"managed config cache is corrupt; treating as no marker, next sync rewrites it: {e}"
);
None
}
}
}
/// A confirmed identity switch vs the marker — both sides of a dimension present and differing (team id or fingerprint).
/// Callers evict prior artifacts on true; a missing marker / `None` / pre-upgrade never counts (first sync / signed-out / legacy never evict).
/// A blank/whitespace value on either side of either dimension (principal or key fingerprint)
/// is "unknown", not a distinct tenant — a malformed `auth.json` parse blip must not confirm a
/// switch and shed a real tenant's policy.
pub fn managed_config_identity_changed(
new_principal: Option<&str>,
new_key_fingerprint: Option<&str>,
) -> bool {
user_kigi_home().is_some_and(|home| {
managed_config_identity_changed_at(&home, new_principal, new_key_fingerprint)
})
}
fn managed_config_identity_changed_at(
home: &Path,
new_principal: Option<&str>,
new_key_fingerprint: Option<&str>,
) -> bool {
let Some(cache) = read_managed_config_cache(home) else {
return false;
};
let principal_changed = matches!(
(cache.principal.as_deref(), new_principal),
(Some(old), Some(new))
if !old.trim().is_empty() && !new.trim().is_empty() && old != new
);
let key_changed = matches!(
(cache.key_fingerprint.as_deref(), new_key_fingerprint),
(Some(old), Some(new))
if !old.trim().is_empty() && !new.trim().is_empty() && old != new
);
principal_changed || key_changed
}
/// True when an artifact the marker recorded serving is now absent. Only served artifacts count, so a config-less
/// principal (or legacy marker) isn't misread as stale. Detects deletion, not edits.
fn cache_missing_required_artifact(cache: &ManagedConfigCache, home: &Path) -> bool {
(cache.had_requirements && !home.join("requirements.toml").exists())
|| (cache.had_managed_config && !home.join("managed_config.toml").exists())
}
/// Whether the cached principal differs from the team serving now — the team dimension only.
/// Deploy-key identity is verified by fingerprint ([`cache_key_fingerprint_mismatch`]); `None` never fires.
fn cache_identity_mismatch(cache: &ManagedConfigCache, identity: &ServingIdentity) -> bool {
match identity {
ServingIdentity::Team(team_id) => cache.principal.as_deref() != Some(team_id.as_str()),
ServingIdentity::DeploymentKey { .. } | ServingIdentity::None => false,
}
}
/// Whether the configured deployment key differs from the cache's, by one-way fingerprint (never the raw key) —
/// the only identity verifiable offline. A pre-upgrade marker (no fingerprint) never fires; only a *changed* key.
fn cache_key_fingerprint_mismatch(cache: &ManagedConfigCache, identity: &ServingIdentity) -> bool {
match identity {
ServingIdentity::DeploymentKey { fingerprint } => {
matches!(cache.key_fingerprint.as_deref(), Some(recorded) if recorded != fingerprint)
}
ServingIdentity::Team(_) | ServingIdentity::None => false,
}
}
/// The team id for the signed-cache check; `None` for a deployment key (bound by the
/// marker's deployment id, not a team) or no identity.
fn serving_team_id(identity: &ServingIdentity) -> Option<&str> {
match identity {
ServingIdentity::Team(team_id) => Some(team_id.as_str()),
ServingIdentity::DeploymentKey { .. } | ServingIdentity::None => None,
}
}
/// Tamper signals for the current identity, split two ways: [`Self::needs_refetch`] (staleness) on ANY
/// signal; [`Self::compromised_for_gate`] (gate) only on artifact-missing or key-change — never a pure
/// identity mismatch (a foreign marker the online refetch rebinds).
#[derive(Clone, Copy)]
struct TamperSignals {
artifact_missing: bool,
identity_mismatch: bool,
key_fingerprint_mismatch: bool,
}
impl TamperSignals {
fn evaluate(cache: &ManagedConfigCache, home: &Path, identity: &ServingIdentity) -> Self {
Self {
artifact_missing: cache_missing_required_artifact(cache, home),
identity_mismatch: cache_identity_mismatch(cache, identity),
key_fingerprint_mismatch: cache_key_fingerprint_mismatch(cache, identity),
}
}
fn needs_refetch(self) -> bool {
self.artifact_missing || self.identity_mismatch || self.key_fingerprint_mismatch
}
fn compromised_for_gate(self) -> bool {
self.artifact_missing || self.key_fingerprint_mismatch
}
}
/// Cache unusable now: different identity, a served artifact missing, or no marker. The session-start refresh blocks
/// (bounded) on this but not timer-staleness, so a present same-identity cache never delays startup offline.
pub fn is_managed_config_hard_stale_for(identity: &ServingIdentity) -> bool {
match user_kigi_home() {
Some(home) => is_managed_config_hard_stale_for_at(&home, identity),
None => false,
}
}
/// Whether the cache can't be used for `identity` — a served artifact missing or a different
/// identity. Shared by the staleness and session-start paths so the siblings can't drift.
fn cache_unusable_for(cache: &ManagedConfigCache, home: &Path, identity: &ServingIdentity) -> bool {
TamperSignals::evaluate(cache, home, identity).needs_refetch()
}
/// The principal the SIGNED cache must be bound to: the live team id, else the marker
/// principal (the recorded deployment id on a deployment-key machine). One derivation
/// shared by the gate and both staleness checks, so a foreign-but-authentic cache
/// reads foreign on every sibling path.
fn expected_signed_principal<'a>(
cache: Option<&'a ManagedConfigCache>,
identity: &'a ServingIdentity,
) -> Option<&'a str> {
serving_team_id(identity).or_else(|| cache.and_then(|c| c.principal.as_deref()))
}
/// A signing-enabled build over a legacy unsigned / edited / forged or foreign-bound
/// cache refetches a signed copy. Dark build or no policy on disk → false, so this is
/// inert until a key is provisioned.
fn signed_cache_needs_refetch(
home: &Path,
cache: Option<&ManagedConfigCache>,
identity: &ServingIdentity,
) -> bool {
crate::signed_policy::cloud_cache_signature_invalid(
home,
expected_signed_principal(cache, identity),
crate::signed_policy::now_unix(),
)
}
fn is_managed_config_hard_stale_for_at(home: &Path, identity: &ServingIdentity) -> bool {
let cache = read_managed_config_cache(home);
cache
.as_ref()
.is_none_or(|cache| cache_unusable_for(cache, home, identity))
|| signed_cache_needs_refetch(home, cache.as_ref(), identity)
}
/// No-network fail-closed predicate: true only on a `fail_closed` policy with tamper for
/// the current identity. With a key compiled in the SIGNED verdict leads (non-forgeable
/// opt-in, catches edits the marker can't, and a fail-closed marker then REQUIRES an
/// authentic sidecar); the dark build uses only the best-effort marker decision.
pub fn managed_policy_compromised_for(identity: &ServingIdentity) -> bool {
user_kigi_home().is_some_and(|home| managed_policy_compromised_for_at(&home, identity))
}
/// Apply writes the policy files before the sidecar with no lock shared with gate
/// readers, so a session start racing a background sync can pair new files with the
/// old sidecar and transiently read Compromised. One pause covers the tiny write gap.
const APPLY_RACE_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(50);
fn managed_policy_compromised_for_at(home: &Path, identity: &ServingIdentity) -> bool {
compromised_with_apply_race_retry(
|| managed_policy_compromised_once(home, identity),
|| std::thread::sleep(APPLY_RACE_RETRY_DELAY),
)
}
/// GATE-only retry for the apply race (see [`APPLY_RACE_RETRY_DELAY`]): on a refusing
/// Compromised verdict, re-evaluate once and use the second verdict — real tamper is
/// still Compromised on the second pass. The staleness/refetch siblings never retry:
/// flagging a transient mismatch there is exactly the self-heal.
fn compromised_with_apply_race_retry(
mut evaluate: impl FnMut() -> (bool, crate::signed_policy::SignedVerdict),
pause: impl FnOnce(),
) -> bool {
match evaluate() {
(false, _) => false,
(true, crate::signed_policy::SignedVerdict::Compromised) => {
pause();
evaluate().0
}
(true, _) => true,
}
}
/// One full evaluation of the gate decision, returning the signed verdict alongside so
/// the retry wrapper can distinguish a (possibly racing) Compromised refusal.
fn managed_policy_compromised_once(
home: &Path,
identity: &ServingIdentity,
) -> (bool, crate::signed_policy::SignedVerdict) {
let cache = read_managed_config_cache(home);
let signed_verdict = crate::signed_policy::signed_cache_compromised(
home,
expected_signed_principal(cache.as_ref(), identity),
crate::signed_policy::now_unix(),
);
// The signature binds a deployment_id, not the local deploy key, so a Trusted verdict
// can't attest the configured key — pass the fingerprint mismatch through so it gates
// on every path.
let key_fingerprint_mismatch = cache
.as_ref()
.is_some_and(|c| cache_key_fingerprint_mismatch(c, identity));
let compromised = managed_policy_compromised_decision(
signed_verdict,
key_fingerprint_mismatch,
cache.as_ref(),
home,
identity,
);
(compromised, signed_verdict)
}
/// Combine the signed verdict with the best-effort marker fallback — one row per
/// verdict; each row's reasoning lives on its [`SignedVerdict`] variant doc. Split
/// out so the signed↔marker integration is unit-testable without a compiled-in key.
fn managed_policy_compromised_decision(
signed_verdict: crate::signed_policy::SignedVerdict,
key_fingerprint_mismatch: bool,
cache: Option<&ManagedConfigCache>,
home: &Path,
identity: &ServingIdentity,
) -> bool {
use crate::signed_policy::SignedVerdict;
// A fail-closed marker that recorded served policy requires an authentic sidecar.
let sidecar_required_but_missing = || {
let required =
cache.is_some_and(|c| c.fail_closed && (c.had_managed_config || c.had_requirements));
if required {
tracing::warn!(
"managed policy fail-closed gate: refusing session — signed sidecar missing or unverifiable"
);
}
required
};
// The best-effort marker decision: refuse only an opted-in marker with gate-grade tamper.
let marker_compromised = || {
cache.is_some_and(|cache| {
if !cache.fail_closed {
return false;
}
let signals = TamperSignals::evaluate(cache, home, identity);
let compromised = signals.compromised_for_gate();
// Booleans only — never the raw key (the fingerprint is already a one-way hash).
if compromised {
tracing::warn!(
artifact_missing = signals.artifact_missing,
identity_mismatch = signals.identity_mismatch,
key_fingerprint_mismatch = signals.key_fingerprint_mismatch,
"managed policy fail-closed gate: refusing session on tamper evidence"
);
} else if signals.identity_mismatch {
tracing::debug!(
identity_mismatch = true,
"managed policy fail-closed gate: foreign marker, not refusing (online refetch rebinds)"
);
}
compromised
})
};
match signed_verdict {
SignedVerdict::Compromised => true,
// Trusted clears the gate — except the deploy-key fingerprint, which the signature can't attest.
SignedVerdict::Trusted => key_fingerprint_mismatch && marker_compromised(),
SignedVerdict::NoAuthenticSidecar => sidecar_required_but_missing() || marker_compromised(),
SignedVerdict::SidecarUnreadable => marker_compromised(),
SignedVerdict::Inactive => marker_compromised(),
}
}
/// Stale when never synced, past the threshold, identity differs, a served artifact is now missing,
/// or (keyed builds) the signed cache no longer verifies. No home → nothing to refresh into → not
/// stale. Reads the marker once.
fn managed_config_stale_at(home: Option<&Path>, identity: &ServingIdentity) -> bool {
let Some(home) = home else {
return false;
};
let Some(cache) = read_managed_config_cache(home) else {
return true; // no marker → never synced → stale
};
if cache_unusable_for(&cache, home, identity) {
return true;
}
// Same signed check as the session-start hard-stale sibling: the background tick
// must also refetch a tampered/foreign-signed cache, not leave it until startup.
if signed_cache_needs_refetch(home, Some(&cache), identity) {
return true;
}
match cache.synced_at {
// `duration_since` errs when `synced_at` is in the future (clock skew);
// treat that as freshly synced rather than stale.
Some(secs) => {
let synced_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
std::time::SystemTime::now()
.duration_since(synced_at)
.is_ok_and(|age| age > managed_config_stale_threshold())
}
None => true,
}
}
/// Override with `KIGI_DEPLOYMENT_CONFIG_CACHE_TTL_SECS` for testing.
fn managed_config_stale_threshold() -> std::time::Duration {
if let Ok(s) = std::env::var("KIGI_DEPLOYMENT_CONFIG_CACHE_TTL_SECS")
&& let Ok(secs) = s.parse::<u64>()
{
return std::time::Duration::from_secs(secs);
}
std::time::Duration::from_secs(30 * 60)
}
// Tests in a sibling file (they dwarf the module) but a child module, for private access.
#[cfg(test)]
#[path = "managed_cache/tests.rs"]
mod tests;