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,332 @@
|
||||
//! Capability-mode filtering for session toolsets.
|
||||
|
||||
use kigi_tools::registry::types::{ToolConfig, ToolServerConfig};
|
||||
use kigi_tools::types::tool::ToolKind;
|
||||
|
||||
/// Capability mode applied to a session's toolset.
|
||||
///
|
||||
/// A partial order is defined via [`CapabilityMode::is_subset_of`]:
|
||||
/// `ReadOnly < ReadWrite < All` and `ReadOnly < Execute < All`.
|
||||
/// `ReadWrite` and `Execute` are *incomparable* (neither is a subset
|
||||
/// of the other). `fork_session` enforces `child <= parent`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CapabilityMode {
|
||||
/// Reading and searching only. No edits, no shell, no background tasks.
|
||||
ReadOnly,
|
||||
/// Read + edit. No shell execution.
|
||||
ReadWrite,
|
||||
/// Read + shell execution + background-task control. No edits.
|
||||
Execute,
|
||||
/// Every tool kind allowed.
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for CapabilityMode {
|
||||
/// Defaults to [`CapabilityMode::ReadWrite`] (subagent default;
|
||||
/// the main/root session is always `All`).
|
||||
fn default() -> Self {
|
||||
Self::ReadWrite
|
||||
}
|
||||
}
|
||||
|
||||
impl CapabilityMode {
|
||||
/// Filter `config.tools` by capability mode, returning a copy with
|
||||
/// disallowed tools dropped.
|
||||
///
|
||||
/// Tools whose `kind` is `None` (baseline, e.g. ad-hoc tools
|
||||
/// declared via `ToolConfig::simple`) are preserved across all
|
||||
/// modes. **MCP-origin** `kind: None` tools are NOT preserved by
|
||||
/// this method; see `resolve_session_toolset` for the asymmetric
|
||||
/// handling.
|
||||
pub fn filter(self, config: &ToolServerConfig) -> ToolServerConfig {
|
||||
let kept: Vec<ToolConfig> = config
|
||||
.tools
|
||||
.iter()
|
||||
.filter(|tool| match tool.kind {
|
||||
None => true,
|
||||
Some(kind) => kind_allowed(self, kind),
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
ToolServerConfig {
|
||||
tools: kept,
|
||||
behavior_preset: config.behavior_preset.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every kind allowed by `self` is also allowed by `other`.
|
||||
/// Used by `fork_session` to reject capability widening.
|
||||
pub fn is_subset_of(self, other: CapabilityMode) -> bool {
|
||||
for kind in ALL_TOOL_KINDS {
|
||||
if kind_allowed(self, *kind) && !kind_allowed(other, *kind) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `ToolKind` variant. Used by `is_subset_of` and by parameterised
|
||||
/// tests. When a new variant is added to `ToolKind`, the compile-time
|
||||
/// assertion below fires so it can't be silently omitted.
|
||||
pub(crate) const ALL_TOOL_KINDS: &[ToolKind] = &[
|
||||
ToolKind::Read,
|
||||
ToolKind::Edit,
|
||||
ToolKind::Delete,
|
||||
ToolKind::ListDir,
|
||||
ToolKind::Write,
|
||||
ToolKind::Move,
|
||||
ToolKind::Search,
|
||||
ToolKind::Lsp,
|
||||
ToolKind::Execute,
|
||||
ToolKind::Plan,
|
||||
ToolKind::WebSearch,
|
||||
ToolKind::WebFetch,
|
||||
ToolKind::BackgroundTaskAction,
|
||||
ToolKind::WaitTasksAction,
|
||||
ToolKind::KillTaskAction,
|
||||
ToolKind::List,
|
||||
ToolKind::Skill,
|
||||
ToolKind::MemorySearch,
|
||||
ToolKind::MemoryGet,
|
||||
ToolKind::Task,
|
||||
ToolKind::EnterPlan,
|
||||
ToolKind::ExitPlan,
|
||||
ToolKind::AskUser,
|
||||
ToolKind::ImageGen,
|
||||
ToolKind::VideoGen,
|
||||
ToolKind::ImageToVideo,
|
||||
ToolKind::ReferenceToVideo,
|
||||
ToolKind::DeployApp,
|
||||
ToolKind::SearchTool,
|
||||
ToolKind::UseTool,
|
||||
ToolKind::Monitor,
|
||||
ToolKind::GoalUpdate,
|
||||
ToolKind::Other,
|
||||
];
|
||||
|
||||
// Compile-time guard: if a new `ToolKind` variant is added but not listed in
|
||||
// `ALL_TOOL_KINDS`, this assertion fails.
|
||||
const _: () = assert!(
|
||||
ALL_TOOL_KINDS.len() == ToolKind::VARIANT_COUNT,
|
||||
"ALL_TOOL_KINDS is out of sync with ToolKind — add the new variant"
|
||||
);
|
||||
|
||||
/// Maps `(CapabilityMode, ToolKind)` -> kept-or-dropped.
|
||||
///
|
||||
/// This `match` is intentionally exhaustive: when `ToolKind` gains a
|
||||
/// new variant the compiler errors here, forcing a triage decision.
|
||||
pub(crate) fn kind_allowed(mode: CapabilityMode, kind: ToolKind) -> bool {
|
||||
use CapabilityMode as M;
|
||||
use ToolKind::*;
|
||||
|
||||
if matches!(mode, M::All) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match kind {
|
||||
// Meta tools: always allowed.
|
||||
Plan | EnterPlan | ExitPlan | AskUser | Skill | SearchTool | GoalUpdate => true,
|
||||
|
||||
// Read class.
|
||||
Read | MemoryGet | MemorySearch => {
|
||||
matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute)
|
||||
}
|
||||
|
||||
// Search class.
|
||||
Search | WebSearch | WebFetch => {
|
||||
matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute)
|
||||
}
|
||||
|
||||
// Inspect class.
|
||||
Lsp | ListDir | List => matches!(mode, M::ReadOnly | M::ReadWrite | M::Execute),
|
||||
|
||||
// Edit class.
|
||||
Edit | Write | Delete | Move | ImageGen | VideoGen | ImageToVideo | ReferenceToVideo
|
||||
| DeployApp => matches!(mode, M::ReadWrite),
|
||||
|
||||
// Bash / shell.
|
||||
Execute => matches!(mode, M::Execute),
|
||||
|
||||
// Process control (background tasks, monitors).
|
||||
BackgroundTaskAction | WaitTasksAction | KillTaskAction | Task | Monitor => {
|
||||
matches!(mode, M::Execute)
|
||||
}
|
||||
|
||||
// Integration dispatch.
|
||||
UseTool => matches!(mode, M::ReadWrite | M::Execute),
|
||||
|
||||
// Catch-all -- only `All` mode keeps it (early-return above).
|
||||
Other => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::session::tool_config::test_support;
|
||||
use kigi_tools::types::tool::ToolKind;
|
||||
|
||||
fn make_cfg(tools: Vec<ToolConfig>) -> ToolServerConfig {
|
||||
ToolServerConfig {
|
||||
tools,
|
||||
behavior_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_filter_table_is_exhaustive_per_kind() {
|
||||
for &mode in &[
|
||||
CapabilityMode::ReadOnly,
|
||||
CapabilityMode::ReadWrite,
|
||||
CapabilityMode::Execute,
|
||||
CapabilityMode::All,
|
||||
] {
|
||||
for &kind in ALL_TOOL_KINDS {
|
||||
let id = format!("kind_{kind:?}");
|
||||
let cfg = make_cfg(vec![test_support::tc(&id, Some(kind))]);
|
||||
let out = mode.filter(&cfg);
|
||||
let expected_present = kind_allowed(mode, kind);
|
||||
let actually_present = out.tools.iter().any(|t| t.id == id);
|
||||
assert_eq!(
|
||||
actually_present, expected_present,
|
||||
"({mode:?}, {kind:?}): expected present={expected_present}, got {actually_present}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_filter_anchored_membership() {
|
||||
let cfg = make_cfg(vec![
|
||||
test_support::tc("read", Some(ToolKind::Read)),
|
||||
test_support::tc("search", Some(ToolKind::Search)),
|
||||
test_support::tc("inspect", Some(ToolKind::Lsp)),
|
||||
test_support::tc("edit", Some(ToolKind::Edit)),
|
||||
test_support::tc("write", Some(ToolKind::Write)),
|
||||
test_support::tc("bash", Some(ToolKind::Execute)),
|
||||
test_support::tc("bg", Some(ToolKind::BackgroundTaskAction)),
|
||||
test_support::tc("plan", Some(ToolKind::Plan)),
|
||||
test_support::tc("ask", Some(ToolKind::AskUser)),
|
||||
test_support::tc("other", Some(ToolKind::Other)),
|
||||
]);
|
||||
|
||||
let names = |c: &ToolServerConfig| -> Vec<String> {
|
||||
c.tools.iter().map(|t| t.id.clone()).collect()
|
||||
};
|
||||
|
||||
let ro = CapabilityMode::ReadOnly.filter(&cfg);
|
||||
assert_eq!(names(&ro), vec!["read", "search", "inspect", "plan", "ask"]);
|
||||
|
||||
let rw = CapabilityMode::ReadWrite.filter(&cfg);
|
||||
assert_eq!(
|
||||
names(&rw),
|
||||
vec!["read", "search", "inspect", "edit", "write", "plan", "ask"]
|
||||
);
|
||||
|
||||
let ex = CapabilityMode::Execute.filter(&cfg);
|
||||
assert_eq!(
|
||||
names(&ex),
|
||||
vec!["read", "search", "inspect", "bash", "bg", "plan", "ask"]
|
||||
);
|
||||
|
||||
let all = CapabilityMode::All.filter(&cfg);
|
||||
assert_eq!(
|
||||
names(&all),
|
||||
vec![
|
||||
"read", "search", "inspect", "edit", "write", "bash", "bg", "plan", "ask", "other"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_baseline_kind_none_always_kept_via_filter() {
|
||||
let cfg = make_cfg(vec![
|
||||
test_support::tc("baseline.opaque", None),
|
||||
test_support::tc("baseline.also_opaque", None),
|
||||
test_support::tc("edit_dropped", Some(ToolKind::Edit)),
|
||||
]);
|
||||
|
||||
for mode in [
|
||||
CapabilityMode::ReadOnly,
|
||||
CapabilityMode::ReadWrite,
|
||||
CapabilityMode::Execute,
|
||||
CapabilityMode::All,
|
||||
] {
|
||||
let filtered = mode.filter(&cfg);
|
||||
let ids: Vec<&str> = filtered.tools.iter().map(|t| t.id.as_str()).collect();
|
||||
assert!(
|
||||
ids.contains(&"baseline.opaque"),
|
||||
"kind: None tool dropped under {mode:?}: {ids:?}"
|
||||
);
|
||||
assert!(
|
||||
ids.contains(&"baseline.also_opaque"),
|
||||
"kind: None tool dropped under {mode:?}: {ids:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_preserves_behavior_preset_across_all_modes() {
|
||||
let mut cfg = make_cfg(vec![test_support::tc("read", Some(ToolKind::Read))]);
|
||||
cfg.behavior_preset = Some("current".to_owned());
|
||||
for mode in [
|
||||
CapabilityMode::ReadOnly,
|
||||
CapabilityMode::ReadWrite,
|
||||
CapabilityMode::Execute,
|
||||
CapabilityMode::All,
|
||||
] {
|
||||
let out = mode.filter(&cfg);
|
||||
assert_eq!(
|
||||
out.behavior_preset.as_deref(),
|
||||
Some("current"),
|
||||
"behavior_preset lost under {mode:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// is_subset_of partial order
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn capability_mode_is_subset_of_reflexive() {
|
||||
for &m in &[
|
||||
CapabilityMode::ReadOnly,
|
||||
CapabilityMode::ReadWrite,
|
||||
CapabilityMode::Execute,
|
||||
CapabilityMode::All,
|
||||
] {
|
||||
assert!(m.is_subset_of(m), "{m:?} must be a subset of itself");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_is_subset_of_strict_chains() {
|
||||
assert!(CapabilityMode::ReadOnly.is_subset_of(CapabilityMode::ReadWrite));
|
||||
assert!(CapabilityMode::ReadOnly.is_subset_of(CapabilityMode::All));
|
||||
assert!(CapabilityMode::ReadWrite.is_subset_of(CapabilityMode::All));
|
||||
assert!(CapabilityMode::ReadOnly.is_subset_of(CapabilityMode::Execute));
|
||||
assert!(CapabilityMode::Execute.is_subset_of(CapabilityMode::All));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_is_subset_of_widening_rejected() {
|
||||
assert!(!CapabilityMode::All.is_subset_of(CapabilityMode::ReadOnly));
|
||||
assert!(!CapabilityMode::ReadWrite.is_subset_of(CapabilityMode::ReadOnly));
|
||||
assert!(!CapabilityMode::Execute.is_subset_of(CapabilityMode::ReadOnly));
|
||||
assert!(!CapabilityMode::All.is_subset_of(CapabilityMode::ReadWrite));
|
||||
assert!(!CapabilityMode::All.is_subset_of(CapabilityMode::Execute));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_mode_is_subset_of_incomparable_pairs() {
|
||||
assert!(!CapabilityMode::ReadWrite.is_subset_of(CapabilityMode::Execute));
|
||||
assert!(!CapabilityMode::Execute.is_subset_of(CapabilityMode::ReadWrite));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user