ha-mcp-dev 8.4.0.dev2510__tar.gz → 8.4.1.dev2516__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-8.4.0.dev2510/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.1.dev2516}/PKG-INFO +1 -1
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/pyproject.toml +1 -1
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/en.json +2 -2
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.html +2 -2
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_search.py +42 -21
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/enforcement.py +270 -39
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/model.py +58 -11
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/resolver.py +147 -81
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/setup.cfg +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/log_filters.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_common.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_sources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_logs.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/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 = "8.4.
|
|
7
|
+
version = "8.4.1.dev2516"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
{ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/en.json
RENAMED
|
@@ -114,13 +114,13 @@
|
|
|
114
114
|
"visibility.entities.help": "One entity_id per line. These stay hidden even without an entity-registry entry.",
|
|
115
115
|
"visibility.entities.denied": "Denied entity IDs",
|
|
116
116
|
"visibility.allowlist.title": "Restrict to an allowlist",
|
|
117
|
-
"visibility.allowlist.help": "Advanced: when any allowlist field is set
|
|
117
|
+
"visibility.allowlist.help": "Advanced: when any allowlist field is set, nonmatching entities are hidden (including entities added later). A match overrides category, Home Assistant hidden-state, and Assist filters; denied entity IDs and excluded areas or labels still win. Leave all three empty to disable.",
|
|
118
118
|
"visibility.allowlist.areas": "Allowed areas",
|
|
119
119
|
"visibility.allowlist.labels": "Allowed labels",
|
|
120
120
|
"visibility.allowlist.entities_help": "Allowed entity IDs, one per line.",
|
|
121
121
|
"visibility.allowlist.entities": "Allowed entity IDs",
|
|
122
122
|
"visibility.assist.title": "Respect Assist exposure",
|
|
123
|
-
"visibility.assist.help": "Hide entities not exposed to Home Assistant's Assist (“conversation”) voice assistant, mirroring the Settings > Voice assistants > Expose list.
|
|
123
|
+
"visibility.assist.help": "Hide entities not exposed to Home Assistant's Assist (“conversation”) voice assistant, mirroring the Settings > Voice assistants > Expose list. Not applied while an allowlist is active; otherwise adds two websocket reads per search.",
|
|
124
124
|
"visibility.assist.only_exposed": "Only show entities exposed to Assist",
|
|
125
125
|
"features.enable_tool_search.label": "Enable tool search",
|
|
126
126
|
"features.enable_tool_search.help": "Replace the full tool catalog with search-based discovery. Reduces idle context by roughly 90%, to about 5K tokens. ⚠️ Do NOT enable this in clients with their own built-in tool search / deferred tools (claude.ai, Claude Desktop, Claude Code) — the two search layers conflict, and the client's built-in tool search is the better choice there; leave this off. Whether tools are deferred depends on your client and model combination: use this when your setup loads the full catalog up front — models without native deferred tools (e.g. Gemini, OpenAI-compatible local models, Claude Haiku), clients that inline all tool schemas regardless of model (e.g. GitHub Copilot CLI), or smaller context windows. Some Codex models and ChatGPT include deferred tools too — check your client/model directly to confirm its features so you don't leave this enabled unnecessarily. Tools are found via ha_search_tools and executed via categorized proxies (read/write/delete). Requires restart to take effect.",
|
|
@@ -518,7 +518,7 @@
|
|
|
518
518
|
|
|
519
519
|
<section class="a11y-section">
|
|
520
520
|
<h2 class="a11y-section-title" data-i18n="visibility.allowlist.title">Restrict to an allowlist</h2>
|
|
521
|
-
<p class="a11y-section-help" data-i18n="visibility.allowlist.help">Advanced: when any allowlist field is set
|
|
521
|
+
<p class="a11y-section-help" data-i18n="visibility.allowlist.help">Advanced: when any allowlist field is set, nonmatching entities are hidden (including entities added later). A match overrides category, Home Assistant hidden-state, and Assist filters; denied entity IDs and excluded areas or labels still win. Leave all three empty to disable.</p>
|
|
522
522
|
<label class="visibility-field"><span data-i18n="visibility.allowlist.areas">Allowed areas</span> <input type="text" id="visibility-allow-areas" placeholder="living_room, kitchen"></label>
|
|
523
523
|
<label class="visibility-field"><span data-i18n="visibility.allowlist.labels">Allowed labels</span> <input type="text" id="visibility-allow-labels" placeholder="voice, main_floor"></label>
|
|
524
524
|
<p class="a11y-section-help" data-i18n="visibility.allowlist.entities_help">Allowed entity IDs, one per line.</p>
|
|
@@ -528,7 +528,7 @@
|
|
|
528
528
|
|
|
529
529
|
<section class="a11y-section">
|
|
530
530
|
<h2 class="a11y-section-title" data-i18n="visibility.assist.title">Respect Assist exposure</h2>
|
|
531
|
-
<p class="a11y-section-help" data-i18n="visibility.assist.help">Hide entities not exposed to Home Assistant's Assist (“conversation”) voice assistant, mirroring the Settings > Voice assistants > Expose list.
|
|
531
|
+
<p class="a11y-section-help" data-i18n="visibility.assist.help">Hide entities not exposed to Home Assistant's Assist (“conversation”) voice assistant, mirroring the Settings > Voice assistants > Expose list. Not applied while an allowlist is active; otherwise adds two websocket reads per search.</p>
|
|
532
532
|
<label class="a11y-option"><input type="checkbox" id="visibility-respect-assist"> <span data-i18n="visibility.assist.only_exposed">Only show entities exposed to Assist</span></label>
|
|
533
533
|
</section>
|
|
534
534
|
|
|
@@ -27,6 +27,7 @@ from ..errors import create_validation_error
|
|
|
27
27
|
from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
|
|
28
28
|
from ..utils.entity_membership import normalize_member_entity_ids
|
|
29
29
|
from ..utils.fuzzy_search import apply_hidden_penalty
|
|
30
|
+
from ..visibility.model import VisibilityWire, wire_has_allowlist_dimensions
|
|
30
31
|
from ..visibility.resolver import (
|
|
31
32
|
device_registry_needed_for_visibility,
|
|
32
33
|
load_hidden_set,
|
|
@@ -1321,6 +1322,11 @@ def _merge_component_visibility_warnings(
|
|
|
1321
1322
|
response,
|
|
1322
1323
|
[w for w in component_visibility_warnings if isinstance(w, str)],
|
|
1323
1324
|
)
|
|
1325
|
+
elif component_visibility_warnings is not None:
|
|
1326
|
+
logger.warning(
|
|
1327
|
+
"component visibility_warnings ignored: expected a list, got %s",
|
|
1328
|
+
type(component_visibility_warnings).__name__,
|
|
1329
|
+
)
|
|
1324
1330
|
|
|
1325
1331
|
|
|
1326
1332
|
async def _scrub_component_config_buckets(
|
|
@@ -2396,13 +2402,15 @@ class SearchTools:
|
|
|
2396
2402
|
# hidden entities through the fast path. The ``search_visibility``
|
|
2397
2403
|
# capability closes that: a component that advertises it accepts the raw
|
|
2398
2404
|
# hide config (``VisibilityConfig.to_wire``) as the ``visibility`` param
|
|
2399
|
-
# and excludes hidden entities before its own counts/pagination
|
|
2400
|
-
#
|
|
2401
|
-
#
|
|
2402
|
-
#
|
|
2403
|
-
#
|
|
2404
|
-
#
|
|
2405
|
-
#
|
|
2405
|
+
# and excludes hidden entities before its own counts/pagination. The
|
|
2406
|
+
# ``search_visibility_allowlist_authorization`` capability adds the
|
|
2407
|
+
# ``allowlist_authorization`` wire key, which opts the component into the
|
|
2408
|
+
# revised allowlist precedence; without it an active allowlist falls back
|
|
2409
|
+
# to legacy (see ``_resolve_component_search_visibility``). With no active
|
|
2410
|
+
# filter, the plain ``search`` route runs without a ``visibility`` param,
|
|
2411
|
+
# so old components
|
|
2412
|
+
# keep working. ``ha_get_overview`` needs no analogous gate — it reapplies
|
|
2413
|
+
# the filter over the component's raw slices. Checked only when it would
|
|
2406
2414
|
# serve, so the common (no-component / filter-off) install pays nothing.
|
|
2407
2415
|
if (
|
|
2408
2416
|
req.query_text
|
|
@@ -2429,7 +2437,7 @@ class SearchTools:
|
|
|
2429
2437
|
|
|
2430
2438
|
async def _resolve_component_search_visibility(
|
|
2431
2439
|
self, caps: Any
|
|
2432
|
-
) -> tuple[bool,
|
|
2440
|
+
) -> tuple[bool, VisibilityWire | None]:
|
|
2433
2441
|
"""Decide the ha_search route under the entity-visibility gate.
|
|
2434
2442
|
|
|
2435
2443
|
Returns ``(route_component, visibility_param)`` for a caller that has
|
|
@@ -2437,13 +2445,21 @@ class SearchTools:
|
|
|
2437
2445
|
|
|
2438
2446
|
- filter inactive → ``(True, None)``: the plain component search, no
|
|
2439
2447
|
``visibility`` param (parity with a pre-``search_visibility`` component).
|
|
2440
|
-
- filter active + ``search_visibility``
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2448
|
+
- filter active + ``search_visibility`` + config serialized:
|
|
2449
|
+
- component also advertises ``search_visibility_allowlist_authorization``
|
|
2450
|
+
→ ``(True, <wire dict + allowlist_authorization: True>)``. The key opts
|
|
2451
|
+
the component into the revised rule (an allow match authorizes past
|
|
2452
|
+
category, HA-hidden, and Assist filters). It is never sent to a
|
|
2453
|
+
component lacking the capability: that component's strict schema would
|
|
2454
|
+
reject it, and the key's absence is what keeps a newer component on the
|
|
2455
|
+
legacy precedence the released server still applies in its own
|
|
2456
|
+
outbound scan.
|
|
2457
|
+
- no allowlist dimensions → ``(True, <wire dict>)``: the nine-key wire;
|
|
2458
|
+
with no allow dimensions the two precedences agree.
|
|
2459
|
+
- allowlist active without the capability → ``(False, None)``.
|
|
2460
|
+
- config could not be loaded → ``(False, None)``: the legacy path applies
|
|
2461
|
+
the filter server-side before the counts/pagination (fail-closed to
|
|
2462
|
+
legacy, matching ``visibility_state_and_wire``'s fail-closed pairing).
|
|
2447
2463
|
|
|
2448
2464
|
``visibility_state_and_wire`` loads the config once for both the active
|
|
2449
2465
|
gate and its wire form, instead of the active check and the wire fetch
|
|
@@ -2452,16 +2468,21 @@ class SearchTools:
|
|
|
2452
2468
|
active, visibility = await visibility_state_and_wire()
|
|
2453
2469
|
if not active:
|
|
2454
2470
|
return True, None
|
|
2455
|
-
if component_supports(caps, "search_visibility")
|
|
2456
|
-
return
|
|
2457
|
-
|
|
2471
|
+
if not component_supports(caps, "search_visibility") or visibility is None:
|
|
2472
|
+
return False, None
|
|
2473
|
+
if component_supports(caps, "search_visibility_allowlist_authorization"):
|
|
2474
|
+
authorized: VisibilityWire = {**visibility, "allowlist_authorization": True}
|
|
2475
|
+
return True, authorized
|
|
2476
|
+
if wire_has_allowlist_dimensions(visibility):
|
|
2477
|
+
return False, None
|
|
2478
|
+
return True, visibility
|
|
2458
2479
|
|
|
2459
2480
|
async def _ha_search_via_component(
|
|
2460
2481
|
self,
|
|
2461
2482
|
req: _ResolvedSearch,
|
|
2462
2483
|
ctx: Context | None,
|
|
2463
2484
|
*,
|
|
2464
|
-
visibility:
|
|
2485
|
+
visibility: VisibilityWire | None = None,
|
|
2465
2486
|
caps: Any = None,
|
|
2466
2487
|
) -> dict[str, Any] | None:
|
|
2467
2488
|
"""Serve ha_search from the component; ``None`` ⇒ run the legacy path.
|
|
@@ -2540,7 +2561,7 @@ class SearchTools:
|
|
|
2540
2561
|
req: _ResolvedSearch,
|
|
2541
2562
|
ctx: Context | None,
|
|
2542
2563
|
*,
|
|
2543
|
-
visibility:
|
|
2564
|
+
visibility: VisibilityWire | None,
|
|
2544
2565
|
) -> dict[str, Any] | None:
|
|
2545
2566
|
"""Serve a ``dashboard``-including request from both legs (issue #2289).
|
|
2546
2567
|
|
|
@@ -2636,7 +2657,7 @@ class SearchTools:
|
|
|
2636
2657
|
async def _send_component_search(
|
|
2637
2658
|
self,
|
|
2638
2659
|
req: _ResolvedSearch,
|
|
2639
|
-
visibility:
|
|
2660
|
+
visibility: VisibilityWire | None = None,
|
|
2640
2661
|
*,
|
|
2641
2662
|
dashboard_split: bool = False,
|
|
2642
2663
|
) -> dict[str, Any]:
|
|
@@ -17,9 +17,14 @@ escape hatch — "refuse on contact":
|
|
|
17
17
|
(a template, an automation body, a service target list), or whose *output* would
|
|
18
18
|
surface one, is refused with a generic ``ENTITY_VISIBILITY_ENFORCED`` error that
|
|
19
19
|
never names the matched id.
|
|
20
|
-
|
|
21
|
-
may still be the hidden entity's data
|
|
22
|
-
|
|
20
|
+
Generic results are refused rather than token-redacted because adjacent values
|
|
21
|
+
may still be the hidden entity's data and mutation could corrupt an arbitrary
|
|
22
|
+
schema. The bounded exception is a JSON ``ha_get_state`` result for a visible
|
|
23
|
+
entity: fields, mapping keys, list items, or related records that name hidden
|
|
24
|
+
entities are omitted (a whole ``attributes`` mapping when any of its content
|
|
25
|
+
does, since attribute values derive from the related entity), a warning naming
|
|
26
|
+
the omitted paths is added, and the result is scanned again. Non-JSON,
|
|
27
|
+
un-warnable, or still-matching state output is refused.
|
|
23
28
|
- **Unscannable surfaces are refused wholesale** while enforce is active — sandbox
|
|
24
29
|
code execution and screenshot/pixel output can read arbitrary state that no text
|
|
25
30
|
scan can attribute to an entity.
|
|
@@ -50,7 +55,8 @@ import re
|
|
|
50
55
|
import time
|
|
51
56
|
from collections.abc import Callable
|
|
52
57
|
from contextvars import ContextVar
|
|
53
|
-
from
|
|
58
|
+
from dataclasses import dataclass
|
|
59
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, NoReturn
|
|
54
60
|
|
|
55
61
|
from fastmcp.exceptions import ToolError
|
|
56
62
|
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
|
|
@@ -89,8 +95,10 @@ _DASHBOARD_TOOL = "ha_config_get_dashboard"
|
|
|
89
95
|
_DASHBOARD_SET_TOOL = "ha_config_set_dashboard"
|
|
90
96
|
_CUSTOM_TOOL = "ha_manage_custom_tool"
|
|
91
97
|
|
|
92
|
-
#
|
|
98
|
+
# Diagnostic tool deliberately outside the visibility barrier unless opted in.
|
|
93
99
|
_REPORT_ISSUE_TOOL = "ha_report_issue"
|
|
100
|
+
# State reads have a shape-bounded scrub exception before the outbound scan.
|
|
101
|
+
_STATE_TOOL = "ha_get_state"
|
|
94
102
|
|
|
95
103
|
_SANDBOX_REASON = (
|
|
96
104
|
"'{name}' executes sandbox code that can read arbitrary Home Assistant state, "
|
|
@@ -104,6 +112,30 @@ _FAIL_CLOSED_REASON = (
|
|
|
104
112
|
"The entity visibility data could not be loaded, so enforce mode fails closed "
|
|
105
113
|
"rather than risk leaking a restricted entity."
|
|
106
114
|
)
|
|
115
|
+
_STATE_CONTENT_WARNING_PREFIX = "Entity Visibility enforce mode omitted "
|
|
116
|
+
_STATE_CONTENT_WARNING_MAX_PATHS = 10
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _state_content_warning(paths: list[str]) -> str:
|
|
120
|
+
"""The ha_get_state omission warning, naming the omitted JSON paths.
|
|
121
|
+
|
|
122
|
+
Paths never contain a hidden entity_id: a whole ``attributes`` mapping is
|
|
123
|
+
reported as one path, and a hidden mapping key is reported as
|
|
124
|
+
``<parent>.<hidden key>``.
|
|
125
|
+
"""
|
|
126
|
+
shown = ", ".join(paths[:_STATE_CONTENT_WARNING_MAX_PATHS])
|
|
127
|
+
extra = len(paths) - _STATE_CONTENT_WARNING_MAX_PATHS
|
|
128
|
+
if extra > 0:
|
|
129
|
+
shown += f" and {extra} more"
|
|
130
|
+
return (
|
|
131
|
+
f"{_STATE_CONTENT_WARNING_PREFIX}{shown} because it referenced entities "
|
|
132
|
+
"hidden by the entity visibility filter. If something you expected is "
|
|
133
|
+
"missing, review the Entity Visibility section of the ha-mcp settings UI "
|
|
134
|
+
"(the rules are described under 'Entity visibility enforce mode' in "
|
|
135
|
+
"SECURITY.md)."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
107
139
|
_CONFIG_LOAD_FAILED_REASON = (
|
|
108
140
|
"The entity visibility config (entity_visibility.json) could not be loaded and "
|
|
109
141
|
"no previously loaded config is available, so the server cannot determine "
|
|
@@ -179,9 +211,7 @@ def _iter_strings(value: Any) -> Any:
|
|
|
179
211
|
yield from _iter_strings(item)
|
|
180
212
|
|
|
181
213
|
|
|
182
|
-
def _scan_value(
|
|
183
|
-
value: Any, hidden: set[str], regex: re.Pattern[str] | None
|
|
184
|
-
) -> tuple[str | None, bool]:
|
|
214
|
+
def _scan_value(value: Any, matcher: _HiddenMatcher) -> tuple[str | None, bool]:
|
|
185
215
|
"""Return ``(first_exact_hidden_id, any_embedded_hidden_id)`` over nested strings.
|
|
186
216
|
|
|
187
217
|
A string that equals a hidden id exactly drives concealment; a string that
|
|
@@ -191,10 +221,10 @@ def _scan_value(
|
|
|
191
221
|
exact: str | None = None
|
|
192
222
|
embedded = False
|
|
193
223
|
for text in _iter_strings(value):
|
|
194
|
-
if text
|
|
224
|
+
if matcher.exact(text):
|
|
195
225
|
if exact is None:
|
|
196
226
|
exact = text
|
|
197
|
-
elif
|
|
227
|
+
elif matcher.embeds(text):
|
|
198
228
|
embedded = True
|
|
199
229
|
return exact, embedded
|
|
200
230
|
|
|
@@ -212,6 +242,30 @@ def _build_hidden_regex(hidden: set[str]) -> re.Pattern[str] | None:
|
|
|
212
242
|
return re.compile(rf"(?<![a-z0-9_])(?:{alternation})(?![a-z0-9_])", re.IGNORECASE)
|
|
213
243
|
|
|
214
244
|
|
|
245
|
+
@dataclass(frozen=True)
|
|
246
|
+
class _HiddenMatcher:
|
|
247
|
+
"""The enforced hidden set together with the boundary regex compiled from it.
|
|
248
|
+
|
|
249
|
+
Built only through :meth:`from_hidden`, so ``regex`` is ``None`` exactly when
|
|
250
|
+
``hidden`` is empty and every scan sees a consistent pair.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
hidden: frozenset[str]
|
|
254
|
+
regex: re.Pattern[str] | None
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def from_hidden(cls, hidden: set[str]) -> _HiddenMatcher:
|
|
258
|
+
return cls(frozenset(hidden), _build_hidden_regex(hidden))
|
|
259
|
+
|
|
260
|
+
def exact(self, text: str) -> bool:
|
|
261
|
+
"""Whether ``text`` is a hidden entity_id."""
|
|
262
|
+
return text in self.hidden
|
|
263
|
+
|
|
264
|
+
def embeds(self, text: str) -> bool:
|
|
265
|
+
"""Whether ``text`` is, or embeds, a hidden entity_id (exact included)."""
|
|
266
|
+
return self.regex is not None and self.regex.search(text) is not None
|
|
267
|
+
|
|
268
|
+
|
|
215
269
|
def _serialize_result(result: ToolResult) -> str:
|
|
216
270
|
"""Flatten a tool result's text content and structured content for scanning."""
|
|
217
271
|
parts: list[str] = []
|
|
@@ -228,6 +282,171 @@ def _serialize_result(result: ToolResult) -> str:
|
|
|
228
282
|
return "\n".join(parts)
|
|
229
283
|
|
|
230
284
|
|
|
285
|
+
# Sentinel returned for a hidden-naming string value or a mapping whose
|
|
286
|
+
# ``entity_id`` is hidden; its enclosing mapping/sequence omits that value. At the
|
|
287
|
+
# top level it reaches ``_scrub_state_representation``, which treats the
|
|
288
|
+
# representation as un-warnable.
|
|
289
|
+
_DROP_HIDDEN_REFERENCE = object()
|
|
290
|
+
# A visible entity's ``attributes`` mapping carries values derived from related
|
|
291
|
+
# entities (a person's coordinates come from its device trackers), so any hidden
|
|
292
|
+
# reference inside it drops the whole mapping rather than the naming field.
|
|
293
|
+
_ATTRIBUTES_KEY = "attributes"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _scrub_hidden_references(
|
|
297
|
+
value: Any, matcher: _HiddenMatcher, path: str
|
|
298
|
+
) -> tuple[Any, list[str]]:
|
|
299
|
+
"""Return a scrubbed JSON-shaped value and the JSON paths it omitted."""
|
|
300
|
+
if isinstance(value, str):
|
|
301
|
+
if matcher.embeds(value):
|
|
302
|
+
return _DROP_HIDDEN_REFERENCE, [path]
|
|
303
|
+
return value, []
|
|
304
|
+
|
|
305
|
+
if isinstance(value, dict):
|
|
306
|
+
return _scrub_hidden_mapping(value, matcher, path)
|
|
307
|
+
|
|
308
|
+
if isinstance(value, (list, tuple)):
|
|
309
|
+
return _scrub_hidden_sequence(value, matcher, path)
|
|
310
|
+
|
|
311
|
+
return value, []
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _scrub_hidden_mapping(
|
|
315
|
+
value: dict[Any, Any], matcher: _HiddenMatcher, path: str
|
|
316
|
+
) -> tuple[Any, list[str]]:
|
|
317
|
+
entity_id = value.get("entity_id")
|
|
318
|
+
if isinstance(entity_id, str) and matcher.embeds(entity_id):
|
|
319
|
+
return _DROP_HIDDEN_REFERENCE, [path]
|
|
320
|
+
|
|
321
|
+
scrubbed_dict: dict[Any, Any] = {}
|
|
322
|
+
omitted: list[str] = []
|
|
323
|
+
for key, item in value.items():
|
|
324
|
+
child_path = f"{path}.{key}" if path else str(key)
|
|
325
|
+
if isinstance(key, str) and matcher.embeds(key):
|
|
326
|
+
omitted.append(f"{path}.<hidden key>" if path else "<hidden key>")
|
|
327
|
+
continue
|
|
328
|
+
scrubbed_item, item_omitted = _scrub_hidden_references(
|
|
329
|
+
item, matcher, child_path
|
|
330
|
+
)
|
|
331
|
+
if key == _ATTRIBUTES_KEY and isinstance(item, dict) and item_omitted:
|
|
332
|
+
omitted.append(child_path)
|
|
333
|
+
continue
|
|
334
|
+
omitted.extend(item_omitted)
|
|
335
|
+
if scrubbed_item is _DROP_HIDDEN_REFERENCE:
|
|
336
|
+
continue
|
|
337
|
+
scrubbed_dict[key] = scrubbed_item
|
|
338
|
+
return scrubbed_dict, omitted
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _scrub_hidden_sequence(
|
|
342
|
+
value: list[Any] | tuple[Any, ...], matcher: _HiddenMatcher, path: str
|
|
343
|
+
) -> tuple[list[Any], list[str]]:
|
|
344
|
+
scrubbed_list: list[Any] = []
|
|
345
|
+
omitted: list[str] = []
|
|
346
|
+
for index, item in enumerate(value):
|
|
347
|
+
scrubbed_item, item_omitted = _scrub_hidden_references(
|
|
348
|
+
item, matcher, f"{path}[{index}]"
|
|
349
|
+
)
|
|
350
|
+
omitted.extend(item_omitted)
|
|
351
|
+
if scrubbed_item is _DROP_HIDDEN_REFERENCE:
|
|
352
|
+
continue
|
|
353
|
+
scrubbed_list.append(scrubbed_item)
|
|
354
|
+
return scrubbed_list, omitted
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _add_state_content_warning(value: Any, warning: str) -> bool:
|
|
358
|
+
"""Attach the omission warning, or return False when the shape cannot carry it."""
|
|
359
|
+
if not isinstance(value, dict):
|
|
360
|
+
return False
|
|
361
|
+
warnings = value.get("warnings")
|
|
362
|
+
if warnings is None:
|
|
363
|
+
value["warnings"] = [warning]
|
|
364
|
+
return True
|
|
365
|
+
if not isinstance(warnings, list):
|
|
366
|
+
return False
|
|
367
|
+
if warning not in warnings:
|
|
368
|
+
warnings.append(warning)
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class _ScrubAttempt(NamedTuple):
|
|
373
|
+
"""One JSON representation's scrub outcome.
|
|
374
|
+
|
|
375
|
+
``value`` is the scrubbed copy (warning attached) only when ``warnable`` is
|
|
376
|
+
true; otherwise it is the untouched original, retained so the outbound scan
|
|
377
|
+
still sees the hidden reference. ``warnable`` is vacuously true when
|
|
378
|
+
``omissions`` is zero.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
value: Any
|
|
382
|
+
omissions: int
|
|
383
|
+
warnable: bool
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class _ScrubTally(NamedTuple):
|
|
387
|
+
"""Committed omissions plus representations that could not carry a warning."""
|
|
388
|
+
|
|
389
|
+
omissions: int
|
|
390
|
+
mutated_representations: int
|
|
391
|
+
unwarnable_representations: int
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _scrub_state_representation(value: Any, matcher: _HiddenMatcher) -> _ScrubAttempt:
|
|
395
|
+
"""Scrub one JSON value, retaining its original unless a warning can attach."""
|
|
396
|
+
scrubbed, omitted = _scrub_hidden_references(value, matcher, "")
|
|
397
|
+
if not omitted:
|
|
398
|
+
return _ScrubAttempt(value, 0, True)
|
|
399
|
+
if scrubbed is _DROP_HIDDEN_REFERENCE or not _add_state_content_warning(
|
|
400
|
+
scrubbed, _state_content_warning(omitted)
|
|
401
|
+
):
|
|
402
|
+
return _ScrubAttempt(value, len(omitted), False)
|
|
403
|
+
return _ScrubAttempt(scrubbed, len(omitted), True)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _scrub_state_result(result: ToolResult, matcher: _HiddenMatcher) -> _ScrubTally:
|
|
407
|
+
"""Scrub ha_get_state JSON representations while preserving a warning.
|
|
408
|
+
|
|
409
|
+
A representation is replaced only when its top-level shape can carry the
|
|
410
|
+
generic omission warning. Otherwise the original is retained and counted in
|
|
411
|
+
``unwarnable_representations`` so the caller refuses the result rather than
|
|
412
|
+
silently returning filtered content.
|
|
413
|
+
"""
|
|
414
|
+
omissions_total = 0
|
|
415
|
+
mutated_representations = 0
|
|
416
|
+
unwarnable_representations = 0
|
|
417
|
+
structured = getattr(result, "structured_content", None)
|
|
418
|
+
if structured is not None:
|
|
419
|
+
attempt = _scrub_state_representation(structured, matcher)
|
|
420
|
+
if attempt.omissions and attempt.warnable:
|
|
421
|
+
result.structured_content = attempt.value
|
|
422
|
+
omissions_total += attempt.omissions
|
|
423
|
+
mutated_representations += 1
|
|
424
|
+
elif attempt.omissions:
|
|
425
|
+
unwarnable_representations += 1
|
|
426
|
+
|
|
427
|
+
for block in getattr(result, "content", None) or []:
|
|
428
|
+
text_value = getattr(block, "text", None)
|
|
429
|
+
if not isinstance(text_value, str):
|
|
430
|
+
continue
|
|
431
|
+
try:
|
|
432
|
+
parsed = json.loads(text_value)
|
|
433
|
+
except json.JSONDecodeError:
|
|
434
|
+
continue
|
|
435
|
+
attempt = _scrub_state_representation(parsed, matcher)
|
|
436
|
+
if attempt.omissions and attempt.warnable:
|
|
437
|
+
block.text = json.dumps(attempt.value)
|
|
438
|
+
omissions_total += attempt.omissions
|
|
439
|
+
mutated_representations += 1
|
|
440
|
+
elif attempt.omissions:
|
|
441
|
+
unwarnable_representations += 1
|
|
442
|
+
|
|
443
|
+
return _ScrubTally(
|
|
444
|
+
omissions_total,
|
|
445
|
+
mutated_representations,
|
|
446
|
+
unwarnable_representations,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
231
450
|
def _tool_error_text(exc: ToolError) -> str:
|
|
232
451
|
"""The scannable text of a tool-raised error (its JSON payload args)."""
|
|
233
452
|
return "\n".join(str(arg) for arg in (getattr(exc, "args", None) or ()))
|
|
@@ -416,9 +635,7 @@ class _VisibilityEnforcementBase(Middleware):
|
|
|
416
635
|
return None
|
|
417
636
|
return config
|
|
418
637
|
|
|
419
|
-
async def
|
|
420
|
-
self, config: VisibilityConfig
|
|
421
|
-
) -> tuple[set[str], re.Pattern[str] | None]:
|
|
638
|
+
async def _hidden_matcher(self, config: VisibilityConfig) -> _HiddenMatcher:
|
|
422
639
|
"""Resolve the hidden set + regex through the module-shared cache."""
|
|
423
640
|
return await _hidden_cache.get(config, self._get_client())
|
|
424
641
|
|
|
@@ -462,11 +679,11 @@ class VisibilityInboundEnforcement(_VisibilityEnforcementBase):
|
|
|
462
679
|
_raise_enforced(effective_name, reason)
|
|
463
680
|
|
|
464
681
|
try:
|
|
465
|
-
|
|
682
|
+
matcher = await self._hidden_matcher(config)
|
|
466
683
|
except VisibilityDataUnavailable:
|
|
467
684
|
_raise_enforced(effective_name, _FAIL_CLOSED_REASON)
|
|
468
685
|
|
|
469
|
-
self._scan_inbound(effective_name, name, args,
|
|
686
|
+
self._scan_inbound(effective_name, name, args, matcher)
|
|
470
687
|
return await call_next(context)
|
|
471
688
|
|
|
472
689
|
def _scan_inbound(
|
|
@@ -474,8 +691,7 @@ class VisibilityInboundEnforcement(_VisibilityEnforcementBase):
|
|
|
474
691
|
name: str,
|
|
475
692
|
outer_name: str,
|
|
476
693
|
args: dict[str, Any],
|
|
477
|
-
|
|
478
|
-
regex: re.Pattern[str] | None,
|
|
694
|
+
matcher: _HiddenMatcher,
|
|
479
695
|
) -> None:
|
|
480
696
|
"""Conceal on an exact hidden-id argument; refuse on an embedded one."""
|
|
481
697
|
exact: str | None = None
|
|
@@ -483,8 +699,8 @@ class VisibilityInboundEnforcement(_VisibilityEnforcementBase):
|
|
|
483
699
|
if current_tool_name(outer_name) in CALL_PROXY_META_TOOLS:
|
|
484
700
|
inner = _unwrap_proxy_call(args)
|
|
485
701
|
if inner is not None:
|
|
486
|
-
exact, embedded = _scan_value(inner[1],
|
|
487
|
-
raw_exact, raw_embedded = _scan_value(args,
|
|
702
|
+
exact, embedded = _scan_value(inner[1], matcher)
|
|
703
|
+
raw_exact, raw_embedded = _scan_value(args, matcher)
|
|
488
704
|
exact = exact or raw_exact
|
|
489
705
|
embedded = embedded or raw_embedded
|
|
490
706
|
if exact is not None:
|
|
@@ -512,11 +728,11 @@ class VisibilityOutboundEnforcement(_VisibilityEnforcementBase):
|
|
|
512
728
|
if config is None:
|
|
513
729
|
return await call_next(context)
|
|
514
730
|
try:
|
|
515
|
-
|
|
731
|
+
matcher = await self._hidden_matcher(config)
|
|
516
732
|
except VisibilityDataUnavailable:
|
|
517
733
|
_raise_enforced(effective_name, _FAIL_CLOSED_REASON)
|
|
518
734
|
return await self._call_and_scan_outbound(
|
|
519
|
-
effective_name, context, call_next,
|
|
735
|
+
effective_name, context, call_next, matcher
|
|
520
736
|
)
|
|
521
737
|
|
|
522
738
|
async def _call_and_scan_outbound(
|
|
@@ -524,19 +740,38 @@ class VisibilityOutboundEnforcement(_VisibilityEnforcementBase):
|
|
|
524
740
|
name: str,
|
|
525
741
|
context: MiddlewareContext,
|
|
526
742
|
call_next: CallNext,
|
|
527
|
-
|
|
743
|
+
matcher: _HiddenMatcher,
|
|
528
744
|
) -> Any:
|
|
529
745
|
"""Run the tool; refuse if its output (or a tool error) names a hidden id."""
|
|
530
746
|
try:
|
|
531
747
|
result = await call_next(context)
|
|
532
748
|
except ToolError as exc:
|
|
533
|
-
if
|
|
749
|
+
if matcher.embeds(_tool_error_text(exc)):
|
|
534
750
|
logger.info("visibility enforce: refused tool error from %s", name)
|
|
535
751
|
_raise_enforced(name, _outbound_reason(name))
|
|
536
752
|
raise
|
|
537
|
-
|
|
753
|
+
scrub_tally = _ScrubTally(0, 0, 0)
|
|
754
|
+
if name == _STATE_TOOL:
|
|
755
|
+
scrub_tally = _scrub_state_result(result, matcher)
|
|
756
|
+
if scrub_tally.unwarnable_representations:
|
|
757
|
+
# A retained original still names a hidden id; refuse here rather
|
|
758
|
+
# than rely on the scan below catching it.
|
|
759
|
+
logger.info(
|
|
760
|
+
"visibility enforce: ha_get_state scrub could not attach its "
|
|
761
|
+
"omission warning to %d JSON representation(s); refused",
|
|
762
|
+
scrub_tally.unwarnable_representations,
|
|
763
|
+
)
|
|
764
|
+
_raise_enforced(name, _outbound_reason(name))
|
|
765
|
+
if matcher.embeds(_serialize_result(result)):
|
|
538
766
|
logger.info("visibility enforce: refused result from %s", name)
|
|
539
767
|
_raise_enforced(name, _outbound_reason(name))
|
|
768
|
+
if scrub_tally.mutated_representations:
|
|
769
|
+
logger.info(
|
|
770
|
+
"visibility enforce: scrubbed %d omission(s) across %d "
|
|
771
|
+
"ha_get_state JSON representation(s)",
|
|
772
|
+
scrub_tally.omissions,
|
|
773
|
+
scrub_tally.mutated_representations,
|
|
774
|
+
)
|
|
540
775
|
return result
|
|
541
776
|
|
|
542
777
|
|
|
@@ -601,41 +836,37 @@ class _HiddenSetCache:
|
|
|
601
836
|
|
|
602
837
|
def __init__(self) -> None:
|
|
603
838
|
self._lock = asyncio.Lock()
|
|
604
|
-
self.
|
|
605
|
-
self._regex: re.Pattern[str] | None = None
|
|
839
|
+
self._matcher: _HiddenMatcher | None = None
|
|
606
840
|
self._key: str | None = None
|
|
607
841
|
self._expiry = 0.0
|
|
608
842
|
|
|
609
|
-
async def get(
|
|
610
|
-
self, config: VisibilityConfig, client: Any
|
|
611
|
-
) -> tuple[set[str], re.Pattern[str] | None]:
|
|
843
|
+
async def get(self, config: VisibilityConfig, client: Any) -> _HiddenMatcher:
|
|
612
844
|
key = _config_cache_key(config)
|
|
613
845
|
now = time.monotonic()
|
|
614
846
|
async with self._lock:
|
|
615
|
-
if self.
|
|
616
|
-
return self.
|
|
847
|
+
if self._matcher is not None and self._key == key and now < self._expiry:
|
|
848
|
+
return self._matcher
|
|
617
849
|
try:
|
|
618
850
|
hidden = await _refresh_hidden_set(config, client)
|
|
619
851
|
except Exception:
|
|
620
|
-
if self.
|
|
852
|
+
if self._matcher is not None and self._key == key:
|
|
621
853
|
logger.warning(
|
|
622
854
|
"visibility enforce: hidden-set refresh failed; using "
|
|
623
855
|
"last-known-good set",
|
|
624
856
|
exc_info=True,
|
|
625
857
|
)
|
|
626
|
-
return self.
|
|
858
|
+
return self._matcher
|
|
627
859
|
logger.warning(
|
|
628
860
|
"visibility enforce: hidden-set refresh failed with no "
|
|
629
861
|
"last-known-good for this config; failing closed",
|
|
630
862
|
exc_info=True,
|
|
631
863
|
)
|
|
632
864
|
raise VisibilityDataUnavailable("hidden set unavailable") from None
|
|
633
|
-
|
|
634
|
-
self.
|
|
635
|
-
self._regex = regex
|
|
865
|
+
matcher = _HiddenMatcher.from_hidden(hidden)
|
|
866
|
+
self._matcher = matcher
|
|
636
867
|
self._key = key
|
|
637
868
|
self._expiry = now + _CACHE_TTL_SECONDS
|
|
638
|
-
return
|
|
869
|
+
return matcher
|
|
639
870
|
|
|
640
871
|
|
|
641
872
|
_hidden_cache = _HiddenSetCache()
|
|
@@ -664,10 +895,10 @@ async def active_hidden_regex(client: Any) -> re.Pattern[str] | None:
|
|
|
664
895
|
and config_has_active_hide_dimensions(config)
|
|
665
896
|
):
|
|
666
897
|
return None
|
|
667
|
-
|
|
898
|
+
matcher = await _hidden_cache.get(config, client)
|
|
668
899
|
except Exception:
|
|
669
900
|
return None
|
|
670
|
-
return regex
|
|
901
|
+
return matcher.regex
|
|
671
902
|
|
|
672
903
|
|
|
673
904
|
def scrub_records(
|