Files
Kigi-CLI/crates/codegen/kigi-workspace/src/config.rs
T
ZacharyZhang-NY 6f31415ed6 §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.
2026-07-18 02:48:46 -04:00

223 lines
9.8 KiB
Rust

//! Workspace and session configuration types.
use crate::capability::CapabilityMode;
use kigi_tools::registry::types::{SessionContext, ToolRegistryBuilder, ToolServerConfig};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
/// Default capacity for the workspace event broadcast channel.
pub const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 64;
/// A session-lifetime terminal backend paired with its explicit shutdown hook.
///
/// The backend (background-task registry + persistent shell) is owned by the
/// [`WorkspaceSession`](crate::session::WorkspaceSession) and injected into
/// every toolset re-resolve for that session, so background tasks and shell
/// state survive toolset swaps. The shutdown hook fires the backend's cancel
/// token — killing every child process group and stopping the actor — so
/// `drop_session`/evict teardown is an explicit act rather than a side effect
/// of the last `Arc` drop.
#[derive(Clone)]
pub struct SessionTerminalBackend {
backend: Arc<dyn kigi_tools::computer::types::TerminalBackend>,
shutdown: Arc<dyn Fn() + Send + Sync>,
}
impl SessionTerminalBackend {
/// Pair an already-erased `backend` with its shutdown hook.
///
/// Extension point for [`SessionContextFactory`] implementors whose
/// backend is not a `LocalTerminalBackend` (the fields are private, so
/// this is the only way to satisfy `build_terminal_backend` for other
/// backend types); in-repo factories use [`Self::local`].
pub fn new(
backend: Arc<dyn kigi_tools::computer::types::TerminalBackend>,
shutdown: Arc<dyn Fn() + Send + Sync>,
) -> Self {
Self { backend, shutdown }
}
/// Wrap a [`LocalTerminalBackend`], wiring the shutdown hook to its
/// cancel token.
///
/// [`LocalTerminalBackend`]: kigi_tools::computer::local::LocalTerminalBackend
pub fn local(backend: kigi_tools::computer::local::LocalTerminalBackend) -> Self {
let canceller = backend.clone();
Self {
backend: Arc::new(backend),
shutdown: Arc::new(move || canceller.cancel()),
}
}
/// The type-erased backend, as injected into toolset resolves.
pub fn backend(&self) -> &Arc<dyn kigi_tools::computer::types::TerminalBackend> {
&self.backend
}
/// Explicitly shut the backend down: kills all of its child process
/// groups and stops its actor.
pub fn shutdown(&self) {
(self.shutdown)();
}
}
impl std::fmt::Debug for SessionTerminalBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionTerminalBackend")
.finish_non_exhaustive()
}
}
/// Pluggable producer of [`SessionContext`] / [`ToolRegistryBuilder`]
/// for each session.
///
/// The workspace itself doesn't know how to construct the tool runtime
/// (terminal backend, file system, persistence path, MCP client config,
/// notification handle, ...) -- those come from the embedder (TUI, SDK,
/// or remote sampler). The embedder hands us a factory at
/// `WorkspaceHandle::new` time and we call it on every session
/// resolution.
pub trait SessionContextFactory: Send + Sync {
/// Build a fresh [`SessionContext`] for the given session, around the
/// given terminal `backend` (constructing one here would waste an actor
/// per resolve — the pipeline rebuilds toolsets around the session-owned
/// backend, so the caller always supplies it).
fn build_session_context(
&self,
session_id: &str,
cwd: PathBuf,
session_env: Arc<HashMap<String, String>>,
backend: Arc<dyn kigi_tools::computer::types::TerminalBackend>,
) -> SessionContext;
/// Build the session-lifetime terminal backend for a new session.
/// Called once per session create/fork; toolset re-resolves reuse the
/// session's stored backend instead of building another.
fn build_terminal_backend(&self) -> SessionTerminalBackend;
/// Build a fresh [`ToolRegistryBuilder`] with the workspace's
/// full set of registered tools.
fn registry_builder(&self) -> ToolRegistryBuilder;
fn known_tool_ids(&self) -> Arc<std::collections::HashSet<String>> {
Arc::new(self.registry_builder().known_tool_ids())
}
}
/// Placeholder for the cross-session memory backend config.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct MemoryConfig {}
/// Top-level config required to construct a [`crate::handle::WorkspaceHandle`].
///
/// `#[non_exhaustive]` so future fields are non-breaking.
#[non_exhaustive]
pub struct WorkspaceConfig {
/// Workspace root directory.
pub root_cwd: PathBuf,
/// Baseline tool config for the main session.
pub default_tool_config: ToolServerConfig,
/// Whether session-scoped fs operations should respect `.gitignore`.
pub respect_gitignore: bool,
/// Optional cross-session memory config.
pub memory_config: Option<MemoryConfig>,
/// Capacity of the workspace event broadcast channel.
pub event_buffer_capacity: usize,
/// Pluggable [`SessionContext`] / [`ToolRegistryBuilder`] producer.
pub session_factory: Arc<dyn SessionContextFactory>,
/// Global hook sources (e.g. `~/.claude/settings.json`, `~/.kigi/hooks/`).
pub hook_global_sources: Vec<HookSourceConfig>,
/// Project-scoped hook sources (e.g. `<project>/.kigi/hooks/`).
pub hook_project_sources: Vec<HookSourceConfig>,
/// Skill discovery configuration: additional skill paths and
/// path-prefix ignore list. Stored on `WorkspaceShared` for
/// `discover_skills` calls. Defaults to empty (no extra paths,
/// no ignores).
pub skills_config: crate::discovery::SkillsConfig,
/// Plugin discovery configuration: CLI plugin dirs, config paths,
/// and disabled/enabled lists. Stored on `WorkspaceShared` for
/// `discover_plugins` calls. Defaults to empty.
pub plugin_discovery_config: crate::discovery::PluginDiscoveryConfig,
/// Runtime-tunable timing/threshold config for the tool server.
pub status_config: crate::status_config::StatusConfig,
/// Folder-trust verdict for repo-local (project-scoped) LSP servers from
/// `<cwd>/.kigi/lsp.json`: `false` drops them at load, `true` keeps them. The
/// shell caller resolves the verdict and threads it in; callers without a
/// folder-trust decision pass `true`.
pub project_lsp_trusted: bool,
/// 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.
pub confine_fs_to_workspace_root: bool,
}
/// Configuration for spawning a subagent session within a workspace.
#[derive(Clone)]
#[non_exhaustive]
pub struct AgentSessionConfig {
/// Unique agent session id. Must be non-empty.
pub agent_id: String,
/// Filesystem isolation strategy.
pub isolation: IsolationMode,
/// Capability mode applied to this session's toolset.
pub capability_mode: CapabilityMode,
/// Per-fork tool override. `None` inherits from parent.
pub tool_config: Option<ToolServerConfig>,
/// Maximum recursion depth for subagent nesting.
pub max_depth: u32,
/// Working directory override. `None` inherits the parent's `cwd`.
pub cwd_override: Option<PathBuf>,
/// Extra env vars to layer on top of the parent's `session_env`.
pub extra_env: HashMap<String, String>,
/// Parent session to inherit from. Required.
pub parent_session_id: Option<String>,
}
impl AgentSessionConfig {
/// Construct a config with the supplied `agent_id` and otherwise
/// minimal/permissive defaults.
pub fn new(agent_id: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
isolation: IsolationMode::None,
capability_mode: CapabilityMode::ReadWrite,
tool_config: None,
max_depth: u32::MAX,
cwd_override: None,
extra_env: HashMap::new(),
parent_session_id: None,
}
}
}
/// WARNING: `tool_config` is intentionally redacted from `Debug` output
/// because `ToolServerConfig.tools[*].params` may contain credentials.
impl std::fmt::Debug for AgentSessionConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentSessionConfig")
.field("agent_id", &self.agent_id)
.field("isolation", &self.isolation)
.field("capability_mode", &self.capability_mode)
.field(
"tool_config",
if self.tool_config.is_some() {
&"Some(<redacted>)"
} else {
&"None"
},
)
.field("max_depth", &self.max_depth)
.field("cwd_override", &self.cwd_override)
.field("extra_env", &self.extra_env)
.field("parent_session_id", &self.parent_session_id)
.finish()
}
}
/// A single hook source: either a JSON settings file or a directory of
/// `*.json` hook files. Maps 1:1 to [`kigi_hooks::discovery::HookSource`]
/// but uses owned `PathBuf` so the config struct is `'static`.
#[derive(Debug, Clone)]
pub enum HookSourceConfig {
/// A single JSON settings file (e.g. `~/.claude/settings.json`).
SettingsFile(PathBuf),
/// A directory of `*.json` hook files (e.g. `~/.kigi/hooks/`).
Directory(PathBuf),
}
/// Filesystem isolation strategy for a forked session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IsolationMode {
/// No isolation: subagent shares the parent's working tree.
#[default]
None,
/// Run the subagent in a copy-on-write git worktree.
Worktree,
/// Run the subagent inside a sandbox/container.
Sandbox,
}