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.
376 lines
9.4 KiB
Rust
376 lines
9.4 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
const EXCLUDED_DIR_NAMES: &[&str] = &[
|
|
".kigi", ".cache", ".daemon", ".config", ".npm", ".cargo", ".rustup", ".vscode", ".gemini",
|
|
".hermes", ".claude",
|
|
];
|
|
|
|
fn known_os_dirs() -> Vec<PathBuf> {
|
|
[
|
|
dirs::desktop_dir(),
|
|
dirs::download_dir(),
|
|
dirs::document_dir(),
|
|
dirs::audio_dir(),
|
|
dirs::video_dir(),
|
|
dirs::picture_dir(),
|
|
dirs::public_dir(),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect()
|
|
}
|
|
|
|
pub fn is_project_dir(cwd: &Path) -> bool {
|
|
if cwd.as_os_str().is_empty() || cwd.parent().is_none() {
|
|
return false;
|
|
}
|
|
|
|
if cwd.ancestors().any(|p| p.join(".git").exists()) {
|
|
return true;
|
|
}
|
|
|
|
if has_excluded_component(cwd) {
|
|
return false;
|
|
}
|
|
|
|
if is_platform_system_dir(cwd) {
|
|
return false;
|
|
}
|
|
|
|
let Some(home) = dirs::home_dir() else {
|
|
return false;
|
|
};
|
|
|
|
if cwd == home {
|
|
return false;
|
|
}
|
|
|
|
if is_platform_home_excluded(cwd, &home) {
|
|
return false;
|
|
}
|
|
|
|
if known_os_dirs().iter().any(|d| cwd == d) {
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn is_platform_system_dir(cwd: &Path) -> bool {
|
|
if cwd == Path::new("/tmp")
|
|
|| cwd.starts_with("/tmp/")
|
|
|| cwd == Path::new("/var/tmp")
|
|
|| cwd.starts_with("/var/tmp/")
|
|
|| cwd.starts_with("/var/folders/")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
if cwd == Path::new("/private/tmp")
|
|
|| cwd.starts_with("/private/tmp/")
|
|
|| cwd == Path::new("/private/var/tmp")
|
|
|| cwd.starts_with("/private/var/tmp/")
|
|
|| cwd.starts_with("/private/var/folders/")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
if cwd == Path::new("/root") {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn is_platform_system_dir(cwd: &Path) -> bool {
|
|
if let Ok(temp) = std::env::var("TEMP").or_else(|_| std::env::var("TMP")) {
|
|
if cwd.starts_with(&temp) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
let path_lower = cwd.to_string_lossy().to_lowercase();
|
|
if path_lower.contains("\\windows\\")
|
|
|| path_lower.ends_with("\\windows")
|
|
|| path_lower.contains("\\program files")
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if cwd.parent().map_or(false, |p| p.parent().is_none()) && cwd.to_string_lossy().len() <= 3 {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn is_platform_home_excluded(cwd: &Path, home: &Path) -> bool {
|
|
if cwd.starts_with(home.join("Library"))
|
|
&& !cwd.starts_with(home.join("Library/Mobile Documents"))
|
|
{
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn is_platform_home_excluded(cwd: &Path, home: &Path) -> bool {
|
|
let Ok(relative) = cwd.strip_prefix(home) else {
|
|
return false;
|
|
};
|
|
if relative.components().count() != 1 {
|
|
return false;
|
|
}
|
|
let Some(std::path::Component::Normal(name)) = relative.components().next() else {
|
|
return false;
|
|
};
|
|
let name = name.to_string_lossy().to_lowercase();
|
|
[
|
|
"desktop",
|
|
"downloads",
|
|
"documents",
|
|
"pictures",
|
|
"music",
|
|
"videos",
|
|
]
|
|
.contains(&name.as_str())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn is_platform_home_excluded(_cwd: &Path, _home: &Path) -> bool {
|
|
false
|
|
}
|
|
|
|
fn has_excluded_component(path: &Path) -> bool {
|
|
for component in path.components() {
|
|
if let std::path::Component::Normal(name) = component {
|
|
let name_lower = name.to_string_lossy().to_lowercase();
|
|
|
|
if EXCLUDED_DIR_NAMES.contains(&name_lower.as_str()) {
|
|
return true;
|
|
}
|
|
|
|
if name_lower.starts_with(".kigi-") {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
mod posix {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn root_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("/")));
|
|
}
|
|
|
|
#[test]
|
|
fn tmp_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("/tmp")));
|
|
assert!(!is_project_dir(Path::new("/tmp/scratch")));
|
|
}
|
|
|
|
#[test]
|
|
fn tmp_prefix_not_greedy() {
|
|
assert!(is_project_dir(Path::new("/tmpdata/foo")));
|
|
}
|
|
|
|
#[test]
|
|
fn var_folders_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("/var/folders/ab/cd")));
|
|
}
|
|
|
|
#[test]
|
|
fn deep_project_is_safe() {
|
|
assert!(is_project_dir(Path::new("/Users/someone/my-project/src")));
|
|
}
|
|
|
|
#[test]
|
|
fn home_subdir_is_safe() {
|
|
assert!(is_project_dir(Path::new("/Users/someone/my-project")));
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod macos {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn private_tmp_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("/private/tmp")));
|
|
assert!(!is_project_dir(Path::new("/private/tmp/scratch")));
|
|
assert!(!is_project_dir(Path::new("/private/var/folders/ab/cd")));
|
|
}
|
|
|
|
#[test]
|
|
fn library_is_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home.join("Library")));
|
|
assert!(!is_project_dir(&home.join("Library/Caches")));
|
|
assert!(!is_project_dir(&home.join("Library/Application Support")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn icloud_drive_projects_are_safe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(is_project_dir(&home.join(
|
|
"Library/Mobile Documents/com~apple~CloudDocs/Projects/my-app"
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod linux {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn bare_root_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("/root")));
|
|
}
|
|
|
|
#[test]
|
|
fn root_project_is_safe() {
|
|
assert!(is_project_dir(Path::new("/root/my-project")));
|
|
}
|
|
}
|
|
|
|
mod config_and_cache {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn kigi_dirs_are_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home.join(".kigi")));
|
|
assert!(!is_project_dir(&home.join(".kigi/bin")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn kigi_prefixed_dirs_are_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home.join(".kigi-proxy-work")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cache_dirs_are_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home.join(".cache/zoe-proc")));
|
|
assert!(!is_project_dir(&home.join(".config/nvim")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn other_ai_tool_dirs_are_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home.join(".gemini/antigravity")));
|
|
assert!(!is_project_dir(&home.join(".hermes/kanban")));
|
|
assert!(!is_project_dir(&home.join(".claude/projects")));
|
|
}
|
|
}
|
|
}
|
|
|
|
mod home_and_os_dirs {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn home_is_unsafe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(!is_project_dir(&home));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn home_project_is_safe() {
|
|
if let Some(home) = dirs::home_dir() {
|
|
assert!(is_project_dir(&home.join("my-project")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bare_desktop_is_unsafe() {
|
|
if let Some(d) = dirs::desktop_dir() {
|
|
assert!(!is_project_dir(&d));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_project_is_safe() {
|
|
if let Some(d) = dirs::desktop_dir() {
|
|
assert!(is_project_dir(&d.join("my-project")));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bare_downloads_is_unsafe() {
|
|
if let Some(d) = dirs::download_dir() {
|
|
assert!(!is_project_dir(&d));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bare_documents_is_unsafe() {
|
|
if let Some(d) = dirs::document_dir() {
|
|
assert!(!is_project_dir(&d));
|
|
}
|
|
}
|
|
}
|
|
|
|
mod edge_cases {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn empty_path_is_unsafe() {
|
|
assert!(!is_project_dir(Path::new("")));
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
#[test]
|
|
fn unicode_paths_work() {
|
|
assert!(is_project_dir(Path::new(
|
|
"/Users/me/code/\u{D3F4}\u{B9AC}\u{B9C8}\u{CF13}"
|
|
)));
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
#[test]
|
|
fn spaces_work() {
|
|
assert!(is_project_dir(Path::new("/Users/me/My Projects/cool app")));
|
|
}
|
|
}
|
|
|
|
mod git_detection {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn inside_git_repo_is_safe() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
std::fs::create_dir(tmp.path().join(".git")).unwrap();
|
|
assert!(is_project_dir(tmp.path()));
|
|
}
|
|
|
|
#[test]
|
|
fn subdirectory_of_git_repo_is_safe() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
std::fs::create_dir(tmp.path().join(".git")).unwrap();
|
|
let sub = tmp.path().join("deep/sub/dir");
|
|
std::fs::create_dir_all(&sub).unwrap();
|
|
assert!(is_project_dir(&sub));
|
|
}
|
|
}
|
|
}
|