§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:
@@ -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();
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
¬_a_worktree,
|
||||
¬_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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user