| """CYPHER V12 M11 — TOOLS_REGISTRY (central tool registry, NEXUS pattern). |
| |
| Aggregates all CYPHER tools from M2-M10 modules into a single dict |
| TOOLS_REGISTRY. Lazy imports (try/except) so missing optional deps don't |
| break the bridge. |
| |
| Public API: |
| - TOOLS_REGISTRY: dict[str, callable] |
| - call_tool(name, **kwargs): execute a tool by name |
| - list_available_tools(): introspection |
| - get_tool_signature(name): runtime signature peek |
| """ |
| from __future__ import annotations |
|
|
| import inspect |
| import logging |
| import os |
| import sys |
| from pathlib import Path |
| from typing import Any, Callable |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
|
|
|
|
| def _env(key: str, default: str | None = None) -> str | None: |
| return os.environ.get(key, default) |
|
|
|
|
| def _key_available(env_key: str) -> bool: |
| return bool(os.environ.get(env_key)) |
|
|
|
|
| |
|
|
|
|
| _MODULE_DIR = Path(__file__).parent |
| if str(_MODULE_DIR) not in sys.path: |
| sys.path.insert(0, str(_MODULE_DIR)) |
|
|
|
|
| def _lazy_import(module_name: str) -> Any: |
| try: |
| return __import__(module_name) |
| except ImportError as e: |
| logger.warning(f"_lazy_import {module_name} failed: {e}") |
| return None |
|
|
|
|
| |
| _singletons: dict[str, Any] = {} |
|
|
|
|
| def _get_singleton(key: str, factory: Callable[[], Any]) -> Any: |
| if key not in _singletons: |
| try: |
| _singletons[key] = factory() |
| except Exception as e: |
| logger.error(f"singleton {key} init failed: {type(e).__name__}: {e}") |
| _singletons[key] = None |
| return _singletons[key] |
|
|
|
|
| |
|
|
|
|
| |
| def _t_family_pull_recent(n: int = 20, since_ts: int | None = None) -> list[dict]: |
| fb = _lazy_import("familybus_client") |
| if fb is None: |
| return [{"error": "familybus_client unavailable"}] |
| client = _get_singleton("familybus", lambda: fb.FamilyBusClient()) |
| if client is None: |
| return [{"error": "FamilyBusClient init failed"}] |
| return client.pull_recent(n=n, since_ts=since_ts) |
|
|
|
|
| def _t_family_broadcast(lesson: str, category: str = "CYBERSEC", |
| trade_summary: dict | None = None) -> bool: |
| fb = _lazy_import("familybus_client") |
| if fb is None: |
| return False |
| client = _get_singleton("familybus", lambda: fb.FamilyBusClient()) |
| if client is None: |
| return False |
| return client.broadcast(lesson=lesson, asi="CYPHER", |
| category=category, trade_summary=trade_summary) |
|
|
|
|
| def _t_family_stats() -> dict: |
| fb = _lazy_import("familybus_client") |
| if fb is None: |
| return {"error": "familybus_client unavailable"} |
| client = _get_singleton("familybus", lambda: fb.FamilyBusClient()) |
| if client is None: |
| return {"error": "init failed"} |
| return client.get_stats() |
|
|
|
|
| |
| def _t_cisa_kev_refresh(force: bool = False) -> dict: |
| cve_mod = _lazy_import("cve_refresh_pipeline") |
| if cve_mod is None: |
| return {"error": "cve_refresh_pipeline unavailable"} |
| kev = _get_singleton("kev", lambda: cve_mod.CISAKevRefresher()) |
| if kev is None: |
| return {"error": "init failed"} |
| return {"count": kev.refresh(force=force)} |
|
|
|
|
| def _t_cisa_kev_lookup(cve_id: str) -> dict | None: |
| cve_mod = _lazy_import("cve_refresh_pipeline") |
| if cve_mod is None: |
| return None |
| kev = _get_singleton("kev", lambda: cve_mod.CISAKevRefresher()) |
| if kev is None: |
| return None |
| return kev.lookup(cve_id) |
|
|
|
|
| def _t_nvd_cve_lookup(cve_id: str) -> dict | None: |
| cve_mod = _lazy_import("cve_refresh_pipeline") |
| if cve_mod is None: |
| return None |
| nvd = _get_singleton("nvd", lambda: cve_mod.NVDLookup()) |
| if nvd is None: |
| return None |
| return nvd.lookup_cve(cve_id) |
|
|
|
|
| def _t_get_cve_context(cve_id: str) -> str: |
| cve_mod = _lazy_import("cve_refresh_pipeline") |
| if cve_mod is None: |
| return f"[CONTEXT: CVE={cve_id} (cve module unavailable)]" |
| kev = _get_singleton("kev", lambda: cve_mod.CISAKevRefresher()) |
| nvd = _get_singleton("nvd", lambda: cve_mod.NVDLookup()) |
| return cve_mod.get_cve_context(cve_id, kev=kev, nvd=nvd) |
|
|
|
|
| |
| def _t_store_memory(content: str, category: str, |
| metadata: dict | None = None) -> str | None: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return None |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return None |
| try: |
| return mem.store_memory(content, category, metadata) |
| except (ValueError, Exception) as e: |
| logger.error(f"store_memory: {type(e).__name__}: {e}") |
| return None |
|
|
|
|
| def _t_recall_memory(query: str, k: int = 3, |
| category: str | None = None) -> list[dict]: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return [] |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return [] |
| return mem.recall_memory(query, k=k, category=category) |
|
|
|
|
| def _t_list_memories(category: str | None = None, limit: int = 50) -> list[dict]: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return [] |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return [] |
| return mem.list_memories(category=category, limit=limit) |
|
|
|
|
| def _t_delete_memory(memory_id: str) -> bool: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return False |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return False |
| return mem.delete_memory(memory_id) |
|
|
|
|
| def _t_memory_stats() -> dict: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return {"error": "memory module unavailable"} |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return {"error": "init failed"} |
| return mem.memory_stats() |
|
|
|
|
| def _t_clear_category(category: str) -> int: |
| mem_mod = _lazy_import("cypher_memory") |
| if mem_mod is None: |
| return 0 |
| mem = _get_singleton("memory", lambda: mem_mod.CypherMemory()) |
| if mem is None: |
| return 0 |
| return mem.clear_category(category) |
|
|
|
|
| |
| def _t_yara_validate(rule_str: str) -> dict: |
| rv = _lazy_import("cypher_rule_validator") |
| if rv is None: |
| return {"valid": None, "error": "rule_validator unavailable", "rule_name": None} |
| return rv.YaraValidator().validate(rule_str) |
|
|
|
|
| def _t_sigma_validate(yaml_str: str) -> dict: |
| rv = _lazy_import("cypher_rule_validator") |
| if rv is None: |
| return {"valid": None, "error": "rule_validator unavailable", "title": None} |
| return rv.SigmaValidator().validate(yaml_str) |
|
|
|
|
| def _t_suricata_validate(rule_str: str) -> dict: |
| rv = _lazy_import("cypher_rule_validator") |
| if rv is None: |
| return {"valid": None, "error": "rule_validator unavailable", "sid": None} |
| return rv.SuricataValidator().validate(rule_str) |
|
|
|
|
| def _t_extract_rules_from_text(text: str) -> dict: |
| rv = _lazy_import("cypher_rule_validator") |
| if rv is None: |
| return {"yara": [], "sigma": [], "suricata": []} |
| return rv.extract_rules_from_text(text) |
|
|
|
|
| |
| def _t_conv_add_turn(user_msg: str, cypher_response: str, |
| category: str | None = None) -> bool: |
| cb = _lazy_import("conversation_context_bank") |
| if cb is None: |
| return False |
| bank = _get_singleton("conv", lambda: cb.ConversationContextBank()) |
| if bank is None: |
| return False |
| bank.add_turn(user_msg, cypher_response, category=category) |
| return True |
|
|
|
|
| def _t_conv_get_context(format: str = "compact") -> str: |
| cb = _lazy_import("conversation_context_bank") |
| if cb is None: |
| return "" |
| bank = _get_singleton("conv", lambda: cb.ConversationContextBank()) |
| if bank is None: |
| return "" |
| return bank.get_context(format=format) |
|
|
|
|
| def _t_conv_clear() -> bool: |
| cb = _lazy_import("conversation_context_bank") |
| if cb is None: |
| return False |
| bank = _get_singleton("conv", lambda: cb.ConversationContextBank()) |
| if bank is None: |
| return False |
| bank.clear() |
| return True |
|
|
|
|
| |
| def _t_describe_image(image_src: str, prompt: str = "Describe briefly.") -> str: |
| mm = _lazy_import("multimodal_stub") |
| if mm is None: |
| return "[multimodal_stub unavailable]" |
| backend = "anthropic" if _key_available("ANTHROPIC_API_KEY") else "mock" |
| stub = _get_singleton(f"mm_{backend}", lambda: mm.MultimodalStub(backend=backend)) |
| if stub is None: |
| return "[init failed]" |
| return stub.describe_image(image_src, prompt=prompt) |
|
|
|
|
| def _t_is_image_request(prompt: str) -> bool: |
| mm = _lazy_import("multimodal_stub") |
| if mm is None: |
| return False |
| return mm.is_image_request(prompt) |
|
|
|
|
| |
|
|
|
|
| TOOLS_REGISTRY: dict[str, dict] = { |
| |
| "family_pull_recent": { |
| "fn": _t_family_pull_recent, |
| "module": "M2", |
| "desc": "Pull recent broadcasts from aether-family-bus HF dataset", |
| }, |
| "family_broadcast": { |
| "fn": _t_family_broadcast, |
| "module": "M2", |
| "desc": "Broadcast a CYPHER lesson to the family (async)", |
| }, |
| "family_stats": { |
| "fn": _t_family_stats, |
| "module": "M2", |
| "desc": "Get FamilyBus cache stats (producers, latest)", |
| }, |
| |
| "cisa_kev_refresh": { |
| "fn": _t_cisa_kev_refresh, |
| "module": "M3", |
| "desc": "Refresh CISA KEV catalog from cisa.gov", |
| }, |
| "cisa_kev_lookup": { |
| "fn": _t_cisa_kev_lookup, |
| "module": "M3", |
| "desc": "Lookup a CVE in CISA KEV", |
| }, |
| "nvd_cve_lookup": { |
| "fn": _t_nvd_cve_lookup, |
| "module": "M3", |
| "desc": "Lookup a CVE in NVD API v2 (rate limited)", |
| }, |
| "get_cve_context": { |
| "fn": _t_get_cve_context, |
| "module": "M3", |
| "desc": "Build [CONTEXT: ...] string for a CVE (KEV + NVD merged)", |
| }, |
| |
| "store_memory": { |
| "fn": _t_store_memory, |
| "module": "M4", |
| "desc": "Persist a memory in chromadb (categories: cve_analysis, trade_postmortem, ioc_confirmed, cybersec_insight, conversation, ecosystem_fact)", |
| }, |
| "recall_memory": { |
| "fn": _t_recall_memory, |
| "module": "M4", |
| "desc": "Recall top-k memories by similarity (filter by category)", |
| }, |
| "list_memories": { |
| "fn": _t_list_memories, |
| "module": "M4", |
| "desc": "List stored memories (filter by category)", |
| }, |
| "delete_memory": { |
| "fn": _t_delete_memory, |
| "module": "M4", |
| "desc": "Delete a memory by ID", |
| }, |
| "memory_stats": { |
| "fn": _t_memory_stats, |
| "module": "M4", |
| "desc": "Memory storage stats per category", |
| }, |
| "clear_category": { |
| "fn": _t_clear_category, |
| "module": "M4", |
| "desc": "Clear all memories of a category", |
| }, |
| |
| "yara_validate": { |
| "fn": _t_yara_validate, |
| "module": "M5", |
| "desc": "Validate YARA rule via yara-python compile", |
| }, |
| "sigma_validate": { |
| "fn": _t_sigma_validate, |
| "module": "M5", |
| "desc": "Validate Sigma rule YAML structure", |
| }, |
| "suricata_validate": { |
| "fn": _t_suricata_validate, |
| "module": "M5", |
| "desc": "Validate Suricata/Snort rule syntax", |
| }, |
| "extract_rules_from_text": { |
| "fn": _t_extract_rules_from_text, |
| "module": "M5", |
| "desc": "Detect YARA/Sigma/Suricata blocks in arbitrary text", |
| }, |
| |
| "conv_add_turn": { |
| "fn": _t_conv_add_turn, |
| "module": "M6", |
| "desc": "Append a turn to the 3-window conversation context bank", |
| }, |
| "conv_get_context": { |
| "fn": _t_conv_get_context, |
| "module": "M6", |
| "desc": "Get formatted conversation context for encoder injection", |
| }, |
| "conv_clear": { |
| "fn": _t_conv_clear, |
| "module": "M6", |
| "desc": "Clear conversation context window", |
| }, |
| |
| "describe_image": { |
| "fn": _t_describe_image, |
| "module": "M10", |
| "desc": "Describe an image via Anthropic/OpenAI vision API", |
| }, |
| "is_image_request": { |
| "fn": _t_is_image_request, |
| "module": "M10", |
| "desc": "Heuristic: does prompt require image analysis?", |
| }, |
| } |
|
|
|
|
| def call_tool(name: str, **kwargs) -> Any: |
| """Invoke a registered tool by name.""" |
| if name not in TOOLS_REGISTRY: |
| raise KeyError(f"Tool not registered: {name!r}. Available: {sorted(TOOLS_REGISTRY)}") |
| fn = TOOLS_REGISTRY[name]["fn"] |
| try: |
| return fn(**kwargs) |
| except TypeError as e: |
| |
| sig = inspect.signature(fn) |
| raise TypeError( |
| f"call_tool({name!r}, **kwargs) signature mismatch. " |
| f"Expected: {sig}. Got kwargs={list(kwargs)}. Error: {e}" |
| ) from e |
|
|
|
|
| def list_available_tools() -> list[dict]: |
| """Return list of tool descriptors.""" |
| out: list[dict] = [] |
| for name, info in TOOLS_REGISTRY.items(): |
| try: |
| sig = str(inspect.signature(info["fn"])) |
| except (TypeError, ValueError): |
| sig = "(?)" |
| out.append({ |
| "name": name, |
| "module": info["module"], |
| "desc": info["desc"], |
| "signature": sig, |
| }) |
| return sorted(out, key=lambda x: (x["module"], x["name"])) |
|
|
|
|
| def get_tool_signature(name: str) -> str: |
| if name not in TOOLS_REGISTRY: |
| return "" |
| try: |
| return str(inspect.signature(TOOLS_REGISTRY[name]["fn"])) |
| except (TypeError, ValueError): |
| return "(?)" |
|
|
|
|
| __all__ = [ |
| "TOOLS_REGISTRY", |
| "call_tool", |
| "list_available_tools", |
| "get_tool_signature", |
| ] |
|
|
|
|
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") |
| print("=== M11 cypher_tools SMOKE TEST ===") |
|
|
| tools = list_available_tools() |
| print(f"Total tools registered: {len(tools)}") |
| by_mod: dict[str, list[str]] = {} |
| for t in tools: |
| by_mod.setdefault(t["module"], []).append(t["name"]) |
| for mod in sorted(by_mod): |
| print(f" {mod} ({len(by_mod[mod])} tools): {by_mod[mod]}") |
|
|
| |
| rules = call_tool("extract_rules_from_text", |
| text="rule Test { strings: $a = \"x\" condition: $a }") |
| print(f"\nextract_rules_from_text: yara={len(rules['yara'])} sigma={len(rules['sigma'])} suricata={len(rules['suricata'])}") |
|
|
| |
| stats = call_tool("family_stats") |
| print(f"family_stats: {stats}") |
|
|
| |
| try: |
| call_tool("yara_validate", wrong_kwarg="oops") |
| except TypeError as e: |
| print(f"Type error caught as expected: {str(e)[:200]}") |
|
|
| |
| try: |
| call_tool("nonexistent_tool") |
| except KeyError as e: |
| print(f"KeyError caught: {str(e)[:120]}") |
|
|
| |
| sig = get_tool_signature("yara_validate") |
| print(f"yara_validate signature: {sig}") |
|
|
| print("=== SMOKE PASS ===") |
|
|