Initial commit

This commit is contained in:
AuroraCrimsonRose
2026-05-27 15:07:22 -05:00
commit cc64e8d41e
31 changed files with 2354 additions and 0 deletions

69
core/config.py Normal file
View File

@@ -0,0 +1,69 @@
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
# =========================
# CORE PATHS
# =========================
WORKSPACE_ROOT = Path(os.getenv("WORKSPACE_ROOT", Path.cwd())).resolve()
LOG_DIR = WORKSPACE_ROOT / "logs"
TMP_DIR = WORKSPACE_ROOT / "tmp"
CONFIG_DIR = WORKSPACE_ROOT / "config"
LOG_DIR.mkdir(parents=True, exist_ok=True)
TMP_DIR.mkdir(parents=True, exist_ok=True)
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
# =========================
# SYSTEM LIMITS
# =========================
MAX_FILE_SIZE = int(os.getenv("MAX_FILE_SIZE", str(10 * 1024 * 1024)))
IGNORE_DIRS = set(
os.getenv(
"IGNORE_DIRS",
".git,__pycache__,node_modules,.venv,venv,dist,build"
).split(",")
)
# =========================
# EVENT / DEBUG FLAGS
# =========================
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
DRY_RUN_DEFAULT = os.getenv("DRY_RUN", "false").lower() == "true"
# =========================
# GITEA CONFIG
# =========================
GITEA_URL = os.getenv("GITEA_URL", "http://localhost:3000")
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
GITEA_USER = os.getenv("GITEA_USER", "admin")
GITEA_API_BASE = f"{GITEA_URL}/api/v1"
# =========================
# GIT CONFIG
# =========================
GIT_AUTHOR_NAME = os.getenv("GIT_AUTHOR_NAME", "CXOS")
GIT_AUTHOR_EMAIL = os.getenv("GIT_AUTHOR_EMAIL", "cxos@local")
# =========================
# MCP / SERVER CONFIG
# =========================
SERVER_NAME = os.getenv("SERVER_NAME", "CXOS-MCP")
API_HOST = os.getenv("API_HOST", "0.0.0.0")
API_PORT = int(os.getenv("API_PORT", "5432"))

108
core/event_store.py Normal file
View File

@@ -0,0 +1,108 @@
from collections import deque
from typing import Optional, Any, Callable, Iterator
from core.events import Event, bus
class EventStore:
def __init__(self, max_size: int = 10_000):
self.buffer: deque[Event] = deque(maxlen=max_size)
self._cursor: int = 0
bus.subscribe(self._capture)
# =========================
# CAPTURE
# =========================
def _capture(self, event: Event):
event.meta["cursor"] = self._cursor
self.buffer.append(event)
self._cursor += 1
# =========================
# QUERY
# =========================
def query(
self,
source: Optional[str] = None,
level: Optional[str] = None,
name: Optional[str] = None,
start_cursor: Optional[int] = None,
end_cursor: Optional[int] = None,
) -> list[Event]:
events = list(self.buffer)
if source is not None:
events = [e for e in events if e.source == source]
if level is not None:
events = [e for e in events if e.level == level]
if name is not None:
events = [e for e in events if e.name == name]
def cursor_of(e: Event) -> int:
return e.meta.get("cursor", -1)
if start_cursor is not None:
events = [e for e in events if cursor_of(e) >= start_cursor]
if end_cursor is not None:
events = [e for e in events if cursor_of(e) <= end_cursor]
return events
# =========================
# PAGINATION
# =========================
def page(self, cursor: int = 0, limit: int = 100) -> dict[str, Any]:
events = list(self.buffer)
sliced = events[cursor:cursor + limit]
return {
"cursor": cursor,
"next_cursor": cursor + len(sliced),
"count": len(sliced),
"events": sliced
}
# =========================
# REPLAY
# =========================
def replay(
self,
source: Optional[str] = None,
level: Optional[str] = None,
name: Optional[str] = None,
handler: Optional[Callable[[Event], Any]] = None,
):
for event in self.query(source, level, name):
if handler:
handler(event)
else:
bus.emit(event)
# =========================
# REVERSE DEBUG
# =========================
def replay_reverse(self, handler: Callable[[Event], Any]):
for event in reversed(self.buffer):
handler(event)
# =========================
# STREAM TAIL
# =========================
def tail(self, last_n: int = 50) -> Iterator[Event]:
yield from list(self.buffer)[-last_n:]
# singleton
event_store = EventStore()

