ha-mcp-dev 7.4.1.dev449__tar.gz → 7.4.1.dev450__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.dev449/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev450}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_addons.py +10 -7
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_automations.py +4 -7
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_dashboards.py +4 -7
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_scripts.py +4 -7
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/python_sandbox.py +182 -22
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/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.dev450"
|
|
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"
|
|
@@ -28,7 +28,11 @@ from ..errors import (
|
|
|
28
28
|
create_error_response,
|
|
29
29
|
create_validation_error,
|
|
30
30
|
)
|
|
31
|
-
from ..utils.python_sandbox import
|
|
31
|
+
from ..utils.python_sandbox import (
|
|
32
|
+
PythonSandboxError,
|
|
33
|
+
format_sandbox_error,
|
|
34
|
+
safe_execute_expression,
|
|
35
|
+
)
|
|
32
36
|
from .helpers import (
|
|
33
37
|
exception_to_structured_error,
|
|
34
38
|
get_connected_ws_client,
|
|
@@ -179,16 +183,15 @@ def _apply_response_transform(response: Any, expr: str) -> Any:
|
|
|
179
183
|
try:
|
|
180
184
|
return safe_execute_expression(expr, {"response": response}, "response")
|
|
181
185
|
except PythonSandboxError as e:
|
|
186
|
+
message, suggestions = format_sandbox_error(
|
|
187
|
+
e, expr, variable_name="response"
|
|
188
|
+
)
|
|
182
189
|
raise_tool_error(
|
|
183
190
|
create_error_response(
|
|
184
191
|
ErrorCode.VALIDATION_FAILED,
|
|
185
|
-
|
|
192
|
+
message,
|
|
186
193
|
context={"expression_preview": expr[:200]},
|
|
187
|
-
suggestions=
|
|
188
|
-
"Operate on the `response` variable (in-place or reassign)",
|
|
189
|
-
"Allowed: dict/list access, assignment, loops, "
|
|
190
|
-
"comprehensions, whitelisted str/list/dict methods",
|
|
191
|
-
],
|
|
194
|
+
suggestions=suggestions,
|
|
192
195
|
)
|
|
193
196
|
)
|
|
194
197
|
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -22,6 +22,7 @@ from ..errors import (
|
|
|
22
22
|
from ..utils.config_hash import compute_config_hash
|
|
23
23
|
from ..utils.python_sandbox import (
|
|
24
24
|
PythonSandboxError,
|
|
25
|
+
format_sandbox_error,
|
|
25
26
|
get_security_documentation,
|
|
26
27
|
safe_execute,
|
|
27
28
|
)
|
|
@@ -552,16 +553,12 @@ class AutomationConfigTools:
|
|
|
552
553
|
try:
|
|
553
554
|
transformed_config = safe_execute(python_transform, current_config)
|
|
554
555
|
except PythonSandboxError as e:
|
|
556
|
+
message, suggestions = format_sandbox_error(e, python_transform)
|
|
555
557
|
raise_tool_error(
|
|
556
558
|
create_error_response(
|
|
557
559
|
ErrorCode.VALIDATION_FAILED,
|
|
558
|
-
|
|
559
|
-
suggestions=
|
|
560
|
-
"Check expression syntax",
|
|
561
|
-
"Ensure only allowed operations are used",
|
|
562
|
-
"See tool description for allowed operations",
|
|
563
|
-
f"Expression: {python_transform[:100]}{'...' if len(python_transform) > 100 else ''}",
|
|
564
|
-
],
|
|
560
|
+
message,
|
|
561
|
+
suggestions=suggestions,
|
|
565
562
|
context={"action": "python_transform", "identifier": identifier},
|
|
566
563
|
)
|
|
567
564
|
)
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -16,6 +16,7 @@ from ..errors import ErrorCode, create_error_response, create_resource_not_found
|
|
|
16
16
|
from ..utils.config_hash import compute_config_hash
|
|
17
17
|
from ..utils.python_sandbox import (
|
|
18
18
|
PythonSandboxError,
|
|
19
|
+
format_sandbox_error,
|
|
19
20
|
get_security_documentation,
|
|
20
21
|
safe_execute,
|
|
21
22
|
)
|
|
@@ -1069,16 +1070,12 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1069
1070
|
try:
|
|
1070
1071
|
transformed_config = safe_execute(python_transform, current_config)
|
|
1071
1072
|
except PythonSandboxError as e:
|
|
1073
|
+
message, suggestions = format_sandbox_error(e, python_transform)
|
|
1072
1074
|
raise_tool_error(
|
|
1073
1075
|
create_error_response(
|
|
1074
1076
|
ErrorCode.VALIDATION_FAILED,
|
|
1075
|
-
|
|
1076
|
-
suggestions=
|
|
1077
|
-
"Check expression syntax",
|
|
1078
|
-
"Ensure only allowed operations are used",
|
|
1079
|
-
"See tool description for allowed operations",
|
|
1080
|
-
f"Expression: {python_transform[:100]}...",
|
|
1081
|
-
],
|
|
1077
|
+
message,
|
|
1078
|
+
suggestions=suggestions,
|
|
1082
1079
|
context={
|
|
1083
1080
|
"action": "python_transform",
|
|
1084
1081
|
"url_path": url_path,
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
@@ -16,6 +16,7 @@ from ..errors import ErrorCode, create_error_response
|
|
|
16
16
|
from ..utils.config_hash import compute_config_hash
|
|
17
17
|
from ..utils.python_sandbox import (
|
|
18
18
|
PythonSandboxError,
|
|
19
|
+
format_sandbox_error,
|
|
19
20
|
get_security_documentation,
|
|
20
21
|
safe_execute,
|
|
21
22
|
)
|
|
@@ -446,16 +447,12 @@ class ConfigScriptTools:
|
|
|
446
447
|
try:
|
|
447
448
|
transformed_config = safe_execute(python_transform, actual_config)
|
|
448
449
|
except PythonSandboxError as e:
|
|
450
|
+
message, suggestions = format_sandbox_error(e, python_transform)
|
|
449
451
|
raise_tool_error(
|
|
450
452
|
create_error_response(
|
|
451
453
|
ErrorCode.VALIDATION_FAILED,
|
|
452
|
-
|
|
453
|
-
suggestions=
|
|
454
|
-
"Check expression syntax",
|
|
455
|
-
"Ensure only allowed operations are used",
|
|
456
|
-
"See tool description for allowed operations",
|
|
457
|
-
f"Expression: {python_transform[:100]}{'...' if len(python_transform) > 100 else ''}",
|
|
458
|
-
],
|
|
454
|
+
message,
|
|
455
|
+
suggestions=suggestions,
|
|
459
456
|
context={"action": "python_transform", "script_id": script_id},
|
|
460
457
|
)
|
|
461
458
|
)
|
|
@@ -11,8 +11,43 @@ from typing import Any, cast
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class PythonSandboxError(Exception):
|
|
14
|
-
"""
|
|
14
|
+
"""Base class for sandbox failures.
|
|
15
15
|
|
|
16
|
+
Catch this when callers don't need to distinguish validation-time
|
|
17
|
+
rejection from runtime exceptions — otherwise prefer the subclasses.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PythonSandboxValidationError(PythonSandboxError):
|
|
22
|
+
"""Raised when AST validation rejects the expression before execution.
|
|
23
|
+
|
|
24
|
+
The expression contains a forbidden node, function, or method, or
|
|
25
|
+
failed to parse. The user can fix the input.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PythonSandboxExecutionError(PythonSandboxError):
|
|
30
|
+
"""Raised when a validated expression raised at runtime.
|
|
31
|
+
|
|
32
|
+
The expression passed AST validation but produced a Python exception
|
|
33
|
+
when executed (e.g. KeyError on a missing key, TypeError on a bad
|
|
34
|
+
operation). Different from a validation failure: the *shape* of the
|
|
35
|
+
expression is fine, but it doesn't apply cleanly to the input data.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Cap on how much of a runtime exception's text gets surfaced. HA configs
|
|
40
|
+
# can carry tokens / passwords / device addresses, and Python's default
|
|
41
|
+
# repr happily embeds dict and list values into KeyError/TypeError text.
|
|
42
|
+
# 240 chars is enough to identify the failure (exception type + a short
|
|
43
|
+
# snippet) without pasting the input config back to the caller.
|
|
44
|
+
_EXECUTION_ERROR_TEXT_LIMIT = 240
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _truncate_for_error(text: str, limit: int = _EXECUTION_ERROR_TEXT_LIMIT) -> str:
|
|
48
|
+
if len(text) <= limit:
|
|
49
|
+
return text
|
|
50
|
+
return text[: limit - 3] + "..."
|
|
16
51
|
|
|
17
52
|
|
|
18
53
|
# Whitelist of safe AST node types
|
|
@@ -23,16 +58,19 @@ SAFE_NODES = {
|
|
|
23
58
|
ast.Assign,
|
|
24
59
|
ast.AugAssign, # +=, -=, etc.
|
|
25
60
|
ast.AnnAssign, # type annotations
|
|
61
|
+
ast.Pass, # explicit no-op
|
|
26
62
|
# Control flow
|
|
27
63
|
ast.If,
|
|
28
64
|
ast.For,
|
|
29
65
|
ast.While,
|
|
30
66
|
ast.Break,
|
|
31
67
|
ast.Continue,
|
|
68
|
+
ast.IfExp, # ternary: x if c else y
|
|
32
69
|
# Data access
|
|
33
70
|
ast.Subscript,
|
|
34
71
|
ast.Attribute,
|
|
35
72
|
ast.Index,
|
|
73
|
+
ast.Slice, # list[1:3]
|
|
36
74
|
ast.Name,
|
|
37
75
|
ast.Load,
|
|
38
76
|
ast.Store,
|
|
@@ -43,6 +81,9 @@ SAFE_NODES = {
|
|
|
43
81
|
ast.Dict,
|
|
44
82
|
ast.Tuple,
|
|
45
83
|
ast.Set,
|
|
84
|
+
ast.Starred, # *iterable in calls/literals: f(*xs), [*xs, y]
|
|
85
|
+
ast.JoinedStr, # f"…" — outer node holding parts
|
|
86
|
+
ast.FormattedValue, # the {expr} part inside an f-string
|
|
46
87
|
# Operations
|
|
47
88
|
ast.Delete,
|
|
48
89
|
ast.BinOp,
|
|
@@ -73,13 +114,54 @@ SAFE_NODES = {
|
|
|
73
114
|
ast.IsNot,
|
|
74
115
|
# Function calls (validated separately)
|
|
75
116
|
ast.Call,
|
|
117
|
+
ast.keyword, # keyword arguments: func(key=value)
|
|
76
118
|
# Comprehensions
|
|
77
119
|
ast.ListComp,
|
|
78
120
|
ast.DictComp,
|
|
79
121
|
ast.SetComp,
|
|
122
|
+
ast.GeneratorExp, # (x for x in ...)
|
|
80
123
|
ast.comprehension,
|
|
81
|
-
# Lambda
|
|
124
|
+
# Lambda — useful as `key=` for sorted/min/max. ast.arguments and
|
|
125
|
+
# ast.arg are the structure nodes ast.walk descends into for the
|
|
126
|
+
# parameter list; they have no execution semantics on their own
|
|
127
|
+
# (FunctionDef would be blocked at the SAFE_NODES check above).
|
|
82
128
|
ast.Lambda,
|
|
129
|
+
ast.arguments,
|
|
130
|
+
ast.arg,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Hints to help agents recover when a forbidden node is encountered.
|
|
135
|
+
# Keyed on AST class name (string, not class) so entries for
|
|
136
|
+
# version-specific nodes like Match (3.10+) or TryStar (3.11+) stay
|
|
137
|
+
# evaluable on any Python. Unmapped keys fall through to the generic
|
|
138
|
+
# "Forbidden node type: X" message in `_validate_node`.
|
|
139
|
+
_NODE_SUGGESTIONS: dict[str, str] = {
|
|
140
|
+
"Try": "validate inputs with isinstance/in/.get() instead of try/except",
|
|
141
|
+
"TryStar": "validate inputs with isinstance/in/.get() instead of try/except",
|
|
142
|
+
"ExceptHandler": "validate inputs with isinstance/in/.get() instead of try/except",
|
|
143
|
+
"With": "perform the inner logic directly; with-blocks aren't supported",
|
|
144
|
+
"AsyncWith": "perform the inner logic directly; with-blocks aren't supported",
|
|
145
|
+
"FunctionDef": "use a list comprehension or inline the logic",
|
|
146
|
+
"AsyncFunctionDef": "use a list comprehension or inline the logic",
|
|
147
|
+
"ClassDef": "use a dict literal instead of defining a class",
|
|
148
|
+
"Yield": "build a list with a comprehension or for-loop append",
|
|
149
|
+
"YieldFrom": "build a list with a comprehension or for-loop append",
|
|
150
|
+
"Global": "assign directly to the variable; scope keywords aren't supported",
|
|
151
|
+
"Nonlocal": "assign directly to the variable; scope keywords aren't supported",
|
|
152
|
+
"Import": "imports aren't available; built-ins like isinstance/len/range are exposed",
|
|
153
|
+
"ImportFrom": "imports aren't available; built-ins like isinstance/len/range are exposed",
|
|
154
|
+
"Match": "use if/elif/else or a dict lookup instead of match/case",
|
|
155
|
+
# If Match ever enters SAFE_NODES, the sub-pattern nodes shouldn't
|
|
156
|
+
# silently slip through with a generic message.
|
|
157
|
+
"MatchAs": "use if/elif/else or a dict lookup instead of match/case",
|
|
158
|
+
"MatchValue": "use if/elif/else or a dict lookup instead of match/case",
|
|
159
|
+
"MatchClass": "use if/elif/else or a dict lookup instead of match/case",
|
|
160
|
+
"MatchSingleton": "use if/elif/else or a dict lookup instead of match/case",
|
|
161
|
+
"MatchSequence": "use if/elif/else or a dict lookup instead of match/case",
|
|
162
|
+
"MatchMapping": "use if/elif/else or a dict lookup instead of match/case",
|
|
163
|
+
"MatchOr": "use if/elif/else or a dict lookup instead of match/case",
|
|
164
|
+
"MatchStar": "use if/elif/else or a dict lookup instead of match/case",
|
|
83
165
|
}
|
|
84
166
|
|
|
85
167
|
# Whitelist of safe methods that can be called
|
|
@@ -201,12 +283,20 @@ def validate_expression(expr: str) -> tuple[bool, str]:
|
|
|
201
283
|
|
|
202
284
|
|
|
203
285
|
def _validate_node(node: ast.AST) -> str | None:
|
|
204
|
-
"""Validate a single AST node. Returns error message or None if safe.
|
|
205
|
-
if type(node) not in SAFE_NODES:
|
|
206
|
-
return f"Forbidden node type: {type(node).__name__}"
|
|
286
|
+
"""Validate a single AST node. Returns error message or None if safe.
|
|
207
287
|
|
|
208
|
-
|
|
209
|
-
|
|
288
|
+
Whitelist check first: any node not in ``SAFE_NODES`` is rejected with
|
|
289
|
+
its class name and (when available) a recovery hint from
|
|
290
|
+
``_NODE_SUGGESTIONS``. After that, only nodes that *are* safe but need
|
|
291
|
+
extra checks (Attribute → block dunder access, Call → block forbidden
|
|
292
|
+
functions/methods) get further validation.
|
|
293
|
+
"""
|
|
294
|
+
if type(node) not in SAFE_NODES:
|
|
295
|
+
name = type(node).__name__
|
|
296
|
+
hint = _NODE_SUGGESTIONS.get(name)
|
|
297
|
+
if hint:
|
|
298
|
+
return f"Forbidden node type: {name} — {hint}"
|
|
299
|
+
return f"Forbidden node type: {name}"
|
|
210
300
|
|
|
211
301
|
if isinstance(node, ast.Attribute):
|
|
212
302
|
if node.attr.startswith("__") and node.attr.endswith("__"):
|
|
@@ -215,15 +305,6 @@ def _validate_node(node: ast.AST) -> str | None:
|
|
|
215
305
|
if isinstance(node, ast.Call):
|
|
216
306
|
return _validate_call_node(node)
|
|
217
307
|
|
|
218
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
219
|
-
return "Forbidden: function/class definitions not allowed"
|
|
220
|
-
|
|
221
|
-
if isinstance(node, (ast.With, ast.AsyncWith)):
|
|
222
|
-
return "Forbidden: with statements not allowed"
|
|
223
|
-
|
|
224
|
-
if isinstance(node, (ast.Try, ast.ExceptHandler)):
|
|
225
|
-
return "Forbidden: try/except not allowed"
|
|
226
|
-
|
|
227
308
|
return None
|
|
228
309
|
|
|
229
310
|
|
|
@@ -279,10 +360,10 @@ def safe_execute_expression(
|
|
|
279
360
|
"""
|
|
280
361
|
valid, error = validate_expression(expr)
|
|
281
362
|
if not valid:
|
|
282
|
-
raise
|
|
363
|
+
raise PythonSandboxValidationError(error)
|
|
283
364
|
|
|
284
365
|
if result_key not in variables:
|
|
285
|
-
raise
|
|
366
|
+
raise PythonSandboxValidationError(
|
|
286
367
|
f"result_key {result_key!r} not found in variables",
|
|
287
368
|
)
|
|
288
369
|
|
|
@@ -295,8 +376,24 @@ def safe_execute_expression(
|
|
|
295
376
|
|
|
296
377
|
try:
|
|
297
378
|
exec(expr, safe_globals, safe_locals)
|
|
379
|
+
except (MemoryError, RecursionError):
|
|
380
|
+
# Resource exhaustion — let the host decide. Reframing
|
|
381
|
+
# "ran out of memory" as "your transform was bad" would
|
|
382
|
+
# mislead the agent into rewriting an expression that
|
|
383
|
+
# was structurally fine.
|
|
384
|
+
#
|
|
385
|
+
# FastMCP's tool dispatch (server.py call_tool) catches
|
|
386
|
+
# `except Exception` and wraps in
|
|
387
|
+
# ``ToolError(f"Error calling tool {name!r}: {e}") from e`` —
|
|
388
|
+
# so the original exception's class name and text reach the
|
|
389
|
+
# agent, with the raw exception preserved as ``__cause__``.
|
|
390
|
+
# That's an acceptable surfacing (not opaque INTERNAL_ERROR).
|
|
391
|
+
raise
|
|
298
392
|
except Exception as e:
|
|
299
|
-
|
|
393
|
+
# Truncate so embedded reprs of input data (config dicts, tokens,
|
|
394
|
+
# etc.) don't reach the caller verbatim.
|
|
395
|
+
detail = _truncate_for_error(f"{type(e).__name__}: {e}")
|
|
396
|
+
raise PythonSandboxExecutionError(detail) from e
|
|
300
397
|
|
|
301
398
|
return safe_locals[result_key]
|
|
302
399
|
|
|
@@ -335,6 +432,54 @@ def safe_execute(expr: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
|
335
432
|
)
|
|
336
433
|
|
|
337
434
|
|
|
435
|
+
def format_sandbox_error(
|
|
436
|
+
error: PythonSandboxError,
|
|
437
|
+
expr: str,
|
|
438
|
+
variable_name: str = "config",
|
|
439
|
+
) -> tuple[str, list[str]]:
|
|
440
|
+
"""Build a (message, suggestions) pair appropriate for the error subclass.
|
|
441
|
+
|
|
442
|
+
``PythonSandboxValidationError`` means the expression's shape was
|
|
443
|
+
rejected before execution — suggestions point at syntax/allowed-ops.
|
|
444
|
+
``PythonSandboxExecutionError`` means the expression was accepted
|
|
445
|
+
but raised at runtime — suggestions point at keys/types/values.
|
|
446
|
+
Plain ``PythonSandboxError`` (no subclass) falls back to the
|
|
447
|
+
validation form.
|
|
448
|
+
|
|
449
|
+
``variable_name`` is the name of the mutable target the expression
|
|
450
|
+
operates on. The default ``"config"`` matches the dashboard /
|
|
451
|
+
automation / script callers; addon helpers pass ``"response"`` and
|
|
452
|
+
a one-liner about that name is prepended to the suggestions so
|
|
453
|
+
agents know which variable to mutate.
|
|
454
|
+
|
|
455
|
+
Used by ``ha_config_set_*`` and addon helpers so each caller emits
|
|
456
|
+
the same shape of MCP error without duplicating the boilerplate.
|
|
457
|
+
"""
|
|
458
|
+
preview = expr[:100] + ("..." if len(expr) > 100 else "")
|
|
459
|
+
if isinstance(error, PythonSandboxExecutionError):
|
|
460
|
+
message = f"Expression raised at runtime: {error}"
|
|
461
|
+
suggestions = [
|
|
462
|
+
"Verify referenced keys/indices exist in the input",
|
|
463
|
+
"Check that types match (e.g. dict vs list operations)",
|
|
464
|
+
"Use .get(key, default) to handle missing keys",
|
|
465
|
+
f"Expression: {preview}",
|
|
466
|
+
]
|
|
467
|
+
else:
|
|
468
|
+
message = f"Expression validation failed: {error}"
|
|
469
|
+
suggestions = [
|
|
470
|
+
"Check expression syntax",
|
|
471
|
+
"Ensure only allowed operations are used",
|
|
472
|
+
"See tool description for allowed operations",
|
|
473
|
+
f"Expression: {preview}",
|
|
474
|
+
]
|
|
475
|
+
if variable_name != "config":
|
|
476
|
+
suggestions = [
|
|
477
|
+
f"Operate on the `{variable_name}` variable (in-place or reassign)",
|
|
478
|
+
*suggestions,
|
|
479
|
+
]
|
|
480
|
+
return message, suggestions
|
|
481
|
+
|
|
482
|
+
|
|
338
483
|
def get_security_documentation() -> str:
|
|
339
484
|
"""
|
|
340
485
|
Get formatted documentation of security restrictions.
|
|
@@ -346,12 +491,18 @@ PYTHON TRANSFORM SECURITY:
|
|
|
346
491
|
|
|
347
492
|
✅ ALLOWED:
|
|
348
493
|
- Dictionary/list access: config['views'][0]['cards'][1]
|
|
494
|
+
- Slicing: config['views'][0]['cards'][1:3]
|
|
349
495
|
- Assignment: config['key'] = 'value'
|
|
350
496
|
- Deletion: del config['key'] or config.pop('key')
|
|
351
497
|
- List methods: append, insert, pop, remove, clear, extend
|
|
352
498
|
- Dict methods: update, get, setdefault, keys, values, items
|
|
353
|
-
- Loops: for, while, if/else
|
|
354
|
-
- Comprehensions: [x for x in ...]
|
|
499
|
+
- Loops: for, while, if/else, pass, break, continue
|
|
500
|
+
- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)
|
|
501
|
+
- Ternary: x if condition else y
|
|
502
|
+
- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]
|
|
503
|
+
- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}
|
|
504
|
+
- Keyword arguments: func(key=value)
|
|
505
|
+
- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])
|
|
355
506
|
- String methods: startswith, endswith, lower, upper, split, join
|
|
356
507
|
- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,
|
|
357
508
|
min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,
|
|
@@ -363,5 +514,14 @@ PYTHON TRANSFORM SECURITY:
|
|
|
363
514
|
- Dunder access: __class__, __bases__, __subclasses__
|
|
364
515
|
- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr
|
|
365
516
|
- Function definitions: def, class
|
|
366
|
-
- Exception handling: try/except (
|
|
517
|
+
- Exception handling: try/except (validate with isinstance/in/.get() instead)
|
|
518
|
+
|
|
519
|
+
🎯 PATTERNS:
|
|
520
|
+
- Filter cards: cards = [c for c in cards if keep(c)]
|
|
521
|
+
- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)
|
|
522
|
+
- Conditionally include: build a new list and `.append(x)` only the
|
|
523
|
+
cards you want, instead of iterating the original and using if/pass
|
|
524
|
+
branches to drop entries
|
|
525
|
+
- Modify in place when possible (single pass, fewer surprises) over
|
|
526
|
+
reconstructing the entire list
|
|
367
527
|
""".strip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev449 → ha_mcp_dev-7.4.1.dev450}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|