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:
@@ -0,0 +1,337 @@
|
||||
//! Data APIs for `grok models`. Clients own display.
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use kigi_acp_lib::{AcpAgentTx, acp_send};
|
||||
|
||||
use crate::agent::config::Config as AgentConfig;
|
||||
|
||||
/// Status for the `grok models` banner (display order ≠ sampling priority; see [`AuthStatus::resolve`]).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AuthStatus {
|
||||
ApiKey,
|
||||
/// Auth host from `grok_ws_origin` (scheme stripped).
|
||||
LoggedIn(String),
|
||||
/// Catalog key of the first model with own `api_key`/`env_key`.
|
||||
ModelCredentials(String),
|
||||
DeploymentKey,
|
||||
NotAuthenticated,
|
||||
}
|
||||
|
||||
impl AuthStatus {
|
||||
/// Banner status: env key → session → BYOK → deployment → none.
|
||||
///
|
||||
/// Differs from sampling (`resolve_credentials`: BYOK → session → env) so a
|
||||
/// logged-in user sees the login host. BYOK uses
|
||||
/// [`crate::agent::auth_method::should_advertise_xai_api_key`] so
|
||||
/// `disable_api_key_auth` is honored.
|
||||
pub fn resolve(agent_config: &AgentConfig) -> Self {
|
||||
if crate::agent::auth_method::has_xai_api_key_env() {
|
||||
return Self::ApiKey;
|
||||
}
|
||||
if agent_config.create_auth_manager().current().is_some() {
|
||||
let origin = &agent_config.grok_com_config.grok_ws_origin;
|
||||
let host = origin
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| origin.strip_prefix("http://"))
|
||||
.unwrap_or(origin);
|
||||
return Self::LoggedIn(host.to_owned());
|
||||
}
|
||||
let models = crate::agent::config::resolve_model_list(agent_config, None);
|
||||
if crate::agent::auth_method::should_advertise_xai_api_key(
|
||||
agent_config.grok_com_config.api_key_auth_disabled(),
|
||||
models.values(),
|
||||
) && let Some(name) = models
|
||||
.iter()
|
||||
.find_map(|(name, entry)| entry.has_own_credentials().then(|| name.clone()))
|
||||
{
|
||||
return Self::ModelCredentials(name);
|
||||
}
|
||||
if agent_config.endpoints.deployment_key.is_some() {
|
||||
return Self::DeploymentKey;
|
||||
}
|
||||
Self::NotAuthenticated
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch model state (available models + default) over an ACP channel.
|
||||
pub async fn list_models(
|
||||
acp_tx: &AcpAgentTx,
|
||||
client_type: &str,
|
||||
client_version: &str,
|
||||
) -> Result<acp::SessionModelState> {
|
||||
let init_resp: acp::InitializeResponse = acp_send(
|
||||
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
|
||||
.client_capabilities(
|
||||
acp::ClientCapabilities::new()
|
||||
.fs(acp::FileSystemCapabilities::new())
|
||||
.terminal(false),
|
||||
)
|
||||
.meta(
|
||||
serde_json::json!({
|
||||
"clientType": client_type,
|
||||
"clientVersion": client_version,
|
||||
})
|
||||
.as_object()
|
||||
.cloned(),
|
||||
),
|
||||
acp_tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let model_state = init_resp
|
||||
.meta
|
||||
.and_then(|m| m.get("modelState").cloned())
|
||||
.ok_or_else(|| anyhow::anyhow!("InitializeResponse missing modelState"))?;
|
||||
let state: acp::SessionModelState = serde_json::from_value(model_state)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse modelState: {}", e))?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::agent::auth_method::{LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR};
|
||||
use crate::agent::config::Config;
|
||||
use crate::auth::{AuthMode, GrokAuth};
|
||||
use kigi_test_support::EnvGuard;
|
||||
use serial_test::serial;
|
||||
|
||||
/// Isolate process-global auth sources that `AuthStatus::resolve` consults.
|
||||
///
|
||||
/// Uses `KIGI_AUTH_PATH` (not `KIGI_SHARE_DIR`) so a OnceLock-cached real home
|
||||
/// with `auth.json` cannot leak into these tests.
|
||||
fn isolate_auth_sources() -> (tempfile::TempDir, [EnvGuard; 7]) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let auth_path = dir.path().join("no-auth.json");
|
||||
let guards = [
|
||||
EnvGuard::unset(XAI_API_KEY_ENV_VAR),
|
||||
EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR),
|
||||
EnvGuard::unset("KIGI_AUTH"),
|
||||
EnvGuard::set("KIGI_AUTH_PATH", auth_path.to_str().unwrap()),
|
||||
EnvGuard::unset("KIGI_DEPLOYMENT_KEY"),
|
||||
EnvGuard::unset("KIGI_WS_ORIGIN"),
|
||||
EnvGuard::unset("KIGI_DISABLE_API_KEY_AUTH"),
|
||||
];
|
||||
(dir, guards)
|
||||
}
|
||||
|
||||
fn byok_and_deployment_toml(model_id: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
[endpoints]
|
||||
deployment_key = "deploy-key"
|
||||
|
||||
[model."{model_id}"]
|
||||
model = "{model_id}"
|
||||
api_key = "sk-byok"
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn config_from_toml(toml_src: &str) -> Config {
|
||||
let toml: toml::Value = toml::from_str(toml_src).unwrap();
|
||||
Config::new_from_toml_cfg(&toml).expect("config should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_api_key_env() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let _key = EnvGuard::set(XAI_API_KEY_ENV_VAR, "xai-test-key");
|
||||
assert_eq!(AuthStatus::resolve(&Config::default()), AuthStatus::ApiKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_legacy_api_key_env() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let _key = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key");
|
||||
assert_eq!(AuthStatus::resolve(&Config::default()), AuthStatus::ApiKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_oauth_session() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let token = GrokAuth {
|
||||
key: "session-token".into(),
|
||||
auth_mode: AuthMode::WebLogin,
|
||||
..GrokAuth::test_default()
|
||||
};
|
||||
let json = serde_json::to_string(&token).unwrap();
|
||||
let _auth = EnvGuard::set("KIGI_AUTH", &json);
|
||||
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&Config::default()),
|
||||
AuthStatus::LoggedIn("grok.com".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_model_api_key_byok() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&format!(
|
||||
r#"
|
||||
[model."{dm}"]
|
||||
model = "{dm}"
|
||||
api_key = "sk-byok-inline"
|
||||
"#
|
||||
));
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&cfg),
|
||||
AuthStatus::ModelCredentials(dm.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_model_env_key_byok() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
const TEST_ENV: &str = "TEST_AUTH_STATUS_BYOK_ENV_KEY";
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&format!(
|
||||
r#"
|
||||
[model."{dm}"]
|
||||
model = "{dm}"
|
||||
env_key = "{TEST_ENV}"
|
||||
"#
|
||||
));
|
||||
|
||||
{
|
||||
let _unset = EnvGuard::unset(TEST_ENV);
|
||||
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::NotAuthenticated);
|
||||
}
|
||||
{
|
||||
let _set = EnvGuard::set(TEST_ENV, "secret-token");
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&cfg),
|
||||
AuthStatus::ModelCredentials(dm.to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_deployment_key() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let mut cfg = Config::default();
|
||||
cfg.endpoints.deployment_key = Some("deploy-key".into());
|
||||
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::DeploymentKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_not_authenticated() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&Config::default()),
|
||||
AuthStatus::NotAuthenticated
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_priority_api_key_over_byok_and_deployment() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let _key = EnvGuard::set(XAI_API_KEY_ENV_VAR, "xai-test-key");
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&byok_and_deployment_toml(dm));
|
||||
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::ApiKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_priority_session_over_byok_and_deployment() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let token = GrokAuth {
|
||||
key: "session-token".into(),
|
||||
auth_mode: AuthMode::WebLogin,
|
||||
..GrokAuth::test_default()
|
||||
};
|
||||
let json = serde_json::to_string(&token).unwrap();
|
||||
let _auth = EnvGuard::set("KIGI_AUTH", &json);
|
||||
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&byok_and_deployment_toml(dm));
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&cfg),
|
||||
AuthStatus::LoggedIn("grok.com".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_priority_byok_over_deployment() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&byok_and_deployment_toml(dm));
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&cfg),
|
||||
AuthStatus::ModelCredentials(dm.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_disable_api_key_auth_suppresses_byok_banner() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&format!(
|
||||
r#"
|
||||
[grok_com_config]
|
||||
disable_api_key_auth = true
|
||||
|
||||
[model."{dm}"]
|
||||
model = "{dm}"
|
||||
api_key = "sk-byok"
|
||||
"#
|
||||
));
|
||||
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::NotAuthenticated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_disable_api_key_auth_falls_through_to_deployment() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let dm = crate::models::default_model();
|
||||
let cfg = config_from_toml(&format!(
|
||||
r#"
|
||||
[grok_com_config]
|
||||
disable_api_key_auth = true
|
||||
|
||||
[endpoints]
|
||||
deployment_key = "deploy-key"
|
||||
|
||||
[model."{dm}"]
|
||||
model = "{dm}"
|
||||
api_key = "sk-byok"
|
||||
"#
|
||||
));
|
||||
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::DeploymentKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn resolve_model_credentials_uses_first_catalog_key() {
|
||||
let (_dir, _g) = isolate_auth_sources();
|
||||
let cfg = config_from_toml(
|
||||
r#"
|
||||
[model."my-openai"]
|
||||
model = "gpt-4o"
|
||||
api_key = "sk-first"
|
||||
|
||||
[model."my-anthropic"]
|
||||
model = "claude"
|
||||
api_key = "sk-second"
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
AuthStatus::resolve(&cfg),
|
||||
AuthStatus::ModelCredentials("my-openai".to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user