147
core/events.py Normal file
View File

@@ -0,0 +1,147 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable
from collections.abc import Awaitable
from datetime import datetime
import asyncio
import uuid
# =========================
# EVENT MODEL (GRAPH-AWARE)
# =========================
@dataclass
class Event:
name: str
source: str
level: str = "INFO"
data: dict[str, Any] | None = None
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
trace_id: str = field(default_factory=lambda: str(uuid.uuid4()))
parent_event_id: str | None = None
meta: dict[str, Any] = field(default_factory=dict)
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
# =========================
# EVENT BUS
# =========================
class EventBus:
def __init__(self):
self._subscribers: list[Callable[[Event], Any]] = []
self._async_subscribers: list[Callable[[Event], Awaitable[Any]]] = []
self._current_trace: str | None = None
self._current_parent: str | None = None
# -------------------------
# SUBSCRIBE
# -------------------------
def subscribe(self, fn: Callable[[Event], Any]):
self._subscribers.append(fn)
def subscribe_async(self, fn: Callable[[Event], Awaitable[Any]]):
self._async_subscribers.append(fn)
# -------------------------
# TRACE CONTROL
# -------------------------
def start_trace(self) -> str:
trace_id = str(uuid.uuid4())
self._current_trace = trace_id
self._current_parent = None
return trace_id
def set_parent(self, event_id: str | None):
self._current_parent = event_id
# -------------------------
# EMIT (CORE SAFE PATH)
# -------------------------
def emit(self, event: Event):
# inject trace
if not event.trace_id:
event.trace_id = self._current_trace or event.trace_id
event.parent_event_id = event.parent_event_id or self._current_parent
# advance causal chain
self._current_parent = event.event_id
# sync subscribers
for fn in self._subscribers:
try:
fn(event)
except Exception:
pass
# async subscribers (SAFE FIX)
if self._async_subscribers:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return # no loop available
for fn in self._async_subscribers:
try:
coro = fn(event)
# IMPORTANT FIX:
# ensure it's actually awaitable before scheduling
if asyncio.iscoroutine(coro):
loop.create_task(coro)
except Exception:
pass
# -------------------------
# STRICT ASYNC EMIT
# -------------------------
async def emit_async(self, event: Event):
for fn in self._subscribers:
try:
fn(event)
except Exception:
pass
await asyncio.gather(
*(fn(event) for fn in self._async_subscribers),
return_exceptions=True
)
# -------------------------
# LOG CONVENIENCE
# -------------------------
def log(
self,
source: str,
name: str,
level: str = "INFO",
data: dict[str, Any] | None = None
):
self.emit(
Event(
name=name,
source=source,
level=level,
data=data or {}
)
)
# =========================
# SINGLETON
# =========================
bus = EventBus()

85
core/exception.py Normal file
View File

@@ -0,0 +1,85 @@
from dataclasses import dataclass
from typing import Any, Optional
from core.events import bus, Event
# -------------------------
# BASE ERROR
# -------------------------
@dataclass
class CXError(Exception):
code: str
message: str
module: str = "CORE"
status: int = 500
context: Optional[Any] = None
# -------------------------
# SERIALIZATION
# -------------------------
def to_dict(self):
return {
"error": True,
"code": self.code,
"message": self.message,
"module": self.module,
"status": self.status,
"context": self.context,
}
# -------------------------
# EVENT EMISSION
# -------------------------
def emit(self):
bus.emit(Event(
name="exception",
source=self.module,
level="ERROR",
data={
"code": self.code,
"message": self.message,
"status": self.status,
"context": self.context,
}
))
# -------------------------
# 4xx CLIENT / AI ERRORS
# -------------------------
class CXBadRequest(CXError):
def __init__(self, msg, module="CORE"):
super().__init__("CX400", msg, module, 400)
class CXNotFound(CXError):
def __init__(self, msg, module="FILESYSTEM"):
super().__init__("CX404", msg, module, 404)
class CXForbidden(CXError):
def __init__(self, msg, module="SECURITY"):
super().__init__("CX403", msg, module, 403)
class CXInvalidOperation(CXError):
def __init__(self, msg, module="CORE"):
super().__init__("CX422", msg, module, 422)
# -------------------------
# 5xx SYSTEM ERRORS
# -------------------------
class CXInternalError(CXError):
def __init__(self, msg, module="CORE"):
super().__init__("CX500", msg, module, 500)
class CXToolFailure(CXError):
def __init__(self, msg, module="TOOLS"):
super().__init__("CX502", msg, module, 502)

