""" Orchestrator tool registry. Declarations live in each domain module alongside their callables. This file assembles them into the unified registry used by both engines. To add a new tool: 1. Implement it in tools/.py — add the async callable + append to DECLARATIONS 2. Import the callable here and add it to _CALLABLES 3. If admin-only, add it to TOOL_ROLES; if confirmation needed, add to CONFIRM_REQUIRED IMPORTANT: These tools are separate from the ae_* MCP tools used by the fleet agents. Do not modify the ae_* MCP server to support orchestrator needs. """ from google.genai import types # ── Callable imports ────────────────────────────────────────────────────────── from tools.web import search as _web_search, http_fetch as _http_fetch, web_read as _web_read, http_post as _http_post from tools.ae_knowledge import ( journal_list as _ae_journal_list, journal_search as _ae_journal_search, journal_entry_read as _ae_journal_entry_read, journal_entries_list as _ae_journal_entries_list, journal_entry_create as _ae_journal_entry_create, journal_entry_update as _ae_journal_entry_update, journal_entry_disable as _ae_journal_entry_disable, journal_entry_append as _ae_journal_entry_append, journal_entry_prepend as _ae_journal_entry_prepend, ) from tools.ae_tasks import task_list as _ae_task_list from tools.files import ( project_file_read as _project_file_read, project_file_list as _project_file_list, file_stat as _file_stat, file_grep as _file_grep, file_diff as _file_diff, file_syntax_check as _file_syntax_check, file_read as _file_read, file_list as _file_list, file_write as _file_write, session_read as _session_read, session_search as _session_search, ) from tools.system import ( shell_exec as _shell_exec, claude_allow_dir as _claude_allow_dir, cortex_restart as _cortex_restart, cortex_logs as _cortex_logs, cortex_status as _cortex_status, cortex_update as _cortex_update, ) from tools.tasks import ( task_list as _task_list, task_create as _task_create, task_update as _task_update, task_complete as _task_complete, ) from tools.cron import ( cron_list as _cron_list, cron_add as _cron_add, cron_remove as _cron_remove, cron_toggle as _cron_toggle, ) from tools.reminders import ( reminders_add as _reminders_add, reminders_list as _reminders_list, reminders_remove as _reminders_remove, reminders_clear as _reminders_clear, ) from tools.scratch import ( scratch_read as _scratch_read, scratch_write as _scratch_write, scratch_append as _scratch_append, scratch_clear as _scratch_clear, ) from tools.notify import nc_talk_send as _nc_talk_send, email_send as _email_send, web_push as _web_push, nc_talk_history as _nc_talk_history from tools.agent_notes import ( agent_notes_read as _agent_notes_read, agent_notes_write as _agent_notes_write, agent_notes_append as _agent_notes_append, agent_notes_clear as _agent_notes_clear, ) from tools.git import ( git_status as _git_status, git_log as _git_log, git_diff as _git_diff, ) from tools.agents import spawn_agent as _spawn_agent from tools.homeassistant import ( ha_get_state as _ha_get_state, ha_get_states as _ha_get_states, ha_call_service as _ha_call_service, ) # ── Declaration imports ─────────────────────────────────────────────────────── import tools.web as _mod_web import tools.ae_knowledge as _mod_ae_knowledge import tools.ae_tasks as _mod_ae_tasks import tools.files as _mod_files import tools.system as _mod_system import tools.tasks as _mod_tasks import tools.cron as _mod_cron import tools.reminders as _mod_reminders import tools.scratch as _mod_scratch import tools.notify as _mod_notify import tools.agent_notes as _mod_agent_notes import tools.git as _mod_git import tools.agents as _mod_agents import tools.homeassistant as _mod_homeassistant # ── Tool categories — used by the Model Registry UI for grouped checkboxes ─── TOOL_CATEGORIES: dict[str, list[str]] = { "Web": ["web_search", "http_fetch", "web_read", "http_post"], "Project Files": ["project_file_read", "project_file_list", "file_stat", "file_grep", "file_diff", "file_syntax_check"], "Git": ["git_status", "git_log", "git_diff"], "System Files": ["file_read", "file_list", "file_write", "session_read", "session_search"], "Shell": ["shell_exec", "claude_allow_dir"], "System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"], "Tasks": ["task_list", "task_create", "task_update", "task_complete"], "Cron": ["cron_list", "cron_add", "cron_remove", "cron_toggle"], "Reminders": ["reminders_add", "reminders_list", "reminders_remove", "reminders_clear"], "Scratchpad": ["scratch_read", "scratch_write", "scratch_append", "scratch_clear"], "Notifications": ["web_push", "email_send", "nc_talk_send", "nc_talk_history"], "Aether Journals": [ "ae_journal_list", "ae_journal_search", "ae_journal_entries_list", "ae_journal_entry_read", "ae_journal_entry_create", "ae_journal_entry_update", "ae_journal_entry_disable", "ae_journal_entry_append", "ae_journal_entry_prepend", ], "Aether Tasks": ["ae_task_list"], "Agent Notes": ["agent_notes_read", "agent_notes_write", "agent_notes_append", "agent_notes_clear"], "Agents": ["spawn_agent"], "Home Assistant": ["ha_get_state", "ha_get_states", "ha_call_service"], } # ── Callable registry ───────────────────────────────────────────────────────── _CALLABLES: dict[str, callable] = { "web_search": _web_search, "http_fetch": _http_fetch, "web_read": _web_read, "http_post": _http_post, "ae_journal_list": _ae_journal_list, "ae_journal_search": _ae_journal_search, "ae_journal_entry_read": _ae_journal_entry_read, "ae_journal_entries_list": _ae_journal_entries_list, "ae_journal_entry_create": _ae_journal_entry_create, "ae_journal_entry_update": _ae_journal_entry_update, "ae_journal_entry_disable": _ae_journal_entry_disable, "ae_journal_entry_append": _ae_journal_entry_append, "ae_journal_entry_prepend": _ae_journal_entry_prepend, "ae_task_list": _ae_task_list, "project_file_read": _project_file_read, "project_file_list": _project_file_list, "file_stat": _file_stat, "file_grep": _file_grep, "file_diff": _file_diff, "file_syntax_check": _file_syntax_check, "file_read": _file_read, "file_list": _file_list, "file_write": _file_write, "session_read": _session_read, "session_search": _session_search, "shell_exec": _shell_exec, "claude_allow_dir": _claude_allow_dir, "cortex_restart": _cortex_restart, "cortex_logs": _cortex_logs, "cortex_status": _cortex_status, "cortex_update": _cortex_update, "task_list": _task_list, "task_create": _task_create, "task_update": _task_update, "task_complete": _task_complete, "cron_list": _cron_list, "cron_add": _cron_add, "cron_remove": _cron_remove, "cron_toggle": _cron_toggle, "reminders_add": _reminders_add, "reminders_list": _reminders_list, "reminders_remove": _reminders_remove, "reminders_clear": _reminders_clear, "scratch_read": _scratch_read, "scratch_write": _scratch_write, "scratch_append": _scratch_append, "scratch_clear": _scratch_clear, "email_send": _email_send, "nc_talk_send": _nc_talk_send, "web_push": _web_push, "nc_talk_history": _nc_talk_history, "agent_notes_read": _agent_notes_read, "agent_notes_write": _agent_notes_write, "agent_notes_append": _agent_notes_append, "agent_notes_clear": _agent_notes_clear, "git_status": _git_status, "git_log": _git_log, "git_diff": _git_diff, "spawn_agent": _spawn_agent, "ha_get_state": _ha_get_state, "ha_get_states": _ha_get_states, "ha_call_service": _ha_call_service, } # ── Role-based access control ───────────────────────────────────────────────── # Minimum role required to use each tool. Unlisted tools default to "user". TOOL_ROLES: dict[str, str] = { "shell_exec": "admin", "claude_allow_dir": "admin", "cortex_restart": "admin", "cortex_logs": "admin", "cortex_status": "admin", "cortex_update": "admin", "file_read": "admin", "file_list": "admin", "file_write": "admin", "ae_task_list": "admin", "spawn_agent": "admin", "email_send": "admin", "nc_talk_send": "admin", "http_post": "admin", "nc_talk_history": "admin", "ha_call_service": "admin", } # Tools that require explicit user confirmation before executing. CONFIRM_REQUIRED: set[str] = { "cortex_restart", "cortex_update", "file_write", "shell_exec", "cron_remove", "reminders_clear", "http_post", "ha_call_service", } # Security risk ratings — informational for now; will drive auto-allow tiers later. # Unlisted tools default to "medium". # # low — read-only, sandboxed, no external side effects # medium — writes to local/controlled data, or reads beyond project scope, # or sends notifications to the same user # high — affects external systems, physical devices, other users, # or the host process/filesystem in ways that are hard to reverse TOOL_RISK: dict[str, str] = { # Web — read-only fetches are low; posting to external services is high "web_search": "low", "http_fetch": "low", "web_read": "low", "http_post": "high", # Project Files — all read-only and project-sandboxed "project_file_read": "low", "project_file_list": "low", "file_stat": "low", "file_grep": "low", "file_diff": "low", "file_syntax_check": "low", # System Files — reads beyond project scope are medium; writes are high "file_read": "medium", "file_list": "medium", "file_write": "high", "session_read": "low", "session_search": "low", # Shell — arbitrary execution and permission changes are high "shell_exec": "high", "claude_allow_dir": "high", # System — read-only status is low; restart/update affect the live service "cortex_logs": "low", "cortex_status": "low", "cortex_restart": "high", "cortex_update": "high", # Tasks — local persona data, all reversible "task_list": "low", "task_create": "low", "task_update": "low", "task_complete": "low", # Cron — list is low; add/remove/toggle affect scheduled behavior "cron_list": "low", "cron_add": "medium", "cron_remove": "medium", "cron_toggle": "medium", # Reminders — single-item ops are low; clear-all is medium "reminders_add": "low", "reminders_list": "low", "reminders_remove": "low", "reminders_clear": "medium", # Scratchpad — local persona file, ephemeral by design "scratch_read": "low", "scratch_write": "low", "scratch_append": "low", "scratch_clear": "low", # Notifications — push to same user is medium; external messages are high "web_push": "medium", "nc_talk_send": "high", "nc_talk_history": "low", "email_send": "high", # Aether Journals — reads are low; writes to external DB are medium "ae_journal_list": "low", "ae_journal_search": "low", "ae_journal_entries_list": "low", "ae_journal_entry_read": "low", "ae_journal_entry_create": "medium", "ae_journal_entry_update": "medium", "ae_journal_entry_disable": "medium", "ae_journal_entry_append": "medium", "ae_journal_entry_prepend": "medium", # Aether Tasks "ae_task_list": "low", # Agent Notes — local persona file "agent_notes_read": "low", "agent_notes_write": "low", "agent_notes_append": "low", "agent_notes_clear": "low", # Git — all read-only inspections "git_status": "low", "git_log": "low", "git_diff": "low", # Agents — spawning a subprocess with broad permissions is high "spawn_agent": "high", # Home Assistant — reads are low; controlling physical devices is high "ha_get_state": "low", "ha_get_states": "low", "ha_call_service": "high", } _RISK_RANK: dict[str, int] = {"low": 0, "medium": 1, "high": 2} _ROLE_RANK: dict[str, int] = {"user": 0, "admin": 1} def _role_allowed(tool_name: str, role: str) -> bool: required = TOOL_ROLES.get(tool_name, "user") return _ROLE_RANK.get(role, 0) >= _ROLE_RANK.get(required, 0) # ── Declaration assembly ────────────────────────────────────────────────────── _ALL_DECLARATIONS: list[types.FunctionDeclaration] = ( _mod_web.DECLARATIONS + _mod_files.DECLARATIONS + _mod_git.DECLARATIONS + _mod_system.DECLARATIONS + _mod_tasks.DECLARATIONS + _mod_cron.DECLARATIONS + _mod_reminders.DECLARATIONS + _mod_scratch.DECLARATIONS + _mod_notify.DECLARATIONS + _mod_ae_knowledge.DECLARATIONS + _mod_ae_tasks.DECLARATIONS + _mod_agent_notes.DECLARATIONS + _mod_agents.DECLARATIONS + _mod_homeassistant.DECLARATIONS ) # Full Gemini Tool object (all tools — use get_tools_for_role() in production) TOOL_DECLARATIONS = [types.Tool(function_declarations=_ALL_DECLARATIONS)] # ── Tool dispatch ───────────────────────────────────────────────────────────── async def call_tool(name: str, args: dict, callables: dict | None = None) -> str: """Dispatch a tool call by name. Returns result as a string. Pass `callables` (from get_tools_for_role) to enforce role restrictions. Falls back to the full _CALLABLES dict if omitted. Every call is recorded to the tool audit log (tool_audit.py). """ import asyncio import tool_audit from persona import get_user user = get_user() or "unknown" dispatch = callables if callables is not None else _CALLABLES fn = dispatch.get(name) if fn is None: asyncio.create_task(tool_audit.record(user, name, args, "denied")) return f"Tool not available or access denied: {name}" try: result = await fn(**args) asyncio.create_task(tool_audit.record(user, name, args, "ok", result)) return result except Exception as e: asyncio.create_task(tool_audit.record(user, name, args, "error", str(e))) raise # ── OpenAI JSON Schema conversion ──────────────────────────────────────────── _GEMINI_TYPE_TO_JSON = { "OBJECT": "object", "STRING": "string", "INTEGER": "integer", "NUMBER": "number", "BOOLEAN": "boolean", "ARRAY": "array", } def _schema_to_json(schema) -> dict: """Recursively convert a Gemini types.Schema to a JSON Schema dict.""" type_name = getattr(getattr(schema, "type", None), "name", "STRING") result: dict = {"type": _GEMINI_TYPE_TO_JSON.get(type_name, "string")} if getattr(schema, "description", None): result["description"] = schema.description props = getattr(schema, "properties", None) or {} if result["type"] == "object": result["properties"] = {k: _schema_to_json(v) for k, v in props.items()} req = getattr(schema, "required", None) if req: result["required"] = list(req) return result def _build_openai_tools() -> list[dict]: out = [] for decl in _ALL_DECLARATIONS: params = ( _schema_to_json(decl.parameters) if decl.parameters else {"type": "object", "properties": {}} ) out.append({ "type": "function", "function": { "name": decl.name, "description": decl.description or "", "parameters": params, }, }) return out # OpenAI-format tool list — all tools (use get_openai_tools_for_role() in production) OPENAI_TOOL_SCHEMAS: list[dict] = _build_openai_tools() # ── Role-filtered tool access ───────────────────────────────────────────────── def _apply_risk_policy( allowed: set[str], max_risk: str | None, whitelist: list[str] | None, blacklist: list[str] | None, ) -> set[str]: """Apply risk-level filtering on top of an already role-gated allowed set. Filtering order (each step can only restrict or restore within what the role already permits — risk policy can never elevate above role): 1. max_risk auto-include: keep tools whose risk ≤ max_risk 2. whitelist union: force-add specific tools (still role-gated) 3. blacklist subtract: force-remove specific tools When max_risk is None, all role-allowed tools remain (no risk filter). """ if max_risk is not None: max_rank = _RISK_RANK.get(max_risk, 2) auto = {n for n in allowed if _RISK_RANK.get(TOOL_RISK.get(n, "medium"), 1) <= max_rank} extra = {n for n in (whitelist or []) if n in allowed} allowed = (auto | extra) if blacklist: allowed -= set(blacklist) return allowed def get_tools_for_role( role: str, tool_list: list[str] | None = None, max_risk: str | None = None, whitelist: list[str] | None = None, blacklist: list[str] | None = None, ) -> tuple[list, dict]: """Return (gemini_tool_declarations, callables_dict) filtered to what the role can use. role — user access level ("user" | "admin"); gates admin-only tools tool_list — optional model-level allow-list; intersected so it can only restrict max_risk — auto-include tools at/below this risk level ("low"|"medium"|"high") whitelist — force-include specific tools above max_risk (still role-gated) blacklist — force-exclude specific tools regardless of max_risk """ allowed = {name for name in _CALLABLES if _role_allowed(name, role)} allowed = _apply_risk_policy(allowed, max_risk, whitelist, blacklist) if tool_list is not None: allowed &= set(tool_list) decls = [d for d in _ALL_DECLARATIONS if d.name in allowed] callables = {k: v for k, v in _CALLABLES.items() if k in allowed} return [types.Tool(function_declarations=decls)], callables def get_openai_tools_for_role( role: str, tool_list: list[str] | None = None, max_risk: str | None = None, whitelist: list[str] | None = None, blacklist: list[str] | None = None, ) -> list[dict]: """Return OpenAI tool schemas filtered to what the role can use. role — user access level ("user" | "admin") tool_list — optional model-level allow-list max_risk — auto-include tools at/below this risk level whitelist — force-include specific tools above max_risk blacklist — force-exclude specific tools """ allowed = {name for name in _CALLABLES if _role_allowed(name, role)} allowed = _apply_risk_policy(allowed, max_risk, whitelist, blacklist) if tool_list is not None: allowed &= set(tool_list) return [t for t in OPENAI_TOOL_SCHEMAS if t["function"]["name"] in allowed]