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,246 @@
|
||||
//! The deployment-config fetch/response contract: the credential source and its
|
||||
//! errors, response parsing, envelope picking, fetched-envelope verification, and
|
||||
//! the apply outcome the sync orchestration consumes.
|
||||
|
||||
use kigi_config::signed_policy::now_unix;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Which credential a config fetch used — tailors error messages and the
|
||||
/// post-fetch confirmation (team vs deployment).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum ManagedConfigSource {
|
||||
DeploymentKey,
|
||||
TeamOauth,
|
||||
}
|
||||
|
||||
impl ManagedConfigSource {
|
||||
pub(super) fn is_team(self) -> bool {
|
||||
matches!(self, Self::TeamOauth)
|
||||
}
|
||||
|
||||
/// The 401/403 error tailored to the credential (don't tell a team user to
|
||||
/// check `KIGI_DEPLOYMENT_KEY`).
|
||||
pub(super) fn auth_rejected_error(self) -> ManagedConfigError {
|
||||
if self.is_team() {
|
||||
ManagedConfigError::TeamAuthRejected
|
||||
} else {
|
||||
ManagedConfigError::DeploymentKeyRejected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManagedConfigError {
|
||||
#[error("Can't reach the server. Check your network connection and try again.\n ({0})")]
|
||||
Network(String),
|
||||
#[error(
|
||||
"The connection to the server was interrupted or timed out before completing. This is usually temporary; please try again.\n ({0})"
|
||||
)]
|
||||
ConnectionInterrupted(String),
|
||||
#[error(
|
||||
"The deployment key was rejected. Confirm that KIGI_DEPLOYMENT_KEY is set correctly and hasn't expired."
|
||||
)]
|
||||
DeploymentKeyRejected,
|
||||
#[error(
|
||||
"Your team sign-in was rejected. It may have expired or lack access. Run `grok login` to sign in again."
|
||||
)]
|
||||
TeamAuthRejected,
|
||||
#[error("The server returned an unexpected error (HTTP {status}). Try again in a few minutes.")]
|
||||
ServerError { status: u16 },
|
||||
#[error("The server returned an unexpected response.\n ({0})")]
|
||||
InvalidResponse(String),
|
||||
#[error(
|
||||
"The configuration request couldn't be completed due to a client-side error (not a server response). This is unexpected; please report it if it persists.\n ({0})"
|
||||
)]
|
||||
RequestFailed(String),
|
||||
#[error(
|
||||
"The server's response could not be verified as authentic managed policy, so nothing was installed. Try again; if this persists, contact your administrator."
|
||||
)]
|
||||
SignatureRejected,
|
||||
#[error(
|
||||
"Can't save the configuration to ~/.kigi. Make sure the directory exists and is writable.\n ({0})"
|
||||
)]
|
||||
DiskWrite(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl ManagedConfigError {
|
||||
/// Transient failure (network / connection interruption / server 5xx) where retrying may succeed.
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Network(_) | Self::ConnectionInterrupted(_) | Self::ServerError { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Auth/eligibility rejection (no access or expired session) — not fixable by retrying.
|
||||
pub fn is_auth_rejection(&self) -> bool {
|
||||
matches!(self, Self::TeamAuthRejected | Self::DeploymentKeyRejected)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub(super) struct ManagedConfigResponse {
|
||||
#[serde(default)]
|
||||
pub(super) deployment_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) team_id: Option<String>,
|
||||
pub(super) managed_config: Option<String>,
|
||||
pub(super) requirements: Option<String>,
|
||||
/// The signed envelopes (additive; absent from old servers), primary first —
|
||||
/// a rollover server dual-signs, each payload signed by ITS OWN key. The
|
||||
/// signed payload's policy is the trusted copy when verification is on.
|
||||
#[serde(default)]
|
||||
pub(super) signatures: Option<Vec<kigi_config::signed_policy::SignatureEnvelope>>,
|
||||
}
|
||||
|
||||
impl ManagedConfigResponse {
|
||||
pub(super) fn config_exists(&self) -> bool {
|
||||
self.deployment_id.is_some() || self.team_id.is_some()
|
||||
}
|
||||
|
||||
/// The signed envelope to verify, when the server included any: the first
|
||||
/// `signatures` entry whose key_id is in the embedded trusted set, else the
|
||||
/// first entry (so verification reports the real UnknownKeyId failure). The
|
||||
/// outer key_id only PICKS — verification re-selects the key from the signed bytes.
|
||||
pub(super) fn signature_sidecar(
|
||||
&self,
|
||||
) -> Option<kigi_config::signed_policy::SignatureEnvelope> {
|
||||
self.signature_sidecar_with(kigi_config::signed_policy::embedded_key_id_trusted)
|
||||
}
|
||||
|
||||
/// Predicate-injected core of [`Self::signature_sidecar`] so tests can pick
|
||||
/// without a compiled-in key set.
|
||||
fn signature_sidecar_with(
|
||||
&self,
|
||||
key_id_trusted: impl Fn(&str) -> bool,
|
||||
) -> Option<kigi_config::signed_policy::SignatureEnvelope> {
|
||||
let envelopes = self.signatures.as_deref()?;
|
||||
envelopes
|
||||
.iter()
|
||||
.find(|e| key_id_trusted(&e.key_id))
|
||||
.or_else(|| envelopes.first())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Non-empty served content, recorded in the marker so staleness can later detect a deleted file.
|
||||
pub(super) fn has_managed_config(&self) -> bool {
|
||||
self.managed_config
|
||||
.as_deref()
|
||||
.is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
pub(super) fn has_requirements(&self) -> bool {
|
||||
self.requirements.as_deref().is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// The served opt-in (`fail_closed`), read from the payload not disk, so it's authoritative even when
|
||||
/// the on-disk apply is skipped under lock contention.
|
||||
pub(super) fn requirements_fail_closed(&self) -> bool {
|
||||
self.requirements
|
||||
.as_deref()
|
||||
.is_some_and(crate::config::fail_closed_flag_from_str)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of [`apply_fetched`].
|
||||
pub(super) enum ApplyOutcome {
|
||||
/// Persisted the policy (`wrote` = at least one artifact written or removed), or
|
||||
/// skipped under lock contention / a vanished credential (`wrote` = false); the
|
||||
/// marker should be recorded either way. `signed_deployment_id` is the VERIFIED
|
||||
/// payload's `deployment_id` (`None` when unsigned) — stronger than the body's,
|
||||
/// which is omitted on the signed-empty response.
|
||||
Applied {
|
||||
wrote: bool,
|
||||
signed_deployment_id: Option<String>,
|
||||
},
|
||||
/// Verification is active and the envelope did not verify — nothing was persisted.
|
||||
/// The marker must NOT be recorded: it would claim a body that was never written.
|
||||
SignatureRejected,
|
||||
}
|
||||
|
||||
impl ApplyOutcome {
|
||||
pub(super) fn wrote(&self) -> bool {
|
||||
matches!(self, Self::Applied { wrote: true, .. })
|
||||
}
|
||||
|
||||
pub(super) fn signature_rejected(&self) -> bool {
|
||||
matches!(self, Self::SignatureRejected)
|
||||
}
|
||||
|
||||
pub(super) fn signed_deployment_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Applied {
|
||||
signed_deployment_id,
|
||||
..
|
||||
} => signed_deployment_id.as_deref(),
|
||||
Self::SignatureRejected => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A fetched envelope that passed verification: the sidecar to persist, plus its
|
||||
/// parsed (now-trusted) payload.
|
||||
pub(super) struct VerifiedEnvelope {
|
||||
pub(super) sidecar: kigi_config::signed_policy::SignatureEnvelope,
|
||||
pub(super) payload: kigi_config::signed_policy::SignedPayload,
|
||||
}
|
||||
|
||||
/// Verify the signed envelope without persisting anything. The legacy body fields must
|
||||
/// equal the signed copy, so what lands on disk is exactly what was signed (and the
|
||||
/// load-time gate can re-verify it). The error is a plain message: the caller only logs it.
|
||||
pub(super) fn verify_signed_envelope(
|
||||
body: &ManagedConfigResponse,
|
||||
active_team_id: Option<&str>,
|
||||
) -> Result<VerifiedEnvelope, String> {
|
||||
use kigi_config::signed_policy;
|
||||
let sidecar = body.signature_sidecar().ok_or_else(|| {
|
||||
"managed policy is required but the server returned no signature".to_owned()
|
||||
})?;
|
||||
let payload = signed_policy::verify_fetched(&sidecar, active_team_id, now_unix())
|
||||
.map_err(|e| e.to_string())?;
|
||||
if body.managed_config != payload.managed_config || body.requirements != payload.requirements {
|
||||
return Err("served policy does not match the signed payload".to_owned());
|
||||
}
|
||||
Ok(VerifiedEnvelope { sidecar, payload })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Picking: the first trusted-key_id entry wins; no trusted entry → the first entry
|
||||
/// (picking must not invent absence); no array (old/unsigned server) → None.
|
||||
#[test]
|
||||
fn signature_sidecar_picks_trusted_envelope_then_falls_back() {
|
||||
use kigi_config::signed_policy::SignatureEnvelope;
|
||||
let envelope = |kid: &str| SignatureEnvelope {
|
||||
signed_payload: format!("payload-{kid}"),
|
||||
signature: format!("sig-{kid}"),
|
||||
key_id: kid.to_owned(),
|
||||
};
|
||||
let body = ManagedConfigResponse {
|
||||
signatures: Some(vec![envelope("v1"), envelope("v2")]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// A rotated client trusting only v2 picks the v2 envelope from the array.
|
||||
let picked = body.signature_sidecar_with(|id| id == "v2").unwrap();
|
||||
assert_eq!(picked.key_id, "v2");
|
||||
assert_eq!(picked.signed_payload, "payload-v2");
|
||||
|
||||
// Trusting v1 picks the primary entry (first in the array).
|
||||
let picked = body.signature_sidecar_with(|id| id == "v1").unwrap();
|
||||
assert_eq!(picked.key_id, "v1");
|
||||
|
||||
// No trusted id → the first entry, so verification reports UnknownKeyId.
|
||||
let picked = body.signature_sidecar_with(|_| false).unwrap();
|
||||
assert_eq!(picked.key_id, "v1");
|
||||
|
||||
// Nothing signed at all → None.
|
||||
assert!(
|
||||
ManagedConfigResponse::default()
|
||||
.signature_sidecar_with(|_| true)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user