62
core/executor.py Normal file
View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import asyncio
import threading
from typing import Any, TypeVar, Coroutine
T = TypeVar("T")
class LoopExecutor:
def __init__(self):
self._loop: asyncio.AbstractEventLoop | None = None
self._thread: threading.Thread | None = None
# -------------------------
# START LOOP
# -------------------------
def start(self):
if self._loop:
return
def runner():
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._loop.run_forever()
self._thread = threading.Thread(target=runner, daemon=True)
self._thread.start()
while self._loop is None:
pass
# -------------------------
# SUBMIT COROUTINE
# -------------------------
def submit(self, coro: Coroutine[Any, Any, T]):
if not self._loop:
raise RuntimeError("Executor not started")
return asyncio.run_coroutine_threadsafe(coro, self._loop)
# -------------------------
# SYNC BRIDGE
# -------------------------
def run_sync(self, coro: Coroutine[Any, Any, T]) -> T:
future = self.submit(coro)
return future.result()
# -------------------------
# STOP
# -------------------------
def stop(self):
if self._loop:
self._loop.call_soon_threadsafe(self._loop.stop)
self._loop = None
executor = LoopExecutor()

125
core/logging_core.py Normal file
View File

@@ -0,0 +1,125 @@
from __future__ import annotations
import sys
from datetime import datetime
from loguru import logger
from core.config import LOG_DIR
from core.events import Event, bus
class CXLoggerSink:
def __init__(self):
logger.remove()
# =========================
# SESSION LOG FILE
# =========================
timestamp = datetime.utcnow().strftime(
"%Y-%m-%d_%H-%M-%S"
)
self.log_path = LOG_DIR / (
f"MCP-{timestamp}.log"
)
self.log_path.parent.mkdir(
parents=True,
exist_ok=True
)
# =========================
# CONSOLE LOGGER
# =========================
logger.add(
sys.stderr,
level="DEBUG",
colorize=True,
backtrace=False,
diagnose=False,
format=(
"<level>{level:<8}</level> | "
"<cyan>{extra[module]:<12}</cyan> | "
"{time:YYYY-MM-DD HH:mm:ss} | "
"{message}"
)
)
# =========================
# FILE LOGGER
# =========================
logger.add(
str(self.log_path),
level="DEBUG",
enqueue=True,
retention="30 days",
compression="zip",
backtrace=False,
diagnose=False,
format=(
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level:<8} | "
"{extra[module]:<12} | "
"{message}"
)
)
logger.bind(module="LOGGER").info(
f"Session log created: {self.log_path.name}"
)
# =========================
# EVENT HANDLER
# =========================
def handle(
self,
event: Event
):
module = event.source.upper()
level = event.level.lower()
message = event.name
if event.data:
message += (
f" | {event.data}"
)
with logger.contextualize(
module=module
):
log_method = getattr(
logger,
level,
logger.info
)
log_method(message)
# =========================
# OPTIONAL DIRECT ACCESS
# =========================
def get_log_path(self) -> str:
return str(self.log_path)
# =========================
# SINGLETON
# =========================
cxlog_sink = CXLoggerSink()
# =========================
# REGISTER TO EVENT BUS
# =========================
bus.subscribe(
cxlog_sink.handle
)

72
core/metrics.py Normal file
View File

