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,220 @@
|
||||
//! Shared harness for the KEYED managed-config integration tests: a test-only
|
||||
//! signing seam injects a throwaway trusted key so the real
|
||||
//! sync → verify → persist → gate paths run with verification ACTIVE (the dark
|
||||
//! behavior is covered by `team_managed_config.rs`).
|
||||
//!
|
||||
//! Every test MUST be `#[serial]` and install its own seam keys first: the test
|
||||
//! binary shares one process-global `KIGI_SHARE_DIR`, process env, and key override.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use base64::Engine as _;
|
||||
use kigi_config::signed_policy::{self, SignedPayload};
|
||||
|
||||
pub const MANAGED: &str = "[cli]\ntheme = \"dark\"\n";
|
||||
pub const REQUIREMENTS_FAIL_CLOSED: &str = "fail_closed = true\n[features]\nweb_fetch = false\n";
|
||||
/// Far-future expiry — envelopes in these tests never expire.
|
||||
pub const TEST_EXPIRES_AT: u64 = 4_000_000_000;
|
||||
/// The sole trusted key id: [`install_test_key`] installs it and [`sign_envelope`]
|
||||
/// signs under it, so the two can't drift.
|
||||
pub const TEST_KEY_ID: &str = "v1";
|
||||
|
||||
/// Shared temp dir used as KIGI_SHARE_DIR for the whole test binary (the kigi_home
|
||||
/// `OnceLock` only allows one value per process); scrubs the env this suite
|
||||
/// depends on before any test thread reads it.
|
||||
pub fn test_home() -> &'static PathBuf {
|
||||
static HOME: OnceLock<PathBuf> = OnceLock::new();
|
||||
HOME.get_or_init(|| {
|
||||
let path = tempfile::TempDir::new().unwrap().keep();
|
||||
// SAFETY: set once at init before other threads read the vars.
|
||||
unsafe {
|
||||
std::env::set_var("KIGI_SHARE_DIR", &path);
|
||||
for var in [
|
||||
"KIGI_DEPLOYMENT_KEY",
|
||||
"KIGI_MANAGED_CONFIG",
|
||||
"KIGI_DEPLOYMENT_CONFIG_REFRESH_INTERVAL_SECS",
|
||||
"KIGI_DEPLOYMENT_CONFIG_CACHE_TTL_SECS",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
] {
|
||||
std::env::remove_var(var);
|
||||
}
|
||||
std::env::set_var("KIGI_DEPLOYMENT_CONFIG_BACKOFF_MS", "10");
|
||||
}
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(home: &std::path::Path) {
|
||||
for f in [
|
||||
"config.toml",
|
||||
"auth.json",
|
||||
"managed_config.toml",
|
||||
"requirements.toml",
|
||||
"managed_config_cache.json",
|
||||
"managed_config.lock",
|
||||
"managed_config.sig.json",
|
||||
] {
|
||||
let _ = std::fs::remove_file(home.join(f));
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal mock deployment-config server serving `body` to every request.
|
||||
pub fn spawn_mock(body: String) -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let Ok(mut stream) = stream else { continue };
|
||||
// Drain the request headers before responding.
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if reader.read_line(&mut line).unwrap_or(0) == 0 || line.trim_end().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let resp = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes());
|
||||
let _ = stream.flush();
|
||||
}
|
||||
});
|
||||
format!("http://{addr}/deployment/config")
|
||||
}
|
||||
|
||||
pub fn write_config(home: &std::path::Path, managed_config_url: &str) {
|
||||
std::fs::write(
|
||||
home.join("config.toml"),
|
||||
format!("[endpoints]\nmanaged_config_url = \"{managed_config_url}\"\n"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// [`write_config`] plus a `deployment_key` (dead-code-allowed: compiled into
|
||||
/// both binaries, called by one).
|
||||
#[allow(dead_code)]
|
||||
pub fn write_dk_config(home: &std::path::Path, managed_config_url: &str, deployment_key: &str) {
|
||||
std::fs::write(
|
||||
home.join("config.toml"),
|
||||
format!(
|
||||
"[endpoints]\nmanaged_config_url = \"{managed_config_url}\"\ndeployment_key = \"{deployment_key}\"\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn write_team_auth(home: &std::path::Path, team_id: &str) {
|
||||
let scope = kigi_shell::auth::GrokComConfig::default().auth_scope();
|
||||
let auth = serde_json::json!({
|
||||
scope: {
|
||||
"key": "team-session-token",
|
||||
"auth_mode": "oidc",
|
||||
"create_time": "2026-01-01T00:00:00Z",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"user_id": "user-1",
|
||||
"principal_type": "Team",
|
||||
"team_id": team_id,
|
||||
}
|
||||
});
|
||||
std::fs::write(home.join("auth.json"), auth.to_string()).unwrap();
|
||||
}
|
||||
|
||||
/// A fresh Ed25519 keypair plus its raw public key, installed as the sole trusted
|
||||
/// key ([`TEST_KEY_ID`]) via the test seam.
|
||||
pub fn install_test_key() -> (ring::signature::Ed25519KeyPair, Vec<u8>) {
|
||||
use ring::signature::KeyPair as _;
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
|
||||
let kp = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
|
||||
let pubkey = kp.public_key().as_ref().to_vec();
|
||||
signed_policy::test_seam::set_embedded_keys(&[(TEST_KEY_ID, &pubkey)]);
|
||||
assert!(
|
||||
signed_policy::verification_active(),
|
||||
"the seam must arm verification"
|
||||
);
|
||||
(kp, pubkey)
|
||||
}
|
||||
|
||||
/// Serialize → sign → base64: the one `signatures[]` entry for `payload`, signed
|
||||
/// by `kp` under the payload's own `key_id` (the untrusted outer hint can't drift
|
||||
/// from the signed one).
|
||||
pub fn sign_envelope(
|
||||
kp: &ring::signature::Ed25519KeyPair,
|
||||
payload: &SignedPayload,
|
||||
) -> serde_json::Value {
|
||||
let signed_payload = serde_json::to_string(payload).unwrap();
|
||||
let signature = base64::engine::general_purpose::STANDARD
|
||||
.encode(kp.sign(signed_payload.as_bytes()).as_ref());
|
||||
serde_json::json!({
|
||||
"signed_payload": signed_payload,
|
||||
"signature": signature,
|
||||
"key_id": payload.key_id.as_str(),
|
||||
})
|
||||
}
|
||||
|
||||
/// A team deployment-config response signed by `kp` under [`TEST_KEY_ID`]. The
|
||||
/// body's legacy fields mirror the payload exactly (the client rejects a divergence).
|
||||
pub fn signed_team_body(
|
||||
kp: &ring::signature::Ed25519KeyPair,
|
||||
team_id: &str,
|
||||
managed: Option<&str>,
|
||||
requirements: Option<&str>,
|
||||
) -> String {
|
||||
let payload = SignedPayload {
|
||||
version: prod_mc_cli_chat_proxy_types::SIGNED_PAYLOAD_VERSION,
|
||||
deployment_id: None,
|
||||
team_id: Some(team_id.to_owned()),
|
||||
managed_config: managed.map(str::to_owned),
|
||||
requirements: requirements.map(str::to_owned),
|
||||
fail_closed: requirements.is_some_and(kigi_config::fail_closed_flag_from_str),
|
||||
expires_at: TEST_EXPIRES_AT,
|
||||
key_id: TEST_KEY_ID.into(),
|
||||
};
|
||||
serde_json::json!({
|
||||
"deployment_id": serde_json::Value::Null,
|
||||
"team_id": team_id,
|
||||
"managed_config": managed,
|
||||
"requirements": requirements,
|
||||
"signatures": [sign_envelope(kp, &payload)],
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A [`signed_team_body`] (managed config only) with the signature corrupted —
|
||||
/// valid base64, wrong bytes — so the verifier must reject the envelope.
|
||||
pub fn forged_team_body(kp: &ring::signature::Ed25519KeyPair, team_id: &str) -> String {
|
||||
let mut body: serde_json::Value =
|
||||
serde_json::from_str(&signed_team_body(kp, team_id, Some(MANAGED), None)).unwrap();
|
||||
body["signatures"][0]["signature"] = base64::engine::general_purpose::STANDARD
|
||||
.encode([0u8; 64])
|
||||
.into();
|
||||
body.to_string()
|
||||
}
|
||||
|
||||
pub fn team_identity(id: &str) -> kigi_shell::config::ServingIdentity {
|
||||
kigi_shell::config::ServingIdentity::Team(id.to_owned())
|
||||
}
|
||||
|
||||
/// True when `path` reads despite `chmod 000` (root / DAC bypass): chmod-based
|
||||
/// tests must then skip LOUDLY — a silent return would pass forever. CI runners
|
||||
/// are assumed unprivileged; the shared guard keeps skips greppable.
|
||||
#[cfg(unix)]
|
||||
#[allow(dead_code)]
|
||||
pub fn skip_as_root(path: &std::path::Path, test: &str) -> bool {
|
||||
let skip = std::fs::read_to_string(path).is_ok();
|
||||
if skip {
|
||||
eprintln!("{test}: skipping — chmod unreadability not enforced (running as root?)");
|
||||
}
|
||||
skip
|
||||
}
|
||||
Reference in New Issue
Block a user