//! 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, shutdown: Arc, } 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, shutdown: Arc, ) -> 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 { &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>, backend: Arc, ) -> 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> { 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, /// Capacity of the workspace event broadcast channel. pub event_buffer_capacity: usize, /// Pluggable [`SessionContext`] / [`ToolRegistryBuilder`] producer. pub session_factory: Arc, /// Global hook sources (e.g. `~/.claude/settings.json`, `~/.kigi/hooks/`). pub hook_global_sources: Vec, /// Project-scoped hook sources (e.g. `/.kigi/hooks/`). pub hook_project_sources: Vec, /// 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 /// `/.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, /// Maximum recursion depth for subagent nesting. pub max_depth: u32, /// Working directory override. `None` inherits the parent's `cwd`. pub cwd_override: Option, /// Extra env vars to layer on top of the parent's `session_env`. pub extra_env: HashMap, /// Parent session to inherit from. Required. pub parent_session_id: Option, } impl AgentSessionConfig { /// Construct a config with the supplied `agent_id` and otherwise /// minimal/permissive defaults. pub fn new(agent_id: impl Into) -> 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()" } 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, }