supercode-harness 0.5.45

The optional native Volter Harness agent and tool harness
Documentation
//! §2 "The Bolt-on Capability Taxonomy" (`docs/composable-harness/
//! COMPOSABLE-HARNESS-DESIGN.md`) — P3 of the composable-harness migration
//! (design §5.2, phase **P3**: "A `ModuleId` enum (the 35 names) + resolved
//! activation set on `Config`").
//!
//! [`ModuleId`] names all 35 §2 modules exactly as the table numbers them
//! (1-35). [`ModuleActivation`] is the resolved activation set — "module
//! activation is *pure config → set*, testable without the loop" (§5.3 risk
//! 2's mitigation): [`ModuleActivation::from_harness`] computes it from a
//! [`crate::configfile::HarnessConfig`] alone, no [`crate::Agent`] required.
//!
//! **Schema-collapse note.** Two of the 35 §2 rows have no *independent*
//! `[capabilities.<name>]` table of their own in the §3.1 schema — they are
//! represented as a field of a SIBLING module's table instead:
//! - module 10 `permissions.approvals` is `[capabilities.permissions]`'s own
//!   `enabled` flag (the `approval = "…"` mode lives in the same table as
//!   modules 11-13's parent, not a nested `approvals` sub-table) —
//!   [`ModuleId::PermissionsApprovals`] reads `capabilities.permissions.enabled`.
//! - module 16 `mcp.server` is `[capabilities.mcp].serve` (a bool field, not
//!   a nested table with its own `enabled`) — [`ModuleId::McpServer`] reads
//!   that field directly.
//!
//! Every other module maps onto exactly the [`crate::configfile::MODULE_NAMES`]
//! / [`crate::configfile::NESTED_MODULE_NAMES`] key P2 already resolves.

use std::collections::BTreeSet;

use crate::configfile::{module_enabled, module_setting_bool, HarnessConfig};

/// The 35 §2 capability modules, numbered exactly as the design's module
/// table (§2, rows 1-35).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ModuleId {
    /// 1. `tools.search` — glob/content-search/dir-listing tools.
    ToolsSearch,
    /// 2. `tools.apply_patch` — the apply_patch envelope tool.
    ToolsApplyPatch,
    /// 3. `tools.persistent_shell` — persistent/interactive shell.
    ToolsPersistentShell,
    /// 4. `tools.background` — background exec + monitor/event feed.
    ToolsBackground,
    /// 5. `tools.web` — web fetch + web search.
    ToolsWeb,
    /// 6. `tools.question` — structured user-question tool.
    ToolsQuestion,
    /// 7. `todos` — plan/task-checklist tool (`update_plan`).
    Todos,
    /// 8. `plan_mode` — plan enter/exit restriction mode.
    PlanMode,
    /// 9. `subagents` — spawn/named/background sub-agents.
    Subagents,
    /// 10. `permissions.approvals` — approval modes, interactive ask, caching.
    PermissionsApprovals,
    /// 11. `permissions.rules` — allow/ask/deny rule language.
    PermissionsRules,
    /// 12. `permissions.sandbox` — OS-level fs/network sandbox.
    PermissionsSandbox,
    /// 13. `permissions.protected_paths` — never-auto-approved paths.
    PermissionsProtectedPaths,
    /// 14. `trust` — project/workspace trust gate.
    Trust,
    /// 15. `mcp.client` — MCP stdio/remote/OAuth/resources/prompts.
    McpClient,
    /// 16. `mcp.server` — harness-as-MCP-server.
    McpServer,
    /// 17. `hooks` — config-registered lifecycle hooks.
    Hooks,
    /// 18. `plugins` — in-process extension API.
    Plugins,
    /// 19. `memory` — auto memory (agent-maintained, cross-session).
    Memory,
    /// 20. `checkpoint` — file checkpointing / shadow-git.
    Checkpoint,
    /// 21. `session.tree` — in-place tree, rewind, branch summaries.
    SessionTree,
    /// 22. `session.share` — public session-sharing links.
    SessionShare,
    /// 23. `reduction` — genuinely-optional lossless reduction policies.
    Reduction,
    /// 24. `deferred_tools` — deferred tool advertising + tool_search.
    DeferredTools,
    /// 25. `cache` — cache-aware architecture (plans, warnings, TTL).
    Cache,
    /// 26. `model.catalog` — aliases, fallback chains, small/utility model.
    ModelCatalog,
    /// 27. `model.oauth` — subscription OAuth login.
    ModelOauth,
    /// 28. `lsp` — LSP diagnostics in the edit path + query tool.
    Lsp,
    /// 29. `formatters` — format-on-write.
    Formatters,
    /// 30. `tui` — full-screen TUI.
    Tui,
    /// 31. `server` — full programmatic RPC/HTTP server.
    Server,
    /// 32. `notify` — external notify program / desktop / email.
    Notify,
    /// 33. `structured_output` — structured final output.
    StructuredOutput,
    /// 34. `telemetry` — OTel/analytics exporters.
    Telemetry,
    /// 35. `integrations` — cloud, IDE/ACP, CI bots, web UI, worktrees.
    Integrations,
}

