ha-mcp-dev 7.4.1.dev412__tar.gz → 7.4.1.dev414__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.4.1.dev412/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev414}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_addons.py +2 -4
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_bug_report.py +177 -7
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_config_dashboards.py +273 -63
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/util_helpers.py +4 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev412 → ha_mcp_dev-7.4.1.dev414}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.4.1.
|
|
7
|
+
version = "7.4.1.dev414"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -35,6 +35,7 @@ from .helpers import (
|
|
|
35
35
|
log_tool_usage,
|
|
36
36
|
raise_tool_error,
|
|
37
37
|
)
|
|
38
|
+
from .util_helpers import ANSI_ESCAPE_RE
|
|
38
39
|
|
|
39
40
|
logger = logging.getLogger(__name__)
|
|
40
41
|
|
|
@@ -45,9 +46,6 @@ _MAX_RESPONSE_SIZE = 50 * 1024
|
|
|
45
46
|
# can lower this but never raise it.
|
|
46
47
|
_MAX_WS_MESSAGES = 1000
|
|
47
48
|
|
|
48
|
-
# ANSI escape code pattern for stripping terminal colors from addon output
|
|
49
|
-
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
50
|
-
|
|
51
49
|
# Substrings that flag a WebSocket message as "signal" for the summarize pass.
|
|
52
50
|
# Keep conservative: false negatives get elided, false positives just mean
|
|
53
51
|
# no elision. Case-insensitive match on the JSON-stringified message.
|
|
@@ -661,7 +659,7 @@ async def _call_addon_ws(
|
|
|
661
659
|
continue
|
|
662
660
|
|
|
663
661
|
# Strip ANSI escape codes
|
|
664
|
-
clean =
|
|
662
|
+
clean = ANSI_ESCAPE_RE.sub("", message)
|
|
665
663
|
collected.append(clean)
|
|
666
664
|
total_size += len(clean)
|
|
667
665
|
|
|
@@ -8,11 +8,13 @@ on how to create effective bug reports.
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import platform
|
|
11
|
+
import re
|
|
11
12
|
import sys
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Annotated, Any
|
|
14
15
|
from urllib.parse import quote_plus
|
|
15
16
|
|
|
17
|
+
import httpx
|
|
16
18
|
from pydantic import Field
|
|
17
19
|
|
|
18
20
|
from ha_mcp import __version__
|
|
@@ -23,6 +25,7 @@ from ..utils.usage_logger import (
|
|
|
23
25
|
get_startup_logs,
|
|
24
26
|
)
|
|
25
27
|
from .helpers import log_tool_usage
|
|
28
|
+
from .util_helpers import ANSI_ESCAPE_RE
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
28
31
|
|
|
@@ -30,6 +33,25 @@ logger = logging.getLogger(__name__)
|
|
|
30
33
|
RUNTIME_BUG_URL = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=runtime_bug.yml"
|
|
31
34
|
AGENT_BEHAVIOR_URL = "https://github.com/homeassistant-ai/ha-mcp/issues/new?template=agent_behavior.yml"
|
|
32
35
|
|
|
36
|
+
# Max characters to include from addon container logs.
|
|
37
|
+
# 3000 chars ≈ 750 LLM tokens — keeps the tool response well below context budgets
|
|
38
|
+
# while still capturing enough recent output to diagnose most issues.
|
|
39
|
+
_ADDON_LOG_MAX_CHARS = 3000
|
|
40
|
+
|
|
41
|
+
# IPv4 sanitization: only redact addresses with strong network context so that
|
|
42
|
+
# four-segment version strings (e.g. "ha-mcp version 1.2.3.4") are preserved.
|
|
43
|
+
_IPV4_OCTET = r"(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
|
|
44
|
+
_IPV4 = rf"(?:{_IPV4_OCTET}\.){{3}}{_IPV4_OCTET}"
|
|
45
|
+
# IP followed by :port or /CIDR — always a network address, never a version.
|
|
46
|
+
_IPV4_WITH_PORT_OR_CIDR_RE = re.compile(rf"\b{_IPV4}(?::\d+|/\d{{1,2}})\b(?!\.\d)")
|
|
47
|
+
# IP preceded by a network keyword (from, to, host=, addr=, etc.).
|
|
48
|
+
_IPV4_AFTER_KEYWORD_RE = re.compile(
|
|
49
|
+
rf"\b((?:from|to|host|hostname|addr|address|ip|src|dst|server|client|peer|via)\b\s*[=:]?\s*){_IPV4}\b(?!\.\d)",
|
|
50
|
+
re.IGNORECASE,
|
|
51
|
+
)
|
|
52
|
+
# IP appearing inside a URL (`scheme://1.2.3.4...`).
|
|
53
|
+
_IPV4_IN_URL_RE = re.compile(rf"(://){_IPV4}\b(?!\.\d)")
|
|
54
|
+
|
|
33
55
|
|
|
34
56
|
def _detect_installation_method() -> str:
|
|
35
57
|
"""
|
|
@@ -81,6 +103,113 @@ def _detect_platform() -> dict[str, str]:
|
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
|
|
106
|
+
def _sanitize_log_text(text: str) -> str:
|
|
107
|
+
"""Best-effort secret scrubber for log text.
|
|
108
|
+
|
|
109
|
+
Defense-in-depth, not exhaustive — bug reports still pass through human
|
|
110
|
+
review (see ``_generate_anonymization_guide``). Rules cover the most common
|
|
111
|
+
leak shapes seen in HA add-on logs:
|
|
112
|
+
JWTs, bearer tokens, long hex tokens, ``key=value`` style credentials,
|
|
113
|
+
URL userinfo, and IPv4 addresses with network context.
|
|
114
|
+
"""
|
|
115
|
+
# JWT tokens (header.payload.signature)
|
|
116
|
+
text = re.sub(
|
|
117
|
+
r"eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",
|
|
118
|
+
"[REDACTED_JWT]",
|
|
119
|
+
text,
|
|
120
|
+
)
|
|
121
|
+
# Bearer tokens — match any casing (BEARER, Bearer, bearer, BeArEr, …)
|
|
122
|
+
# via re.IGNORECASE, but preserve the original casing in the output by
|
|
123
|
+
# echoing m.group(1) back through the lambda.
|
|
124
|
+
text = re.sub(
|
|
125
|
+
r"\b(bearer)\s+\S+",
|
|
126
|
+
lambda m: f"{m.group(1)} [REDACTED]",
|
|
127
|
+
text,
|
|
128
|
+
flags=re.IGNORECASE,
|
|
129
|
+
)
|
|
130
|
+
# Generic key=value credentials (api_key, token, secret, password, etc.).
|
|
131
|
+
# Negative lookbehind for a letter so OPENAI_API_KEY=... still matches
|
|
132
|
+
# (underscore is a word-char, so \b doesn't fire there).
|
|
133
|
+
# "authorization" is intentionally omitted — the Bearer rule above already
|
|
134
|
+
# handles "Authorization: Bearer ..." and overlapping rules double-tap.
|
|
135
|
+
text = re.sub(
|
|
136
|
+
r"(?<![A-Za-z])(api[_-]?key|access[_-]?key|secret[_-]?key|token|secret|password|passwd)\b(\s*[:=]\s*)\S+",
|
|
137
|
+
r"\1\2[REDACTED]",
|
|
138
|
+
text,
|
|
139
|
+
flags=re.IGNORECASE,
|
|
140
|
+
)
|
|
141
|
+
# URL userinfo: scheme://user:password@host -> scheme://user:[REDACTED]@host
|
|
142
|
+
text = re.sub(
|
|
143
|
+
r"([a-zA-Z][a-zA-Z0-9+.-]*://)([^:/?#\s@]+):([^@/\s]+)@",
|
|
144
|
+
r"\1\2:[REDACTED]@",
|
|
145
|
+
text,
|
|
146
|
+
)
|
|
147
|
+
# Long hex strings (API keys, tokens) - 32+ contiguous hex chars
|
|
148
|
+
text = re.sub(
|
|
149
|
+
r"(?<![a-fA-F0-9])[a-fA-F0-9]{32,}(?![a-fA-F0-9])",
|
|
150
|
+
"[REDACTED_HEX]",
|
|
151
|
+
text,
|
|
152
|
+
)
|
|
153
|
+
# IPv4 addresses — only when there's strong network context, so that
|
|
154
|
+
# four-segment version strings (e.g. "version 1.2.3.4") survive intact.
|
|
155
|
+
text = _IPV4_WITH_PORT_OR_CIDR_RE.sub("[IP]", text)
|
|
156
|
+
text = _IPV4_IN_URL_RE.sub(r"\1[IP]", text)
|
|
157
|
+
text = _IPV4_AFTER_KEYWORD_RE.sub(r"\1[IP]", text)
|
|
158
|
+
return text
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _fetch_addon_logs() -> str:
|
|
162
|
+
"""Fetch ha-mcp addon container logs via the Supervisor REST API.
|
|
163
|
+
|
|
164
|
+
Only works when running as a Home Assistant add-on (SUPERVISOR_TOKEN set).
|
|
165
|
+
Uses /addons/self/logs which resolves to the calling addon's own logs via
|
|
166
|
+
the Supervisor's per-addon token binding — no slug interpolation needed.
|
|
167
|
+
|
|
168
|
+
Direct httpx against ``http://supervisor`` is the documented add-on access
|
|
169
|
+
pattern: it uses the Supervisor token directly (no extra HA hop) and
|
|
170
|
+
preserves the ``self`` shortcut, which the WebSocket ``supervisor/api``
|
|
171
|
+
proxy used by other tools may not.
|
|
172
|
+
|
|
173
|
+
Returns sanitized log text (last _ADDON_LOG_MAX_CHARS chars, with a
|
|
174
|
+
truncation marker prepended when truncation occurs), or empty string on
|
|
175
|
+
failure.
|
|
176
|
+
"""
|
|
177
|
+
# Redundant with the caller's `install_method == "addon"` gate, but kept
|
|
178
|
+
# as a defensive guard for any direct callers added later.
|
|
179
|
+
token = os.environ.get("SUPERVISOR_TOKEN", "")
|
|
180
|
+
if not token:
|
|
181
|
+
return ""
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
async with httpx.AsyncClient(timeout=10.0) as http_client:
|
|
185
|
+
resp = await http_client.get(
|
|
186
|
+
"http://supervisor/addons/self/logs",
|
|
187
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
188
|
+
)
|
|
189
|
+
if resp.status_code != 200:
|
|
190
|
+
logger.info(
|
|
191
|
+
"Addon log fetch returned HTTP %s", resp.status_code
|
|
192
|
+
)
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
# Strip ANSI escape codes first, then sanitize, then truncate.
|
|
196
|
+
# Sanitizing before truncating prevents secrets that straddle the
|
|
197
|
+
# truncation boundary from leaking through.
|
|
198
|
+
cleaned = ANSI_ESCAPE_RE.sub("", resp.text)
|
|
199
|
+
sanitized = _sanitize_log_text(cleaned)
|
|
200
|
+
if len(sanitized) > _ADDON_LOG_MAX_CHARS:
|
|
201
|
+
marker = (
|
|
202
|
+
f"[...truncated, showing last {_ADDON_LOG_MAX_CHARS} of "
|
|
203
|
+
f"{len(sanitized)} chars...]\n"
|
|
204
|
+
)
|
|
205
|
+
return marker + sanitized[-_ADDON_LOG_MAX_CHARS:]
|
|
206
|
+
return sanitized
|
|
207
|
+
except httpx.RequestError as e:
|
|
208
|
+
logger.warning(f"Failed to fetch addon logs: {e}")
|
|
209
|
+
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
|
|
84
213
|
def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
85
214
|
"""Register bug report tools with the MCP server."""
|
|
86
215
|
|
|
@@ -140,7 +269,12 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
140
269
|
- "That was inefficient"
|
|
141
270
|
|
|
142
271
|
**OUTPUT:**
|
|
143
|
-
Returns both templates
|
|
272
|
+
Returns both templates plus diagnostic data. Key fields:
|
|
273
|
+
- `runtime_bug_template`, `agent_behavior_template` — pick based on context
|
|
274
|
+
- `recent_logs`, `startup_logs` — captured ha-mcp tool/server log entries
|
|
275
|
+
- `addon_logs` — addon container stdout/stderr (HA add-on installs only;
|
|
276
|
+
empty string otherwise)
|
|
277
|
+
- `suggested_title`, `duplicate_check_urls`, `anonymization_guide`
|
|
144
278
|
"""
|
|
145
279
|
# Detect installation method and platform
|
|
146
280
|
install_method = _detect_installation_method()
|
|
@@ -184,6 +318,11 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
184
318
|
# Get startup logs (first minute of server operation)
|
|
185
319
|
startup_logs = get_startup_logs()
|
|
186
320
|
|
|
321
|
+
# Fetch addon container logs when running as HA add-on
|
|
322
|
+
addon_logs = ""
|
|
323
|
+
if install_method == "addon":
|
|
324
|
+
addon_logs = await _fetch_addon_logs()
|
|
325
|
+
|
|
187
326
|
# Format logs for inclusion (sanitized summary)
|
|
188
327
|
log_summary = _format_logs_for_report(recent_logs)
|
|
189
328
|
startup_log_summary = _format_startup_logs(startup_logs)
|
|
@@ -221,11 +360,19 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
221
360
|
log_summary,
|
|
222
361
|
])
|
|
223
362
|
|
|
363
|
+
if addon_logs:
|
|
364
|
+
report_lines.extend([
|
|
365
|
+
"",
|
|
366
|
+
"=== Add-on Container Logs ===",
|
|
367
|
+
addon_logs,
|
|
368
|
+
])
|
|
369
|
+
|
|
224
370
|
formatted_report = "\n".join(report_lines)
|
|
225
371
|
|
|
226
372
|
# Generate BOTH templates
|
|
227
373
|
runtime_bug_template = _generate_runtime_bug_template(
|
|
228
|
-
diagnostic_info, log_summary, startup_log_summary, recent_logs, startup_logs
|
|
374
|
+
diagnostic_info, log_summary, startup_log_summary, recent_logs, startup_logs,
|
|
375
|
+
addon_logs=addon_logs,
|
|
229
376
|
)
|
|
230
377
|
|
|
231
378
|
agent_behavior_template = _generate_agent_behavior_template(
|
|
@@ -250,6 +397,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
250
397
|
"diagnostic_info": diagnostic_info,
|
|
251
398
|
"recent_logs": recent_logs,
|
|
252
399
|
"startup_logs": startup_logs,
|
|
400
|
+
"addon_logs": addon_logs,
|
|
253
401
|
"log_count": len(recent_logs),
|
|
254
402
|
"startup_log_count": len(startup_logs),
|
|
255
403
|
"formatted_report": formatted_report,
|
|
@@ -291,7 +439,8 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
291
439
|
f" - Runtime bugs: {RUNTIME_BUG_URL}\n"
|
|
292
440
|
f" - Agent behavior: {AGENT_BEHAVIOR_URL}\n"
|
|
293
441
|
" d. Ask them to fill in the description sections\n"
|
|
294
|
-
" e.
|
|
442
|
+
" e. For HA add-on installs, the runtime bug template includes a collapsible '📦 Add-on Container Logs' section auto-filled from addon_logs — keep it as-is\n"
|
|
443
|
+
" f. Remind them to review for any remaining personal information before submitting\n\n"
|
|
295
444
|
"CRITICAL: Always ANONYMIZE the report BEFORE presenting it in markdown code blocks!"
|
|
296
445
|
),
|
|
297
446
|
}
|
|
@@ -312,8 +461,8 @@ def _format_logs_for_report(logs: list[dict[str, Any]]) -> str:
|
|
|
312
461
|
|
|
313
462
|
line = f" {timestamp} | {tool_name} | {success} | {exec_time:.0f}ms"
|
|
314
463
|
if error:
|
|
315
|
-
#
|
|
316
|
-
error_short = str(error)[:100]
|
|
464
|
+
# Sanitize before truncating so secrets straddling the cut survive redaction.
|
|
465
|
+
error_short = _sanitize_log_text(str(error))[:100]
|
|
317
466
|
line += f" | Error: {error_short}"
|
|
318
467
|
lines.append(line)
|
|
319
468
|
|
|
@@ -332,7 +481,8 @@ def _format_startup_logs(logs: list[dict[str, Any]]) -> str:
|
|
|
332
481
|
logger_name = log.get("logger", "")
|
|
333
482
|
message = log.get("message", "")
|
|
334
483
|
|
|
335
|
-
#
|
|
484
|
+
# Sanitize before truncating so secrets straddling the cut survive redaction.
|
|
485
|
+
message = _sanitize_log_text(message)
|
|
336
486
|
if len(message) > 200:
|
|
337
487
|
message = message[:200] + "..."
|
|
338
488
|
|
|
@@ -447,6 +597,8 @@ def _generate_runtime_bug_template(
|
|
|
447
597
|
startup_log_summary: str,
|
|
448
598
|
recent_logs: list[dict[str, Any]],
|
|
449
599
|
startup_logs: list[dict[str, Any]],
|
|
600
|
+
*,
|
|
601
|
+
addon_logs: str = "",
|
|
450
602
|
) -> str:
|
|
451
603
|
"""
|
|
452
604
|
Generate a runtime bug report template matching runtime_bug.md format.
|
|
@@ -475,6 +627,24 @@ def _generate_runtime_bug_template(
|
|
|
475
627
|
{startup_log_summary}
|
|
476
628
|
```
|
|
477
629
|
|
|
630
|
+
</details>
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
# Show addon container logs section only when available (addon installs only)
|
|
634
|
+
addon_section = ""
|
|
635
|
+
if addon_logs:
|
|
636
|
+
addon_section = f"""
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## 📦 Add-on Container Logs
|
|
640
|
+
|
|
641
|
+
<details>
|
|
642
|
+
<summary>Click to expand ha-mcp add-on logs</summary>
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
{addon_logs}
|
|
646
|
+
```
|
|
647
|
+
|
|
478
648
|
</details>
|
|
479
649
|
"""
|
|
480
650
|
|
|
@@ -539,7 +709,7 @@ def _generate_runtime_bug_template(
|
|
|
539
709
|
```
|
|
540
710
|
|
|
541
711
|
</details>
|
|
542
|
-
{startup_section}
|
|
712
|
+
{startup_section}{addon_section}
|
|
543
713
|
---
|
|
544
714
|
|
|
545
715
|
## 💡 Additional Context
|