@@ -0,0 +1,72 @@
from collections import defaultdict
from typing import Any, Optional
from datetime import datetime
from core.events import bus, Event
class CXMetrics:
def __init__(self):
self.counters = defaultdict(int)
self.events: list[dict[str, Any]] = []
self.max_events = 5000
# attach to event bus
bus.subscribe(self._on_event)
# -------------------------
# EVENT HOOK
# -------------------------
def _on_event(self, event: Event):
self.counters[f"event_{event.name}"] += 1
self.counters[f"level_{event.level}"] += 1
self.events.append({
"name": event.name,
"source": event.source,
"level": event.level,
"data": event.data,
"timestamp": event.timestamp
})
if len(self.events) > self.max_events:
self.events = self.events[-self.max_events:]
# -------------------------
# MANUAL COUNTERS
# -------------------------
def inc(self, key: str, value: int = 1):
self.counters[key] += value
def event(self, name: str, data: Optional[dict] = None):
bus.emit(Event(
name=name,
source="metrics",
level="INFO",
data=data or {}
))
# -------------------------
# SNAPSHOT (Prometheus / API)
# -------------------------
def snapshot(self) -> dict:
return {
"counters": dict(self.counters),
"recent_events": self.events[-100:],
"timestamp": datetime.utcnow().isoformat()
}
# -------------------------
# SIMPLE EXPORT FORMAT (future Prometheus hook)
# -------------------------
def prometheus_format(self) -> str:
lines = []
for k, v in self.counters.items():
lines.append(f"{k} {v}")
return "\n".join(lines)
metrics = CXMetrics()

181
core/safety.py Normal file
View File

@@ -0,0 +1,181 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Awaitable, Callable
import asyncio
from core.config import (
WORKSPACE_ROOT,
MAX_FILE_SIZE,
IGNORE_DIRS,
DRY_RUN_DEFAULT,
)
from core.events import bus, Event
from core.metrics import metrics
# =========================
# SAFETY ERROR
# =========================
class SafetyError(Exception):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
self.message = message
# =========================
# SAFETY ENGINE
# =========================
class SafetyEngine:
def __init__(self):
self.dry_run = DRY_RUN_DEFAULT
self.block_destructive = True
# -------------------------
# MODE CONTROL
# -------------------------
def set_dry_run(self, value: bool):
self.dry_run = value
bus.emit(Event(
name="safety_dry_run",
source="SAFETY",
level="INFO",
data={"enabled": value}
))
# -------------------------
# PATH SANDBOXING
# -------------------------
def validate_path(self, path: str) -> Path:
target = (WORKSPACE_ROOT / path).resolve()
if not str(target).startswith(str(WORKSPACE_ROOT)):
self._violation("PATH_ESCAPE", path)
raise SafetyError("PATH_ESCAPE", "Outside workspace")
for part in target.parts:
if part in IGNORE_DIRS:
self._violation("IGNORED_PATH", path)
raise SafetyError("IGNORED_PATH", f"Blocked directory: {part}")
return target
# -------------------------
# FILE SIZE GUARD
# -------------------------
def check_file_write(self, path: Path, content: str):
if len(content.encode("utf-8")) > MAX_FILE_SIZE:
self._violation("FILE_TOO_LARGE", str(path))
raise SafetyError("FILE_TOO_LARGE", "Exceeded limit")
# -------------------------
# DESTRUCTIVE OPS
# -------------------------
def allow_action(self, action: str):
blocked = {
"delete_file",
"rm_rf",
"format",
"wipe",
"drop_db",
}
if self.block_destructive and action in blocked:
self._violation("DESTRUCTIVE_BLOCK", action)
raise SafetyError("DESTRUCTIVE_BLOCK", f"Blocked: {action}")
# =========================================================
# ASYNC SAFE WRAPPER (NEW CORE ENTRYPOINT)
# =========================================================
async def wrap_async(
self,
tool_name: str,
fn: Callable[..., Any | Awaitable[Any]],
*args: Any,
**kwargs: Any,
) -> Any:
bus.emit(Event(
name="tool_start",
source="SAFETY",
level="INFO",
data={"tool": tool_name}
))
if self.dry_run:
return {
"dry_run": True,
"tool": tool_name,
"args": args,
"kwargs": kwargs,
}
try:
result = fn(*args, **kwargs)
if asyncio.iscoroutine(result):
result = await result
metrics.inc(f"tool_{tool_name}")
metrics.inc("tools_executed")
bus.emit(Event(
name="tool_success",
source="SAFETY",
level="SUCCESS",
data={"tool": tool_name}
))
return result
except SafetyError as e:
bus.emit(Event(
name="tool_blocked",
source="SAFETY",
level="ERROR",
data={"code": e.code, "message": e.message}
))
metrics.inc("safety_blocks")
return {"error": True, "code": e.code, "message": e.message}
except Exception as e:
bus.emit(Event(
name="tool_crash",
source="SAFETY",
level="ERROR",
data={"tool": tool_name, "error": str(e)}
))
metrics.inc("tool_errors")
return {"error": True, "code": "UNEXPECTED", "message": str(e)}
# -------------------------
# INTERNAL VIOLATION LOGGER
# -------------------------
def _violation(self, code: str, context: str):
bus.emit(Event(
name="safety_violation",
source="SAFETY",
level="ERROR",
data={"code": code, "context": context}
))
metrics.inc(f"safety_{code}")
# singleton
safety = SafetyEngine()