impl ModuleId {
    /// Every module, in §2 table order (1-35).
    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,
    ];

    /// The §3.1 config key this module reads (the same strings
    /// [`crate::configfile::MODULE_NAMES`]/[`crate::configfile::NESTED_MODULE_NAMES`]
    /// use), for diagnostics. The two schema-collapsed modules (see the
    /// module doc comment) report their HOST table's name, since they have
    /// no independent table of their own.
    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",
        }
    }

    /// Whether this module is active in a resolved `HarnessConfig` — the
    /// same [`module_enabled`]/`module_setting_bool` logic the P2 resolver
    /// already computes (§3.5 step 7's "module-activation set"), reused
    /// verbatim rather than re-derived, so [`ModuleActivation`] can never
    /// diverge from [`crate::configfile::Resolved::modules`].
    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())
    }
}

/// `[capabilities.tools_search]`'s three per-tool sub-flags (§3.1 module 1:
/// `{ enabled, glob, content_search, list_dir }`), each defaulting to `true`
/// per the schema when the module itself is active and the flag is unset.
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)
}

/// P4c: `[capabilities.tools_web]`'s two per-tool sub-flags (§3.1 module 5:
/// `{ enabled, fetch, search }`), each defaulting to `true` per the schema
/// when the module itself is active and the flag is unset — same shape as
/// [`tools_search_subflag`].
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)
}

/// The resolved activation set for all 35 modules (§5.2 P3: "a resolved
/// activation set on `Config`") — pure `HarnessConfig` → set, no loop
/// required (§5.3 risk 2's testability mitigation). Also carries
/// `tools_search`'s three per-tool sub-flags, since [`crate::tools::ToolRegistry::from_config`]
/// needs them to decide which of `glob`/`search`/`list_dir` to register —
/// and (P4c) `tools_web`'s two, for `web_fetch`/`web_search`.
#[derive(Debug, Clone, Default)]
pub struct ModuleActivation {
    active: BTreeSet<ModuleId>,
    /// `[capabilities.tools_search].glob` (default `true`).
    pub tools_search_glob: bool,
    /// `[capabilities.tools_search].content_search` (default `true`).
    pub tools_search_content_search: bool,
    /// `[capabilities.tools_search].list_dir` (default `true`).
    pub tools_search_list_dir: bool,
    /// P4c: `[capabilities.tools_web].fetch` (default `true`).
    pub tools_web_fetch: bool,
    /// P4c: `[capabilities.tools_web].search` (default `true`).
    pub tools_web_search: bool,
    /// BP-13: `[capabilities.tools_apply_patch].per_model` (default
    /// `false`) — arms the per-model capability bits that decide which
    /// write surface a model is offered (see
    /// [`crate::tools::ToolRegistry::from_config`]). Its other reader is
    /// the resolver's §2.2 C1 check, which is why this flag existed at all
    /// before the bits did.
    pub tools_apply_patch_per_model: bool,
}

impl ModuleActivation {
    /// Compute the activation set from a resolved `HarnessConfig` (the
    /// output of [`crate::configfile::resolve`]'s folding, before `Config`
    /// materialization).
    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"),
            tools_apply_patch_per_model: hc
                .capabilities
                .get("tools_apply_patch")
                .and_then(|c| c.settings.get("per_model"))
                .and_then(|v| v.as_bool())
                .unwrap_or(false),
        }
    }

    /// Whether `id` is active.
    pub fn is_active(&self, id: ModuleId) -> bool {
        self.active.contains(&id)
    }

    /// Every active module, in [`ModuleId::ALL`] order.
    pub fn iter(&self) -> impl Iterator<Item = &ModuleId> {
        self.active.iter()
    }

    /// Count of active modules.
    pub fn len(&self) -> usize {
        self.active.len()
    }

    /// Whether no module is active at all.
    pub fn is_empty(&self) -> bool {
        self.active.is_empty()
    }
}