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,241 @@
|
||||
//! Identifier newtypes.
|
||||
//!
|
||||
//! Every wire-traveling id has a dedicated newtype to prevent accidental
|
||||
//! mixing (e.g. passing a `SessionId` where a `ToolId` is expected).
|
||||
//! Constructors validate; `Deserialize` re-uses the constructor, so values
|
||||
//! that round-trip from the wire share the same invariants as values built
|
||||
//! locally.
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
/// Errors produced by id constructors and validators.
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IdError {
|
||||
#[error("identifier must not be empty")]
|
||||
Empty,
|
||||
#[error("identifier {value:?} has invalid format")]
|
||||
InvalidFormat { value: String },
|
||||
#[error("identifier {value:?} uses a reserved prefix")]
|
||||
ReservedPrefix { value: String },
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_id_char(c: char) -> bool {
|
||||
c.is_ascii_alphanumeric() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn is_valid_segment(s: &str) -> bool {
|
||||
!s.is_empty() && s.chars().all(is_id_char)
|
||||
}
|
||||
|
||||
fn ensure_non_empty(s: &str) -> Result<(), IdError> {
|
||||
if s.is_empty() {
|
||||
Err(IdError::Empty)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a string-backed opaque id newtype.
|
||||
///
|
||||
/// Emits `new`, `as_str`, `into_inner`, `AsRef<str>`, `Display`, `FromStr`,
|
||||
/// `TryFrom<String>`, and a validating `Deserialize` (which routes through
|
||||
/// `Self::new`). `Serialize` is derived transparently.
|
||||
///
|
||||
/// An optional `extra_validator = $path` clause accepts a
|
||||
/// `fn(&str) -> Result<(), IdError>` that runs after the empty-string
|
||||
/// check.
|
||||
macro_rules! opaque_id {
|
||||
($(#[$meta:meta])* $name:ident $(, extra_validator = $validator:path)?) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
/// Construct, validating the id's invariants.
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, IdError> {
|
||||
let value = value.into();
|
||||
ensure_non_empty(&value)?;
|
||||
$($validator(&value)?;)?
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for $name {
|
||||
type Err = IdError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for $name {
|
||||
type Error = IdError;
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $name {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
Self::new(raw).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
opaque_id!(
|
||||
/// Session identifier. Service-issued or carried from a JWT claim.
|
||||
SessionId
|
||||
);
|
||||
opaque_id!(
|
||||
/// User identifier (the JWT `sub` claim).
|
||||
UserId
|
||||
);
|
||||
opaque_id!(
|
||||
/// Per-connection identifier issued by the computer hub.
|
||||
ConnectionId
|
||||
);
|
||||
opaque_id!(
|
||||
/// JSON-RPC request id as it appears on the wire.
|
||||
RequestId
|
||||
);
|
||||
opaque_id!(
|
||||
/// End-to-end identifier for a single tool invocation.
|
||||
///
|
||||
/// SDKs SHOULD use UUID v7 (see [`ToolCallId::new_v7`]).
|
||||
ToolCallId
|
||||
);
|
||||
|
||||
impl ToolCallId {
|
||||
/// Generate a fresh UUID v7-backed `ToolCallId`.
|
||||
pub fn new_v7() -> Self {
|
||||
Self(uuid::Uuid::now_v7().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
const SERVER_ID_RESERVED_PREFIX: &str = "auto:";
|
||||
|
||||
fn validate_server_id(s: &str) -> Result<(), IdError> {
|
||||
if s.starts_with(SERVER_ID_RESERVED_PREFIX) {
|
||||
return Err(IdError::ReservedPrefix {
|
||||
value: s.to_owned(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
opaque_id!(
|
||||
/// Server identifier.
|
||||
///
|
||||
/// Opaque non-empty string; the lexical prefix `auto:` is reserved for
|
||||
/// computer-hub-synthesised ids and rejected from client-supplied values.
|
||||
ServerId,
|
||||
extra_validator = validate_server_id
|
||||
);
|
||||
|
||||
impl ServerId {
|
||||
/// Synthesise the deterministic computer-hub-side id for a single-tool
|
||||
/// `register_tool` that omits `server_id`.
|
||||
///
|
||||
/// Bypasses [`ServerId::new`]'s reserved-prefix check.
|
||||
/// `connection_id` is part of the signature so callers can't omit
|
||||
/// the connection scope they are implicitly relying on, even though
|
||||
/// the current encoding does not mix it in. Two connections that
|
||||
/// register the same `tool_id` without an explicit `server_id`
|
||||
/// share the synthesised id but stay distinct in the registry's
|
||||
/// primary `(connection_id, tool_id)` table.
|
||||
pub fn synthesize_for_tool(
|
||||
#[allow(unused_variables)] connection_id: &ConnectionId,
|
||||
tool_id: &ToolId,
|
||||
) -> Self {
|
||||
Self(format!("{SERVER_ID_RESERVED_PREFIX}tool:{tool_id}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_tool_id(s: &str) -> Result<(), IdError> {
|
||||
if !is_well_formed_tool_id(s) {
|
||||
return Err(IdError::InvalidFormat {
|
||||
value: s.to_owned(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_well_formed_tool_id(s: &str) -> bool {
|
||||
let mut parts = s.splitn(3, ':');
|
||||
let Some(first) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
match (parts.next(), parts.next()) {
|
||||
(None, _) => is_valid_segment(first),
|
||||
(Some(second), None) => is_valid_segment(first) && is_valid_segment(second),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
opaque_id!(
|
||||
/// Tool identifier.
|
||||
///
|
||||
/// Format: `{namespace}:{name}` or `{name}`. Each segment must match
|
||||
/// `[a-zA-Z0-9_-]+`.
|
||||
ToolId,
|
||||
extra_validator = validate_tool_id
|
||||
);
|
||||
|
||||
/// Per-connection monotonic notification sequence (starts at 0 on every new
|
||||
/// connection).
|
||||
///
|
||||
/// The inner `u64` is private so `new`, `From<u64>`, and `Default` are the
|
||||
/// only construction paths.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct FrameSeq(u64);
|
||||
|
||||
impl FrameSeq {
|
||||
pub const fn new(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub const fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FrameSeq {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for FrameSeq {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user