use std::collections::BTreeSet;
use crate::configfile::{module_enabled, module_setting_bool, HarnessConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ModuleId {
ToolsSearch,
ToolsApplyPatch,
ToolsPersistentShell,
ToolsBackground,
ToolsWeb,
ToolsQuestion,
Todos,
PlanMode,
Subagents,
PermissionsApprovals,
PermissionsRules,
PermissionsSandbox,
PermissionsProtectedPaths,
Trust,
McpClient,
McpServer,
Hooks,
Plugins,
Memory,
Checkpoint,
SessionTree,
SessionShare,
Reduction,
DeferredTools,
Cache,
ModelCatalog,
ModelOauth,
Lsp,
Formatters,
Tui,
Server,
Notify,
StructuredOutput,
Telemetry,
Integrations,
}
impl ModuleId {
pub const ALL: &'static [ModuleId] = &[
ModuleId::ToolsSearch,
ModuleId::ToolsApplyPatch,
ModuleId::ToolsPersistentShell,
ModuleId::ToolsBackground,
ModuleId::ToolsWeb,
ModuleId::ToolsQuestion,
ModuleId::Todos,
ModuleId::PlanMode,
ModuleId::Subagents,
ModuleId::PermissionsApprovals,
ModuleId::PermissionsRules,
ModuleId::PermissionsSandbox,
ModuleId::PermissionsProtectedPaths,
ModuleId::Trust,
ModuleId::McpClient,
ModuleId::McpServer,
ModuleId::Hooks,
ModuleId::Plugins,
ModuleId::Memory,
ModuleId::Checkpoint,
ModuleId::SessionTree,
ModuleId::SessionShare,
ModuleId::Reduction,
ModuleId::DeferredTools,
ModuleId::Cache,
ModuleId::ModelCatalog,
ModuleId::ModelOauth,
ModuleId::Lsp,
ModuleId::Formatters,
ModuleId::Tui,
ModuleId::Server,
ModuleId::Notify,
ModuleId::StructuredOutput,
ModuleId::Telemetry,
ModuleId::Integrations,
];
pub fn config_key(&self) -> &'static str {
match self {
ModuleId::ToolsSearch => "tools_search",
ModuleId::ToolsApplyPatch => "tools_apply_patch",
ModuleId::ToolsPersistentShell => "tools_persistent_shell",
ModuleId::ToolsBackground => "tools_background",
ModuleId::ToolsWeb => "tools_web",
ModuleId::ToolsQuestion => "tools_question",
ModuleId::Todos => "todos",
ModuleId::PlanMode => "plan_mode",
ModuleId::Subagents => "subagents",
ModuleId::PermissionsApprovals => "permissions",
ModuleId::PermissionsRules => "permissions.rules",
ModuleId::PermissionsSandbox => "permissions.sandbox",
ModuleId::PermissionsProtectedPaths => "permissions.protected_paths",
ModuleId::Trust => "trust",
ModuleId::McpClient => "mcp",
ModuleId::McpServer => "mcp.serve",
ModuleId::Hooks => "hooks",
ModuleId::Plugins => "plugins",
ModuleId::Memory => "memory",
ModuleId::Checkpoint => "checkpoint",
ModuleId::SessionTree => "session_tree",
ModuleId::SessionShare => "session_share",
ModuleId::Reduction => "reduction",
ModuleId::DeferredTools => "deferred_tools",
ModuleId::Cache => "cache",
ModuleId::ModelCatalog => "model_catalog",
ModuleId::ModelOauth => "model_oauth",
ModuleId::Lsp => "lsp",
ModuleId::Formatters => "formatters",
ModuleId::Tui => "tui",
ModuleId::Server => "server",
ModuleId::Notify => "notify",
ModuleId::StructuredOutput => "structured_output",
ModuleId::Telemetry => "telemetry",
ModuleId::Integrations => "integrations",
}
}
pub fn is_active(&self, hc: &HarnessConfig) -> bool {
match self {
ModuleId::McpServer => module_setting_bool(hc, "mcp", "serve"),
other => module_enabled(hc, other.config_key()),
}
}
}
impl std::fmt::Display for ModuleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.config_key())
}
}
fn tools_search_subflag(hc: &HarnessConfig, key: &str) -> bool {
hc.capabilities
.get("tools_search")
.and_then(|c| c.settings.get(key))
.and_then(|v| v.as_bool())
.unwrap_or(true)
}
fn tools_web_subflag(hc: &HarnessConfig, key: &str) -> bool {
hc.capabilities
.get("tools_web")
.and_then(|c| c.settings.get(key))
.and_then(|v| v.as_bool())
.unwrap_or(true)
}
#[derive(Debug, Clone, Default)]
pub struct ModuleActivation {
active: BTreeSet<ModuleId>,
pub tools_search_glob: bool,
pub tools_search_content_search: bool,
pub tools_search_list_dir: bool,
pub tools_web_fetch: bool,
pub tools_web_search: bool,
}
impl ModuleActivation {
pub fn from_harness(hc: &HarnessConfig) -> Self {
let mut active = BTreeSet::new();
for &m in ModuleId::ALL {
if m.is_active(hc) {
active.insert(m);
}
}
ModuleActivation {
active,
tools_search_glob: tools_search_subflag(hc, "glob"),
tools_search_content_search: tools_search_subflag(hc, "content_search"),
tools_search_list_dir: tools_search_subflag(hc, "list_dir"),
tools_web_fetch: tools_web_subflag(hc, "fetch"),
tools_web_search: tools_web_subflag(hc, "search"),
}
}
pub fn is_active(&self, id: ModuleId) -> bool {
self.active.contains(&id)
}
pub fn iter(&self) -> impl Iterator<Item = &ModuleId> {
self.active.iter()
}
pub fn len(&self) -> usize {
self.active.len()
}
pub fn is_empty(&self) -> bool {
self.active.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::configfile::HarnessConfig;
#[test]
fn all_has_exactly_35_modules_matching_the_design_table() {
assert_eq!(ModuleId::ALL.len(), 35);
let set: BTreeSet<ModuleId> = ModuleId::ALL.iter().copied().collect();
assert_eq!(set.len(), 35);
}
#[test]
fn default_harness_config_activates_nothing() {
let hc = HarnessConfig::default();
let act = ModuleActivation::from_harness(&hc);
assert!(act.is_empty(), "expected no modules active by default");
for &m in ModuleId::ALL {
assert!(!act.is_active(m), "{m} unexpectedly active by default");
}
}
#[test]
fn mcp_server_reads_the_serve_bool_not_a_nested_table() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.mcp]
enabled = true
serve = true
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.is_active(ModuleId::McpClient));
assert!(act.is_active(ModuleId::McpServer));
}
#[test]
fn permissions_approvals_reads_the_parent_tables_own_enabled() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.permissions]
enabled = true
approval = "untrusted"
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.is_active(ModuleId::PermissionsApprovals));
assert!(!act.is_active(ModuleId::PermissionsRules));
assert!(!act.is_active(ModuleId::PermissionsSandbox));
}
#[test]
fn tools_search_subflags_default_true_when_module_active() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.tools_search]
enabled = true
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.is_active(ModuleId::ToolsSearch));
assert!(act.tools_search_glob);
assert!(act.tools_search_content_search);
assert!(act.tools_search_list_dir);
}
#[test]
fn tools_search_subflags_honor_explicit_false() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.tools_search]
enabled = true
list_dir = false
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.tools_search_glob);
assert!(!act.tools_search_list_dir);
}
#[test]
fn tools_web_subflags_default_true_when_module_active() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.tools_web]
enabled = true
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.is_active(ModuleId::ToolsWeb));
assert!(act.tools_web_fetch);
assert!(act.tools_web_search);
}
#[test]
fn tools_web_subflags_honor_explicit_false() {
let hc = HarnessConfig::from_toml_str(
r#"
[capabilities.tools_web]
enabled = true
search = false
"#,
)
.expect("parses");
let act = ModuleActivation::from_harness(&hc);
assert!(act.tools_web_fetch);
assert!(!act.tools_web_search);
}
#[test]
fn tools_web_inactive_by_default() {
let hc = HarnessConfig::default();
let act = ModuleActivation::from_harness(&hc);
assert!(!act.is_active(ModuleId::ToolsWeb));
}
}