211
core/subprocess.py Normal file
View File

@@ -0,0 +1,211 @@
from __future__ import annotations
import subprocess
from pathlib import Path
from typing import Any
from core.events import bus, Event
from core.metrics import metrics
from core.safety import safety
# =========================
# PROCESS RUNNER
# =========================
def run_command(
cmd: list[str],
cwd: str | None = None,
timeout: int = 60
) -> dict[str, Any]:
# -------------------------
# VALIDATION
# -------------------------
if not cmd:
raise ValueError(
"Command list cannot be empty"
)
for item in cmd:
if not isinstance(item, str):
raise ValueError(
"cmd must contain only strings"
)
executable = cmd[0].lower()
# -------------------------
# SAFETY CHECKS
# -------------------------
blocked_commands = {
"rm",
"rmdir",
"del",
"erase",
"shutdown",
"reboot",
"mkfs",
"format",
"diskpart"
}
if executable in blocked_commands:
bus.emit(Event(
name="process_blocked",
source="SUBPROCESS",
level="ERROR",
data={
"cmd": cmd,
"reason": "blocked_command"
}
))
metrics.inc(
"subprocess_blocked"
)
return {
"ok": False,
"error": (
f"Blocked command: "
f"{executable}"
),
"code": -1
}
resolved_cwd: Path | None = None
if cwd is not None:
resolved_cwd = (
safety.validate_path(cwd)
)
# -------------------------
# START EVENT
# -------------------------
bus.emit(Event(
name="process_start",
source="SUBPROCESS",
level="INFO",
data={
"cmd": cmd,
"cwd": str(resolved_cwd)
if resolved_cwd else None,
"timeout": timeout
}
))
metrics.inc(
"subprocess_runs"
)
# -------------------------
# EXECUTE
# -------------------------
try:
result = subprocess.run(
cmd,
cwd=(
str(resolved_cwd)
if resolved_cwd
else None
),
capture_output=True,
text=True,
timeout=timeout,
check=False
)
stdout = (
result.stdout or ""
)
stderr = (
result.stderr or ""
)
ok = (
result.returncode == 0
)
# -------------------------
# COMPLETE EVENT
# -------------------------
bus.emit(Event(
name="process_complete",
source="SUBPROCESS",
level=(
"SUCCESS"
if ok
else "ERROR"
),
data={
"cmd": cmd,
"code": result.returncode,
"stdout": stdout,
"stderr": stderr
}
))
return {
"ok": ok,
"stdout": stdout,
"stderr": stderr,
"code": result.returncode
}
# -------------------------
# TIMEOUT
# -------------------------
except subprocess.TimeoutExpired:
metrics.inc(
"subprocess_timeouts"
)
bus.emit(Event(
name="process_timeout",
source="SUBPROCESS",
level="ERROR",
data={
"cmd": cmd,
"timeout": timeout
}
))
return {
"ok": False,
"error": "timeout",
"code": -1
}
# -------------------------
# UNEXPECTED ERROR
# -------------------------
except Exception as e:
metrics.inc(
"subprocess_errors"
)
bus.emit(Event(
name="process_error",
source="SUBPROCESS",
level="ERROR",
data={
"cmd": cmd,
"error": str(e)
}
))
return {
"ok": False,
"error": str(e),
"code": -1
}

