§9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed

The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
This commit is contained in:
2026-07-18 02:48:46 -04:00
parent 86e3724310
commit 6f31415ed6
1056 changed files with 8410 additions and 18307 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ pub struct WorkspaceConfig {
/// shell caller resolves the verdict and threads it in; callers without a
/// folder-trust decision pass `true`.
pub project_lsp_trusted: bool,
/// Confine `x.ai/fs/*` / `workspace.fs_*` resolution to the workspace root
/// Confine `kigi/fs/*` / `workspace.fs_*` resolution to the workspace root
/// (reject `..`, absolute-outside-root, symlink escapes). Default `false`
/// (unconfined) — set to `true` only by the workspace server on a remote
/// sandbox, where the root is a real tenant boundary.
@@ -78,7 +78,7 @@ pub async fn discover_agents_md(root_cwd: &Path) -> Vec<Value> {
files
.into_iter()
.map(|mut file| {
// Strip rules-file YAML frontmatter so it does not leak as raw YAML (matches grok-build render).
// Strip rules-file YAML frontmatter so it does not leak as raw YAML (matches kigi render).
if file.file_path.contains("/.kigi/rules/")
|| file.file_path.contains("/.claude/rules/")
{
@@ -332,7 +332,7 @@ mod tests {
#[test]
fn agent_config_file_wire_matches_workspace_types_mirror() {
// The RPC serializes grok-build's AgentConfigFile and the remote
// The RPC serializes kigi's AgentConfigFile and the remote
// consumer deserializes the workspace-types mirror; pin the cross-crate
// serde shape so a rename/attr drift on either side can't silently
// break discovery.
@@ -484,10 +484,10 @@ mod tests {
#[test]
fn load_project_config_reads_toml_as_json() {
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
fs::create_dir_all(&grok_dir).unwrap();
let kigi_dir = tmp.path().join(".kigi");
fs::create_dir_all(&kigi_dir).unwrap();
fs::write(
grok_dir.join("config.toml"),
kigi_dir.join("config.toml"),
"[skills]\npaths = [\"/extra/skills\"]\n\n[plugins]\ndisabled = [\"noisy-plugin\"]\n",
)
.unwrap();
@@ -536,7 +536,7 @@ mod tests {
// Note: `resolve_permissions_with_provenance` checks system-managed
// settings and requirements.toml from the global config, so on a
// developer machine with Grok installed it may return non-Null even
// developer machine with Kigi installed it may return non-Null even
// for a temp directory. Both branches assert a concrete condition.
#[tokio::test]
@@ -237,7 +237,7 @@ async fn render_blob_attachment(blob: &BlobResourceContents) -> Option<String> {
/// `/tmp`) never collide on identical content-hash paths and
/// race each other's cleanup. The real session_dir lives in shell persistence.
fn session_scratch_root() -> PathBuf {
std::env::temp_dir().join(format!("grok-test-sessions-{}", std::process::id()))
std::env::temp_dir().join(format!("kigi-test-sessions-{}", std::process::id()))
}
/// Write content to session subdir, return absolute path on success.
/// Uses content hash prefix for dedup: identical content → same path, different content → unique path.
@@ -508,8 +508,8 @@ mod tests {
let _ = std::fs::remove_file(&expected);
}
#[tokio::test]
async fn test_grok_render_embedded_resource_uses_file_contents_tag() {
let _info = test_info("grok-text");
async fn test_kigi_render_embedded_resource_uses_file_contents_tag() {
let _info = test_info("kigi-text");
let resource = EmbeddedResource::new(EmbeddedResourceResource::TextResourceContents(
agent_client_protocol::TextResourceContents::new(
"const x = 1;\nconst y = 2;\n",
@@ -524,8 +524,8 @@ mod tests {
assert!(!rendered.contains("code_selection"), "got: {rendered}");
}
#[tokio::test]
async fn test_grok_render_embedded_resource_full_file_uses_is_full_file() {
let _info = test_info("grok-full-file");
async fn test_kigi_render_embedded_resource_full_file_uses_is_full_file() {
let _info = test_info("kigi-full-file");
let resource = EmbeddedResource::new(EmbeddedResourceResource::TextResourceContents(
agent_client_protocol::TextResourceContents::new(
"fn main() {}\n",
@@ -125,7 +125,7 @@ mod tests {
//
// These tests verify the core lazy-start behavior:
// `CodebaseIndexManager::get()` returns None before the index is created,
// which maps to `x.ai/code/status` reporting `reason: notStarted`.
// which maps to `kigi/code/status` reporting `reason: notStarted`.
// `get_or_create()` is the lazy-start entry point called by
// `MvpAgent::start_codebase_index_for_code_nav` on the first code-nav
// request for an eligible session.
@@ -134,7 +134,7 @@ mod tests {
/// An empty CodebaseIndexManager returns None for any path.
///
/// This is the steady-state before ANY code-nav request has been made.
/// In `x.ai/code/status`, `resolve_index_handle()` calls
/// In `kigi/code/status`, `resolve_index_handle()` calls
/// `agent.get_codebase_index(cwd)` which calls `mgr.get(cwd)`.
/// When this returns None the status reports `reason: notStarted` — the
/// key non-starting guarantee from the plan.
@@ -1,9 +1,9 @@
//! Filesystem extension ops (`workspace.fs_*`) — the server-proxied backing
//! for the shell's `x.ai/fs/*` ACP extension methods.
//! for the shell's `kigi/fs/*` ACP extension methods.
//!
//! These mirror the pure functions that previously lived only in the
//! shell (`kigi-shell/src/session/file_system.rs`) so that, in proxy
//! mode, a `x.ai/fs/*` request executes on the *remote* workspace server
//! mode, a `kigi/fs/*` request executes on the *remote* workspace server
//! instead of the agent host. Each request type implements
//! [`WorkspaceOp`], so it runs in-process for local sessions and routes
//! over the server `workspace_rpc` tool for proxy sessions — identical wire
@@ -42,7 +42,7 @@ pub fn clamp_read_length(length: Option<u64>, max_bytes: u64) -> u64 {
.min(MAX_READ_BYTES)
}
/// Walk configuration. Field semantics mirror the `x.ai/fs/list` request.
/// Walk configuration. Field semantics mirror the `kigi/fs/list` request.
pub(super) struct FsWalk<'a> {
pub depth: usize,
pub follow_symlinks: bool,
@@ -119,7 +119,7 @@ pub fn decide_inputs_with_interactive(
/// this binary — true on a local/dev build (no `KIGI_VERSION` release stamp).
///
/// THE single security short-circuit: every explicit trust auto-grant site calls
/// this (greppable via `folder_trust_inert`). When true a self-built grok never
/// this (greppable via `folder_trust_inert`). When true a self-built kigi never
/// prompts, never gates repo-local `.envrc`/`.claude`/hooks/plugins/MCP/LSP, and
/// does NO `trusted_folders.toml` I/O. Release-stamped builds are unaffected.
pub fn folder_trust_inert() -> bool {
@@ -145,7 +145,7 @@ fn is_local_build() -> bool {
/// Resolve whether the folder-trust gate is enabled.
///
/// On a local/dev build (no `KIGI_VERSION` release stamp) the feature is OFF
/// regardless of env/config/remote — a self-built grok auto-trusts (never
/// regardless of env/config/remote — a self-built kigi auto-trusts (never
/// prompts, never gates repo-local MCP/LSP). Folder-trust applies only to
/// shipped, release-stamped binaries.
///
@@ -162,7 +162,7 @@ pub fn feature_enabled(remote: Option<&RemoteSettings>) -> bool {
fn feature_enabled_for_build(remote: Option<&RemoteSettings>, is_local_build: bool) -> bool {
// Local/dev builds never gate (auto-trust): folder-trust applies only to
// shipped, release-stamped binaries. Even an explicit KIGI_FOLDER_TRUST/config
// opt-in is ignored here so a self-built grok never prompts.
// opt-in is ignored here so a self-built kigi never prompts.
if is_local_build {
return false;
}
@@ -354,7 +354,7 @@ fn collect_repo_config_kinds(cwd: &Path, first_only: bool) -> Vec<&'static str>
// Project PLUGIN dirs: project-scoped plugins are unified under folder-trust
// too, so a repo-local plugin dir is repo-controlled code-exec (hooks/MCP)
// that must be gated — else a plugin clone (e.g. `.kigi/plugins/evil/`, even
// one in a subdir launched via `cd sub && grok`) would resolve trusted and
// one in a subdir launched via `cd sub && kigi`) would resolve trusted and
// run ungated. Uses the shared SSOT walk (cwd→git root) so detection matches
// exactly what `discover_plugins` scans for Project scope (errs secure).
if !kigi_agent::plugins::project_plugin_dirs_in(&chain.dirs).is_empty() {
@@ -522,20 +522,20 @@ mod tests {
}
#[test]
fn repo_configs_present_detects_grok_config_mcp_servers() {
fn repo_configs_present_detects_kigi_config_mcp_servers() {
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("config.toml"), "[mcp_servers.x]\ncommand=\"y\"\n").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("config.toml"), "[mcp_servers.x]\ncommand=\"y\"\n").unwrap();
assert!(repo_configs_present(tmp.path()));
}
#[test]
fn repo_configs_present_detects_grok_lsp_json() {
fn repo_configs_present_detects_kigi_lsp_json() {
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("lsp.json"), "{}").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("lsp.json"), "{}").unwrap();
assert!(repo_configs_present(tmp.path()));
}
@@ -647,21 +647,21 @@ mod tests {
// A project config whose `[mcp_servers]` table is empty has nothing to
// gate, so it must not trip the gate.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("config.toml"), "[mcp_servers]\n").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("config.toml"), "[mcp_servers]\n").unwrap();
assert!(!repo_configs_present(tmp.path()));
}
#[test]
fn repo_configs_present_detects_grok_config_plugins_paths() {
fn repo_configs_present_detects_kigi_config_plugins_paths() {
// A repo whose ONLY repo-local config is `[plugins].paths` (no plugin
// dir, no MCP/LSP/hooks) must still be gated: those paths load as
// auto-trusted ConfigPath plugins, so an ungated clone is a live RCE.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("config.toml"), "[plugins]\npaths = [\"./x\"]\n").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("config.toml"), "[plugins]\npaths = [\"./x\"]\n").unwrap();
assert!(repo_configs_present(tmp.path()));
}
@@ -670,9 +670,9 @@ mod tests {
// An empty `[plugins].paths` (or a `[plugins]` table without `paths`)
// contributes no plugin code-exec, so it must not trip the gate.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("config.toml"), "[plugins]\npaths = []\n").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("config.toml"), "[plugins]\npaths = []\n").unwrap();
assert!(!repo_configs_present(tmp.path()));
}
@@ -685,9 +685,9 @@ mod tests {
// `.kigi/agents` — even when launched from a SUBDIR (the cwd→git-root walk
// that `first_only` shares). Guards against silent drift between the two.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(grok.join("agents")).unwrap();
std::fs::write(grok.join("config.toml"), "[plugins]\npaths = [\"./x\"]\n").unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(kigi.join("agents")).unwrap();
std::fs::write(kigi.join("config.toml"), "[plugins]\npaths = [\"./x\"]\n").unwrap();
let claude = tmp.path().join(".claude");
std::fs::create_dir_all(&claude).unwrap();
std::fs::write(claude.join("settings.json"), r#"{"env":{"X":"1"}}"#).unwrap();
@@ -789,7 +789,7 @@ mod tests {
#[test]
fn local_build_ignores_explicit_env_optin() {
// Auto-trust is absolute on a local build: even an explicit
// KIGI_FOLDER_TRUST=1 does NOT enable the feature (so a self-built grok
// KIGI_FOLDER_TRUST=1 does NOT enable the feature (so a self-built kigi
// never prompts). KIGI_SHARE_DIR isolated so on-disk config can't influence it.
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let home = tempfile::tempdir().unwrap();
+55 -58
View File
@@ -18,7 +18,7 @@ use std::sync::Arc;
pub(crate) static WORKSPACE_TERMINAL_BACKEND_ORPHANED_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_terminal_backend_orphaned_total",
"kigi_workspace_terminal_backend_orphaned_total",
"Terminal backends detected orphaned from their session, by detection path \
(tripwire, expected 0)",
&["path"]
@@ -43,7 +43,7 @@ use kigi_tool_protocol::turn_hook::{AfterTurnAckPayload, AfterTurnAckStatus};
pub(crate) static REWIND_CHECKPOINT_CAPTURE_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_rewind_checkpoint_capture_total",
"kigi_workspace_rewind_checkpoint_capture_total",
"Total rewind-checkpoint domain captures",
&["domain", "outcome"]
)
@@ -53,7 +53,7 @@ pub(crate) static REWIND_CHECKPOINT_CAPTURE_TOTAL: std::sync::LazyLock<IntCounte
pub(crate) static REWIND_CHECKPOINT_FINALIZE_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_rewind_checkpoint_finalize_total",
"kigi_workspace_rewind_checkpoint_finalize_total",
"Total rewind-checkpoint finalizes",
&["outcome"]
)
@@ -63,7 +63,7 @@ pub(crate) static REWIND_CHECKPOINT_FINALIZE_TOTAL: std::sync::LazyLock<IntCount
pub(crate) static REWIND_RESTORE_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_rewind_restore_total",
"kigi_workspace_rewind_restore_total",
"Total rewind-checkpoint domain restores",
&["domain", "result"]
)
@@ -73,7 +73,7 @@ pub(crate) static REWIND_RESTORE_TOTAL: std::sync::LazyLock<IntCounterVec> =
pub(crate) static REWIND_CHECKPOINT_DURATION: std::sync::LazyLock<HistogramVec> =
std::sync::LazyLock::new(|| {
register_histogram_vec!(
"grok_workspace_rewind_checkpoint_duration_seconds",
"kigi_workspace_rewind_checkpoint_duration_seconds",
"Duration of rewind-checkpoint per-domain capture operations",
&["domain"],
vec![0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
@@ -85,7 +85,7 @@ pub(crate) static REWIND_CHECKPOINT_DURATION: std::sync::LazyLock<HistogramVec>
pub(crate) static REWIND_NON_COMPLETED_FINALIZE_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_rewind_non_completed_finalize_total",
"kigi_workspace_rewind_non_completed_finalize_total",
"Non-Completed after_turn boundaries that produced a rewind finalize",
&["outcome"]
)
@@ -889,7 +889,7 @@ impl WorkspaceHandle {
}
/// Run one poll tick for an active fuzzy search. Returns the next batch of
/// results (paths absolutized against the search root) or a signal to keep
/// polling / stop. Drives the `x.ai/search/fuzzy/status` notification loop.
/// polling / stop. Drives the `kigi/search/fuzzy/status` notification loop.
pub async fn fuzzy_poll(
&self,
search_id: &str,
@@ -966,7 +966,7 @@ impl WorkspaceHandle {
sink(method, params);
}
}
/// Drive the `x.ai/search/fuzzy/status` stream for an active search: poll
/// Drive the `kigi/search/fuzzy/status` stream for an active search: poll
/// until done / closed / superseded, emitting each new result batch to the
/// client through the ext-notification sink. Co-located with the manager so
/// it polls in-process in both local and proxy mode.
@@ -1016,7 +1016,7 @@ impl WorkspaceHandle {
.unwrap_or_default(), }
);
}
self.emit_client_ext("x.ai/search/fuzzy/status".to_string(), params);
self.emit_client_ext("kigi/search/fuzzy/status".to_string(), params);
if data.done {
break;
}
@@ -1024,7 +1024,7 @@ impl WorkspaceHandle {
}
/// Run a content search (ripgrep) and return results.
/// Run a streaming content (ripgrep) search rooted at `cwd`, emitting each
/// batch as `x.ai/search/content/status` via the client sink, and returning
/// batch as `kigi/search/content/status` via the client sink, and returning
/// the final result. Co-located with the sink so it streams in both modes.
pub async fn run_content_search(
&self,
@@ -1042,7 +1042,7 @@ impl WorkspaceHandle {
.total_files, "done" : batch.done, "truncated" : batch.truncated,
}
);
handle.emit_client_ext("x.ai/search/content/status".to_string(), params);
handle.emit_client_ext("kigi/search/content/status".to_string(), params);
})
.await
.map_err(|e| WorkspaceError::Internal(e.to_string()))
@@ -1277,11 +1277,11 @@ fn decode_cancellation_category(s: Option<&str>) -> Option<CancellationCategory>
})
}
/// Per-process ephemeral workspace home for handles constructed without an
/// explicit home (tests, local mode). Never the real grok home —
/// explicit home (tests, local mode). Never the real kigi home —
/// only [`connect_local_workspace`] resolves `$KIGI_WORKSPACE_HOME` — so the
/// default path can never collide with a real workspace's state dir.
fn ephemeral_workspace_home() -> std::path::PathBuf {
std::env::temp_dir().join(format!("grok-workspace-ephemeral-{}", std::process::id()))
std::env::temp_dir().join(format!("kigi-workspace-ephemeral-{}", std::process::id()))
}
/// Resolve `workspace_rewind_all_outcomes` from `KIGI_WORKSPACE_REWIND_ALL_OUTCOMES` (default off).
fn rewind_all_outcomes_from_env() -> bool {
@@ -1464,7 +1464,7 @@ pub(crate) mod tests {
.get()
}
fn explicit_cfg(name_override: &str) -> ToolServerConfig {
let mut renamed = tc("GrokBuild:read_file", Some(ToolKind::Read));
let mut renamed = tc("Kigi:read_file", Some(ToolKind::Read));
renamed.name_override = Some(name_override.to_owned());
ToolServerConfig {
tools: vec![renamed],
@@ -1476,13 +1476,10 @@ pub(crate) mod tests {
pub(crate) fn background_capable_cfg() -> ToolServerConfig {
ToolServerConfig {
tools: vec![
tc("GrokBuild:read_file", Some(ToolKind::Read)),
tc("GrokBuild:run_terminal_cmd", Some(ToolKind::Execute)),
tc(
"GrokBuild:get_task_output",
Some(ToolKind::BackgroundTaskAction),
),
tc("GrokBuild:kill_task", Some(ToolKind::KillTaskAction)),
tc("Kigi:read_file", Some(ToolKind::Read)),
tc("Kigi:run_terminal_cmd", Some(ToolKind::Execute)),
tc("Kigi:get_task_output", Some(ToolKind::BackgroundTaskAction)),
tc("Kigi:kill_task", Some(ToolKind::KillTaskAction)),
],
behavior_preset: None,
}
@@ -1537,10 +1534,10 @@ pub(crate) mod tests {
let backend = session.terminal_backend().clone();
let out_dir = tempfile::tempdir().expect("temp dir");
let bg = start_background_sleep(&session, out_dir.path(), "snapshot-bg").await;
handle.shared.mcp_tools_snapshot.store(Arc::new(vec![tc(
"GrokBuild:read_file",
Some(ToolKind::Read),
)]));
handle
.shared
.mcp_tools_snapshot
.store(Arc::new(vec![tc("Kigi:read_file", Some(ToolKind::Read))]));
let rebuilt = handle
.shared
.re_resolve_all_sessions("mcp_snapshot_changed", true)
@@ -1607,10 +1604,10 @@ pub(crate) mod tests {
!local.toolset_terminal_is_session_owned().await,
"precondition: the installed toolset's Terminal must be external"
);
handle.shared.mcp_tools_snapshot.store(Arc::new(vec![tc(
"GrokBuild:read_file",
Some(ToolKind::Read),
)]));
handle
.shared
.mcp_tools_snapshot
.store(Arc::new(vec![tc("Kigi:read_file", Some(ToolKind::Read))]));
handle
.shared
.re_resolve_all_sessions("mcp_snapshot_changed", true)
@@ -1730,10 +1727,10 @@ pub(crate) mod tests {
"the shell must have entered the subdir: {}",
cwd_before.display()
);
handle.shared.mcp_tools_snapshot.store(Arc::new(vec![tc(
"GrokBuild:read_file",
Some(ToolKind::Read),
)]));
handle
.shared
.mcp_tools_snapshot
.store(Arc::new(vec![tc("Kigi:read_file", Some(ToolKind::Read))]));
let rebuilt = handle
.shared
.re_resolve_all_sessions("mcp_snapshot_changed", true)
@@ -1936,7 +1933,7 @@ pub(crate) mod tests {
async fn client_ext_sink_receives_emitted_notification() {
let handle = make_handle();
assert!(!handle.has_client_ext_sink());
handle.emit_client_ext("x.ai/noop".to_string(), serde_json::json!({}));
handle.emit_client_ext("kigi/noop".to_string(), serde_json::json!({}));
let captured = Arc::new(parking_lot::Mutex::new(Vec::new()));
let sink_captured = captured.clone();
handle.set_client_ext_sink(Arc::new(move |method, params| {
@@ -1944,17 +1941,17 @@ pub(crate) mod tests {
}));
assert!(handle.has_client_ext_sink());
handle.emit_client_ext(
"x.ai/search/fuzzy/status".to_string(),
"kigi/search/fuzzy/status".to_string(),
serde_json::json!({ "a" : 1 }),
);
let got = captured.lock();
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, "x.ai/search/fuzzy/status");
assert_eq!(got[0].0, "kigi/search/fuzzy/status");
assert_eq!(got[0].1, serde_json::json!({ "a" : 1 }));
}
/// End-to-end local streaming: open + change a fuzzy search over real files,
/// run the notification driver, and assert a correctly-shaped
/// `x.ai/search/fuzzy/status` is delivered through the sink with the match.
/// `kigi/search/fuzzy/status` is delivered through the sink with the match.
#[tokio::test]
async fn fuzzy_change_streams_status_through_sink() {
use crate::file_system::TargetClientId;
@@ -1965,7 +1962,7 @@ pub(crate) mod tests {
let captured = Arc::new(parking_lot::Mutex::new(Vec::<serde_json::Value>::new()));
let sink_captured = captured.clone();
handle.set_client_ext_sink(Arc::new(move |method, params| {
if method == "x.ai/search/fuzzy/status" {
if method == "kigi/search/fuzzy/status" {
sink_captured.lock().push(params);
}
}));
@@ -2042,7 +2039,7 @@ pub(crate) mod tests {
sid,
&BeforeTurnPayload {
turn_number: 7,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: false,
conversation_message_count: 5,
session_relationship: "subagent".to_owned(),
@@ -2071,7 +2068,7 @@ pub(crate) mod tests {
outcome: TurnHookOutcome::Completed,
duration_ms: 1234,
tool_call_count: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
written_repo_paths: Vec::new(),
cancellation_category: None,
cancellation_context: None,
@@ -2094,7 +2091,7 @@ pub(crate) mod tests {
let ts = by_type("turn_started");
assert_eq!(ts["session_id"], sid);
assert_eq!(ts["turn_number"], 7);
assert_eq!(ts["model_id"], "grok-4");
assert_eq!(ts["model_id"], "kigi-4");
assert_eq!(ts["yolo_mode"], false);
assert_eq!(ts["conversation_message_count"], 5);
assert_eq!(ts["session_relationship"], "subagent");
@@ -2133,7 +2130,7 @@ pub(crate) mod tests {
"main",
&BeforeTurnPayload {
turn_number: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: true,
..Default::default()
},
@@ -2145,7 +2142,7 @@ pub(crate) mod tests {
"main",
&TurnHookRequest::Before(BeforeTurnPayload {
turn_number: 2,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: false,
..Default::default()
}),
@@ -2165,7 +2162,7 @@ pub(crate) mod tests {
"never-bound",
&TurnHookRequest::Before(BeforeTurnPayload {
turn_number: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: true,
..Default::default()
}),
@@ -2187,7 +2184,7 @@ pub(crate) mod tests {
sid,
&BeforeTurnPayload {
turn_number: turn,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: yolo,
..Default::default()
},
@@ -2239,7 +2236,7 @@ pub(crate) mod tests {
sid,
&BeforeTurnPayload {
turn_number: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: false,
conversation_message_count: 0,
session_relationship: "primary".to_owned(),
@@ -2260,7 +2257,7 @@ pub(crate) mod tests {
outcome: TurnHookOutcome::Completed,
duration_ms: 1,
tool_call_count: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
written_repo_paths: Vec::new(),
cancellation_category: None,
cancellation_context: None,
@@ -2290,7 +2287,7 @@ pub(crate) mod tests {
sid,
&BeforeTurnPayload {
turn_number: 1,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: false,
conversation_message_count: 0,
session_relationship: "primary".to_owned(),
@@ -2389,7 +2386,7 @@ pub(crate) mod tests {
let child_ids: Vec<String> = child_baseline.tools.iter().map(|t| t.id.clone()).collect();
assert_eq!(child_ids, parent_ids);
let new_parent_baseline = ToolServerConfig {
tools: vec![tc("GrokBuild:read_file", Some(ToolKind::Read))],
tools: vec![tc("Kigi:read_file", Some(ToolKind::Read))],
behavior_preset: None,
};
let factory = handle.shared.session_factory.clone();
@@ -2425,8 +2422,8 @@ pub(crate) mod tests {
let handle = make_handle();
let custom = ToolServerConfig {
tools: vec![
tc("GrokBuild:read_file", Some(ToolKind::Read)),
tc("GrokBuild:list_dir", Some(ToolKind::ListDir)),
tc("Kigi:read_file", Some(ToolKind::Read)),
tc("Kigi:list_dir", Some(ToolKind::ListDir)),
],
behavior_preset: None,
};
@@ -2452,7 +2449,7 @@ pub(crate) mod tests {
async fn fork_session_uses_main_session_when_parent_session_id_is_none() {
let handle = make_handle();
let marker_config = ToolServerConfig {
tools: vec![tc("GrokBuild:read_file", Some(ToolKind::Read))],
tools: vec![tc("Kigi:read_file", Some(ToolKind::Read))],
behavior_preset: None,
};
let main = handle.session("main").expect("main present");
@@ -2488,13 +2485,13 @@ pub(crate) mod tests {
.iter()
.map(|t| t.id.clone())
.collect();
assert_eq!(baseline_ids, vec!["GrokBuild:read_file".to_string()]);
assert_eq!(baseline_ids, vec!["Kigi:read_file".to_string()]);
}
#[tokio::test]
async fn fork_session_uses_named_parent_when_parent_session_id_is_set() {
let handle = make_handle();
let custom = ToolServerConfig {
tools: vec![tc("GrokBuild:read_file", Some(ToolKind::Read))],
tools: vec![tc("Kigi:read_file", Some(ToolKind::Read))],
behavior_preset: None,
};
handle
@@ -2813,7 +2810,7 @@ pub(crate) mod tests {
.await
.expect("subB ok");
let mut rx = handle.shared.events.subscribe();
let mcp_tool = tc("GrokBuild:read_file", Some(ToolKind::Read));
let mcp_tool = tc("Kigi:read_file", Some(ToolKind::Read));
let rebuilt = handle.on_mcp_snapshot_changed(vec![mcp_tool]);
assert_eq!(rebuilt, 3, "main + 2 subagents");
let mut got: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
@@ -3537,7 +3534,7 @@ pub(crate) mod tests {
outcome: TurnHookOutcome::Completed,
duration_ms: 10,
tool_call_count: 0,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
written_repo_paths: Vec::new(),
cancellation_category: None,
cancellation_context: None,
@@ -3592,7 +3589,7 @@ pub(crate) mod tests {
sid,
&BeforeTurnPayload {
turn_number: 2,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
yolo_mode: false,
conversation_message_count: 0,
session_relationship: "primary".to_owned(),
@@ -3608,7 +3605,7 @@ pub(crate) mod tests {
outcome: TurnHookOutcome::Cancelled,
duration_ms: 10,
tool_call_count: 0,
model_id: "grok-4".to_owned(),
model_id: "kigi-4".to_owned(),
written_repo_paths: Vec::new(),
cancellation_category: Some("permission_rejected".to_owned()),
cancellation_context: Some(serde_json::json!({ "recovery" : false })),
+3 -3
View File
@@ -150,17 +150,17 @@ mod init_metrics_tests {
})
};
assert!(has(
"grok_workspace_toolset_swap_rejected_total",
"kigi_workspace_toolset_swap_rejected_total",
&[("reason", "turn_active"), ("trigger", "update_tool_config")]
));
assert!(has(
"grok_workspace_rewind_checkpoint_capture_total",
"kigi_workspace_rewind_checkpoint_capture_total",
&[("domain", "fs"), ("outcome", "completed")]
));
assert!(
families
.iter()
.any(|mf| mf.name() == "grok_workspace_terminal_backend_orphaned_total")
.any(|mf| mf.name() == "kigi_workspace_terminal_backend_orphaned_total")
);
}
}
@@ -1,6 +1,6 @@
//! Auto permission mode: LLM transcript classifier with safe fast-paths.
//!
//! Port of common agent auto-permission classifier semantics adapted to Grok's
//! Port of common agent auto-permission classifier semantics adapted to Kigi's
//! `AccessKind` permission gate (classifier blocks prompt the user; upstream
//! denial-limit tracking is intentionally not ported).
@@ -874,7 +874,7 @@ pub fn access_requires_user_interaction(tool_name: &str, access: &AccessKind) ->
pub type SharedClassifier = Arc<dyn PermissionClassifier>;
/// Tools / access kinds that never need a classifier call (safe allowlist
/// mapped to Grok access kinds + known names).
/// mapped to Kigi access kinds + known names).
pub fn is_auto_mode_allowlisted_access(access: &AccessKind) -> bool {
matches!(
access,
@@ -21,7 +21,7 @@ pub struct ClaudeSettings {
pub permissions: Option<ParsedPermissions>,
/// Raw `defaultMode` string when present (canonical under `permissions`, or
/// grok-only root legacy). Recognized values: `acceptEdits`,
/// kigi-only root legacy). Recognized values: `acceptEdits`,
/// `bypassPermissions`, `default`, `plan`, `dontAsk`, `auto`.
#[serde(default)]
pub default_mode: Option<String>,
@@ -85,7 +85,7 @@ impl ParsedPermissions {
/// from `serde_json::Value`. Non-string entries are skipped with warnings.
/// - This enables partial success when some entries are malformed.
/// - `defaultMode` / `additionalDirectories` prefer the canonical location
/// under `permissions.*`. Root-level keys are **grok legacy only** (not in
/// under `permissions.*`. Root-level keys are **kigi legacy only** (not in
/// the vendor schema) and are used only when the nested key is **absent** —
/// not when it is present but the wrong type.
pub fn load_claude_settings(path: &Path) -> Option<ClaudeSettings> {
@@ -123,7 +123,7 @@ pub fn load_claude_settings(path: &Path) -> Option<ClaudeSettings> {
}
});
// Canonical vendor settings store these under `permissions`; root is grok legacy only.
// Canonical vendor settings store these under `permissions`; root is kigi legacy only.
let default_mode = extract_default_mode(&value, path);
let additional_directories = extract_additional_directories(&value, path);
@@ -140,7 +140,7 @@ pub fn load_claude_settings(path: &Path) -> Option<ClaudeSettings> {
/// Canonical key is `permissions.defaultMode`.
///
/// Root `defaultMode` is grok-only back-compat for older tests / hand-written
/// Root `defaultMode` is kigi-only back-compat for older tests / hand-written
/// configs. Fall back to root only when the nested key is **absent**. If nested
/// is present but not a string, do not resurrect a root value (malformed
/// canonical key must not revive stale legacy).
@@ -161,7 +161,7 @@ pub(crate) fn extract_default_mode(value: &serde_json::Value, path: &Path) -> Op
};
}
// Nested key absent — optional grok legacy root.
// Nested key absent — optional kigi legacy root.
match value.get("defaultMode") {
Some(dm) => match dm.as_str() {
Some(s) => Some(s.to_string()),
@@ -169,7 +169,7 @@ pub(crate) fn extract_default_mode(value: &serde_json::Value, path: &Path) -> Op
warn!(
path = %path.display(),
actual_type = %dm.type_of(),
"root defaultMode (grok legacy): expected string, ignoring"
"root defaultMode (kigi legacy): expected string, ignoring"
);
None
}
@@ -182,7 +182,7 @@ pub(crate) fn extract_default_mode(value: &serde_json::Value, path: &Path) -> Op
/// legacy/compat. Nested wins when both are present.
fn extract_additional_directories(value: &serde_json::Value, path: &Path) -> Option<Vec<String>> {
// Mirror `extract_default_mode`: prefer the Claude-canonical nested key, and
// when it is present but the wrong type, do *not* resurrect the grok-legacy
// when it is present but the wrong type, do *not* resurrect the kigi-legacy
// root value (a malformed canonical key must not revive stale legacy).
let arr = if let Some(nested) = value
.get("permissions")
@@ -514,7 +514,7 @@ pub fn is_claude_import_marked() -> bool {
// lives in kigi-shell (inaccessible from here at runtime). They also
// set this env var so the workspace-resident gate honours the override
// without a cross-crate dependency.
if std::env::var("_GROK_CLAUDE_MARKER_OVERRIDE").as_deref() == Ok("1") {
if std::env::var("_KIGI_CLAUDE_MARKER_OVERRIDE").as_deref() == Ok("1") {
return true;
}
let Some(config_path) = kigi_config::user_kigi_home().map(|g| g.join("config.toml")) else {
@@ -18,7 +18,7 @@ use crate::permission::types::{
AccessKind, ClientType, Decision, EditPolicy, PermissionCommand, PermissionEvent, PromptPolicy,
};
use kigi_paths::AbsPathBuf;
use kigi_tools::implementations::grok_build::web_fetch::{DomainMatcher, domain::normalize_domain};
use kigi_tools::implementations::kigi::web_fetch::{DomainMatcher, domain::normalize_domain};
/// Canonical `decision_reason` triggers for the uploaded artifact. Single source
/// so the emit sites can't drift or misspell (the field doc lists these values).
@@ -911,7 +911,7 @@ fn spawn_permission_manager_with_pin(
//
// Prior to this change, that choice would set edit_policy=Allow and
// persist it to ~/.kigi/sessions/<cwd>/permission.toml. This caused
// the allow to survive full restarts (new grok process, new agent
// the allow to survive full restarts (new kigi process, new agent
// session in the same directory), which did not match the label or
// user expectation (and did not match upstream session-scoped
// behavior).
@@ -1579,7 +1579,7 @@ fn spawn_permission_manager_with_pin(
(Decision::Allow, "allow_edits_for_session")
}
PromptOutcome::AllowAlways => {
// Fallback clients (Generic / GrokWeb /
// Fallback clients (Generic / KigiWeb /
// Extension) submit the legacy `"always-allow"` option
// id, which the prompter maps to plain `AllowAlways`.
// They have no scope toggle, so default to tool-scope
@@ -2400,7 +2400,7 @@ mod tests {
/// Spawn a manager whose prompter is wired to a live gateway receiver backed
/// by `client`, so prompting performs a real `request_permission` round-trip.
/// `client_type` selects the option set the prompter builds (e.g. the
/// always-approve option is only offered for `GrokTUI | GrokPager | Desktop`).
/// always-approve option is only offered for `KigiTUI | KigiPager | Desktop`).
fn manager_with_recording_client(
cwd: &AbsPathBuf,
config: Option<crate::permission::types::PermissionConfig>,
@@ -4930,10 +4930,10 @@ mod tests {
let cwd = AbsPathBuf::new(tmp.path().to_path_buf()).unwrap();
let client = RecordingClient::default();
let prompts = client.prompts.clone();
// GrokPager wires the always-approve option through to its YOLO
// KigiPager wires the always-approve option through to its YOLO
// toggle; it is the option set the auto path prompts under.
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
// Force classify to Block. Interactive auto mode must now prompt
// on the FIRST block instead of denying.
@@ -5000,7 +5000,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5050,7 +5050,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5098,7 +5098,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5144,7 +5144,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5193,7 +5193,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5239,7 +5239,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -5324,7 +5324,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":false,"reason":"x"}"#,
@@ -5373,7 +5373,7 @@ mod tests {
let client = RecordingClient::default();
let prompts = client.prompts.clone();
let (mgr, _e) =
manager_with_recording_client(&cwd, None, client, ClientType::GrokPager);
manager_with_recording_client(&cwd, None, client, ClientType::KigiPager);
mgr.set_auto_mode(true);
mgr.set_classifier(Some(LlmPermissionClassifier::with_fixed_model_text(
r#"{"thinking":"t","shouldBlock":true,"reason":"x"}"#,
@@ -3,7 +3,7 @@ use crate::permission::shell_access::combine_decisions;
use crate::permission::types::{
AccessKind, Decision, PatternMode, PermissionConfig, PermissionRule, RuleAction, ToolFilter,
};
use kigi_tools::implementations::grok_build::web_fetch::domain::normalize_domain;
use kigi_tools::implementations::kigi::web_fetch::domain::normalize_domain;
#[derive(Clone, Copy)]
enum MatchContext {
@@ -9,9 +9,9 @@ use crate::permission::{
use agent_client_protocol::{self as acp, Client as _};
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
use kigi_file_utils::events::{Event, EventWriter, PermissionDecision};
use kigi_tools::implementations::grok_build::web_fetch::domain_from_url;
use kigi_tools::implementations::kigi::web_fetch::domain_from_url;
const REJECT_ONCE_LABEL: &str = "No, and tell Grok what to do differently";
const REJECT_ONCE_LABEL: &str = "No, and tell Kigi what to do differently";
/// Stable option id for the edit prompt's "Yes, allow all edits during this
/// session" choice. Distinct from the generic `"always-allow"` id (used by
@@ -40,7 +40,7 @@ pub const ALLOW_EDITS_SESSION_OPTION_ID: &str = "allow-edits-session";
/// 2. Drains any queued permission requests with `AllowOnce` responses
/// 3. Persists `[ui] permission_mode = "always-approve"` to
/// `~/.kigi/config.toml` via the `Effect::PersistPermissionMode` effect
/// 4. Sends the existing `x.ai/yolo_mode_changed` ACP notification so
/// 4. Sends the existing `kigi/yolo_mode_changed` ACP notification so
/// the agent's permission manager flips its `yolo_mode` flag
///
/// This split keeps the wire protocol bog-standard ACP (no new methods or
@@ -96,7 +96,7 @@ fn enable_always_approve_option() -> acp::PermissionOption {
}
/// Returns whether the given option is the special "enable always-approve mode"
/// (global yolo) option that is prepended for GrokTUI / GrokPager / Desktop.
/// (global yolo) option that is prepended for KigiTUI / KigiPager / Desktop.
///
/// This is the canonical way to identify the option instead of matching on
/// its human-facing label or assuming position 0. Callers that need to
@@ -108,12 +108,12 @@ pub fn is_enable_always_approve_option(opt: &acp::PermissionOption) -> bool {
/// Returns `true` if the given client type should see the prepended
/// "enable always-approve mode" option. Limited to the three clients
/// (`GrokTUI`, `GrokPager`, `Desktop`) that wire the option id through
/// (`KigiTUI`, `KigiPager`, `Desktop`) that wire the option id through
/// to their YOLO toggle. Other clients keep their existing option set.
fn client_supports_enable_always_approve(client_type: ClientType) -> bool {
matches!(
client_type,
ClientType::GrokTUI | ClientType::GrokPager | ClientType::Desktop
ClientType::KigiTUI | ClientType::KigiPager | ClientType::Desktop
)
}
@@ -188,8 +188,8 @@ pub fn mcp_tool_action<'a>(tool_name: &'a str, server_prefix: Option<&str>) -> &
/// Pretty-format a single MCP server- or tool-name segment for display:
/// split on `'_'`, title-case each word, join with spaces. Leaves
/// non-underscore characters (camelCase, hyphens) intact, so
/// `"list_issues"` → `"List Issues"`, `"grok_com_notion"` →
/// `"Grok Com Notion"`, and `"getMyTaskList"` → `"GetMyTaskList"`.
/// `"list_issues"` → `"List Issues"`, `"kigi_com_notion"` →
/// `"Kigi Com Notion"`, and `"getMyTaskList"` → `"GetMyTaskList"`.
pub fn mcp_titleize_segment(name: &str) -> String {
name.split('_')
.map(|word| {
@@ -248,10 +248,10 @@ pub struct McpToolPermission {
/// e.g. `"Always allow:"`. Mirrors `BashCommandPermission::prompt_prefix`.
pub prompt_prefix: String,
/// Full tool name as the agent called it
/// (e.g. `"grok_com_notion__notion-fetch"`).
/// (e.g. `"kigi_com_notion__notion-fetch"`).
pub tool_name: String,
/// Server segment (everything before the single `__` separator,
/// e.g. `"grok_com_notion"`). `None` if the tool name has no `__`,
/// e.g. `"kigi_com_notion"`). `None` if the tool name has no `__`,
/// in which case the view hides the scope toggle and only offers
/// tool-scope.
pub server_prefix: Option<String>,
@@ -397,7 +397,7 @@ impl AcpPrompter {
),
);
// Bash options for GrokTUI - interactive selection with expandable/contractable terms
// Bash options for KigiTUI - interactive selection with expandable/contractable terms
let mut bash_options: IndexMap<acp::PermissionOptionId, acp::PermissionOption> =
IndexMap::new();
bash_options.insert(
@@ -543,7 +543,7 @@ impl AcpPrompter {
if self.remember_tool_approvals
&& matches!(
self.client_type,
ClientType::GrokTUI | ClientType::GrokPager | ClientType::Desktop
ClientType::KigiTUI | ClientType::KigiPager | ClientType::Desktop
) =>
{
serde_json::to_value(primary_command_from_script(bash_command))
@@ -565,11 +565,11 @@ impl AcpPrompter {
match access {
AccessKind::Edit(_) => self.edit_options.clone(),
AccessKind::Bash(bash_command) => {
// For GrokTUI clients, use the fancy interactive options with term selection
// For KigiTUI clients, use the fancy interactive options with term selection
// For generic clients (web, etc.), use simpler options that work without
// special UI handling
match self.client_type {
ClientType::GrokTUI | ClientType::GrokPager | ClientType::Desktop => {
ClientType::KigiTUI | ClientType::KigiPager | ClientType::Desktop => {
let mut bash_commands: IndexMap<
acp::PermissionOptionId,
acp::PermissionOption,
@@ -601,7 +601,7 @@ impl AcpPrompter {
bash_commands
}
ClientType::Generic
| ClientType::GrokWeb
| ClientType::KigiWeb
| ClientType::Nebula
| ClientType::Extension => {
// For generic clients, use simpler options that display well
@@ -654,7 +654,7 @@ impl AcpPrompter {
// `fallback_options` (`always-allow`) and the manager's
// plain `AllowAlways` arm persists tool-scope.
match self.client_type {
ClientType::GrokTUI | ClientType::GrokPager | ClientType::Desktop => {
ClientType::KigiTUI | ClientType::KigiPager | ClientType::Desktop => {
let mut options: IndexMap<acp::PermissionOptionId, acp::PermissionOption> =
IndexMap::new();
let server_prefix = tool_name.split_once("__").map(|(s, _)| s.to_owned());
@@ -694,7 +694,7 @@ impl AcpPrompter {
options
}
ClientType::Generic
| ClientType::GrokWeb
| ClientType::KigiWeb
| ClientType::Nebula
| ClientType::Extension => self.fallback_options.clone(),
}
@@ -991,7 +991,7 @@ mod tests {
#[test]
fn gate_off_strips_bash_always_allow_keeps_yes_no() {
let p = prompter_with_gate(ClientType::GrokPager, false);
let p = prompter_with_gate(ClientType::KigiPager, false);
let access = AccessKind::Bash("kubectl get pods".to_owned());
let opts = p.build_options(&access);
assert!(
@@ -1012,7 +1012,7 @@ mod tests {
#[test]
fn gate_on_includes_bash_always_allow() {
let p = prompter_with_gate(ClientType::GrokPager, true);
let p = prompter_with_gate(ClientType::KigiPager, true);
let access = AccessKind::Bash("kubectl get pods".to_owned());
let opts = p.build_options(&access);
assert!(
@@ -1027,7 +1027,7 @@ mod tests {
#[test]
fn gate_off_strips_mcp_always_allow() {
let p = prompter_with_gate(ClientType::GrokPager, false);
let p = prompter_with_gate(ClientType::KigiPager, false);
let access = AccessKind::MCPTool {
name: "linear__list".to_owned(),
input: serde_json::Value::Null,
@@ -1040,7 +1040,7 @@ mod tests {
#[test]
fn gate_off_strips_generic_bash_always_and_reject_always() {
let p = prompter_with_gate(ClientType::GrokWeb, false);
let p = prompter_with_gate(ClientType::KigiWeb, false);
let access = AccessKind::Bash("kubectl get pods".to_owned());
let opts = p.build_options(&access);
assert!(!has_option(&opts, "always-allow"));
@@ -1051,7 +1051,7 @@ mod tests {
#[test]
fn gate_off_strips_web_fetch_always_allow_domain() {
let p = prompter_with_gate(ClientType::GrokPager, false);
let p = prompter_with_gate(ClientType::KigiPager, false);
let access = AccessKind::WebFetch("https://example.com/x".to_owned());
let opts = p.build_options(&access);
assert!(!has_option(&opts, "allow-always-domain"));
@@ -1063,7 +1063,7 @@ mod tests {
fn bash_meta_present_only_when_gate_on_for_fancy_clients() {
let access = AccessKind::Bash("kubectl get pods".to_owned());
// Gate on + fancy client → meta carries the parsed command parts.
let on = prompter_with_gate(ClientType::GrokPager, true);
let on = prompter_with_gate(ClientType::KigiPager, true);
let meta = on.bash_selection_meta(&access).expect("meta present");
assert!(
serde_json::from_value::<
@@ -1073,10 +1073,10 @@ mod tests {
"meta must deserialize back into BashCommandHighlights"
);
// Gate off → no meta (no allow-always-command row to scope).
let off = prompter_with_gate(ClientType::GrokPager, false);
let off = prompter_with_gate(ClientType::KigiPager, false);
assert!(off.bash_selection_meta(&access).is_none());
// Generic client never gets the fancy-UI meta, even with the gate on.
let generic = prompter_with_gate(ClientType::GrokWeb, true);
let generic = prompter_with_gate(ClientType::KigiWeb, true);
assert!(generic.bash_selection_meta(&access).is_none());
// Non-bash access never carries bash meta.
assert!(
@@ -1087,7 +1087,7 @@ mod tests {
#[test]
fn bash_reject_always_command_maps_selected_words() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let access = AccessKind::Bash("cargo test --workspace".to_owned());
let opts = p.build_options(&access);
// Pager path: the ←/→ word-scope selection arrives as
@@ -1121,7 +1121,7 @@ mod tests {
#[test]
fn gate_off_keeps_edit_session_allow() {
// The edit session allow is governed separately, not by this gate.
let p = prompter_with_gate(ClientType::GrokPager, false);
let p = prompter_with_gate(ClientType::KigiPager, false);
let access = AccessKind::Edit("src/main.rs".to_owned());
let opts = p.build_options(&access);
assert!(
@@ -1142,7 +1142,7 @@ mod tests {
#[test]
fn mcp_prompt_includes_allow_always_with_meta() {
let p = prompter(ClientType::GrokTUI);
let p = prompter(ClientType::KigiTUI);
let access = AccessKind::MCPTool {
name: "linear__list".to_owned(),
input: serde_json::Value::Null,
@@ -1161,7 +1161,7 @@ mod tests {
#[test]
fn mcp_prompt_no_separator_hides_server_scope() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let access = AccessKind::MCPTool {
name: "standalone".to_owned(),
input: serde_json::Value::Null,
@@ -1178,7 +1178,7 @@ mod tests {
#[test]
fn mcp_response_tool_scope() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let access = AccessKind::MCPTool {
name: "linear__list".to_owned(),
input: serde_json::Value::Null,
@@ -1197,7 +1197,7 @@ mod tests {
#[test]
fn mcp_response_server_scope() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let access = AccessKind::MCPTool {
name: "linear__list".to_owned(),
input: serde_json::Value::Null,
@@ -1216,7 +1216,7 @@ mod tests {
#[test]
fn mcp_response_empty_server_falls_back_to_tool() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let access = AccessKind::MCPTool {
name: "linear__list".to_owned(),
input: serde_json::Value::Null,
@@ -1238,7 +1238,7 @@ mod tests {
// TUI / Desktop case: option id is `allow-always-mcp` but the renderer
// does not build the toggle meta. The prompter must default to
// tool-scope using the access-kind name.
let p = prompter(ClientType::GrokTUI);
let p = prompter(ClientType::KigiTUI);
let access = AccessKind::MCPTool {
name: "notion__fetch".to_owned(),
input: serde_json::Value::Null,
@@ -1253,7 +1253,7 @@ mod tests {
#[test]
fn mcp_fallback_client_returns_plain_allow_always() {
// non-TUI clients (Generic / GrokWeb / Extension / …) see `fallback_options`. The
// non-TUI clients (Generic / KigiWeb / Extension / …) see `fallback_options`. The
// legacy `"always-allow"` id maps to plain `PromptOutcome::AllowAlways`;
// the manager arm persists tool-scope from there.
let p = prompter(ClientType::Generic);
@@ -1303,7 +1303,7 @@ mod tests {
fn mcp_titleize_segment_handles_snake_camel_kebab() {
// snake_case → words split + each title-cased
assert_eq!(mcp_titleize_segment("list_issues"), "List Issues");
assert_eq!(mcp_titleize_segment("grok_com_notion"), "Grok Com Notion");
assert_eq!(mcp_titleize_segment("kigi_com_notion"), "Kigi Com Notion");
// single word: just capitalize first letter
assert_eq!(mcp_titleize_segment("linear"), "Linear");
// camelCase preserved (no `_` to split on, only first letter touched)
@@ -1330,7 +1330,7 @@ mod tests {
/// pin position 0 with an exhaustive enumeration.
#[test]
fn enable_always_approve_is_first_option_for_pager() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let cases: Vec<(&str, AccessKind)> = vec![
("edit", AccessKind::Edit("write".to_owned())),
("bash", AccessKind::Bash("ls -la".to_owned())),
@@ -1362,13 +1362,13 @@ mod tests {
}
}
/// Same pin as above for `GrokTUI` and `Desktop` — both client types
/// Same pin as above for `KigiTUI` and `Desktop` — both client types
/// route the option id through to the YOLO toggle, so both must
/// see it. A copy-paste regression that limits the prepend to one
/// client only would be caught here.
#[test]
fn enable_always_approve_is_first_for_tui_and_desktop() {
for ct in [ClientType::GrokTUI, ClientType::Desktop] {
for ct in [ClientType::KigiTUI, ClientType::Desktop] {
let p = prompter(ct);
let opts = p.build_options(&AccessKind::Edit("write".to_owned()));
assert_eq!(
@@ -1387,7 +1387,7 @@ mod tests {
fn enable_always_approve_omitted_for_non_tui_clients() {
for ct in [
ClientType::Generic,
ClientType::GrokWeb,
ClientType::KigiWeb,
ClientType::Nebula,
ClientType::Extension,
] {
@@ -1406,7 +1406,7 @@ mod tests {
/// state for this id. Pin the override for every access kind.
#[test]
fn enable_always_approve_maps_to_allow_once_for_every_access_kind() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let cases: Vec<(&str, AccessKind)> = vec![
("edit", AccessKind::Edit("write".to_owned())),
("bash", AccessKind::Bash("ls".to_owned())),
@@ -1440,7 +1440,7 @@ mod tests {
/// target kind is in play. Pin the kind regardless.
#[test]
fn enable_always_approve_uses_allow_once_kind() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
let opts = p.build_options(&AccessKind::Edit("write".to_owned()));
let opt = opts
.get(&enable_always_approve_id())
@@ -1461,7 +1461,7 @@ mod tests {
/// allow-once, reject-once, reject-always-command].
#[test]
fn bash_option_order_toggle_first_reject_always_last() {
let p = prompter(ClientType::GrokPager);
let p = prompter(ClientType::KigiPager);
// "ls" has a parseable primary command, so `allow-always-command`
// will be inserted into the option list.
let access = AccessKind::Bash("ls -la".to_owned());
@@ -71,7 +71,7 @@ fn synthetic_rules_for_default_mode(
}
/// Parse a raw defaultMode string: unknown → [`DefaultPermissionMode::Default`]
/// (fail-safe) with a warn + skip record for `grok inspect`.
/// (fail-safe) with a warn + skip record for `kigi inspect`.
fn parse_default_mode_claiming_scope(
raw: &str,
path: &Path,
@@ -214,7 +214,7 @@ fn load_requirements_permissions() -> Vec<Sourced<PermissionRule>> {
///
/// Returned paths are ordered from repo root (lowest priority) to `cwd`
/// (highest priority), matching `kigi-shell::config::find_project_configs`.
fn find_project_grok_configs(cwd: &Path) -> Vec<PathBuf> {
fn find_project_kigi_configs(cwd: &Path) -> Vec<PathBuf> {
let git_root = git2::Repository::discover(cwd)
.ok()
.and_then(|repo| repo.workdir().map(|p| p.to_path_buf()));
@@ -242,7 +242,7 @@ fn find_project_grok_configs(cwd: &Path) -> Vec<PathBuf> {
configs
}
/// Load `[permission]` rules from native Grok TOML config files:
/// Load `[permission]` rules from native Kigi TOML config files:
///
/// * `~/.kigi/config.toml` (lowest priority)
/// * Each `.kigi/config.toml` from the git repo root down to `cwd`
@@ -272,7 +272,7 @@ fn load_config_toml_permissions(cwd: &Path) -> Vec<Sourced<PermissionRule>> {
}
// Project-scoped configs walking from git root down to cwd.
for path in find_project_grok_configs(cwd) {
for path in find_project_kigi_configs(cwd) {
match kigi_config::load_config_file(&path) {
Ok(value) => rules.extend(extract_toml_permissions(&value, || {
RequirementSource::Config { path: path.clone() }
@@ -303,7 +303,7 @@ fn managed_config_permissions(
// Fallback Resolver
// ═════════════════════════════════════════════════════════════════════════════
/// Resolve permission config, merging native Grok and Claude sources.
/// Resolve permission config, merging native Kigi and Claude sources.
/// Evaluation is order-independent (deny > ask > allow); merge order affects
/// provenance display only.
///
@@ -392,7 +392,7 @@ fn is_admin_source(source: &RequirementSource) -> bool {
}
/// Under the pin, drop untrusted catch-all Allow rules (they substitute for the
/// blocked `--yolo`); keep admin-tier ones. Records each drop for `grok inspect`.
/// blocked `--yolo`); keep admin-tier ones. Records each drop for `kigi inspect`.
fn drop_untrusted_catchall_allows(
rules: Vec<Sourced<PermissionRule>>,
policy_block: Option<&'static str>,
@@ -459,7 +459,7 @@ impl ResolveInputs<'static> {
/// **Always-approve (yolo) is independent of defaultMode:** session always-approve
/// still auto-approves before [`PromptPolicy::Deny`] (`dontAsk`) is consulted,
/// so always-approve outranks `defaultMode` unless
/// bypass is pinned off via grok `requirements.toml`
/// bypass is pinned off via kigi `requirements.toml`
/// (`[ui] disable_bypass_permissions_mode = true`). Pair managed `dontAsk` with
/// that pin when org policy must not be bypassable by `--always-approve`.
pub async fn resolve_permissions_with_provenance(cwd: &Path) -> Option<ResolvedPermissions> {
@@ -540,7 +540,7 @@ async fn resolve_permissions_with_provenance_inner(
// `--allow '*'` is filtered at its own merge site (acp_session).
let all_rules = drop_untrusted_catchall_allows(all_rules, policy_block, &mut skipped);
// Keep skip-only resolutions alive so the drop reaches `grok inspect`; zero
// Keep skip-only resolutions alive so the drop reaches `kigi inspect`; zero
// rules with Ask is a no-op for the evaluator, identical to the `None` arm.
if all_rules.is_empty() && prompt_policy == PromptPolicy::Ask && skipped.is_empty() {
return None;
@@ -627,7 +627,7 @@ fn resolve_claude_settings_inner(
warn!(path = %path.display(), "{}", w);
}
// Rules *or* skip-only parse failures still own provenance for
// `grok inspect` (all-invalid allow/deny/ask must not leave
// `kigi inspect` (all-invalid allow/deny/ask must not leave
// primary_source_path unset and panic below).
if (!cfg.rules.is_empty() || !warnings.is_empty()) && primary_source_path.is_none() {
primary_source_path = Some(path.clone());
@@ -665,7 +665,7 @@ fn resolve_claude_settings_inner(
// A blocked bypass, a claimed defaultMode (incl. typo→default), or skip
// records still resolve (possibly zero rules) so provenance reaches
// `grok inspect` via the outer resolver.
// `kigi inspect` via the outer resolver.
if all_rules.is_empty()
&& prompt_policy == PromptPolicy::Ask
&& !bypass_blocked
@@ -954,10 +954,10 @@ pub const YOLO_PIN_REASON_LEGACY_YOLO: &str =
/// `Some(reason)` iff a requirements layer sets `[ui]
/// disable_bypass_permissions_mode = true` (or legacy `[ui] yolo = false`).
/// Vendor `managed-settings.json` `disableBypassPermissionsMode` is deliberately
/// not consulted: grok must not inherit a host-wide always-approve lockdown from
/// that file. grok still honors that file's permission rules / MCP / marketplace
/// not consulted: kigi must not inherit a host-wide always-approve lockdown from
/// that file. kigi still honors that file's permission rules / MCP / marketplace
/// allowlists, and the user's own `--yolo` / `[ui] permission_mode` / runtime
/// toggle drive always-approve; to disable it in grok use a root-owned
/// toggle drive always-approve; to disable it in kigi use a root-owned
/// `requirements.toml`. Fails open on user-writable layers.
pub fn yolo_disabled_by_policy() -> Option<&'static str> {
let layers = kigi_config::requirements_layers();
@@ -1204,12 +1204,12 @@ impl McpServerAllowlist {
}
}
/// Namespace prefix for managed (grok.com-injected) MCP server names. Defined
/// Namespace prefix for managed (kimi.com-injected) MCP server names. Defined
/// here (shell depends on workspace) and re-exported by shell's `to_managed_name`
/// so the prefix and policy matching never drift.
pub const MANAGED_MCP_PREFIX: &str = "grok_com_";
pub const MANAGED_MCP_PREFIX: &str = "kigi_com_";
/// Max `char` length of a managed runtime name (`grok_com_` + normalized display
/// Max `char` length of a managed runtime name (`kigi_com_` + normalized display
/// name), sized to the 64-char tool-name budget. Shared by `to_managed_name` and
/// `mcp_name_matches` so a long policy `serverName` still matches its truncated
/// runtime name.
@@ -1236,7 +1236,7 @@ fn mcp_server_name(server: &agent_client_protocol::McpServer) -> &str {
/// Match a policy `serverName` against a runtime server name.
///
/// Both sides reduce to one key (strip `grok_com_`, [`normalize_managed_name`],
/// Both sides reduce to one key (strip `kigi_com_`, [`normalize_managed_name`],
/// truncate to the cap) compared by exact equality — never substring, so deny
/// `foo` can't leak onto `foobar`; an empty key never matches.
fn mcp_name_matches(pattern: &str, name: &str) -> bool {
@@ -1942,7 +1942,7 @@ mod tests {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let home = tempfile::tempdir().unwrap();
let _home_guard = EnvVarGuard::set("KIGI_SHARE_DIR", home.path());
let _marker_guard = EnvVarGuard::unset("_GROK_CLAUDE_MARKER_OVERRIDE");
let _marker_guard = EnvVarGuard::unset("_KIGI_CLAUDE_MARKER_OVERRIDE");
let tmp = tempfile::tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
@@ -1976,7 +1976,7 @@ mod tests {
let home = tempfile::tempdir().unwrap();
let _home_guard = EnvVarGuard::set("KIGI_SHARE_DIR", home.path());
let _real_home_guard = EnvVarGuard::set("HOME", home.path());
let _marker_guard = EnvVarGuard::unset("_GROK_CLAUDE_MARKER_OVERRIDE");
let _marker_guard = EnvVarGuard::unset("_KIGI_CLAUDE_MARKER_OVERRIDE");
let tmp = tempfile::tempdir().unwrap();
let env = load_claude_env_with_project(tmp.path(), true);
assert!(env.is_empty());
@@ -1992,7 +1992,7 @@ mod tests {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let home = tempfile::tempdir().unwrap();
let _home_guard = EnvVarGuard::set("KIGI_SHARE_DIR", home.path());
let _marker_guard = EnvVarGuard::unset("_GROK_CLAUDE_MARKER_OVERRIDE");
let _marker_guard = EnvVarGuard::unset("_KIGI_CLAUDE_MARKER_OVERRIDE");
let tmp = tempfile::tempdir().unwrap();
let claude_dir = tmp.path().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
@@ -2431,11 +2431,11 @@ mod tests {
fn mcp_name_matches_strips_managed_prefix_both_sides_exactly() {
// Exact match after stripping the prefix — never substring.
assert!(mcp_name_matches("foo", "foo"));
assert!(mcp_name_matches("foo", "grok_com_foo"));
assert!(mcp_name_matches("grok_com_foo", "foo"));
assert!(mcp_name_matches("grok_com_foo", "grok_com_foo"));
assert!(mcp_name_matches("foo", "kigi_com_foo"));
assert!(mcp_name_matches("kigi_com_foo", "foo"));
assert!(mcp_name_matches("kigi_com_foo", "kigi_com_foo"));
assert!(!mcp_name_matches("foo", "foobar"));
assert!(!mcp_name_matches("foo", "grok_com_foobar"));
assert!(!mcp_name_matches("foo", "kigi_com_foobar"));
assert!(!mcp_name_matches("foo", "barfoo"));
assert!(!mcp_name_matches("foo", "bar"));
assert!(!mcp_name_matches("", "foo"));
@@ -2453,14 +2453,14 @@ mod tests {
fn mcp_name_matches_is_case_and_space_insensitive() {
// A display-cased policy serverName matches to_managed_name's normalized
// runtime name, for managed and local servers alike.
assert!(mcp_name_matches("Slack", "grok_com_slack"));
assert!(mcp_name_matches("My Server", "grok_com_my_server"));
assert!(mcp_name_matches("grok_com_my_server", "My Server"));
assert!(mcp_name_matches("Slack", "kigi_com_slack"));
assert!(mcp_name_matches("My Server", "kigi_com_my_server"));
assert!(mcp_name_matches("kigi_com_my_server", "My Server"));
assert!(mcp_name_matches("My Server", "my_server"));
assert!(mcp_name_matches("SLACK", "slack"));
assert!(!mcp_name_matches("My Server", "my_server_2"));
assert!(!mcp_name_matches("", ""));
assert!(!mcp_name_matches("grok_com_", "grok_com_anything"));
assert!(!mcp_name_matches("kigi_com_", "kigi_com_anything"));
}
#[test]
@@ -2503,17 +2503,17 @@ mod tests {
assert!(al.is_server_denied(&bare));
assert!(!al.is_server_allowed(&bare));
let managed = http_named("grok_com_foo", "https://foo.example.com/mcp");
let managed = http_named("kigi_com_foo", "https://foo.example.com/mcp");
assert!(al.is_server_denied(&managed));
assert!(!al.is_server_allowed(&managed));
// Name match is transport-agnostic.
let stdio = stdio_named("grok_com_foo", "npx");
let stdio = stdio_named("kigi_com_foo", "npx");
assert!(al.is_server_denied(&stdio));
assert!(!al.is_server_allowed(&stdio));
// Unrelated names are NOT denied — exact match after strip, never substring.
for unrelated in ["foobar", "grok_com_foobar", "barfoo", "bar"] {
for unrelated in ["foobar", "kigi_com_foobar", "barfoo", "bar"] {
let s = http_named(unrelated, "https://x.example.com/mcp");
assert!(
!al.is_server_denied(&s),
@@ -2536,8 +2536,8 @@ mod tests {
// A name allowlist is transport-agnostic: the named server is allowed on
// any transport regardless of URL/command, others are blocked.
assert!(al.is_server_allowed(&http_named("foo", "https://anything.example.com/x")));
assert!(al.is_server_allowed(&http_named("grok_com_foo", "https://evil.example.com/x")));
assert!(al.is_server_allowed(&stdio_named("grok_com_foo", "/usr/bin/whatever")));
assert!(al.is_server_allowed(&http_named("kigi_com_foo", "https://evil.example.com/x")));
assert!(al.is_server_allowed(&stdio_named("kigi_com_foo", "/usr/bin/whatever")));
let bar_http = http_named("bar", "https://anything.example.com/x");
assert!(!al.is_server_allowed(&bar_http));
@@ -2555,7 +2555,7 @@ mod tests {
for s in [
http_named("foo", "https://foo.example.com/x"),
http_named("grok_com_foo", "https://foo.example.com/x"),
http_named("kigi_com_foo", "https://foo.example.com/x"),
] {
assert!(al.is_server_denied(&s));
assert!(
@@ -2569,13 +2569,13 @@ mod tests {
fn server_name_prefix_edge_cases_vice_versa() {
// Reverse case: prefixed policy vs bare runtime still matches after strip.
let al = allowlist_from(serde_json::json!({
"deniedMcpServers": [ { "serverName": "grok_com_foo" } ]
"deniedMcpServers": [ { "serverName": "kigi_com_foo" } ]
}));
assert!(al.is_server_denied(&http_named("foo", "https://x.example.com/mcp")));
assert!(al.is_server_denied(&http_named("grok_com_foo", "https://x.example.com/mcp")));
assert!(al.is_server_denied(&http_named("kigi_com_foo", "https://x.example.com/mcp")));
assert!(!al.is_server_denied(&http_named("foobar", "https://x.example.com/mcp")));
assert!(!al.is_server_denied(&http_named("grok_com_foobar", "https://x.example.com/mcp")));
assert!(!al.is_server_denied(&http_named("kigi_com_foobar", "https://x.example.com/mcp")));
}
#[test]
@@ -3748,7 +3748,7 @@ mod tests {
skipped
.iter()
.any(|s| s.rule.contains("dontask") || s.rule.contains("defaultMode=")),
"typo should be recorded for grok inspect"
"typo should be recorded for kigi inspect"
);
}
@@ -16,11 +16,11 @@ pub struct PermissionState {
/// Domains the user has approved for `web_fetch`
/// during this session.
pub allowed_web_fetch_domains: HashSet<String>,
/// Exact MCP tool names (e.g. `"grok_com_notion__notion-fetch"`)
/// Exact MCP tool names (e.g. `"kigi_com_notion__notion-fetch"`)
/// the user has granted "always allow" for. Lookup is exact.
pub allowed_mcp_tools: HashSet<String>,
/// MCP server prefixes (everything before the first `__`,
/// e.g. `"grok_com_notion"`) for which the user has granted
/// e.g. `"kigi_com_notion"`) for which the user has granted
/// "always allow" to every tool. Lookup is "tool name starts with
/// `<prefix>__`".
pub allowed_mcp_servers: HashSet<String>,
@@ -299,7 +299,7 @@ mod tests {
let mut state = PermissionState::default();
state
.allowed_mcp_tools
.insert("grok_com_notion__notion-fetch".to_string());
.insert("kigi_com_notion__notion-fetch".to_string());
state
.allowed_mcp_tools
.insert("linear__list_issues".to_string());
@@ -311,7 +311,7 @@ mod tests {
assert!(
restored
.allowed_mcp_tools
.contains("grok_com_notion__notion-fetch")
.contains("kigi_com_notion__notion-fetch")
);
assert!(restored.allowed_mcp_tools.contains("linear__list_issues"));
assert!(restored.allowed_mcp_servers.is_empty());
@@ -322,14 +322,14 @@ mod tests {
let mut state = PermissionState::default();
state
.allowed_mcp_servers
.insert("grok_com_slack".to_string());
.insert("kigi_com_slack".to_string());
state.allowed_mcp_servers.insert("linear".to_string());
let toml_str = toml::to_string_pretty(&state).unwrap();
let restored: PermissionState = toml::from_str(&toml_str).unwrap();
assert_eq!(restored.allowed_mcp_servers.len(), 2);
assert!(restored.allowed_mcp_servers.contains("grok_com_slack"));
assert!(restored.allowed_mcp_servers.contains("kigi_com_slack"));
assert!(restored.allowed_mcp_servers.contains("linear"));
assert!(restored.allowed_mcp_tools.is_empty());
}
@@ -78,36 +78,36 @@ pub enum ClientType {
#[default]
#[serde(
rename = "generic",
alias = "grok-shell",
alias = "grok_shell",
alias = "kigi-shell",
alias = "kigi_shell",
alias = "kigi"
)]
Generic,
/// Grok TUI client - show fancy options with interactive bash term selection
#[serde(rename = "grok-tui", alias = "grok_tui")]
GrokTUI,
/// Grok Web client - identified by clientIdentifier "grok-web"
#[serde(rename = "grok_web")]
GrokWeb,
/// Kigi TUI client - show fancy options with interactive bash term selection
#[serde(rename = "kigi-tui", alias = "kigi_tui")]
KigiTUI,
/// Kigi Web client - identified by clientIdentifier "kigi-web"
#[serde(rename = "kigi_web")]
KigiWeb,
/// Named client (`"nebula"`) — uses the generic permission UI
#[serde(rename = "nebula")]
Nebula,
/// IDE extension client (VS Code and similar) - identified by clientIdentifier "grok-code-extension"
/// IDE extension client (VS Code and similar) - identified by clientIdentifier "kigi-code-extension"
#[serde(rename = "extension")]
Extension,
/// Grok Pager client - TUI-like terminal pager with interactive permission UI.
/// Treated identically to GrokTUI for permission options (gets bash highlights +
/// Kigi Pager client - TUI-like terminal pager with interactive permission UI.
/// Treated identically to KigiTUI for permission options (gets bash highlights +
/// interactive selection). Reports as "pager" for telemetry attribution.
///
/// Accepts both the hyphenated `"grok-pager"` (what the pager actually
/// Accepts both the hyphenated `"kigi-pager"` (what the pager actually
/// sends over the wire, matching `PAGER_CLIENT_TYPE`) and the underscored
/// `"grok_pager"` form for symmetry with the rest of this enum.
#[serde(rename = "grok-pager", alias = "grok_pager")]
GrokPager,
/// Grok Desktop (Electron) client - identified by clientIdentifier "grok-desktop".
/// `"kigi_pager"` form for symmetry with the rest of this enum.
#[serde(rename = "kigi-pager", alias = "kigi_pager")]
KigiPager,
/// Kigi Desktop (Electron) client - identified by clientIdentifier "kigi-desktop".
/// Uses TUI-style bash permission options (primary command extraction + prefix matching)
/// but without interactive `<`/`>` word selection.
#[serde(rename = "grok_desktop")]
#[serde(rename = "kigi_desktop")]
Desktop,
}
impl ClientType {
@@ -118,30 +118,30 @@ impl ClientType {
pub fn user_agent_label(&self) -> &'static str {
match self {
Self::Generic => "kigi",
Self::GrokTUI => "grok-tui",
Self::GrokWeb => "grok-web",
Self::KigiTUI => "kigi-tui",
Self::KigiWeb => "kigi-web",
Self::Nebula => "nebula",
Self::Extension => "grok-code-extension",
Self::GrokPager => "kigi",
Self::Desktop => "grok-desktop",
Self::Extension => "kigi-code-extension",
Self::KigiPager => "kigi",
Self::Desktop => "kigi-desktop",
}
}
/// Resolve from ACP `clientIdentifier` string (e.g. `"grok-web"`, `"grok-desktop"`).
/// Resolve from ACP `clientIdentifier` string (e.g. `"kigi-web"`, `"kigi-desktop"`).
pub fn from_client_identifier(id: Option<&str>) -> Self {
match id {
Some("grok-web") => Self::GrokWeb,
Some("kigi-web") => Self::KigiWeb,
Some("nebula") => Self::Nebula,
Some("grok-code-extension") => Self::Extension,
Some("grok-desktop") => Self::Desktop,
Some("grok-pager") => Self::GrokPager,
Some("kigi-code-extension") => Self::Extension,
Some("kigi-desktop") => Self::Desktop,
Some("kigi-pager") => Self::KigiPager,
_ => Self::Generic,
}
}
/// Label for feedback reporting and experiment filtering.
pub fn feedback_label(&self) -> &'static str {
match self {
Self::GrokTUI | Self::GrokPager => "tui",
Self::GrokWeb => "web",
Self::KigiTUI | Self::KigiPager => "tui",
Self::KigiWeb => "web",
Self::Nebula => "nebula",
Self::Extension => "extension",
Self::Generic => "agent",
@@ -498,7 +498,7 @@ mod tests {
}
#[test]
fn hashline_edit_maps_to_edit_access() {
use kigi_tools::implementations::grok_build_hashline::edit::types::HashlineEditInput;
use kigi_tools::implementations::kigi_hashline::edit::types::HashlineEditInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::HashlineEdit(HashlineEditInput {
file_path: "src/main.rs".into(),
@@ -512,7 +512,7 @@ mod tests {
}
#[test]
fn bash_maps_to_bash_access() {
use kigi_tools::implementations::grok_build::bash::BashToolInput;
use kigi_tools::implementations::kigi::bash::BashToolInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::Bash(BashToolInput {
command: "cargo test".into(),
@@ -543,7 +543,7 @@ mod tests {
}
#[test]
fn monitor_maps_to_bash_access() {
use kigi_tools::implementations::grok_build::monitor::types::MonitorInput;
use kigi_tools::implementations::kigi::monitor::types::MonitorInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::Monitor(MonitorInput {
command: "tail -f /var/log/syslog".into(),
@@ -560,7 +560,7 @@ mod tests {
}
#[test]
fn search_replace_maps_to_edit_access() {
use kigi_tools::implementations::grok_build::search_replace::SearchReplaceInput;
use kigi_tools::implementations::kigi::search_replace::SearchReplaceInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::SearchReplace(SearchReplaceInput {
file_path: "lib.rs".into(),
@@ -576,7 +576,7 @@ mod tests {
}
#[test]
fn web_fetch_maps_to_web_fetch_access() {
use kigi_tools::implementations::grok_build::web_fetch::WebFetchInput;
use kigi_tools::implementations::kigi::web_fetch::WebFetchInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::WebFetch(WebFetchInput {
url: "https://custom.example.com/api".into(),
@@ -590,7 +590,7 @@ mod tests {
}
#[test]
fn web_search_maps_to_web_search_access() {
use kigi_tools::implementations::grok_build::web_search::WebSearchInput;
use kigi_tools::implementations::kigi::web_search::WebSearchInput;
use kigi_tools::types::ToolInput;
let input = ToolInput::WebSearch(WebSearchInput {
query: "rust lang".into(),
@@ -631,13 +631,13 @@ mod tests {
);
}
#[test]
fn client_type_deserializes_grok_shell_as_generic() {
fn client_type_deserializes_kigi_shell_as_generic() {
assert_eq!(
serde_json::from_value::<ClientType>("grok-shell".into()).unwrap(),
serde_json::from_value::<ClientType>("kigi-shell".into()).unwrap(),
ClientType::Generic,
);
assert_eq!(
serde_json::from_value::<ClientType>("grok_shell".into()).unwrap(),
serde_json::from_value::<ClientType>("kigi_shell".into()).unwrap(),
ClientType::Generic,
);
assert_eq!(
@@ -42,7 +42,7 @@ pub(crate) fn find_mcp_json_files_in(chain_dirs: &[PathBuf]) -> Vec<PathBuf> {
}
/// True when `config_path` is `$KIGI_SHARE_DIR/config.toml` (user tier, not project).
fn is_user_grok_config_file(config_path: &Path) -> bool {
fn is_user_kigi_config_file(config_path: &Path) -> bool {
let Some(user_home) = kigi_config::user_kigi_home() else {
return false;
};
@@ -79,7 +79,7 @@ pub(crate) fn find_project_configs_in(chain_dirs: &[PathBuf]) -> Vec<PathBuf> {
.iter()
.rev()
.map(|dir| dir.join(".kigi").join("config.toml"))
.filter(|config_path| config_path.is_file() && !is_user_grok_config_file(config_path))
.filter(|config_path| config_path.is_file() && !is_user_kigi_config_file(config_path))
.collect()
}
@@ -88,7 +88,7 @@ mod tests {
use super::*;
#[test]
fn find_project_configs_excludes_user_grok_config_file() {
fn find_project_configs_excludes_user_kigi_config_file() {
let Some(user_home) = kigi_config::user_kigi_home() else {
return;
};
@@ -98,10 +98,10 @@ mod tests {
let home = std::env::home_dir().expect("home dir");
let from_home = find_project_configs(&home);
assert!(
!from_home.iter().any(|p| is_user_grok_config_file(p)),
!from_home.iter().any(|p| is_user_kigi_config_file(p)),
"user config leaked into project configs: {from_home:?}"
);
assert!(is_user_grok_config_file(&user_config));
assert!(is_user_kigi_config_file(&user_config));
}
let tmp = tempfile::tempdir().unwrap();
@@ -110,6 +110,6 @@ mod tests {
std::fs::write(project.join(".kigi/config.toml"), "# project\n").unwrap();
let found = find_project_configs(&project);
assert_eq!(found.len(), 1);
assert!(!is_user_grok_config_file(&found[0]));
assert!(!is_user_kigi_config_file(&found[0]));
}
}
@@ -778,7 +778,7 @@ fn collect_diff_stats(
}
DiffStatsResult { stats, paths }
}
/// Payload for the `x.ai/git_head_changed` ACP extension notification.
/// Payload for the `kigi/git_head_changed` ACP extension notification.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitHeadChanged {
@@ -1790,7 +1790,7 @@ pub async fn stash_before_destructive_op(
return StashOutcome::Skipped(reason);
}
let message = format!(
"grok: pre-{label} {} {}",
"kigi: pre-{label} {} {}",
session_id,
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ")
);
@@ -1908,7 +1908,7 @@ pub async fn checkout_session_commit(
/// The restore-code path runs `git fetch origin` + `git checkout <sha>`,
/// which *detaches HEAD*. That is only acceptable in two situations:
///
/// 1. `supplied_cwd` is a grok-managed worktree (`~/.kigi/worktrees/...`).
/// 1. `supplied_cwd` is a kigi-managed worktree (`~/.kigi/worktrees/...`).
/// These are disposable snapshots that exist precisely to carry a
/// detached session HEAD.
/// 2. `supplied_cwd` is exactly the cwd the session was persisted with
@@ -3522,7 +3522,7 @@ mod restore_code_tests {
assert!(porcelain.trim().is_empty(), "got: {porcelain:?}");
let list = git_cli(tmp.path(), &["stash", "list"]).await.unwrap();
assert!(
list.contains("grok: pre-test sess-2"),
list.contains("kigi: pre-test sess-2"),
"stash list missing session id: {list}"
);
}
@@ -448,7 +448,7 @@ pub struct WorkspaceShared {
/// `discovery` module.
pub(crate) plugin_discovery_config: crate::discovery::PluginDiscoveryConfig,
/// Sink for workspace-originated ext-notifications to the client (e.g.
/// `x.ai/search/fuzzy/status`). Mode-agnostic: the shell wires it to the
/// `kigi/search/fuzzy/status`). Mode-agnostic: the shell wires it to the
/// agent gateway in local mode, and to the server in proxy mode. `None` until
/// set via [`WorkspaceHandle::set_client_ext_sink`](crate::handle::WorkspaceHandle::set_client_ext_sink).
pub(crate) client_ext_sink: arc_swap::ArcSwap<Option<ClientExtSink>>,
@@ -13,7 +13,7 @@ use crate::session::WorkspaceSession;
pub(crate) static WORKSPACE_TOOLSET_SWAP_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_toolset_swap_total",
"kigi_workspace_toolset_swap_total",
"Session toolset installs and swaps, by trigger and guard state",
&["trigger", "turn_active", "in_flight"]
)
@@ -26,7 +26,7 @@ pub(crate) static WORKSPACE_TOOLSET_SWAP_TOTAL: std::sync::LazyLock<IntCounterVe
pub(crate) static WORKSPACE_TOOLSET_SWAP_REJECTED_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_toolset_swap_rejected_total",
"kigi_workspace_toolset_swap_rejected_total",
"Session toolset swaps rejected by the turn-safety guards, by reason and trigger",
&["reason", "trigger"]
)
@@ -39,7 +39,7 @@ pub(crate) static WORKSPACE_TOOLSET_SWAP_REJECTED_TOTAL: std::sync::LazyLock<Int
static WORKSPACE_BIND_REBIND_RERESOLVE_TOTAL: std::sync::LazyLock<IntCounterVec> =
std::sync::LazyLock::new(|| {
register_int_counter_vec!(
"grok_workspace_bind_rebind_reresolve_total",
"kigi_workspace_bind_rebind_reresolve_total",
"session.bind rebinds that re-resolved a changed explicit toolset, by result",
&["result"]
)
@@ -295,7 +295,7 @@ impl SessionContextFactory for WorkspaceSessionContextFactory {
web_fetch_config: build_web_fetch_config(),
lsp: None,
app_builder_deployer_config:
kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig::default(),
kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig::default(),
api_key_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
@@ -317,8 +317,8 @@ impl SessionContextFactory for WorkspaceSessionContextFactory {
}
/// Build web fetch config. Enabled with default params unless
/// `KIGI_DISABLE_WEB_FETCH=1` is set.
fn build_web_fetch_config() -> kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig {
use kigi_tools::implementations::grok_build::web_fetch::{WebFetchConfig, WebFetchParams};
fn build_web_fetch_config() -> kigi_tools::implementations::kigi::web_fetch::WebFetchConfig {
use kigi_tools::implementations::kigi::web_fetch::{WebFetchConfig, WebFetchParams};
if std::env::var("KIGI_DISABLE_WEB_FETCH").is_ok_and(|v| v == "1" || v == "true") {
return WebFetchConfig::Disabled;
}
@@ -414,10 +414,10 @@ pub mod test_support {
pub fn baseline_config() -> ToolServerConfig {
ToolServerConfig {
tools: vec![
tc("GrokBuild:read_file", Some(ToolKind::Read)),
tc("GrokBuild:search_replace", Some(ToolKind::Edit)),
tc("GrokBuild:grep", Some(ToolKind::Search)),
tc("GrokBuild:list_dir", Some(ToolKind::ListDir)),
tc("Kigi:read_file", Some(ToolKind::Read)),
tc("Kigi:search_replace", Some(ToolKind::Edit)),
tc("Kigi:grep", Some(ToolKind::Search)),
tc("Kigi:list_dir", Some(ToolKind::ListDir)),
],
behavior_preset: None,
}
@@ -470,13 +470,10 @@ mod tests {
async fn resolve_session_toolset_mcp_merge_dedup_by_id_baseline_wins() {
let factory = factory_for_test();
let baseline = ToolServerConfig {
tools: vec![test_support::tc(
"GrokBuild:read_file",
Some(ToolKind::Read),
)],
tools: vec![test_support::tc("Kigi:read_file", Some(ToolKind::Read))],
behavior_preset: None,
};
let mut mcp_dup = test_support::tc("GrokBuild:read_file", Some(ToolKind::Read));
let mut mcp_dup = test_support::tc("Kigi:read_file", Some(ToolKind::Read));
mcp_dup.name_override = Some("mcp_read".into());
let snapshot = vec![mcp_dup];
let (_eff, ts, _backend) = resolve_session_toolset(
@@ -507,14 +504,14 @@ mod tests {
#[test]
fn backfill_tool_kinds_fills_known_kindless_ids_only() {
let kinds = HashMap::from([
("GrokBuild:search_replace".to_owned(), ToolKind::Edit),
("GrokBuild:read_file".to_owned(), ToolKind::Read),
("Kigi:search_replace".to_owned(), ToolKind::Edit),
("Kigi:read_file".to_owned(), ToolKind::Read),
]);
let config = ToolServerConfig {
tools: vec![
test_support::tc("GrokBuild:search_replace", None),
test_support::tc("Kigi:search_replace", None),
test_support::tc("adhoc.opaque", None),
test_support::tc("GrokBuild:read_file", Some(ToolKind::Search)),
test_support::tc("Kigi:read_file", Some(ToolKind::Search)),
],
behavior_preset: Some("current".to_owned()),
};
@@ -527,14 +524,14 @@ mod tests {
.expect("tool present")
.kind
};
assert_eq!(kind_of("GrokBuild:search_replace"), Some(ToolKind::Edit));
assert_eq!(kind_of("Kigi:search_replace"), Some(ToolKind::Edit));
assert_eq!(
kind_of("adhoc.opaque"),
None,
"ids unknown to the registry stay kind-less"
);
assert_eq!(
kind_of("GrokBuild:read_file"),
kind_of("Kigi:read_file"),
Some(ToolKind::Search),
"an explicit kind wins over the registry's"
);
@@ -550,11 +547,11 @@ mod tests {
let factory = factory_for_test();
let baseline = ToolServerConfig {
tools: vec![
test_support::tc("GrokBuild:read_file", None),
test_support::tc("GrokBuild:grep", None),
test_support::tc("GrokBuild:list_dir", None),
test_support::tc("GrokBuild:search_replace", None),
test_support::tc("GrokBuild:run_terminal_cmd", None),
test_support::tc("Kigi:read_file", None),
test_support::tc("Kigi:grep", None),
test_support::tc("Kigi:list_dir", None),
test_support::tc("Kigi:search_replace", None),
test_support::tc("Kigi:run_terminal_cmd", None),
],
behavior_preset: None,
};
@@ -594,10 +591,7 @@ mod tests {
#[test]
fn resolve_session_toolset_mcp_edit_dropped_under_readonly() {
let baseline = ToolServerConfig {
tools: vec![test_support::tc(
"GrokBuild:read_file",
Some(ToolKind::Read),
)],
tools: vec![test_support::tc("Kigi:read_file", Some(ToolKind::Read))],
behavior_preset: None,
};
let mcp_edit = test_support::tc("mcp.editor", Some(ToolKind::Edit));
@@ -609,7 +603,7 @@ mod tests {
let factory = factory_for_test();
let baseline = ToolServerConfig {
tools: vec![
test_support::tc("GrokBuild:read_file", Some(ToolKind::Read)),
test_support::tc("Kigi:read_file", Some(ToolKind::Read)),
test_support::tc("baseline.opaque", None),
],
behavior_preset: None,
@@ -626,7 +620,7 @@ mod tests {
"MCP kind: None MUST be dropped under ReadOnly: {kept_ids:?}"
);
assert!(
kept_ids.contains(&"GrokBuild:read_file"),
kept_ids.contains(&"Kigi:read_file"),
"baseline Read kind must survive ReadOnly: {kept_ids:?}"
);
let _ = factory;
+23 -23
View File
@@ -112,7 +112,7 @@ impl TrustStore {
Self::default_path_in(kigi_config::user_kigi_home())
}
/// Map a resolved user-grok-home to the store path, preserving "no home" as
/// Map a resolved user-kigi-home to the store path, preserving "no home" as
/// "no path" (never synthesizing a fallback). Split from [`Self::default_path`]
/// as a pure seam so the no-home branch is unit-testable without the
/// process-global home cache.
@@ -252,7 +252,7 @@ impl TrustStore {
tracing::warn!(
path = %canonical.display(),
trusted,
"folder trust: no user grok home resolved; trust decision not recorded"
"folder trust: no user kigi home resolved; trust decision not recorded"
);
return Ok(());
};
@@ -351,15 +351,15 @@ impl TrustStore {
/// The key is the canonicalized git repository root when `cwd` is inside a
/// repo (trust applies to the whole repo), otherwise the canonicalized `cwd`.
///
/// A grok-managed worktree first collapses onto its recorded source repo's git
/// ROOT (via the `~/.kigi/worktrees.db` registry), so every `grok -w` worktree
/// A kigi-managed worktree first collapses onto its recorded source repo's git
/// ROOT (via the `~/.kigi/worktrees.db` registry), so every `kigi -w` worktree
/// shares one trust key regardless of creation mode — including standalone clones
/// that git can't link back to their source — and regardless of the subdir
/// `grok -w` was launched from (the recorded source repo may be a repo subdir).
/// `kigi -w` was launched from (the recorded source repo may be a repo subdir).
/// Non-registry git worktrees fall through to the git-topology collapse below.
///
/// A linked git worktree collapses onto its MAIN checkout's root so every
/// `grok -w` worktree of a repo shares one trust key. The collapse fires ONLY
/// `kigi -w` worktree of a repo shares one trust key. The collapse fires ONLY
/// for the conventional `<workdir>/.git` layout — i.e. the common gitdir
/// resolves back to `<main_workdir>/.git`. For bare or `--separate-git-dir`
/// repos (where the common gitdir's inferred workdir would be the gitdir's
@@ -383,11 +383,11 @@ pub fn workspace_key(cwd: &Path) -> PathBuf {
/// Git-topology-derived workspace key (pre-safety-guard); see [`workspace_key`],
/// which rejects an over-broad derived root in favor of the cwd.
fn git_derived_workspace_key(cwd: &Path) -> PathBuf {
// A grok-managed worktree (any creation mode, incl. standalone clones git
// A kigi-managed worktree (any creation mode, incl. standalone clones git
// can't link) collapses onto its recorded source repo so trust is shared.
if let Some(source_repo) = crate::worktree::source_repo_for_cwd(&cwd.to_string_lossy()) {
// Key on the source repo's git ROOT so every worktree of one repo shares
// ONE key regardless of the subdir grok -w was launched from (parity with
// ONE key regardless of the subdir kigi -w was launched from (parity with
// the git-topology branch below). Fall back to the recorded path when the
// source repo is gone (deleted-source standalone worktrees still work).
let root = git2::Repository::discover(&source_repo)
@@ -490,7 +490,7 @@ impl Drop for ExclusiveLock {
/// path per line; each becomes a folder-trust grant so the unified gate honors
/// prior decisions. The legacy file is then renamed to `*.migrated` so it is
/// read only once. A no-op when the legacy file is absent/already migrated or no
/// user grok home resolves.
/// user kigi home resolves.
pub fn migrate_legacy_hook_trust() {
// Local/dev builds do NO trust-store I/O: skip the load + legacy-file rename.
if crate::folder_trust::folder_trust_inert() {
@@ -513,7 +513,7 @@ pub fn migrate_legacy_hook_trust() {
}
/// Seam for [`migrate_legacy_hook_trust`] with explicit paths, so the migration
/// is testable without the process-global grok-home cache. Returns the number
/// is testable without the process-global kigi-home cache. Returns the number
/// of grants seeded into `store`.
fn migrate_legacy_hook_trust_in(legacy_file: &Path, store: &mut TrustStore) -> usize {
// A read error must NOT be mistaken for "no grants": bail without renaming so
@@ -1277,7 +1277,7 @@ mod tests {
#[test]
fn workspace_key_collapses_linked_worktrees_onto_main_checkout() {
// Every linked `grok -w` worktree of a repo must share ONE trust key:
// Every linked `kigi -w` worktree of a repo must share ONE trust key:
// its main checkout's root. Build a real repo + two linked worktrees and
// assert each collapses onto the main checkout (so it is trusted once,
// not re-prompted per worktree).
@@ -1416,20 +1416,20 @@ mod tests {
);
}
// ── workspace_key registry collapse (grok-managed worktrees) ─────────
// ── workspace_key registry collapse (kigi-managed worktrees) ─────────
// Crate-shared env lock + env guards bundled as ONE value so the env restores
// before the lock releases by struct field order (see lib.rs), regardless of
// how the caller binds the fixture's return.
use crate::LockedTestEnv;
/// Point `KIGI_SHARE_DIR` at an isolated tempdir and register one grok-managed
/// Point `KIGI_SHARE_DIR` at an isolated tempdir and register one kigi-managed
/// worktree at `<home>/worktrees/repo/<name>` recording `source_repo` and
/// `creation_mode`. The worktree dir is a PLAIN directory — NOT a git linked
/// worktree — so only the registry can collapse it. Returns `(env, worktree
/// dir)`; the [`LockedTestEnv`] holds the lock and restores `KIGI_SHARE_DIR` on
/// drop (before releasing the lock), so the caller may bind it any way.
fn register_grok_worktree(
fn register_kigi_worktree(
temp: &tempfile::TempDir,
name: &str,
source_repo: &Path,
@@ -1440,7 +1440,7 @@ mod tests {
// Canonicalize so macOS /var -> /private/var agrees between the stored
// record path and the canonicalized lookup query.
let root = dunce::canonicalize(temp.path()).unwrap();
let home = root.join("grok-home");
let home = root.join("kigi-home");
let wt = home.join("worktrees").join("repo").join(name);
std::fs::create_dir_all(&wt).unwrap();
@@ -1470,7 +1470,7 @@ mod tests {
}
#[test]
fn workspace_key_collapses_standalone_grok_worktree_onto_source_repo() {
fn workspace_key_collapses_standalone_kigi_worktree_onto_source_repo() {
// A standalone worktree is a full clone with its OWN `.git`, so git
// topology can't link it to its source; the registry (worktrees.db) must
// collapse it onto the recorded source repo so trust is shared. The
@@ -1484,13 +1484,13 @@ mod tests {
std::fs::create_dir_all(&source_repo).unwrap();
git2::Repository::init(&source_repo).unwrap();
let (_env, wt) = register_grok_worktree(&temp, "wt", &source_repo, "standalone");
let (_env, wt) = register_kigi_worktree(&temp, "wt", &source_repo, "standalone");
let expected = canonicalize_or_owned(&source_repo);
assert_eq!(
workspace_key(&wt),
expected,
"a standalone grok worktree must collapse onto its recorded source repo"
"a standalone kigi worktree must collapse onto its recorded source repo"
);
// A cwd nested below the worktree root collapses onto the same key (the
// registry walk ascends to the registered worktree).
@@ -1517,7 +1517,7 @@ mod tests {
let subdir = repo.join("crates").join("sub");
std::fs::create_dir_all(&subdir).unwrap();
let (_env, wt) = register_grok_worktree(&temp, "wt", &subdir, "standalone");
let (_env, wt) = register_kigi_worktree(&temp, "wt", &subdir, "standalone");
assert_eq!(
workspace_key(&wt),
@@ -1532,7 +1532,7 @@ mod tests {
// `<kigi_home>/worktrees`: `worktree_record_for_cwd` skips the registry
// there, so the key falls back to git/cwd. Non-vacuous: the registry IS
// populated with a real git source repo that WOULD be returned for a
// worktree cwd, and `outside` is its OWN git repo (under grok HOME but not
// worktree cwd, and `outside` is its OWN git repo (under kigi HOME but not
// under its `worktrees/`) so the fallback is deterministic (no conditional
// skip) — we assert the key is `outside`'s own root, never the source repo.
let temp = tempfile::TempDir::new().unwrap();
@@ -1541,10 +1541,10 @@ mod tests {
std::fs::create_dir_all(&source_repo).unwrap();
git2::Repository::init(&source_repo).unwrap();
let (_env, _wt) = register_grok_worktree(&temp, "wt", &source_repo, "standalone");
let (_env, _wt) = register_kigi_worktree(&temp, "wt", &source_repo, "standalone");
// Under grok HOME but NOT under `<home>/worktrees`, and its own git repo.
let outside = root.join("grok-home").join("not-worktrees").join("proj");
// Under kigi HOME but NOT under `<home>/worktrees`, and its own git repo.
let outside = root.join("kigi-home").join("not-worktrees").join("proj");
std::fs::create_dir_all(&outside).unwrap();
git2::Repository::init(&outside).unwrap();
@@ -618,11 +618,11 @@ pub fn resolve_label_collision(base_dir: &Path, label: &str) -> String {
// Worktree Base Directory Resolution
// ============================================================================
/// Resolve the grok home for worktree paths via the **same** resolver used for
/// Resolve the kigi home for worktree paths via the **same** resolver used for
/// `worktrees.db` (`kigi_fast_worktree::resolve_kigi_home`), so checkout dirs and
/// the metadata DB always live under the same `.kigi` tree. That resolver
/// canonicalizes its `$HOME` fallback to match `kigi_config::kigi_home()`,
/// so worktree paths also agree with trust/hooks and other grok-home paths.
/// so worktree paths also agree with trust/hooks and other kigi-home paths.
fn kigi_home() -> std::path::PathBuf {
kigi_fast_worktree::resolve_kigi_home().unwrap_or_else(|_| {
dirs::home_dir()
@@ -641,7 +641,7 @@ pub fn worktree_base_dir(git_root: &Path) -> std::path::PathBuf {
}
/// Resolves the worktree base directory (`~/.kigi/worktrees/<repo_name>`)
/// for a given source path, correctly handling grok-managed worktrees.
/// for a given source path, correctly handling kigi-managed worktrees.
///
/// When `source_path` is already under `~/.kigi/worktrees/<repo>/...`, the
/// repo name is derived from the directory structure directly. This avoids
@@ -649,7 +649,7 @@ pub fn worktree_base_dir(git_root: &Path) -> std::path::PathBuf {
/// as the main repo root (returning the worktree itself instead of the
/// original repo).
///
/// For paths outside the grok worktree directory, falls back to
/// For paths outside the kigi worktree directory, falls back to
/// `find_main_repo_root_from_path` + `worktree_base_dir`.
pub fn worktree_base_dir_for_source(source_path: &Path) -> Result<std::path::PathBuf> {
let worktrees_dir = kigi_home().join("worktrees");
@@ -724,7 +724,7 @@ fn worktree_record_for_cwd(cwd: &str) -> Option<(WorktreeDb, WorktreeRecord)> {
None
}
/// The recorded source repo of the grok-managed worktree containing `cwd`, if any.
/// The recorded source repo of the kigi-managed worktree containing `cwd`, if any.
///
/// Thin wrapper over [`worktree_record_for_cwd`] that drops the DB handle;
/// returns `None` (without DB I/O) for paths outside `~/.kigi/worktrees/`.
@@ -2276,7 +2276,7 @@ pub async fn remove_jj_workspace(workspace_path: &str) -> Result<()> {
/// Request to resume an existing session in a fresh worktree.
///
/// ACP equivalent of `grok -w -r <session_id>` (optionally with `--ref`).
/// ACP equivalent of `kigi -w -r <session_id>` (optionally with `--ref`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumeSessionInWorktreeRequest {
@@ -2296,7 +2296,7 @@ pub struct ResumeSessionInWorktreeRequest {
pub git_ref: Option<String>,
}
/// Response from `x.ai/git/worktree/resume_session`.
/// Response from `kigi/git/worktree/resume_session`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResumeSessionInWorktreeResponse {
@@ -2339,7 +2339,7 @@ pub struct RehydrateSessionRequest {
pub worktree_path: Option<String>,
}
/// Response from `x.ai/session/rehydrate`.
/// Response from `kigi/session/rehydrate`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RehydrateSessionResponse {
@@ -2596,7 +2596,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/subagents/dispose-1";
let ref_name = "refs/kigi/subagents/dispose-1";
let returned = snapshot_and_remove_subagent_worktree(&wt, &repo, ref_name)
.await
.unwrap();
@@ -2622,7 +2622,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/subagents/dispose-2";
let ref_name = "refs/kigi/subagents/dispose-2";
snapshot_and_remove_subagent_worktree(&wt, &repo, ref_name)
.await
.unwrap();
@@ -2669,7 +2669,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/subagents/standalone-1";
let ref_name = "refs/kigi/subagents/standalone-1";
let returned = snapshot_subagent_worktree(&wt, &repo, ref_name)
.await
.unwrap();
@@ -2719,7 +2719,7 @@ mod tests {
let result = snapshot_and_remove_subagent_worktree(
&not_a_worktree,
&not_a_worktree,
"refs/grok/subagents/dispose-3",
"refs/kigi/subagents/dispose-3",
)
.await;
@@ -2750,7 +2750,7 @@ mod tests {
// Canonicalize so macOS /var -> /private/var agrees between the stored
// record path and `db.get`'s canonicalized query path.
let root = dunce::canonicalize(temp.path()).unwrap();
let home = root.join("grok-home");
let home = root.join("kigi-home");
let wt = home.join("worktrees").join("repo").join("wt");
std::fs::create_dir_all(&wt).unwrap();
// Acquire the lock, then set the env under it (LockedTestEnv restores the
@@ -2849,7 +2849,7 @@ mod tests {
git_commit_all(&repo, "initial");
// Unique basename → unique DB id, so a concurrent open_default writer
// can't clobber this row (GrokHomeFixture is not visible across crates).
// can't clobber this row (KigiHomeFixture is not visible across crates).
let wt = temp.path().join("fork-cancel-wt");
WorktreeBuilder::new(&repo, &wt).create().unwrap();