# [DEF:ADR-0008:ADR] # @STATUS ACTIVE # @PURPOSE Replace the manual five-file tool registration (tool catalog, dispatch, permissions, safe-ops, command parser) with a single `@assistant_tool` decorator-based registry that automatically generates the LLM catalog, dispatches operations, and enforces permissions — eliminating cross-file desynchronisation when adding new assistant commands. # @RELATION DEPENDS_ON -> [ADR-0001:ADR] # @RELATION DEPENDS_ON -> [AssistantSchemas] # @RELATION CALLS -> [AssistantToolRegistry] # @RATIONALE The current architecture requires editing 4–5 separate files per new tool: `_build_tool_catalog()` (tool metadata), `_dispatch_intent()` (routing), `INTENT_PERMISSION_CHECKS` (permissions), `_SAFE_OPS` (safety classification), and optionally `_parse_command()` (keyword fallback). In practice, developers (both human and AI) frequently miss one of these, resulting in tools that appear in the LLM catalog but fail at dispatch, or dispatch successfully but bypass permission checks. A decorator-based registry collocates all tool metadata with its handler, making it impossible to register an incomplete tool. The registry also enables automatic catalog generation, eliminating the hand-maintained `_build_tool_catalog()`. # @REJECTED Centralised YAML/JSON config file (e.g. `tools.yaml`) — rejected because it separates tool metadata from implementation, requiring developers to jump between config and code. It also cannot express permission-check logic or handler imports, so it would still need a parallel registration step, reducing the benefit. # @REJECTED Automatic discovery via filesystem scanning (e.g. `importlib` scanning `_tool_*.py`) — rejected because it makes the tool set implicit; adding a `_tool_*.py` file silently registers a tool, which is dangerous for guarded or destructive operations. Explicit decorator registration in a known `_tool_registry.py` module keeps the tool set fully visible and auditable. # @REJECTED Incremental improvement of existing pattern (add one more dict, add one more if) — rejected because it does not solve the root problem: the five-file scatter. Any new file or dict that must be kept in sync with the handler will eventually desynchronise. # @DATA_CONTRACT Handler[Intent, User, TaskManager, ConfigManager, Session] -> [text: str, task_id: str | None, actions: list[AssistantAction]] ## Decision ### 1. New file: `_tool_registry.py` A central registry module that provides: ```python @assistant_tool( operation="search_dashboards", domain="dashboards", description="Search and list dashboards by name, id, or get all dashboards", optional_entities=["search", "environment", "page", "page_size"], risk_level="safe", requires_confirmation=False, permission_checks=[("dashboards", "READ")], ) async def handle_search_dashboards(intent, user, task_manager, config_manager, db): ... ``` The decorator stores the tool definition in a module-level `dict[str, AssistantTool]` and attaches it as `func.__assistant_tool__` for optional introspection. ### 2. Auto-generated catalog `_build_tool_catalog()` is replaced by `get_catalog(user)` which: 1. Iterates all registered tools 2. Filters by user permissions via `_has_any_permission()` 3. Returns LLM-ready dict list ### 3. Auto-dispatched routing `_dispatch_intent()` is replaced by `dispatch(operation, ...)` which: 1. Looks up `operation` in the registry 2. Calls `tool.handler(intent, user, task_manager, config_manager, db)` 3. Returns `(text, task_id, actions)` ### 4. Derived lists `_SAFE_OPS` is derived from `risk_level == "safe"` in the registry. `INTENT_PERMISSION_CHECKS` is derived from `tool.permission_checks`. Both hardcoded dicts are removed. ### 5. Migration strategy Each existing tool handler is moved from `_dispatch.py:_dispatch_intent()` into its own `_tool_*.py` file with an `@assistant_tool` decorator. Tools are migrated one at a time, in this order: | Step | Tool | File | |------|------|------| | 1 | `show_capabilities` | `_tool_capabilities.py` | | 2 | `get_task_status` | `_tool_task_status.py` | | 3 | `create_branch` | `_tool_create_branch.py` | | 4 | `commit_changes` | `_tool_commit.py` | | 5 | `deploy_dashboard` | `_tool_deploy.py` | | 6 | `execute_migration` | `_tool_migration.py` | | 7 | `run_backup` | `_tool_backup.py` | | 8 | `run_llm_validation` | `_tool_llm_validation.py` | | 9 | `run_llm_documentation` | `_tool_llm_documentation.py` | | 10 | `get_health_summary` | `_tool_health_summary.py` | After migration, `_dispatch.py` is reduced to a thin `dispatch()` wrapper. The `_parse_command()` function is updated to also use the registry for keyword-to-operation mapping. > **Step 8 update (v2):** `run_llm_validation` now creates a persistent validation task (policy) via `POST /api/validation-tasks` (i.e. `ValidationTaskService.create_task()`) and triggers an immediate run via `ValidationTaskService.trigger_run()`, replacing the previous ad-hoc `task_manager.create_task(plugin_id="llm_dashboard_validation")` dispatch. This makes LLM validation repeatable, schedulable, and auditable — each run is tracked as a `ValidationRun` with pass/fail/warn counts. ### 6. File layout after migration ``` backend/src/api/routes/assistant/ ├── __init__.py ├── _admin_routes.py # admin endpoints (list_conversations, delete, history, audit) ├── _routes.py # main router (send_message, confirm, cancel) ├── _tool_registry.py # @assistant_tool decorator, get_catalog(), dispatch() ├── _tool_capabilities.py ├── _tool_task_status.py ├── _tool_create_branch.py ├── _tool_commit.py ├── _tool_deploy.py ├── _tool_migration.py ├── _tool_backup.py ├── _tool_llm_validation.py ├── _tool_llm_documentation.py ├── _tool_health_summary.py ├── _schemas.py # Pydantic models, in-memory stores ├── _history.py # Conversation history helpers ├── _dispatch.py # removed or thin wrapper ├── _llm_planner.py # removed _build_tool_catalog, uses get_catalog() ├── _llm_planner_intent.py ├── _command_parser.py ├── _resolvers.py ├── _dataset_review.py └── _dataset_review_dispatch.py ``` ## Consequences ### Positive - **Single point of registration** — adding a tool requires editing exactly one file: the tool handler with its `@assistant_tool` decorator - **No desync** — catalog, dispatch, permissions, and safety classification all derive from the same decorator metadata - **Testable** — `_tool_registry.py` can be unit-tested without DB or HTTP fixtures - **Discoverable** — `from _tool_registry import _tools` gives a complete inventory of all registered tools - **Safe-by-default** — a tool without `@assistant_tool` is not registered and cannot be invoked - **Task-based validation** — `run_llm_validation` now creates persistent validation tasks (policies) via `ValidationTaskService`, making each validation run repeatable, schedulable, and auditable through the `ValidationRun` aggregate model ### Negative - **One-time migration cost** — 10 existing handlers must be moved to new files (~200 LOC total) - **Import overhead** — each `_tool_*.py` must be imported somewhere to trigger decorator registration (solved by importing them in `__init__.py` or `_tool_registry.py`) - **Dynamic dispatch** — replaces static `if/elif` with dict lookup, which is marginally less explicit (mitigated by strong typing of `AssistantTool` dataclass) # [/DEF:ADR-0008:ADR]