ha-mcp-dev 7.6.0.dev612__tar.gz → 7.6.0.dev613__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.6.0.dev612/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.6.0.dev613}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/pyproject.toml +1 -1
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/auth/provider.py +163 -12
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/setup.cfg +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/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.6.0.
|
|
7
|
+
version = "7.6.0.dev613"
|
|
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"
|
|
@@ -7,13 +7,16 @@ provide their Long-Lived Access Token (LLAT).
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import binascii
|
|
10
|
+
import contextlib
|
|
10
11
|
import hashlib
|
|
11
12
|
import hmac
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
15
|
+
import os
|
|
14
16
|
import secrets
|
|
15
17
|
import time
|
|
16
18
|
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
|
19
|
+
from pathlib import Path
|
|
17
20
|
from typing import Any
|
|
18
21
|
from urllib.parse import urlencode
|
|
19
22
|
|
|
@@ -32,11 +35,12 @@ from mcp.server.auth.provider import (
|
|
|
32
35
|
construct_redirect_uri,
|
|
33
36
|
)
|
|
34
37
|
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
|
35
|
-
from pydantic import AnyHttpUrl
|
|
38
|
+
from pydantic import AnyHttpUrl, ValidationError
|
|
36
39
|
from starlette.requests import Request
|
|
37
40
|
from starlette.responses import HTMLResponse, RedirectResponse, Response
|
|
38
41
|
from starlette.routing import Route
|
|
39
42
|
|
|
43
|
+
from ..utils.data_paths import get_data_dir
|
|
40
44
|
from .consent_form import create_consent_html, create_error_html
|
|
41
45
|
|
|
42
46
|
logger = logging.getLogger(__name__)
|
|
@@ -66,9 +70,16 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
66
70
|
|
|
67
71
|
The consent form collects the user's Long-Lived Access Token (LLAT),
|
|
68
72
|
which is encoded into both access and refresh tokens as base64 JSON.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
Tokens are stateless (HMAC-signed base64 JSON) so no per-token state is
|
|
74
|
+
stored server-side.
|
|
75
|
+
|
|
76
|
+
DCR client registrations and the HMAC signing secret are persisted to
|
|
77
|
+
``get_data_dir() / "oauth_clients.json"`` and
|
|
78
|
+
``get_data_dir() / "oauth_hmac_secret"`` respectively, so registered
|
|
79
|
+
clients survive container restarts. If the data directory is not
|
|
80
|
+
writable (e.g. Docker without a mounted volume) the server falls back
|
|
81
|
+
to in-memory-only storage and logs a warning; existing clients must
|
|
82
|
+
re-register after each restart in that case.
|
|
72
83
|
|
|
73
84
|
Security comes from HTTPS transport and the LLAT itself being the
|
|
74
85
|
authorization boundary — revoking the LLAT in Home Assistant
|
|
@@ -117,8 +128,10 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
117
128
|
required_scopes=required_scopes,
|
|
118
129
|
)
|
|
119
130
|
|
|
120
|
-
#
|
|
121
|
-
self.clients: dict[str, OAuthClientInformationFull] =
|
|
131
|
+
# DCR client registry — persisted to disk so clients survive restarts.
|
|
132
|
+
self.clients: dict[str, OAuthClientInformationFull] = self._load_clients()
|
|
133
|
+
|
|
134
|
+
# Short-lived in-memory state (no persistence needed or wanted)
|
|
122
135
|
self.auth_codes: dict[str, AuthorizationCode] = {}
|
|
123
136
|
|
|
124
137
|
# Home Assistant credentials storage (keyed by client_id)
|
|
@@ -129,13 +142,149 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
129
142
|
self.pending_authorizations: dict[str, dict[str, Any]] = {}
|
|
130
143
|
|
|
131
144
|
# Server-side secret for HMAC-protecting LLATs in refresh tokens.
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
# and clients will re-authenticate via the consent form.
|
|
135
|
-
self._hmac_secret = secrets.token_bytes(32)
|
|
145
|
+
# Persisted so existing refresh tokens survive server restarts.
|
|
146
|
+
self._hmac_secret = self._load_hmac_secret()
|
|
136
147
|
|
|
137
148
|
logger.info(f"HomeAssistantOAuthProvider initialized with base_url={base_url}")
|
|
138
149
|
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Persistence helpers
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _clients_file() -> Path:
|
|
156
|
+
return get_data_dir() / "oauth_clients.json"
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _hmac_secret_file() -> Path:
|
|
160
|
+
return get_data_dir() / "oauth_hmac_secret"
|
|
161
|
+
|
|
162
|
+
def _load_clients(self) -> dict[str, OAuthClientInformationFull]:
|
|
163
|
+
"""Load persisted DCR client registrations from disk.
|
|
164
|
+
|
|
165
|
+
Returns an empty dict if the file is absent, unreadable, or corrupt.
|
|
166
|
+
"""
|
|
167
|
+
path = self._clients_file()
|
|
168
|
+
try:
|
|
169
|
+
data: dict[str, Any] = json.loads(path.read_text())
|
|
170
|
+
clients = {
|
|
171
|
+
cid: OAuthClientInformationFull.model_validate(c)
|
|
172
|
+
for cid, c in data.items()
|
|
173
|
+
}
|
|
174
|
+
logger.info("Loaded %d OAuth client(s) from %s", len(clients), path)
|
|
175
|
+
return clients
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
logger.debug("No persistent OAuth client registry found at %s", path)
|
|
178
|
+
return {}
|
|
179
|
+
except (json.JSONDecodeError, OSError, ValidationError) as exc:
|
|
180
|
+
logger.warning(
|
|
181
|
+
"Could not load OAuth client registry from %s (%s: %s); "
|
|
182
|
+
"starting with empty registry.",
|
|
183
|
+
path,
|
|
184
|
+
type(exc).__name__,
|
|
185
|
+
exc,
|
|
186
|
+
)
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
def _save_clients(self) -> None:
|
|
190
|
+
"""Persist DCR client registrations to disk.
|
|
191
|
+
|
|
192
|
+
Failures are logged as warnings; the server continues with in-memory
|
|
193
|
+
state so existing sessions are not disrupted.
|
|
194
|
+
"""
|
|
195
|
+
if not self.clients:
|
|
196
|
+
return
|
|
197
|
+
path = self._clients_file()
|
|
198
|
+
# Serialize before touching the filesystem. A model that won't
|
|
199
|
+
# serialize is a programming error and should surface loudly, not be
|
|
200
|
+
# swallowed as if it were a transient disk fault in the write below.
|
|
201
|
+
data = {
|
|
202
|
+
cid: client.model_dump(mode="json") for cid, client in self.clients.items()
|
|
203
|
+
}
|
|
204
|
+
payload = json.dumps(data, indent=2).encode()
|
|
205
|
+
tmp_path = path.parent / f".{path.name}.tmp"
|
|
206
|
+
try:
|
|
207
|
+
# Write to a temp file first, then atomically rename. Use os.open
|
|
208
|
+
# with mode 0o600 (owner read/write only) because a confidential
|
|
209
|
+
# client's client_secret can be stored in this file.
|
|
210
|
+
fd = os.open(str(tmp_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
211
|
+
try:
|
|
212
|
+
os.write(fd, payload)
|
|
213
|
+
finally:
|
|
214
|
+
os.close(fd)
|
|
215
|
+
os.replace(str(tmp_path), str(path))
|
|
216
|
+
logger.debug("Persisted %d OAuth client(s) to %s", len(data), path)
|
|
217
|
+
except OSError as exc:
|
|
218
|
+
with contextlib.suppress(OSError):
|
|
219
|
+
tmp_path.unlink()
|
|
220
|
+
logger.warning(
|
|
221
|
+
"Failed to persist OAuth client registry to %s (%s: %s). "
|
|
222
|
+
"Clients will need to re-register after the next restart. "
|
|
223
|
+
"Mount a persistent volume to fix this: "
|
|
224
|
+
"docker run -v ha_mcp_data:/home/mcpuser/.ha-mcp ...",
|
|
225
|
+
path,
|
|
226
|
+
type(exc).__name__,
|
|
227
|
+
exc,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _load_hmac_secret(self) -> bytes:
|
|
231
|
+
"""Load or generate the HMAC signing secret.
|
|
232
|
+
|
|
233
|
+
Persists a newly generated secret so refresh tokens survive restarts.
|
|
234
|
+
Falls back to a fresh (non-persisted) secret if the file is unreadable.
|
|
235
|
+
"""
|
|
236
|
+
path = self._hmac_secret_file()
|
|
237
|
+
try:
|
|
238
|
+
hex_secret = path.read_text().strip()
|
|
239
|
+
if not hex_secret:
|
|
240
|
+
raise ValueError("HMAC secret file is empty")
|
|
241
|
+
secret = bytes.fromhex(hex_secret)
|
|
242
|
+
if len(secret) != 32:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Invalid HMAC secret length: {len(secret)} bytes (expected 32)"
|
|
245
|
+
)
|
|
246
|
+
logger.debug("Loaded HMAC secret from %s", path)
|
|
247
|
+
return secret
|
|
248
|
+
except FileNotFoundError:
|
|
249
|
+
pass # first start — generate and persist below
|
|
250
|
+
except (OSError, ValueError) as exc:
|
|
251
|
+
logger.warning(
|
|
252
|
+
"Could not load HMAC secret from %s (%s: %s); "
|
|
253
|
+
"generating a new one — existing refresh tokens will be invalidated.",
|
|
254
|
+
path,
|
|
255
|
+
type(exc).__name__,
|
|
256
|
+
exc,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
secret = secrets.token_bytes(32)
|
|
260
|
+
tmp_path = path.parent / f".{path.name}.tmp"
|
|
261
|
+
try:
|
|
262
|
+
# Write to a temp file first, then atomically rename — prevents a
|
|
263
|
+
# partial write from corrupting the secret on an interrupted flush.
|
|
264
|
+
# os.open with mode 0o600 keeps the file owner read/write only.
|
|
265
|
+
fd = os.open(str(tmp_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
266
|
+
try:
|
|
267
|
+
os.write(fd, secret.hex().encode())
|
|
268
|
+
finally:
|
|
269
|
+
os.close(fd)
|
|
270
|
+
os.replace(str(tmp_path), str(path))
|
|
271
|
+
logger.debug("Persisted new HMAC secret to %s", path)
|
|
272
|
+
except OSError as exc:
|
|
273
|
+
with contextlib.suppress(OSError):
|
|
274
|
+
tmp_path.unlink()
|
|
275
|
+
logger.warning(
|
|
276
|
+
"Failed to persist HMAC secret to %s (%s: %s). "
|
|
277
|
+
"Refresh tokens will be invalidated on the next restart.",
|
|
278
|
+
path,
|
|
279
|
+
type(exc).__name__,
|
|
280
|
+
exc,
|
|
281
|
+
)
|
|
282
|
+
return secret
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# OAuthProvider interface
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
|
|
139
288
|
def _sign_payload(self, payload_json: bytes) -> str:
|
|
140
289
|
"""Compute HMAC-SHA256 signature of a token payload."""
|
|
141
290
|
return hmac.new(self._hmac_secret, payload_json, hashlib.sha256).hexdigest()
|
|
@@ -159,8 +308,9 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
159
308
|
timestamp, and (for refresh tokens) client/scope metadata and
|
|
160
309
|
expiry.
|
|
161
310
|
|
|
162
|
-
|
|
163
|
-
Clients re-authenticate via
|
|
311
|
+
The signing secret is persisted (see ``_load_hmac_secret``), so signed
|
|
312
|
+
tokens remain valid across server restarts. Clients re-authenticate via
|
|
313
|
+
the consent form only when a token expires or the secret is rotated.
|
|
164
314
|
"""
|
|
165
315
|
payload: dict[str, Any] = {
|
|
166
316
|
"ha_token": ha_token,
|
|
@@ -373,6 +523,7 @@ class HomeAssistantOAuthProvider(OAuthProvider):
|
|
|
373
523
|
|
|
374
524
|
self.clients[client_info.client_id] = client_info
|
|
375
525
|
logger.info(f"Registered OAuth client: {client_info.client_id}")
|
|
526
|
+
self._save_clients()
|
|
376
527
|
|
|
377
528
|
async def authorize(
|
|
378
529
|
self, client: OAuthClientInformationFull, params: AuthorizationParams
|
|
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
|
|
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.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/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
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/tools/tools_config_scripts.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.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/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.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.6.0.dev612 → ha_mcp_dev-7.6.0.dev613}/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
|