93
core/tools/base.py Normal file
View File

@@ -0,0 +1,93 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol
from core.events import bus
# =========================
# TOOL CONTEXT
# =========================
@dataclass
class ToolContext:
user: str | None = None
dry_run: bool = False
# safe mutable metadata container
meta: dict[str, Any] = field(default_factory=dict)
# =========================
# TOOL CONTRACT
# =========================
class Tool(Protocol):
name: str
def execute(self, payload: dict[str, Any], ctx: ToolContext) -> Any:
...
# =========================
# BASE TOOL
# =========================
class BaseTool:
name: str = "base"
# -------------------------
# LIFECYCLE WRAPPER
# -------------------------
def run(self, payload: dict[str, Any], ctx: ToolContext):
bus.log(self.name, "tool_start", "INFO", {"payload": payload})
try:
# attach execution metadata (useful for event graph later)
ctx.meta.setdefault("tool", self.name)
ctx.meta.setdefault("dry_run", ctx.dry_run)
if ctx.dry_run:
result = self.preview(payload, ctx)
else:
result = self.execute(payload, ctx)
bus.log(
self.name,
"tool_success",
"SUCCESS",
{
"result_type": type(result).__name__,
"result": repr(result)[:500] # prevent log explosion
}
)
return result
except Exception as e:
bus.log(
self.name,
"tool_error",
"ERROR",
{
"error_type": type(e).__name__,
"error": str(e)
}
)
raise
# -------------------------
# OVERRIDABLE
# -------------------------
def execute(self, payload: dict[str, Any], ctx: ToolContext):
raise NotImplementedError("Tool must implement execute()")
def preview(self, payload: dict[str, Any], ctx: ToolContext):
return {
"dry_run": True,
"tool": self.name,
"payload": payload
}

124
core/tools/registry.py Normal file
View File

@@ -0,0 +1,124 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.events import bus
# =========================
# TOOL REGISTRY
# =========================
class ToolRegistry:
def __init__(self):
self._tools: dict[str, BaseTool] = {}
# -------------------------
# REGISTER
# -------------------------
def register(self, tool: BaseTool):
self._tools[tool.name] = tool
bus.log(
"REGISTRY",
"tool_registered",
"INFO",
{
"tool": tool.name
}
)
# -------------------------
# RESOLVE
# -------------------------
def get(self, name: str) -> BaseTool:
tool = self._tools.get(name)
if tool is None:
bus.log(
"REGISTRY",
"tool_not_found",
"ERROR",
{"tool": name}
)
raise ValueError(f"Tool not found: {name}")
return tool
# -------------------------
# EXECUTE
# -------------------------
def run(
self,
name: str,
payload: dict[str, Any],
ctx: ToolContext
):
tool = self.get(name)
# registry metadata
ctx.meta.setdefault("registry", True)
ctx.meta.setdefault("tool_name", name)
bus.log(
"REGISTRY",
"tool_dispatch",
"INFO",
{
"tool": name,
"dry_run": ctx.dry_run,
}
)
try:
result = tool.run(payload, ctx)
bus.log(
"REGISTRY",
"tool_completed",
"SUCCESS",
{
"tool": name,
"result_type": type(result).__name__
}
)
return result
except Exception as e:
bus.log(
"REGISTRY",
"tool_failed",
"ERROR",
{
"tool": name,
"error": str(e),
"error_type": type(e).__name__
}
)
raise
# -------------------------
# DISCOVERY (NEW)
# -------------------------
def all_tools(self) -> list[BaseTool]:
return list(self._tools.values())
def names(self) -> list[str]:
return list(self._tools.keys())
def exists(self, name: str) -> bool:
return name in self._tools
# =========================
# SINGLETON
# =========================
registry = ToolRegistry()