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,294 @@
|
||||
//! `[[campaigns]]` overlays. Priority (first id wins): requirements > remote >
|
||||
//! user > managed > system_managed. Applied after layer merge.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config_override::{
|
||||
ConfigOverrideEntry, PATCH_STRIP_KEYS, PatchPath, apply_patches, patch_touches_any,
|
||||
take_patch_array,
|
||||
};
|
||||
|
||||
pub const CAMPAIGNS_KEY: &str = "campaigns";
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CampaignMeta {
|
||||
#[serde(default, alias = "campaign_id")]
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CampaignEntry {
|
||||
pub id: String,
|
||||
pub patch: toml::Table,
|
||||
}
|
||||
|
||||
/// Disk campaigns grouped by source layer. Merged with the remote layer (by
|
||||
/// priority, first id wins) in [`crate::loader::ConfigLayers::resolve_campaigns`].
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CampaignOverrides {
|
||||
pub requirements: Vec<CampaignEntry>,
|
||||
pub user: Vec<CampaignEntry>,
|
||||
pub managed: Vec<CampaignEntry>,
|
||||
pub system_managed: Vec<CampaignEntry>,
|
||||
}
|
||||
|
||||
pub fn take_campaigns(config: &mut toml::Value) -> Vec<ConfigOverrideEntry<CampaignMeta>> {
|
||||
match take_patch_array::<CampaignMeta>(config, CAMPAIGNS_KEY) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "campaigns: failed to deserialize; ignoring entries");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_campaign_entries(
|
||||
taken: Vec<ConfigOverrideEntry<CampaignMeta>>,
|
||||
layer: &'static str,
|
||||
) -> Vec<CampaignEntry> {
|
||||
let mut out = Vec::with_capacity(taken.len());
|
||||
for entry in taken {
|
||||
let Some(id) = entry
|
||||
.meta
|
||||
.id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_owned)
|
||||
else {
|
||||
tracing::warn!(layer, "campaigns: entry missing id; skipped");
|
||||
continue;
|
||||
};
|
||||
// Skip no-op entries (id only, no fields to overlay).
|
||||
if entry.patch.is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.push(CampaignEntry {
|
||||
id,
|
||||
patch: entry.patch,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `sources` in priority order; first `id` wins.
|
||||
pub fn merge_campaign_entries(sources: &[&[CampaignEntry]]) -> Vec<CampaignEntry> {
|
||||
let mut seen = std::collections::HashSet::<String>::new();
|
||||
let mut out = Vec::new();
|
||||
for source in sources {
|
||||
for entry in *source {
|
||||
if !seen.insert(entry.id.clone()) {
|
||||
continue;
|
||||
}
|
||||
out.push(entry.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Drop dismissed ids from a priority-merged list, preserving order.
|
||||
pub fn filter_active_campaigns(
|
||||
merged: Vec<CampaignEntry>,
|
||||
dismissed_ids: &std::collections::HashSet<String>,
|
||||
) -> Vec<CampaignEntry> {
|
||||
merged
|
||||
.into_iter()
|
||||
.filter(|e| !dismissed_ids.contains(&e.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Ids of `active` campaigns whose patch touches any of `paths` — used to dismiss
|
||||
/// campaigns when the user persists a value at one of those paths.
|
||||
pub fn ids_touching_paths(active: &[CampaignEntry], paths: &[PatchPath]) -> Vec<String> {
|
||||
active
|
||||
.iter()
|
||||
.filter(|e| patch_touches_any(&e.patch, paths))
|
||||
.map(|e| e.id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `active` is highest-priority-first; patches apply lowest-first (`.rev()`) so the
|
||||
/// highest-priority source wins a leaf conflict.
|
||||
pub fn apply_active_campaign_patches(effective: &mut toml::Value, active: &[CampaignEntry]) {
|
||||
apply_patches(
|
||||
effective,
|
||||
active.iter().rev().map(|e| e.patch.clone()),
|
||||
PATCH_STRIP_KEYS,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn take_campaign_entries(config: &mut toml::Value, layer: &'static str) -> Vec<CampaignEntry> {
|
||||
build_campaign_entries(take_campaigns(config), layer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(s: &str) -> toml::Value {
|
||||
toml::from_str(s).unwrap()
|
||||
}
|
||||
|
||||
fn models_default_patch(default: &str) -> toml::Table {
|
||||
let mut models = toml::map::Map::new();
|
||||
models.insert("default".into(), toml::Value::String(default.into()));
|
||||
let mut t = toml::map::Map::new();
|
||||
t.insert("models".into(), toml::Value::Table(models));
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_overlays_any_field_over_user_config() {
|
||||
let mut layer = parse(
|
||||
r#"
|
||||
[[campaigns]]
|
||||
id = "c1"
|
||||
[campaigns.models]
|
||||
default = "new-model"
|
||||
[campaigns.features]
|
||||
web_fetch = true
|
||||
"#,
|
||||
);
|
||||
let entries = take_campaign_entries(&mut layer, "managed");
|
||||
assert!(layer.get(CAMPAIGNS_KEY).is_none());
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
let mut effective =
|
||||
parse("[models]\ndefault = \"old-model\"\n[features]\nweb_fetch = false\n");
|
||||
apply_active_campaign_patches(&mut effective, &entries);
|
||||
assert_eq!(effective["models"]["default"].as_str(), Some("new-model"));
|
||||
assert_eq!(effective["features"]["web_fetch"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_first_source_wins_duplicate_id() {
|
||||
let req = [CampaignEntry {
|
||||
id: "same".into(),
|
||||
patch: models_default_patch("from-req"),
|
||||
}];
|
||||
let remote = [CampaignEntry {
|
||||
id: "same".into(),
|
||||
patch: models_default_patch("from-remote"),
|
||||
}];
|
||||
let merged = merge_campaign_entries(&[&req, &remote]);
|
||||
assert_eq!(merged.len(), 1);
|
||||
assert_eq!(
|
||||
merged[0].patch["models"]["default"].as_str(),
|
||||
Some("from-req")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_highest_priority_wins_on_leaf_conflict() {
|
||||
// Two *distinct* ids both set models.default; the higher-priority source
|
||||
// (earlier in the merged list) must win the leaf.
|
||||
let req = [CampaignEntry {
|
||||
id: "req".into(),
|
||||
patch: models_default_patch("from-req"),
|
||||
}];
|
||||
let managed = [CampaignEntry {
|
||||
id: "managed".into(),
|
||||
patch: models_default_patch("from-managed"),
|
||||
}];
|
||||
let merged = merge_campaign_entries(&[&req, &managed]);
|
||||
assert_eq!(merged.len(), 2);
|
||||
|
||||
let mut effective = parse("[models]\ndefault = \"user-old\"\n");
|
||||
apply_active_campaign_patches(&mut effective, &merged);
|
||||
assert_eq!(effective["models"]["default"].as_str(), Some("from-req"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_campaign_entries_skips_missing_id() {
|
||||
// A `None` id and a whitespace-only id are both dropped (with a warn);
|
||||
// only the entry carrying a real id survives.
|
||||
let taken = vec![
|
||||
ConfigOverrideEntry {
|
||||
meta: CampaignMeta { id: None },
|
||||
patch: models_default_patch("dropped-none"),
|
||||
},
|
||||
ConfigOverrideEntry {
|
||||
meta: CampaignMeta {
|
||||
id: Some(" ".into()),
|
||||
},
|
||||
patch: models_default_patch("dropped-blank"),
|
||||
},
|
||||
ConfigOverrideEntry {
|
||||
meta: CampaignMeta {
|
||||
id: Some("valid".into()),
|
||||
},
|
||||
patch: models_default_patch("kept"),
|
||||
},
|
||||
];
|
||||
let out = build_campaign_entries(taken, "managed");
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].id, "valid");
|
||||
assert_eq!(out[0].patch["models"]["default"].as_str(), Some("kept"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_id_alias_is_accepted_in_toml_and_does_not_leak_into_patch() {
|
||||
for src in [
|
||||
"[[campaigns]]\ncampaign_id = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
|
||||
"[[campaigns]]\nid = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
|
||||
] {
|
||||
let mut layer = parse(src);
|
||||
let entries = take_campaign_entries(&mut layer, "user");
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].id, "c1");
|
||||
// The id key (either spelling) must be consumed by the meta, never
|
||||
// land in the patch — a leaked key would deep-merge a junk top-level
|
||||
// `id` into every effective config.
|
||||
assert!(
|
||||
entries[0].patch.get("id").is_none()
|
||||
&& entries[0].patch.get("campaign_id").is_none(),
|
||||
"id keys must not leak into the patch: {src}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_win_over_campaign() {
|
||||
use crate::loader::ConfigLayers;
|
||||
// A campaign (even from a lower layer) can't override a field the admin
|
||||
// set in requirements: `apply_campaign_overrides` re-merges requirements on top.
|
||||
let mut layers = ConfigLayers {
|
||||
user: parse("[models]\ndefault = \"user-old\"\n"),
|
||||
user_requirements: Some(parse("[models]\ndefault = \"pinned\"\n")),
|
||||
..Default::default()
|
||||
};
|
||||
layers.campaigns.user = vec![CampaignEntry {
|
||||
id: "c1".into(),
|
||||
patch: models_default_patch("campaign"),
|
||||
}];
|
||||
let effective =
|
||||
layers.effective_config_with_campaigns(&[], &std::collections::HashSet::new());
|
||||
assert_eq!(
|
||||
effective["models"]["default"].as_str(),
|
||||
Some("pinned"),
|
||||
"requirements must beat a campaign for the same field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_config_honors_dismiss() {
|
||||
use crate::loader::ConfigLayers;
|
||||
// A dismissed campaign id stops overriding; the user's stored value returns.
|
||||
let mut layers = ConfigLayers {
|
||||
user: parse("[models]\ndefault = \"user-old\"\n"),
|
||||
..Default::default()
|
||||
};
|
||||
layers.campaigns.managed = vec![CampaignEntry {
|
||||
id: "c1".into(),
|
||||
patch: models_default_patch("new"),
|
||||
}];
|
||||
|
||||
let none = std::collections::HashSet::new();
|
||||
let active = layers.effective_config_with_campaigns(&[], &none);
|
||||
assert_eq!(active["models"]["default"].as_str(), Some("new"));
|
||||
|
||||
let dismissed: std::collections::HashSet<_> = ["c1".into()].into_iter().collect();
|
||||
let off = layers.effective_config_with_campaigns(&[], &dismissed);
|
||||
assert_eq!(off["models"]["default"].as_str(), Some("user-old"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user