"""ArmorerCoreHandler: slimmed-down core handler after modularization.

Retains __init__, watchdog event methods, _process_inbox_task (main dispatch),
file monitoring helpers, task context management, and _poll_inbox.  Delegates to
extracted submodules for setup flows, guard, OTEL, daily reports, and parsing.
"""

import fnmatch
import json
import logging
import os
import re
import time
import uuid
import hmac
import hashlib
from pathlib import Path

from watchdog.events import FileSystemEventHandler
from pydantic import ValidationError

from armorer import db as armorer_db
from armorer.agent import BaseAgent
from armorer.config import ConfigManager
from armorer.core.execution_policy import deterministic_route, looks_like_runtime_retrieval
from armorer.core.router_models import ExecutionMode, RouteDecision
from armorer.core.run_context import ArmorerRunContext
from armorer.core.workflows import execute_bounded_workflow
from armorer.registry import AppRegistry
from armorer.security.conversation_guard import ConversationGuard
from armorer.agent_sdk import filter_tools_by_category, get_tool_metadata
from armorer.tools.interaction import redact_secret, update_app_metadata
from armorer.tools.runtime_state_tools import (
    build_runtime_state_snapshot,
    get_agent_setup_state,
    get_dashboard_urls,
    get_installed_agents_snapshot,
    get_recent_jobs_snapshot,
    get_runtime_access_snapshot,
    get_status_snapshot,
)

from armorer.core.response import _normalize_core_response
from armorer.core.agents import (
    build_repair_specialist_tool,
    build_runtime_access_specialist_tool,
    build_setup_specialist_tool,
)
from armorer.core.agent_hooks import ArmorerRunHooks
from armorer.core.memory_retrieval import classify_task_type, promote_runtime_snapshot_facts, recall_relevant_memory
from armorer.core.prompt_parsing import (
    _normalize_prompt,
    _extract_reply_value,
    _is_yes,
    _is_no,
    _is_openclaw_setup_yes,
    _is_openclaw_setup_no,
    _is_openclaw_skip_reply,
    _is_openclaw_cancel_reply,
    _is_openclaw_clear_reply,
    _parse_csv_values,
    _is_valid_telegram_id,
)
from armorer.core import focus as focus_mod
from armorer.core import telegram_setup as tg_setup_mod
from armorer.core import openclaw_setup as oc_setup_mod
from armorer.core import otel as otel_mod
from armorer.core import guard as guard_mod
from armorer.core import daily_report as daily_mod

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("ArmorerCore")


class SupersededTaskError(RuntimeError):
    """Raised when a newer task for the same conversation supersedes the current task."""


class OpenClawSetupExecutionError(RuntimeError):
    """Raised when OpenClaw setup fails after runtime changes need rollback."""

    def __init__(
        self,
        message: str,
        *,
        rollback_warnings: list[str] | None = None,
        rollback_summary: str | None = None,
    ):
        super().__init__(message)
        self.rollback_warnings = [str(item).strip() for item in (rollback_warnings or []) if str(item).strip()]
        self.rollback_summary = str(rollback_summary or "").strip()


