§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:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user