bctx-nexus 0.1.22

bctx-nexus — MCP/Nexus gateway with permission enforcement and tool registry
Documentation
use serde_json::Value;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct ToolManifest {
    pub name: String,
    pub description: String,
    pub input_schema: Value,
    pub scope: ToolScope,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolScope {
    ReadOnly,
    FileSystem,
    Shell,
    Network,
    Memory,
    Cloud,
}

pub struct ToolRegistry {
    tools: HashMap<String, ToolManifest>,
}

impl ToolRegistry {
    pub fn new() -> Self {
        Self {
            tools: HashMap::new(),
        }
    }

    pub fn register(&mut self, manifest: ToolManifest) {
        self.tools.insert(manifest.name.clone(), manifest);
    }

    pub fn get(&self, name: &str) -> Option<&ToolManifest> {
        self.tools.get(name)
    }

    pub fn all(&self) -> Vec<&ToolManifest> {
        self.tools.values().collect()
    }

    pub fn default_registry() -> Self {
        let mut r = Self::new();
        let atlas = atlas::SkillRegistry::default_registry();
        let fallback_schema = serde_json::json!({
            "type": "object",
            "properties": { "input": { "type": "string" } }
        });
        for (name, desc, scope) in BUILT_IN_TOOLS {
            let input_schema = atlas
                .get(name)
                .map(|s| s.input_schema())
                .unwrap_or_else(|| fallback_schema.clone());
            r.register(ToolManifest {
                name: name.to_string(),
                description: desc.to_string(),
                input_schema,
                scope: *scope,
            });
        }
        r
    }
}

impl Default for ToolRegistry {
    fn default() -> Self {
        Self::default_registry()
    }
}

const BUILT_IN_TOOLS: &[(&str, &str, ToolScope)] = &[
    (
        "sieve",
        "Filter raw output to task-relevant lines",
        ToolScope::ReadOnly,
    ),
    (
        "cartograph",
        "Scan directory → structured project map",
        ToolScope::FileSystem,
    ),
    (
        "chisel",
        "Extract AST symbols from files",
        ToolScope::FileSystem,
    ),
    ("sediment", "Persist facts into Vault", ToolScope::Memory),
    (
        "prism",
        "Full AST parse + index of project directory",
        ToolScope::FileSystem,
    ),
    (
        "compass",
        "Fuse BM25 + graph + Vault into ranked results",
        ToolScope::FileSystem,
    ),
    (
        "condenser",
        "Compress content via Lens stack",
        ToolScope::ReadOnly,
    ),
    (
        "archivist",
        "Query Vault for facts (read-only)",
        ToolScope::Memory,
    ),
    (
        "scout",
        "Execute shell command with output compression",
        ToolScope::Shell,
    ),
    (
        "chronicler",
        "Generate session narrative (read-only)",
        ToolScope::ReadOnly,
    ),
    (
        "alchemist",
        "Transform noisy content → structured JSON",
        ToolScope::ReadOnly,
    ),
    (
        "sentinel",
        "Static security risk assessment",
        ToolScope::ReadOnly,
    ),
    (
        "cartridge",
        "Package current context into portable bundle",
        ToolScope::ReadOnly,
    ),
    (
        "resonator",
        "Dense-vector semantic search over ProjectIndex",
        ToolScope::FileSystem,
    ),
    (
        "surveyor",
        "Compute dependency topology and impact analysis",
        ToolScope::FileSystem,
    ),
    // Wave 2 — 21 new skills
    (
        "parallax",
        "Read multiple files in one call with per-file lens",
        ToolScope::FileSystem,
    ),
    (
        "blueprint",
        "Emit compact structural outline of a file",
        ToolScope::FileSystem,
    ),
    (
        "pinpoint",
        "Extract a single named symbol from a file by name",
        ToolScope::FileSystem,
    ),
    (
        "crossroads",
        "Extract API route definitions from files",
        ToolScope::FileSystem,
    ),
    (
        "drift",
        "Return lines changed since a git ref, token-budgeted",
        ToolScope::Shell,
    ),
    (
        "panorama",
        "High-level project overview: languages, entry points, key dirs",
        ToolScope::FileSystem,
    ),
    (
        "ripple",
        "Impact analysis: files that depend on a given file",
        ToolScope::FileSystem,
    ),
    (
        "meridian",
        "Snapshot current agent context: executions, vault facts",
        ToolScope::ReadOnly,
    ),
    (
        "forecast",
        "Predict which files the agent will need next",
        ToolScope::FileSystem,
    ),
    (
        "unfold",
        "Restore a condensed block to its original content",
        ToolScope::FileSystem,
    ),
    (
        "thermal",
        "Token heatmap: identify which file sections consume the most tokens",
        ToolScope::ReadOnly,
    ),
    (
        "echo",
        "Record compression feedback (good/bad) for adaptive tuning",
        ToolScope::Memory,
    ),
    (
        "ledger",
        "Real-time cost estimate: tokens × model pricing → USD",
        ToolScope::ReadOnly,
    ),
    (
        "steward",
        "Session role management: coder/reviewer/debugger/ops/admin + budget",
        ToolScope::Memory,
    ),
    (
        "dispatch",
        "Meta-tool: invoke any bctx skill by name via one stable schema",
        ToolScope::ReadOnly,
    ),
    (
        "arbiter",
        "Code review: structured findings from diff or file content",
        ToolScope::ReadOnly,
    ),
    (
        "diviner",
        "Infer agent intent from file access patterns and commands",
        ToolScope::ReadOnly,
    ),
    (
        "crucible",
        "Run compression benchmarks across all lenses, return savings table",
        ToolScope::ReadOnly,
    ),
    (
        "scanner",
        "Find files/symbols matching a query (fuzzy + content)",
        ToolScope::FileSystem,
    ),
    (
        "relay_ctx",
        "Package and hand off agent context to another session",
        ToolScope::Memory,
    ),
    (
        "witness",
        "Record and replay tool-call sequences for testing",
        ToolScope::Memory,
    ),
    // Wave 3 — 5 new skills
    (
        "scribe",
        "Context-aware file editor — create, replace, append, or delete content via MCP",
        ToolScope::FileSystem,
    ),
    (
        "flux",
        "Surface what changed in a file since last commit (HEAD baseline), structured diff",
        ToolScope::Shell,
    ),
    (
        "pathfinder",
        "Extract HTTP routes from web framework source files (10+ frameworks)",
        ToolScope::FileSystem,
    ),
    (
        "harvest",
        "Batch read up to 50 files in one MCP call, optional per-file compression",
        ToolScope::FileSystem,
    ),
    (
        "render",
        "Generate a Mermaid or DOT diagram from the project's import dependency graph",
        ToolScope::FileSystem,
    ),
];

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_registry_has_41_tools() {
        let reg = ToolRegistry::default_registry();
        assert_eq!(
            reg.all().len(),
            41,
            "expected 41 tools, got {}: {:?}",
            reg.all().len(),
            reg.all().iter().map(|m| &m.name).collect::<Vec<_>>()
        );
    }

    #[test]
    fn wave3_skills_are_registered() {
        let reg = ToolRegistry::default_registry();
        for name in &["scribe", "flux", "pathfinder", "harvest", "render"] {
            assert!(
                reg.get(name).is_some(),
                "Wave 3 skill '{name}' missing from ToolRegistry"
            );
        }
    }

    #[test]
    fn no_tool_uses_fallback_input_schema() {
        let reg = ToolRegistry::default_registry();
        let fallback = serde_json::json!({
            "type": "object",
            "properties": { "input": { "type": "string" } }
        });
        for manifest in reg.all() {
            assert_ne!(
                manifest.input_schema, fallback,
                "tool '{}' is using the generic fallback schema — add it to Atlas skill_schema()",
                manifest.name
            );
        }
    }
}