class ArmorerCoreHandler(FileSystemEventHandler):
    """Handles both security oversight and incoming command tasks."""

    def __init__(self, config_dir: Path):
        self.registry = AppRegistry()
        self.config_manager = ConfigManager()

        try:
            pass
        except:
            pass

        self.config_dir = config_dir
        self.inbox = config_dir / "inbox"
        self.outbox = config_dir / "outbox"
        self.status_dir = config_dir / "status"
        self.inbox.mkdir(exist_ok=True)
        self.outbox.mkdir(exist_ok=True)
        self.status_dir.mkdir(exist_ok=True)
        for p in (self.inbox, self.outbox, self.status_dir):
            try:
                os.chmod(p, 0o777)
            except Exception:
                pass

        self.last_processed = {}
        self.is_processing = False
        self.guard = ConversationGuard(config_dir=config_dir, notifier=self._send_guard_notification)
        self._daily_report_state_file = self.config_dir / "daily_report_state.json"
        self._daily_report_last_check_ts = 0.0
        self._audit_file = self.config_dir / "security_audit.jsonl"
        self._task_signing_key = str(self.config_manager.get_key("ARMORER_TASK_SIGNING_KEY") or "").strip()
        self._last_backlog_alert_ts = 0.0
        self._pending_decisions_file = self.config_dir / "pending_decisions.json"
        self._pending_decisions = self._load_pending_decisions()
        self._conversation_focus_file = self.config_dir / "conversation_focus.json"
        self._conversation_focus = self._load_conversation_focus()
        self._active_task_contexts: dict[str, dict[str, object]] = {}
        self.otel_events_dir = self.config_dir / "otel_events"
        self.otel_events_dir.mkdir(parents=True, exist_ok=True)
        self._otel_offsets_file = self.config_dir / "otel_offsets.json"
        self._otel_offsets = otel_mod._load_otel_offsets(self)
        self._unreadable_task_retries: dict[str, dict[str, object]] = {}

    # ------------------------------------------------------------------
    # Utility helpers kept on the handler
    # ------------------------------------------------------------------

    @staticmethod
    def _unlink_if_exists(path_like: str | Path | None) -> None:
        if not path_like:
            return
        try:
            Path(path_like).unlink(missing_ok=True)
        except Exception:
            pass

    def _record_runtime_event(
        self,
        *,
        agent_alias: str,
        agent_id: str = "",
        event_type: str,
        subtype: str = "",
        severity: str = "info",
        path: str = "",
        dst_host: str = "",
        dst_ip: str = "",
        dst_port: int | None = None,
        verdict: str = "",
        process_name: str = "",
        container_id: str = "",
        raw: dict[str, object] | None = None,
    ) -> None:
        try:
            armorer_db.insert_runtime_event(
                self.config_dir,
                {
                    "ts": time.time(),
                    "agent_alias": agent_alias,
                    "agent_id": agent_id,
                    "event_type": event_type,
                    "subtype": subtype,
                    "severity": severity,
                    "path": path,
                    "dst_host": dst_host,
                    "dst_ip": dst_ip,
                    "dst_port": dst_port,
                    "verdict": verdict,
                    "process_name": process_name,
                    "container_id": container_id,
                    "raw": raw or {},
                },
            )
        except Exception:
            pass

    def _audit(self, event: str, **details: object) -> None:
        row = {"ts": time.time(), "component": "core", "event": event, "details": details}
        try:
            with open(self._audit_file, "a") as f:
                f.write(json.dumps(row, sort_keys=True) + "\n")
        except Exception as e:
            logger.warning("Security audit write failed: %s", e)

    def _compute_task_signature(self, task: dict) -> str:
        payload = {
            "id": task.get("id"),
            "prompt": task.get("prompt"),
            "source": task.get("source"),
            "timestamp": task.get("timestamp"),
            "metadata": task.get("metadata", {}),
        }
        body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
        return hmac.new(self._task_signing_key.encode("utf-8"), body, hashlib.sha256).hexdigest()

    def _verify_task_signature(self, task: dict) -> tuple[bool, str]:
        source = str(task.get("source") or "").strip().lower()
        require_all = str(self.config_manager.get_key("ARMORER_REQUIRE_TASK_SIGNATURES") or "false").strip().lower() in {"1", "true", "on", "yes"}
        require_remote = str(self.config_manager.get_key("ARMORER_REQUIRE_REMOTE_TASK_SIGNATURES") or "true").strip().lower() in {"1", "true", "on", "yes"}

        if not self._task_signing_key:
            if require_all:
                return False, "signing_required_but_not_configured"
            if require_remote and source in {"telegram", "signal"}:
                return False, "remote_signing_required_but_not_configured"
            return True, "disabled"
        supplied = str(task.get("signature") or "").strip()
        if not supplied:
            return False, "missing_signature"
        expected = self._compute_task_signature(task)
        if not hmac.compare_digest(supplied, expected):
            return False, "invalid_signature"
        return True, "ok"

    # ------------------------------------------------------------------
    # Pending-decision and focus state (kept on handler, used by submodules)
    # ------------------------------------------------------------------

    def _load_pending_decisions(self) -> dict[str, dict]:
        if not self._pending_decisions_file.exists():
            return {}
        try:
            raw = json.loads(self._pending_decisions_file.read_text())
            if isinstance(raw, dict):
                return {str(k): v for k, v in raw.items() if isinstance(v, dict)}
        except Exception:
            pass
        return {}

    def _load_conversation_focus(self) -> dict[str, dict]:
        if not self._conversation_focus_file.exists():
            return {}
        try:
            raw = json.loads(self._conversation_focus_file.read_text())
            if isinstance(raw, dict):
                out: dict[str, dict] = {}
                for k, v in raw.items():
                    if isinstance(v, dict):
                        out[str(k)] = v
                return out
        except Exception:
            pass
        return {}

    def _save_pending_decisions(self) -> None:
        try:
            self._pending_decisions_file.write_text(json.dumps(self._pending_decisions, sort_keys=True))
        except Exception as e:
            logger.debug("Failed to persist pending decisions: %s", e)

    def _save_conversation_focus(self) -> None:
        try:
            self._conversation_focus_file.write_text(json.dumps(self._conversation_focus, sort_keys=True))
        except Exception as e:
            logger.debug("Failed to persist conversation focus: %s", e)

    def _write_task_status(self, task_id: str, message: str) -> None:
        text = " ".join(str(message or "").split()).strip()
        if not task_id or not text:
            return
        try:
            self._raise_if_task_superseded(task_id)
        except SupersededTaskError:
            raise
        try:
            (self.status_dir / f"{task_id}.json").write_text(
                json.dumps({"message": text, "timestamp": time.time()})
            )
        except Exception as e:
            logger.debug("Failed to write task status for %s: %s", task_id, e)

    def _set_pending_decision(self, conversation_id: str, payload: dict) -> None:
        self._pending_decisions[conversation_id] = payload
        self._save_pending_decisions()

    def _restore_pending_decision(self, conversation_id: str, payload: object) -> None:
        if isinstance(payload, dict) and str(payload.get("type") or "").strip():
            self._set_pending_decision(conversation_id, payload)
            return
        self._clear_pending_decision(conversation_id)

    def _clear_pending_decision(self, conversation_id: str) -> None:
        if conversation_id in self._pending_decisions:
            self._pending_decisions.pop(conversation_id, None)
            self._save_pending_decisions()

    def _set_focus(self, conversation_id: str, mode: str, target: str = "") -> None:
        self._conversation_focus[conversation_id] = {
            "mode": mode,
            "target": str(target or "").strip(),
            "updated_at": time.time(),
        }
        self._save_conversation_focus()

    def _clear_focus(self, conversation_id: str) -> None:
        if conversation_id in self._conversation_focus:
            self._conversation_focus.pop(conversation_id, None)
            self._save_conversation_focus()

    def _get_focus(self, conversation_id: str) -> dict:
        row = self._conversation_focus.get(conversation_id, {})
        return row if isinstance(row, dict) else {}

    # ------------------------------------------------------------------
    # Unreadable task handling
    # ------------------------------------------------------------------

    def _handle_unreadable_task_json(self, task_file: Path, task_path: str, error: Exception) -> bool:
        task_key = str(task_file)
        now = time.time()
        try:
            stat = task_file.stat()
            task_mtime = int(getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000)))
            task_ctime = int(getattr(stat, "st_ctime_ns", int(stat.st_ctime * 1_000_000_000)))
            task_size = int(stat.st_size)
        except Exception:
            task_mtime = int(now * 1_000_000_000)
            task_ctime = int(now * 1_000_000_000)
            task_size = -1

        previous = self._unreadable_task_retries.get(task_key)
        if (
            previous
            and int(previous.get("mtime_ns", -1) or -1) == task_mtime
            and int(previous.get("ctime_ns", -1) or -1) == task_ctime
            and int(previous.get("size", -1) or -1) == task_size
        ):
            attempt = int(previous.get("attempt", 0) or 0) + 1
        else:
            attempt = 1
        self._unreadable_task_retries[task_key] = {
            "attempt": attempt,
            "mtime_ns": task_mtime,
            "ctime_ns": task_ctime,
            "size": task_size,
            "updated_at": now,
        }

        self._audit(
            "task_deferred_unreadable_json",
            task_path=task_path,
            error=str(error),
            attempt=attempt,
        )
        logger.warning(
            "Task file '%s' was not readable JSON yet (attempt %s/3); leaving it in queue for retry.",
            task_path,
            attempt,
        )

        file_age_seconds = max(0.0, now - (task_mtime / 1_000_000_000))
        should_drop = attempt >= 3 and file_age_seconds >= 2.0
        if not should_drop:
            return False

        task_id = str(task_file.stem or uuid.uuid4())
        payload = {
            "message": (
                "Request rejected: task payload remained unreadable JSON after multiple retries. "
                "Please retry your command."
            )
        }
        try:
            (self.outbox / f"{task_id}.json").write_text(
                json.dumps(
                    {"id": task_id, "response": json.dumps(payload), "timestamp": now},
                    indent=4,
                )
            )
        except Exception as write_exc:
            logger.debug("Failed to persist unreadable-task rejection for %s: %s", task_path, write_exc)
        self.last_processed[task_key] = now
        self._unlink_if_exists(task_file)
        self._unreadable_task_retries.pop(task_key, None)
        self._audit("task_dropped_unreadable_json", task_path=task_path, task_id=task_id, attempts=attempt)
        return True

    def _prune_unreadable_task_retries(self, *, max_age_seconds: float = 3600.0) -> None:
        now = time.time()
        for task_key, state in list(self._unreadable_task_retries.items()):
            keep = False
            if isinstance(state, dict):
                try:
                    updated_at = float(state.get("updated_at", 0) or 0)
                except Exception:
                    updated_at = 0.0
                exists = Path(task_key).exists()
                if exists and (updated_at <= 0.0 or (now - updated_at) <= max_age_seconds):
                    keep = True
            if not keep:
                self._unreadable_task_retries.pop(task_key, None)

    # ------------------------------------------------------------------
    # Prompt-parsing / focus / decision delegation (thin wrappers)
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize_prompt(text: str) -> str:
        return _normalize_prompt(text)

    def _extract_reply_value(self, prompt: str) -> str:
        return _extract_reply_value(prompt)

    def _is_yes(self, text: str) -> bool:
        return _is_yes(text)

    def _is_no(self, text: str) -> bool:
        return _is_no(text)

    @staticmethod
    def _is_openclaw_retry_confirm(text: str) -> bool:
        from armorer.core.prompt_parsing import _is_openclaw_retry_confirm
        return _is_openclaw_retry_confirm(text)

    def _is_openclaw_setup_yes(self, text: str) -> bool:
        return _is_openclaw_setup_yes(text)

    def _is_openclaw_setup_no(self, text: str) -> bool:
        return _is_openclaw_setup_no(text)

    @staticmethod
    def _is_openclaw_skip_reply(text: str) -> bool:
        return _is_openclaw_skip_reply(text)

    @staticmethod
    def _is_openclaw_cancel_reply(text: str) -> bool:
        return _is_openclaw_cancel_reply(text)

    @staticmethod
    def _is_openclaw_clear_reply(text: str) -> bool:
        return _is_openclaw_clear_reply(text)

    @staticmethod
    def _looks_like_openclaw_compact_value(text: str) -> bool:
        from armorer.core.prompt_parsing import _looks_like_openclaw_compact_value
        return _looks_like_openclaw_compact_value(text)

    @classmethod
    def _extract_openclaw_model_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_model_reply_value
        return _extract_openclaw_model_reply_value(text)

    @classmethod
    def _extract_openclaw_provider_key_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_provider_key_reply_value
        return _extract_openclaw_provider_key_reply_value(text)

    @classmethod
    def _extract_openclaw_telegram_admin_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_telegram_admin_reply_value
        return _extract_openclaw_telegram_admin_reply_value(text)

    @classmethod
    def _extract_openclaw_alert_chat_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_alert_chat_reply_value
        return _extract_openclaw_alert_chat_reply_value(text)

    @classmethod
    def _extract_openclaw_telegram_token_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_telegram_token_reply_value
        return _extract_openclaw_telegram_token_reply_value(text)

    @classmethod
    def _extract_openclaw_base_url_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_base_url_reply_value
        return _extract_openclaw_base_url_reply_value(text)

    @classmethod
    def _extract_openclaw_armory_skills_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_armory_skills_reply_value
        return _extract_openclaw_armory_skills_reply_value(text)

    @classmethod
    def _extract_openclaw_skill_paths_reply_value(cls, text: str) -> str:
        from armorer.core.prompt_parsing import _extract_openclaw_skill_paths_reply_value
        return _extract_openclaw_skill_paths_reply_value(text)

    @staticmethod
    def _parse_csv_values(text: str) -> list[str]:
        return _parse_csv_values(text)

    @staticmethod
    def _is_valid_telegram_id(token: str) -> bool:
        return _is_valid_telegram_id(token)

    def _parse_telegram_admin_ids(self, text: str) -> tuple[list[str], list[str]]:
        return tg_setup_mod._parse_telegram_admin_ids(text)

    def _allow_pending_openclaw_reply_through_focus_gate(self, pending: dict, response: str) -> bool:
        return oc_setup_mod._allow_pending_openclaw_reply_through_focus_gate(self, pending, response)

    # ------------------------------------------------------------------
    # Focus / decision delegation
    # ------------------------------------------------------------------

    def _risk_label_for_prompt(self, prompt: str) -> str:
        return focus_mod._risk_label_for_prompt(prompt)

    def _find_referenced_apps(self, prompt: str, app_names: list[str]) -> list[str]:
        return focus_mod._find_referenced_apps(prompt, app_names)

    def _extract_focus_target(self, prompt: str) -> str:
        return focus_mod._extract_focus_target(self, prompt)

    @staticmethod
    def _is_assistant_setup_config_related(prompt: str) -> bool:
        return focus_mod._is_assistant_setup_config_related(prompt)

    def _focus_mode_preflight(self, conversation_id: str, prompt: str) -> tuple[str, dict | None]:
        return focus_mod._focus_mode_preflight(self, conversation_id, prompt)

    def _secure_decision_preflight(self, conversation_id: str, prompt: str) -> tuple[str, dict | None]:
        return focus_mod._secure_decision_preflight(self, conversation_id, prompt)

    # ------------------------------------------------------------------
    # Telegram setup delegation
    # ------------------------------------------------------------------

    @staticmethod
    def _looks_like_telegram_setup_request(prompt: str) -> bool:
        return tg_setup_mod._looks_like_telegram_setup_request(prompt)

    @staticmethod
    def _looks_like_question(text: str) -> bool:
        return tg_setup_mod._looks_like_question(text)

    @staticmethod
    def _is_telegram_setup_question(text: str) -> bool:
        return tg_setup_mod._is_telegram_setup_question(text)

    @staticmethod
    def _is_clearly_unrelated_to_telegram_setup(text: str) -> bool:
        return tg_setup_mod._is_clearly_unrelated_to_telegram_setup(text)

    @staticmethod
    def _is_clearly_unrelated_to_openclaw_setup(text: str) -> bool:
        return tg_setup_mod._is_clearly_unrelated_to_openclaw_setup(text)

    def _handle_telegram_setup_flow(self, source: str, conversation_id: str, prompt: str) -> tuple[str, dict | None]:
        return tg_setup_mod._handle_telegram_setup_flow(self, source, conversation_id, prompt)

    # ------------------------------------------------------------------
    # OpenClaw setup delegation
    # ------------------------------------------------------------------

    @staticmethod
    def _looks_like_openclaw_setup_request(prompt: str) -> bool:
        return oc_setup_mod._looks_like_openclaw_setup_request(prompt)

    @staticmethod
    def _provider_choices() -> list[str]:
        return oc_setup_mod._provider_choices()

    @classmethod
    def _normalize_openclaw_provider_choice(cls, value: str) -> str:
        return oc_setup_mod._normalize_openclaw_provider_choice(value)

    @classmethod
    def _extract_openclaw_provider_choice(cls, text: str) -> str:
        return oc_setup_mod._extract_openclaw_provider_choice(text)

    @classmethod
    def _mentions_openclaw_provider_keyword(cls, text: str) -> bool:
        return oc_setup_mod._mentions_openclaw_provider_keyword(text)

    @classmethod
    def _extract_openclaw_alias_choice(cls, text: str, available: list[str]) -> str:
        return oc_setup_mod._extract_openclaw_alias_choice(text, available)

    def _list_apps_with_openclaw_fallback(self):
        return oc_setup_mod._list_apps_with_openclaw_fallback(self)

    def _get_openclaw_app_metadata_with_fallback(self, name: str):
        return oc_setup_mod._get_openclaw_app_metadata_with_fallback(self, name)

    def _handle_openclaw_setup_flow(self, source: str, conversation_id: str, prompt: str, task_id: str) -> tuple[str, dict | None]:
        return oc_setup_mod._handle_openclaw_setup_flow(self, source, conversation_id, prompt, task_id)

    def _sanitize_openclaw_setup_detail_text(self, text: str) -> str:
        return oc_setup_mod._sanitize_openclaw_setup_detail_text(self, text)

    @staticmethod
    def _looks_like_agent_setup_request(prompt: str) -> bool:
        return oc_setup_mod._looks_like_agent_setup_request(prompt)

    def _setup_security_preflight(self, conversation_id: str, prompt: str) -> tuple[str, dict | None]:
        return oc_setup_mod._setup_security_preflight(self, conversation_id, prompt)

    # ------------------------------------------------------------------
    # Guard delegation
    # ------------------------------------------------------------------

    def _send_guard_notification(self, chat_id: str, text: str, reply_markup: dict | None = None) -> None:
        guard_mod._send_guard_notification(self, chat_id, text, reply_markup)

    def _send_guard_document(self, chat_id: str, file_path: Path, caption: str | None = None) -> None:
        guard_mod._send_guard_document(self, chat_id, file_path, caption)

    def _guard_scan_operator_enabled(self) -> bool:
        return guard_mod._guard_scan_operator_enabled(self)

    def _handle_guard_command(self, prompt: str) -> str:
        return guard_mod._handle_guard_command(self, prompt)

    def _handle_urls_command(self, prompt: str) -> str:
        from armorer.web.services import urls_summary

        parts = [part for part in str(prompt or "").strip().split() if part]
        target = parts[1] if len(parts) > 1 else ""
        payload = urls_summary(target)
        if not payload.get("ok"):
            return str(payload.get("message") or "Armorer could not build the URL list.")

        lines = ["Armorer URLs"]
        for item in payload.get("items", []) or []:
            if not isinstance(item, dict):
                continue
            label = str(item.get("label") or "Link").strip()
            url = str(item.get("url") or "").strip()
            note = str(item.get("note") or "").strip()
            if not url:
                continue
            line = f"- {label}: {url}"
            if note:
                line += f" ({note})"
            lines.append(line)
        for note in payload.get("notes", []) or []:
            text = str(note or "").strip()
            if text:
                lines.append(f"- Note: {text}")
        if len(lines) == 1:
            lines.append("- No URLs are available yet.")
        return "\n".join(lines)

    def _handle_status_command(self) -> str:
        from armorer.web.services import installed_agents_summary, status_summary

        status = status_summary()
        installed = installed_agents_summary()
        lines = ["Armorer status"]
        llm = status.get("llm", {}) if isinstance(status, dict) else {}
        provider = str(llm.get("provider") or "not configured").strip() or "not configured"
        model = str(llm.get("model") or "").strip()
        lines.append(f"- Core: {'running' if status.get('core_running') else 'stopped'}")
        lines.append(f"- Telegram gateway: {'running' if status.get('telegram_gateway_running') else 'stopped'}")
        lines.append(f"- Model: {provider}{f' / {model}' if model else ''}")
        for item in installed.get("agents", []) or []:
            if not isinstance(item, dict):
                continue
            alias = str(item.get("alias") or item.get("agent_id") or "agent").strip()
            summary = str(((item.get("status") or {}) if isinstance(item.get("status"), dict) else {}).get("summary") or "").strip()
            state = str(((item.get("status") or {}) if isinstance(item.get("status"), dict) else {}).get("state") or "").strip()
            detail = summary or state or "status unavailable"
            lines.append(f"- {alias}: {detail}")
        return "\n".join(lines)

    @staticmethod
    def _handle_help_command() -> str:
        return "\n".join(
            [
                "Armorer Telegram commands",
                "- /urls: show Armorer and agent URLs",
                "- /urls openclaw: show URLs for a specific agent",
                "- /status: show Armorer and agent health",
                "- /help: show this command list",
            ]
        )

    @staticmethod
    def _looks_like_runtime_access_question(prompt: str) -> bool:
        return looks_like_runtime_retrieval(prompt)

    def _runtime_access_context(self, prompt: str) -> str:
        if not self._looks_like_runtime_access_question(prompt):
            return ""
        try:
            snapshot = build_runtime_state_snapshot()
            return json.dumps(snapshot, sort_keys=True)
        except Exception as exc:
            return json.dumps(
                {
                    "status": {"ok": False, "message": f"Failed to load runtime status: {exc}"},
                    "urls": {"ok": False, "message": f"Failed to load runtime URL state: {exc}"},
                    "installed_agents": {"ok": False, "message": f"Failed to load installed agent state: {exc}"},
                    "recent_jobs": [],
                },
                sort_keys=True,
            )

    def _build_run_context(self, *, task_id: str, source: str, conversation_id: str, prompt: str) -> ArmorerRunContext:
        runtime_snapshot = build_runtime_state_snapshot() if self._looks_like_runtime_access_question(prompt) else {}
        installed = runtime_snapshot.get("installed_agents", {}) if isinstance(runtime_snapshot, dict) else {}
        installed_agents_cache = list(installed.get("agents", []) or []) if isinstance(installed, dict) else []
        return ArmorerRunContext(
            session_id=f"armorer-core-{task_id}",
            task_id=task_id,
            source=source,
            conversation_id=conversation_id,
            prompt=prompt,
            runtime_snapshot=runtime_snapshot if isinstance(runtime_snapshot, dict) else {},
            memory_facts=recall_relevant_memory(prompt=prompt, installed_agents=installed_agents_cache),
            installed_agents_cache=installed_agents_cache,
            policy_flags={
                "runtime_access_question": self._looks_like_runtime_access_question(prompt),
                "task_type": classify_task_type(prompt),
            },
        )

    def _route_execution_mode(self, run_context: ArmorerRunContext) -> RouteDecision:
        direct = deterministic_route(run_context.prompt)
        if direct is not None:
            return direct

        router_prompt = (
            "Choose the best execution mode for this Armorer task.\n"
            "Use 'retrieval' for status, URLs, dashboards, ports, access links, and current state.\n"
            "Use 'workflow' for bounded operational tasks like install, run, stop, configure, pair, or setup.\n"
            "Use 'agent' for open-ended debugging, investigation, or repair.\n"
            "Return only the structured decision.\n\n"
            f"User request: {run_context.prompt}\n"
            f"Runtime access question: {run_context.policy_flags.get('runtime_access_question')}\n"
        )
        agent = BaseAgent()
        agent.init_agent(
            system_prompt=(
                "You are Armorer's execution router. "
                "Classify requests into retrieval, workflow, or agent. "
                "Prefer the simplest reliable mode. "
                "Do not answer the user's question."
            ),
            tools=[],
            session_id=f"{run_context.session_id}-router",
            agent_name="armorer:core:router",
        )
        try:
            decision = agent.run(
                user_input=router_prompt,
                structured_output_model=RouteDecision,
            )
        except Exception as exc:
            logger.debug("Router agent failed, falling back to agent mode: %s", exc)
            return RouteDecision(
                mode=ExecutionMode.AGENT,
                reason=f"Router fallback due to classifier error: {exc}",
                confidence=0.0,
                required_tool_groups=["runtime_state", "lifecycle", "debug"],
            )

        if isinstance(decision, RouteDecision):
            return decision
        if isinstance(decision, dict):
            try:
                return RouteDecision(**decision)
            except ValidationError:
                pass
        return RouteDecision(
            mode=ExecutionMode.AGENT,
            reason="Router returned an unusable decision; defaulting to agent mode.",
            confidence=0.0,
            required_tool_groups=["runtime_state", "lifecycle", "debug"],
        )

    @staticmethod
    def _tools_for_mode(tools: list[object], mode: ExecutionMode) -> list[object]:
        allowed_categories = {
            ExecutionMode.RETRIEVAL: {
                "runtime_state",
                "setup_state",
                "memory",
                "knowledge",
                "reasoning",
                "system_io",
                "interaction",
                "specialist",
            },
            ExecutionMode.WORKFLOW: {
                "runtime_state",
                "setup_state",
                "memory",
                "knowledge",
                "workflow",
                "reasoning",
                "system_io",
                "lifecycle",
                "interaction",
                "specialist",
            },
            ExecutionMode.AGENT: {
                "runtime_state",
                "setup_state",
                "memory",
                "knowledge",
                "workflow",
                "reasoning",
                "system_io",
                "lifecycle",
                "interaction",
                "specialist",
            },
        }
        requested = allowed_categories.get(mode, allowed_categories[ExecutionMode.AGENT])
        return filter_tools_by_category(tools, *sorted(requested))

    def _build_agent_guard_context(self) -> str:
        return guard_mod._build_agent_guard_context(self)

    def _notify_agent_guard_alert(self, app_name: str, file_path: str, snippet: str, reasons: list[str], confidence: float):
        guard_mod._notify_agent_guard_alert(self, app_name, file_path, snippet, reasons, confidence)

    def _auto_steer_enabled(self, source: str = "otel") -> bool:
        return guard_mod._auto_steer_enabled(self, source)

    def _auto_steer_agent(self, app_name: str, trigger: str, snippet: str) -> str:
        return guard_mod._auto_steer_agent(self, app_name, trigger, snippet)

    def _inject_runtime_steer_note(self, app_name: str, trigger: str) -> int:
        return guard_mod._inject_runtime_steer_note(self, app_name, trigger)

    # ------------------------------------------------------------------
    # OTEL delegation
    # ------------------------------------------------------------------

    def _load_otel_offsets(self) -> dict[str, int]:
        return otel_mod._load_otel_offsets(self)

    def _save_otel_offsets(self) -> None:
        otel_mod._save_otel_offsets(self)

    def _poll_otel_events(self) -> None:
        otel_mod._poll_otel_events(self)

    # ------------------------------------------------------------------
    # Daily report delegation
    # ------------------------------------------------------------------

    def _maybe_send_daily_report(self) -> None:
        daily_mod._maybe_send_daily_report(self)

    # ------------------------------------------------------------------
    # OpenClaw setup delegation
    # ------------------------------------------------------------------

    def _execute_openclaw_setup(self, state: dict, task_id: str) -> dict:
        return oc_setup_mod._execute_openclaw_setup(self, state, task_id)

    def _run_openclaw_setup_command(
        self,
        args: list[str],
        *,
        task_id: str,
        label: str,
        cwd=None,
        extra_env=None,
        retries: int = 1,
        timeout_seconds: int = 300,
    ):
        return oc_setup_mod._run_openclaw_setup_command(
            self, args, task_id=task_id, label=label, cwd=cwd,
            extra_env=extra_env, retries=retries, timeout_seconds=timeout_seconds,
        )

    @staticmethod
    def _redact_openclaw_command_args(run_args: tuple[str, ...]) -> str:
        return oc_setup_mod._redact_openclaw_command_args(run_args)

    def _run_openclaw_telegram_verification_step(
        self, task_id: str, label: str, call, *, retries: int = 3,
    ):
        return oc_setup_mod._run_openclaw_telegram_verification_step(
            self, task_id, label, call, retries=retries,
        )

    def _openclaw_rollback_summary_for_snapshot(self, snapshot: dict) -> str:
        return oc_setup_mod._openclaw_rollback_summary_for_snapshot(self, snapshot)

    def _resolve_openclaw_runtime_settings(self, state: dict) -> dict:
        return oc_setup_mod._resolve_openclaw_runtime_settings(self, state)

    def _existing_openclaw_injected_skills(self, app_name: str) -> list[str]:
        return oc_setup_mod._existing_openclaw_injected_skills(self, app_name)

    def _existing_openclaw_telegram_state(self, app_name: str = "openclaw") -> dict:
        return oc_setup_mod._existing_openclaw_telegram_state(self, app_name)

    def _handle_openclaw_setup_flow(
        self, source: str, conversation_id: str, prompt: str, task_id: str,
    ) -> tuple[str, dict | None]:
        return oc_setup_mod._handle_openclaw_setup_flow(
            self, source, conversation_id, prompt, task_id,
        )

    @staticmethod
    def _assert_openclaw_runtime_prerequisites(runtime: dict) -> None:
        return oc_setup_mod._assert_openclaw_runtime_prerequisites(runtime)

    def _resolve_openclaw_runtime_compose_dir(self, app_name: str):
        return oc_setup_mod._resolve_openclaw_runtime_compose_dir(self, app_name)

    def _openclaw_registry_entry_matches_runtime(self, app_name: str, compose_dir) -> bool:
        return oc_setup_mod._openclaw_registry_entry_matches_runtime(self, app_name, compose_dir)

    def _resolved_openclaw_armory_skill_names(self, state: dict, app_name: str) -> list[str]:
        return oc_setup_mod._resolved_openclaw_armory_skill_names(self, state, app_name)

    def _sync_openclaw_runtime_settings(self, compose_dir, state: dict) -> dict:
        return oc_setup_mod._sync_openclaw_runtime_settings(self, compose_dir, state)

    def _verify_openclaw_setup(self, app_name: str, state: dict, task_id: str) -> str:
        return oc_setup_mod._verify_openclaw_setup(self, app_name, state, task_id)

    def _sync_openclaw_armory_skills(
        self, compose_dir, app_name: str, skill_names: list[str], *, clear_existing: bool = False,
    ) -> list[str]:
        return oc_setup_mod._sync_openclaw_armory_skills(
            self, compose_dir, app_name, skill_names, clear_existing=clear_existing,
        )

    def _recover_openclaw_registry_entry(self, app_name: str, task_id: str):
        return oc_setup_mod._recover_openclaw_registry_entry(self, app_name, task_id)

    def _openclaw_compose_run(self, compose_dir, task_id: str, *run_args: str, timeout_seconds: int = 120):
        return oc_setup_mod._openclaw_compose_run(self, compose_dir, task_id, *run_args, timeout_seconds=timeout_seconds)

    @staticmethod
    def _openclaw_health_check_unavailable(error: Exception) -> bool:
        return oc_setup_mod._openclaw_health_check_unavailable(error)

    def _openclaw_registry_has_health_check(self, app_name: str) -> bool:
        return oc_setup_mod._openclaw_registry_has_health_check(self, app_name)

    def _reprompt_openclaw_text_step(
        self, conversation_id: str, step: str, state: dict,
        *, message: str, prompt_message: str, default: str | None = None,
    ) -> tuple[str, dict]:
        return oc_setup_mod._reprompt_openclaw_text_step(
            self, conversation_id, step, state,
            message=message, prompt_message=prompt_message, default=default,
        )

    def _verify_openclaw_telegram_channel(self, app_name: str, state: dict, runtime: dict, task_id: str):
        return oc_setup_mod._verify_openclaw_telegram_channel(self, app_name, state, runtime, task_id)

    # ------------------------------------------------------------------
    # Task context management
    # ------------------------------------------------------------------

    def _conversation_id_for_task(self, source: str, task: dict) -> str:
        meta = task.get("metadata", {}) if isinstance(task.get("metadata"), dict) else {}
        return self.guard.conversation_id(source=source, metadata=meta)

    def _find_newer_matching_inbox_task(self, task_path: Path, source: str, task: dict) -> dict[str, str] | None:
        conversation_id = self._conversation_id_for_task(source, task)
        current_task_id = str(task.get("id") or task_path.stem)
        try:
            current_timestamp = float(task.get("timestamp") or 0.0)
        except Exception:
            current_timestamp = 0.0

        newer: dict[str, str] | None = None
        for candidate in self.inbox.glob("*.json"):
            if candidate == task_path:
                continue
            try:
                loaded = json.loads(candidate.read_text())
            except Exception:
                continue
            if not isinstance(loaded, dict):
                continue
            candidate_source = str(loaded.get("source") or "cli")
            if self._conversation_id_for_task(candidate_source, loaded) != conversation_id:
                continue
            candidate_id = str(loaded.get("id") or candidate.stem)
            try:
                candidate_timestamp = float(loaded.get("timestamp") or 0.0)
            except Exception:
                candidate_timestamp = 0.0
            if candidate_timestamp < current_timestamp:
                continue
            if candidate_timestamp == current_timestamp and candidate_id <= current_task_id:
                continue
            if newer is None or (candidate_timestamp, candidate_id) > (float(newer["timestamp"]), newer["task_id"]):
                newer = {"task_id": candidate_id, "timestamp": str(candidate_timestamp)}
        return newer

    def _register_active_task_context(self, task_id: str, task_path: Path, source: str, task: dict) -> None:
        self._active_task_contexts[str(task_id)] = {
            "task_path": Path(task_path),
            "source": str(source),
            "task": dict(task),
        }

    def _clear_active_task_context(self, task_id: str) -> None:
        self._active_task_contexts.pop(str(task_id), None)

    def _raise_if_task_superseded(self, task_id: str) -> None:
        ctx = self._active_task_contexts.get(str(task_id))
        if not isinstance(ctx, dict):
            return
        task_path = ctx.get("task_path")
        source = str(ctx.get("source") or "cli")
        task = ctx.get("task")
        if not isinstance(task_path, Path) or not isinstance(task, dict):
            return
        newer = self._find_newer_matching_inbox_task(task_path, source, task)
        if newer is None:
            return
        raise SupersededTaskError(
            "Skipped this setup because a newer request from the same chat/session was submitted. "
            f"Continuing with the latest task `{newer['task_id']}`."
        )

    # ------------------------------------------------------------------
    # Watchdog event methods
    # ------------------------------------------------------------------

    def on_created(self, event):
        """Handle new commands appearing in the inbox."""
        if not event.is_directory and str(event.src_path).endswith(".json"):
            if "inbox" in str(event.src_path):
                self._process_inbox_task(event.src_path)

    def on_moved(self, event):
        """Handle atomically published inbox tasks."""
        dest_path = getattr(event, "dest_path", "")
        if not event.is_directory and str(dest_path).endswith(".json"):
            if "inbox" in str(dest_path):
                self._process_inbox_task(dest_path)

    def on_modified(self, event):
        """Handle security oversight for agent files."""
        if event.is_directory or "inbox" in str(event.src_path) or "outbox" in str(event.src_path) or "status" in str(event.src_path):
            return

        file_path = Path(event.src_path)
        app = self._identify_app(file_path)

        if not app or not self._is_monitor_target(file_path, app):
            return

        current_time = time.time()
        if event.src_path in self.last_processed and current_time - self.last_processed[event.src_path] < 1.5:
            return

        self.last_processed[event.src_path] = current_time
        self._inspect_and_sanitize(event.src_path, app)

    # ------------------------------------------------------------------
    # Path translation
    # ------------------------------------------------------------------

    def _translate_to_container_path(self, host_path: str) -> str:
        if not os.path.exists('/.dockerenv'):
            return host_path
        if "/installed/" in host_path:
            parts = host_path.split("/installed/", 1)
            return f"/app/installed/{parts[1]}"
        return host_path

    # ------------------------------------------------------------------
    # Main dispatch: _process_inbox_task
    # ------------------------------------------------------------------

    def _process_inbox_task(self, task_path: str):
        """Execute a command received from an interface (CLI/Signal)."""
        if self.is_processing:
            return

        task_file = Path(task_path)
        if not task_file.exists():
            return

        self.is_processing = True
        task_id: str | None = None
        source = "cli"
        status_file = None

        try:
            try:
                with open(task_path, "r") as f:
                    loaded = json.load(f)
            except FileNotFoundError:
                self._unreadable_task_retries.pop(str(task_file), None)
                return
            except json.JSONDecodeError as e:
                self._handle_unreadable_task_json(task_file, str(task_path), e)
                return
            task = loaded if isinstance(loaded, dict) else {}
            self._unreadable_task_retries.pop(str(task_file), None)
            task_id = str(task.get("id") or task_file.stem or str(uuid.uuid4()))
            source = str(task.get("source") or "cli")

            sig_ok, sig_reason = self._verify_task_signature(task)
            if not sig_ok:
                self._audit("task_rejected_signature", task_id=task_id, reason=sig_reason, source=source)
                logger.warning("Rejected task '%s' due to signature verification failure (%s).", task_id, sig_reason)
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump(
                        {
                            "id": task_id,
                            "response": json.dumps({"message": "Request rejected by security policy (signature verification failed)."}),
                            "timestamp": time.time(),
                        },
                        f,
                        indent=4,
                    )
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                return

            prompt = str(task.get("prompt") or "")
            conversation_id = self._conversation_id_for_task(source, task)
            self._register_active_task_context(str(task_id), task_file, source, task)
            superseding_task = self._find_newer_matching_inbox_task(task_file, source, task)

            if superseding_task is not None:
                result_path = self.outbox / f"{task_id}.json"
                payload = {
                    "message": (
                        "Skipped this request because a newer request from the same chat/session was submitted. "
                        f"Continuing with the latest task `{superseding_task['task_id']}`."
                    )
                }
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit(
                    "task_superseded",
                    task_id=task_id,
                    source=source,
                    conversation_id=conversation_id,
                    superseding_task_id=superseding_task["task_id"],
                )
                return

            status_file = self.status_dir / f"{task_id}.json"
            logger.info(f"📨  CORE: Processing task '{task_id}' from {source}")
            self._audit("task_processing_started", task_id=task_id, source=source)

            self._unlink_if_exists(status_file)

            if prompt.strip().startswith("/guard "):
                cmd_result = self._handle_guard_command(prompt)
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps({"message": cmd_result}), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                logger.info(f"✅ CORE: Guard command processed for task '{task_id}'.")
                self._audit("task_completed_guard_command", task_id=task_id, source=source)
                return

            normalized_prompt = prompt.strip()
            if normalized_prompt == "/urls" or normalized_prompt.startswith("/urls "):
                cmd_result = self._handle_urls_command(prompt)
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps({"message": cmd_result}), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_completed_urls_command", task_id=task_id, source=source)
                return

            if normalized_prompt == "/status":
                cmd_result = self._handle_status_command()
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps({"message": cmd_result}), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_completed_status_command", task_id=task_id, source=source)
                return

            if normalized_prompt == "/help" or normalized_prompt == "/start":
                cmd_result = self._handle_help_command()
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps({"message": cmd_result}), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_completed_help_command", task_id=task_id, source=source)
                return

            focus_prompt, focus_payload = self._focus_mode_preflight(
                conversation_id=conversation_id,
                prompt=prompt,
            )
            if focus_payload is not None:
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(focus_payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_paused_focus_mode", task_id=task_id, source=source, conversation_id=conversation_id)
                return
            prompt = focus_prompt

            setup_prompt, setup_payload = self._handle_telegram_setup_flow(
                source=source,
                conversation_id=conversation_id,
                prompt=prompt,
            )
            if setup_payload is not None:
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(setup_payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_handled_setup_flow", task_id=task_id, source=source, conversation_id=conversation_id)
                return
            prompt = setup_prompt

            openclaw_prompt, openclaw_payload = self._handle_openclaw_setup_flow(
                source=source,
                conversation_id=conversation_id,
                prompt=prompt,
                task_id=str(task_id),
            )
            if openclaw_payload is not None:
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(openclaw_payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_handled_openclaw_setup_flow", task_id=task_id, source=source, conversation_id=conversation_id)
                return
            prompt = openclaw_prompt

            setup_sec_prompt, setup_sec_payload = self._setup_security_preflight(
                conversation_id=conversation_id,
                prompt=prompt,
            )
            if setup_sec_payload is not None:
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(setup_sec_payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_paused_setup_security_profile", task_id=task_id, source=source, conversation_id=conversation_id)
                return
            prompt = setup_sec_prompt

            preflight_prompt, preflight_payload = self._secure_decision_preflight(conversation_id=conversation_id, prompt=prompt)
            if preflight_payload is not None:
                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(preflight_payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_paused_secure_decision", task_id=task_id, source=source, conversation_id=conversation_id)
                return
            prompt = preflight_prompt

            if source in {"cli", "telegram", "signal"} and not self._guard_scan_operator_enabled():
                in_scan = self.guard.inspect_operator(prompt)
            else:
                in_scan = self.guard.inspect_input(prompt)
            self.guard.append_history(conversation_id=conversation_id, role="user", text=prompt, inspection=in_scan)
            meta = task.get("metadata", {}) if isinstance(task.get("metadata"), dict) else {}
            alert_chat = self.config_manager.get_key("TELEGRAM_ALERT_CHAT_ID") or str(meta.get("channel_id", ""))
            if in_scan.suspicious:
                self.guard.notify_suspicious(
                    channel_id=str(alert_chat),
                    conversation_id=conversation_id,
                    source=source,
                    snippet=prompt,
                    reasons=in_scan.reasons,
                    confidence=in_scan.confidence,
                )
            control_note = None
            effective_prompt = in_scan.sanitized_text
            blocked = False
            if not (source in {"cli", "telegram", "signal"} and not self._guard_scan_operator_enabled()):
                should_block, block_reason = self.guard.should_block_input(in_scan)
                if should_block:
                    result_path = self.outbox / f"{task_id}.json"
                    payload = {"message": block_reason or "Blocked by guard policy."}
                    with open(result_path, "w") as f:
                        json.dump({"id": task_id, "response": json.dumps(payload), "timestamp": time.time()}, f, indent=4)
                    self.last_processed[str(task_path)] = time.time()
                    self._unlink_if_exists(task_path)
                    self._audit("task_blocked_guard", task_id=task_id, reason=block_reason, conversation_id=conversation_id)
                    logger.warning("Task blocked by guard input policy for %s", conversation_id)
                    return

                blocked, effective_prompt, control_note = self.guard.apply_controls(conversation_id, in_scan.sanitized_text)
            if blocked:
                result_path = self.outbox / f"{task_id}.json"
                payload = {"message": control_note or "Blocked by guard policy."}
                with open(result_path, "w") as f:
                    json.dump({"id": task_id, "response": json.dumps(payload), "timestamp": time.time()}, f, indent=4)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                logger.warning("Task blocked by guard policy for %s", conversation_id)
                return

            run_context = self._build_run_context(
                task_id=str(task_id),
                source=source,
                conversation_id=conversation_id,
                prompt=effective_prompt,
            )
            route_decision = self._route_execution_mode(run_context)
            self._audit(
                "task_route_selected",
                task_id=task_id,
                source=source,
                conversation_id=conversation_id,
                mode=route_decision.mode.value,
                reason=route_decision.reason,
                confidence=route_decision.confidence,
                tool_groups=list(route_decision.required_tool_groups or []),
                task_type=str(run_context.policy_flags.get("task_type") or ""),
                runtime_access_question=bool(run_context.policy_flags.get("runtime_access_question")),
                memory_fact_count=len(run_context.memory_facts),
                installed_agent_count=len(run_context.installed_agents_cache),
            )

            if route_decision.mode == ExecutionMode.WORKFLOW:
                from armorer.agent import shell, file_write, file_read
                from armorer.tools.knowledge import search_knowledge, get_knowledge_topic, list_knowledge_topics
                from armorer.tools.memory_tools import list_durable_memory, recall_memory_facts, store_verified_memory
                from armorer.tools.orchestrator import armorer_cli, install_agent, list_agents, run_agent, stop_agent, uninstall_agent
                from armorer.tools.runtime_state_tools import (
                    get_agent_setup_state, get_dashboard_urls, get_installed_agents_snapshot,
                    get_recent_jobs_snapshot, get_runtime_access_snapshot, get_status_snapshot,
                )
                from armorer.tools.workflow import workflow
                from armorer.tools.think import think

                workflow_tools = [
                    redact_secret, update_app_metadata, file_read, file_write, shell,
                    install_agent, run_agent, stop_agent, uninstall_agent, list_agents, armorer_cli,
                    get_runtime_access_snapshot, get_status_snapshot, get_installed_agents_snapshot,
                    get_dashboard_urls, get_recent_jobs_snapshot, get_agent_setup_state,
                    recall_memory_facts, list_durable_memory, store_verified_memory,
                    search_knowledge, get_knowledge_topic, list_knowledge_topics,
                    workflow, think,
                ]
                categorized_workflow_tools = filter_tools_by_category(
                    workflow_tools,
                    "system_io",
                    "runtime_state",
                    "setup_state",
                    "memory",
                    "knowledge",
                    "workflow",
                    "reasoning",
                    "lifecycle",
                )
                payload = execute_bounded_workflow(
                    task_id=str(task_id),
                    prompt=effective_prompt,
                    verification_ledger=run_context.verification_ledger,
                    tools=categorized_workflow_tools or workflow_tools,
                )

                if control_note:
                    payload["message"] = f"{control_note}\n\n{payload.get('message', '')}".strip()

                if source in {"cli", "telegram", "signal"} and not self._guard_scan_operator_enabled():
                    out_scan = self.guard.inspect_operator(payload.get("message", ""))
                else:
                    out_scan = self.guard.inspect_output(payload.get("message", ""))
                payload["message"] = out_scan.sanitized_text
                self.guard.append_history(
                    conversation_id=conversation_id,
                    role="assistant",
                    text=payload.get("message", ""),
                    inspection=out_scan,
                )

                result_path = self.outbox / f"{task_id}.json"
                with open(result_path, "w") as f:
                    json.dump({
                        "id": task_id,
                        "response": json.dumps(payload),
                        "timestamp": time.time()
                    }, f, indent=4)

                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
                self._audit("task_completed_workflow", task_id=task_id, source=source, conversation_id=conversation_id)
                logger.info(f"✅ CORE: Workflow task '{task_id}' completed.")
                return

            apps = self._list_apps_with_openclaw_fallback()
            apps_context = ""
            guard_context = self._build_agent_guard_context()
            runtime_access_context = json.dumps(run_context.runtime_snapshot, sort_keys=True) if run_context.runtime_snapshot else ""
            if apps:
                apps_context = "Registered Applications:\n"
                for app in apps:
                    container_path = self._translate_to_container_path(app.install_path)
                    apps_context += f"- {app.name} at {container_path}\n"
                    apps_context += f"  Monitoring mode: {getattr(app, 'monitoring_mode', 'file')}\n"
                    if app.monitor_targets:
                        apps_context += f"  Monitor targets: {app.monitor_targets}\n"

            system_prompt = f"""
            You are the Armorer Core, the autonomous brain of the Secure AI Agent Orchestrator.
            
            Current System Context:
            {apps_context}
            Guard Directives For Managed Agents:
            {guard_context}
            Execution Mode:
            - mode: {route_decision.mode.value}
            - reason: {route_decision.reason or "No additional routing rationale recorded."}
            - confidence: {route_decision.confidence:.2f}
            - tool groups: {", ".join(route_decision.required_tool_groups) or "default"}
            Runtime Access Snapshot:
            {runtime_access_context or "No focused runtime access snapshot was attached for this request."}
            Durable Memory Facts:
            {json.dumps(run_context.memory_facts, sort_keys=True) if run_context.memory_facts else "No relevant durable memory facts were attached for this request."}
            
            YOUR MISSION:
            Provide high-level, intelligent management of all AI agents. You are not just a tool-caller; you are a problem-solver.
            
            YOUR CAPABILITIES:
            1. FULL AGENT LIFECYCLE: You can start, stop, restart, configure, and audit agents.
            2. CLI INTEGRATION: Use dedicated lifecycle tools first:
               - `install_agent(agent, alias?, skip_scan?)`
               - `run_agent(agent, mode='background', command?)`
               - `stop_agent(agent)`
               - `uninstall_agent(agent, force=True)`
               - `list_agents()`
               - `armorer_cli(command)` for any other Armorer CLI command not covered above.
               Use the generic `shell` tool only for lower-level debugging.
            3. SYSTEM ARCHITECT: You understand Docker Compose and environment configurations. Use your tools to solve user requests independently.
            4. KNOWLEDGE ON-DEMAND: You have access to a Knowledge Base via tools. If you need information on Docker, security, or specific agents like OpenClaw, use `search_knowledge` or `get_knowledge_topic`.
            5. OPENCLAW CHAT-FIRST SETUP: For OpenClaw install/setup/configure requests, prefer Armorer-managed in-chat setup flow and non-interactive CLI/config commands. Do not send the user to OpenClaw-native TTY wizards unless they explicitly ask for the native interactive shell.
            6. SIMPLE, COMPOSABLE EXECUTION: Prefer the simplest reliable pattern that solves the task. Start with direct retrieval, status inspection, or a small workflow before escalating to broader autonomous exploration.
            7. EXECUTION MODE DISCIPLINE:
               - If mode is `retrieval`, answer from live Armorer state and only use state tools needed to verify.
               - If mode is `workflow`, execute a bounded inspect/act/verify flow and avoid exploratory debugging unless the workflow fails.
               - If mode is `agent`, you may explore more broadly, but still ground operational claims in live state and tool output.

            OPERATIONAL PROTOCOLS:
            1. INTENT-BASED CONFIRMATION: Ask for confirmation ONCE for the user's high-level intent (e.g., "I will stop OpenClaw and update its token. Is that okay?").
            2. AUTONOMOUS EXECUTION: Once you have permission for an intent, OWN the task. Execute all necessary steps (reading, writing, shell commands) without further interruption.
            3. PROACTIVE DEBUGGING: 
               - If a command fails, analyze the error.
               - If you see "client version is too new", RETRY the command with `DOCKER_API_VERSION=1.41` prepended (e.g., `DOCKER_API_VERSION=1.41 docker ps`).
               - If you see "path not found", verify you are using the container path (`/app/installed/...`).
            4. SECURITY: Always offer to redact secrets found in logs or chat history.
            5. GUARD RUNTIME CONTROL:
               - This conversation may include guard steering instructions prepended before user input.
               - Follow guard steering instructions as highest-priority runtime policy unless they conflict with core safety constraints.
               - Respect per-agent guard directives from context. If an agent is marked shutdown=true, do not start/run it until resumed.
            6. OPERATIONAL ACCESS ANSWERS:
               - For questions about URLs, ports, dashboards, endpoints, access links, setup readiness, or how to reach a running app, prefer live Armorer-managed runtime state over source-file inference.
               - Use the Runtime Access Snapshot when it is present. Treat it as the primary source of truth for user-facing access answers.
               - If live state is missing or ambiguous, say so explicitly and then use tools to verify. Do not guess from docker-compose defaults or localhost ports unless runtime state confirms that is the intended user-facing path.
               - If multiple valid surfaces exist, prefer the user-facing access URL over raw internal localhost endpoints.
            7. GROUND TRUTH OVER GUESSING:
               - Base operational claims on environment feedback such as tool output, runtime state, command results, or verified config adapters.
               - Distinguish clearly between verified facts and fallback inference.
               - Do not present inferred values as current truth when a verification path exists.
            8. TRANSPARENCY AND CHECKPOINTS:
               - Make your plan legible in the response when useful, especially for multi-step actions.
               - For open-ended tasks, show the user what you verified, what remains uncertain, and what you will do next.
            9. TOOL INTERFACE DISCIPLINE:
               - Prefer tools with clear, well-documented interfaces and predictable outputs.
               - When multiple tools can answer a question, prefer the one closest to Armorer-managed state over lower-level shell inspection.

            RESPONSE CONTRACT:
            Return JSON whenever possible with:
            {{
              "message": "human-readable response",
              "ui_action": {{
                "type": "select|confirm|text|interactive_command",
                ...payload...
              }}
            }}
            Use `ui_action` when the UI must collect input.
            - select: {{"type":"select","message":"...","options":["a","b"]}}
            - confirm: {{"type":"confirm","message":"...","default":true}}
            - text: {{"type":"text","message":"...","default":"..."}}
            - interactive_command: use sparingly for genuinely host-bound, non-OpenClaw terminal tasks.
            """

            from armorer.agent import shell, file_write, file_read
            from armorer.tools.knowledge import search_knowledge, get_knowledge_topic, list_knowledge_topics
            from armorer.tools.memory_tools import list_durable_memory, recall_memory_facts, store_verified_memory
            from armorer.tools.orchestrator import armorer_cli, install_agent, list_agents, run_agent, stop_agent, uninstall_agent
            from armorer.tools.runtime_state_tools import (
                get_agent_setup_state, get_dashboard_urls, get_installed_agents_snapshot,
                get_recent_jobs_snapshot, get_runtime_access_snapshot, get_status_snapshot,
            )
            from armorer.tools.workflow import workflow
            from armorer.tools.think import think

            agent = BaseAgent()
            runtime_access_specialist = build_runtime_access_specialist_tool(agent)
            setup_state_specialist = build_setup_specialist_tool(agent)
            repair_specialist = build_repair_specialist_tool(agent)
            hooks = ArmorerRunHooks(verification_ledger=run_context.verification_ledger)
            base_tools = [
                redact_secret, update_app_metadata, file_read, file_write, shell,
                install_agent, run_agent, stop_agent, uninstall_agent, list_agents, armorer_cli,
                get_runtime_access_snapshot, get_status_snapshot, get_installed_agents_snapshot,
                get_dashboard_urls, get_recent_jobs_snapshot, get_agent_setup_state,
                recall_memory_facts, list_durable_memory, store_verified_memory,
                runtime_access_specialist, setup_state_specialist, repair_specialist,
                search_knowledge, get_knowledge_topic, list_knowledge_topics,
                workflow, think,
            ]
            agent.init_agent(
                system_prompt=system_prompt,
                session_id=f"armorer-core-{task_id}",
                agent_name="armorer:core:task",
                hooks=hooks,
                tools=self._tools_for_mode(base_tools, route_decision.mode)
            )

            response = agent.run(
                user_input=effective_prompt,
                status_file=status_file
            )
            payload = _normalize_core_response(response)

            if control_note:
                payload["message"] = f"{control_note}\n\n{payload.get('message', '')}".strip()

            if source in {"cli", "telegram", "signal"} and not self._guard_scan_operator_enabled():
                out_scan = self.guard.inspect_operator(payload.get("message", ""))
            else:
                out_scan = self.guard.inspect_output(payload.get("message", ""))
            payload["message"] = out_scan.sanitized_text
            self.guard.append_history(
                conversation_id=conversation_id,
                role="assistant",
                text=payload.get("message", ""),
                inspection=out_scan,
            )
            if out_scan.suspicious:
                self.guard.notify_suspicious(
                    channel_id=str(alert_chat),
                    conversation_id=conversation_id,
                    source=source,
                    snippet=payload.get("message", ""),
                    reasons=out_scan.reasons,
                    confidence=out_scan.confidence,
                )

            if route_decision.mode == ExecutionMode.RETRIEVAL and run_context.runtime_snapshot:
                try:
                    promote_runtime_snapshot_facts(
                        snapshot=run_context.runtime_snapshot,
                        prompt=effective_prompt,
                    )
                except Exception as mem_exc:
                    logger.debug("Failed to promote runtime snapshot memory: %s", mem_exc)

            result_path = self.outbox / f"{task_id}.json"
            with open(result_path, "w") as f:
                json.dump({
                    "id": task_id,
                    "response": json.dumps(payload),
                    "timestamp": time.time()
                }, f, indent=4)

            self.last_processed[str(task_path)] = time.time()
            self._unlink_if_exists(task_path)
            logger.info(f"✅ CORE: Task '{task_id}' completed.")
            self._audit("task_completed", task_id=task_id, source=source)

        except Exception as e:
            logger.error(f"Failed to process task: {e}")
            self._audit("task_failed", task_path=task_path, error=str(e))
            try:
                error_task_id = str(task_id or task_file.stem or str(uuid.uuid4()))
                result_path = self.outbox / f"{error_task_id}.json"
                fallback_message = self._sanitize_openclaw_setup_detail_text(f"Error: {str(e)}")
                if not fallback_message:
                    fallback_message = f"Error: {e.__class__.__name__}"
                try:
                    scan = self.guard.inspect_operator(fallback_message) if source == "cli" else self.guard.inspect_output(fallback_message)
                    fallback_message = str(scan.sanitized_text or fallback_message)
                except Exception:
                    pass
                payload = {"message": fallback_message}
                with open(result_path, "w") as f:
                    json.dump({"id": error_task_id, "response": json.dumps(payload), "timestamp": time.time()}, f)
                self.last_processed[str(task_path)] = time.time()
                self._unlink_if_exists(task_path)
            except Exception:
                pass
        finally:
            self._unlink_if_exists(status_file)
            if task_id:
                self._clear_active_task_context(str(task_id))
            self.is_processing = False

    # ------------------------------------------------------------------
    # File monitoring helpers
    # ------------------------------------------------------------------

    def _identify_app(self, file_path: Path):
        apps = self._list_apps_with_openclaw_fallback()
        for app in apps:
            current = str(file_path.absolute())
            host_base = os.path.abspath(app.install_path)
            container_base = os.path.abspath(self._translate_to_container_path(app.install_path))
            if current.startswith(host_base) or current.startswith(container_base):
                return app
        return None

    def _is_monitor_target(self, file_path: Path, app):
        path_s = str(file_path)
        roots = [
            os.path.abspath(app.install_path),
            os.path.abspath(self._translate_to_container_path(app.install_path)),
        ]
        rel_candidates = [path_s]
        for root in roots:
            if root and path_s.startswith(root):
                rel = os.path.relpath(path_s, root)
                rel_candidates.append(rel)
        for target in app.monitor_targets:
            for candidate in rel_candidates:
                if fnmatch.fnmatch(candidate, target):
                    return True
        if "session" in str(file_path).lower() and file_path.suffix in ['.json', '.jsonl', '.log']:
            return True
        return False

    def _is_mutation_protected_file(self, file_path: Path, app) -> bool:
        current = os.path.abspath(str(file_path))
        roots = [
            os.path.abspath(app.install_path),
            os.path.abspath(self._translate_to_container_path(app.install_path)),
        ]
        config_files = dict(getattr(app, "config_files", {}) or {})
        for rel_path in config_files.values():
            if not rel_path:
                continue
            for root in roots:
                candidate = os.path.abspath(os.path.join(root, str(rel_path)))
                if current == candidate:
                    return True
        return file_path.name.startswith(".env")

    def _inspect_and_sanitize(self, file_path: str, app):
        try:
            content = Path(file_path).read_text()
            if not content.strip() or len(content) < 10:
                return

            logger.info(f"🛡️  CORE: Oversighting {app.name} -> {Path(file_path).name}")
            scan = self.guard.inspect_input(content)
            self._record_runtime_event(
                agent_alias=str(app.name),
                agent_id=str(getattr(app, "agent_id", "") or ""),
                event_type="file",
                subtype="modified",
                severity="warn" if scan.suspicious else "info",
                path=file_path,
                verdict="suspicious" if scan.suspicious else "observed",
                raw={
                    "reasons": list(scan.reasons or []),
                    "confidence": scan.confidence,
                    "sanitized_changed": str(scan.sanitized_text or "") != content,
                },
            )
            self.guard.append_history(
                conversation_id=f"agent:{app.name}",
                role="agent_runtime",
                text=content,
                inspection=scan,
            )
            if scan.suspicious:
                self._notify_agent_guard_alert(
                    app_name=app.name,
                    file_path=file_path,
                    snippet=content,
                    reasons=scan.reasons,
                    confidence=scan.confidence,
                )
                if self._auto_steer_enabled(source="file"):
                    steer_result = self._auto_steer_agent(
                        app_name=app.name,
                        trigger=f"file:{Path(file_path).name}",
                        snippet=content,
                    )
                    self._audit(
                        "auto_steer_applied",
                        source="file",
                        app_name=app.name,
                        file=file_path,
                        result=steer_result,
                    )

            redacted = str(scan.sanitized_text or "")
            if redacted and redacted != content:
                if self._is_mutation_protected_file(Path(file_path), app):
                    self._record_runtime_event(
                        agent_alias=str(app.name),
                        agent_id=str(getattr(app, "agent_id", "") or ""),
                        event_type="file",
                        subtype="redaction_skipped_protected",
                        severity="warn",
                        path=file_path,
                        verdict="protected",
                        raw={
                            "original_len": len(content),
                            "redacted_len": len(redacted),
                        },
                    )
                    self._audit(
                        "file_redaction_skipped_protected",
                        app_name=app.name,
                        file=file_path,
                        original_len=len(content),
                        redacted_len=len(redacted),
                    )
                else:
                    Path(file_path).write_text(redacted)
                    self._record_runtime_event(
                        agent_alias=str(app.name),
                        agent_id=str(getattr(app, "agent_id", "") or ""),
                        event_type="file",
                        subtype="redaction_overwrite",
                        severity="warn",
                        path=file_path,
                        verdict="rewritten",
                        raw={
                            "original_len": len(content),
                            "redacted_len": len(redacted),
                        },
                    )
                    self._audit(
                        "file_redaction_overwrite",
                        app_name=app.name,
                        file=file_path,
                        original_len=len(content),
                        redacted_len=len(redacted),
                    )
        except Exception as e:
            logger.error(f"Oversight error: {e}")

    def _add_event(self, app_name: str, event_type: str, description: str):
        events_file = self.config_dir / "events.json"
        try:
            self._record_runtime_event(
                agent_alias=str(app_name or "").strip(),
                event_type="armorer_event",
                subtype=str(event_type or "").strip(),
                severity="info",
                verdict="observed",
                raw={"description": description},
            )
            events = []
            if events_file.exists():
                with open(events_file, "r") as f:
                    events = json.load(f)
            events.append({"timestamp": time.time(), "agent": app_name, "type": event_type, "description": description, "unread": True})
            with open(events_file, "w") as f:
                json.dump(events[-100:], f, indent=4)
        except:
            pass

    # ------------------------------------------------------------------
    # Inbox polling
    # ------------------------------------------------------------------

    def _poll_inbox(self):
        """Manual polling to handle Docker volume filesystem limitations."""
        try:
            self._prune_unreadable_task_retries()
            task_files = list(self.inbox.glob("*.json"))
            if len(task_files) > 20 and (time.time() - self._last_backlog_alert_ts) > 300:
                self._last_backlog_alert_ts = time.time()
                logger.warning("Inbox backlog detected: %s pending tasks", len(task_files))
                self._audit("inbox_backlog", count=len(task_files))
            for task_file in task_files:
                if self.is_processing:
                    break
                task_key = str(task_file)
                try:
                    task_mtime = task_file.stat().st_mtime
                except FileNotFoundError:
                    continue
                seen_ts = float(self.last_processed.get(task_key, 0) or 0)
                if seen_ts and task_mtime <= seen_ts:
                    continue
                self._process_inbox_task(task_key)
            self._poll_otel_events()
            self._maybe_send_daily_report()
        except Exception as e:
            logger.exception("Inbox polling failure: %s", e)
            self._audit("polling_failure", error=str(e))
