Files
Kigi-CLI/crates/codegen/kigi-shell/src/managed_config/response.rs
T
ZacharyZhang-NY d6c20fc13f 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).
2026-07-17 05:31:01 -04:00

247 lines
9.5 KiB
Rust

//! 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()
);
}
}