ha-mcp-dev 8.3.0.dev2395__tar.gz → 8.3.0.dev2397__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.3.0.dev2395/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2397}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/pyproject.toml +1 -1
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/client/rest_client.py +16 -3
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/errors.py +27 -2
- ha_mcp_dev-8.3.0.dev2397/src/ha_mcp/policy/evaluator.py +314 -0
- ha_mcp_dev-8.3.0.dev2397/src/ha_mcp/policy/middleware.py +442 -0
- ha_mcp_dev-8.3.0.dev2397/src/ha_mcp/tools/bulk_selector.py +858 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/registry.py +49 -3
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_service.py +676 -58
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/util_helpers.py +6 -2
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- ha_mcp_dev-8.3.0.dev2395/src/ha_mcp/policy/evaluator.py +0 -166
- ha_mcp_dev-8.3.0.dev2395/src/ha_mcp/policy/middleware.py +0 -260
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2395 → ha_mcp_dev-8.3.0.dev2397}/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.3.0.
|
|
7
|
+
version = "8.3.0.dev2397"
|
|
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
|
|
@@ -409,13 +409,26 @@ class HomeAssistantClient:
|
|
|
409
409
|
return await self._request("GET", "/config")
|
|
410
410
|
|
|
411
411
|
async def get_states(self) -> list[dict[str, Any]]:
|
|
412
|
-
"""Get all entity states.
|
|
412
|
+
"""Get all entity states.
|
|
413
|
+
|
|
414
|
+
Raises ``HomeAssistantConnectionError`` when the response isn't the
|
|
415
|
+
JSON array ``/states`` always returns on success -- including
|
|
416
|
+
``_request``'s own empty-dict fallback for an unparseable body.
|
|
417
|
+
Silently returning ``[]`` here would let a genuine fetch failure
|
|
418
|
+
masquerade as "this instance has zero entities", which no real,
|
|
419
|
+
running Home Assistant instance produces; callers that need to
|
|
420
|
+
verify something against the live entity set (e.g. a bulk-control
|
|
421
|
+
safety check) would otherwise treat that failure as "verified,
|
|
422
|
+
nothing to worry about" instead of "could not verify".
|
|
423
|
+
"""
|
|
413
424
|
logger.debug("Fetching all entity states")
|
|
414
425
|
result = await self._request("GET", "/states")
|
|
415
426
|
if isinstance(result, list):
|
|
416
427
|
return result
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
raise HomeAssistantConnectionError(
|
|
429
|
+
f"Home Assistant /states returned an unexpected response shape: "
|
|
430
|
+
f"{type(result).__name__}"
|
|
431
|
+
)
|
|
419
432
|
|
|
420
433
|
async def get_entity_state(self, entity_id: str) -> dict[str, Any]:
|
|
421
434
|
"""
|
|
@@ -100,6 +100,16 @@ class ErrorCode(StrEnum):
|
|
|
100
100
|
USER_DENIED = "USER_DENIED"
|
|
101
101
|
POLICY_LOAD_FAILED = "POLICY_LOAD_FAILED"
|
|
102
102
|
|
|
103
|
+
# Distinct from POLICY_LOAD_FAILED above: the policy file itself loaded
|
|
104
|
+
# fine, but this call's own arguments were too deeply nested to
|
|
105
|
+
# evaluate against it safely (see normalize_stringified_containers'
|
|
106
|
+
# RecursionError handling in middleware.py). A different failure with a
|
|
107
|
+
# different, caller-shaped remedy ("reduce the nesting depth and
|
|
108
|
+
# retry") -- sharing POLICY_LOAD_FAILED would make the two
|
|
109
|
+
# indistinguishable to anything grouping on the code (a dashboard, an
|
|
110
|
+
# operator grepping logs after "my policy broke").
|
|
111
|
+
POLICY_ARGS_TOO_DEEPLY_NESTED = "POLICY_ARGS_TOO_DEEPLY_NESTED"
|
|
112
|
+
|
|
103
113
|
# Read Only Mode (discussion #1569). A write operation was blocked
|
|
104
114
|
# because the server-wide Read Only Mode toggle is on.
|
|
105
115
|
READ_ONLY_MODE = "READ_ONLY_MODE"
|
|
@@ -210,6 +220,10 @@ DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
|
|
|
210
220
|
"Check documentation for required fields",
|
|
211
221
|
"Ensure all required parameters are provided",
|
|
212
222
|
],
|
|
223
|
+
ErrorCode.VALIDATION_FAILED: [
|
|
224
|
+
"Check the parameter values and format against the tool documentation",
|
|
225
|
+
"Review the message and details fields for the specific constraint that failed",
|
|
226
|
+
],
|
|
213
227
|
ErrorCode.VALIDATION_INVALID_JSON: [
|
|
214
228
|
"Ensure the parameter is valid JSON",
|
|
215
229
|
"Check for syntax errors in JSON",
|
|
@@ -310,10 +324,21 @@ def create_connection_error(
|
|
|
310
324
|
details: str | None = None,
|
|
311
325
|
timeout: bool = False,
|
|
312
326
|
context: dict[str, Any] | None = None,
|
|
327
|
+
suggestions: list[str] | None = None,
|
|
313
328
|
) -> dict[str, Any]:
|
|
314
|
-
"""Create a connection error response.
|
|
329
|
+
"""Create a connection error response.
|
|
330
|
+
|
|
331
|
+
``suggestions`` overrides ``DEFAULT_SUGGESTIONS[CONNECTION_FAILED/TIMEOUT]``
|
|
332
|
+
(network/URL/connectivity checks) for a caller whose failure is
|
|
333
|
+
CONNECTION_FAILED-shaped (unavailable, not the caller's fault to fix by
|
|
334
|
+
editing input) but not actually a network problem — e.g. malformed local
|
|
335
|
+
registry data or an unloadable local config file, where "check your
|
|
336
|
+
HOMEASSISTANT_URL" is not an actionable next step.
|
|
337
|
+
"""
|
|
315
338
|
code = ErrorCode.CONNECTION_TIMEOUT if timeout else ErrorCode.CONNECTION_FAILED
|
|
316
|
-
return create_error_response(
|
|
339
|
+
return create_error_response(
|
|
340
|
+
code, message, details, suggestions=suggestions, context=context
|
|
341
|
+
)
|
|
317
342
|
|
|
318
343
|
|
|
319
344
|
# Authentication-error suggestions for Home Assistant add-on installs. On the
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Evaluate a tool call against a Policy. Pure functions — no I/O, no state."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..tools.util_helpers import loads_if_json_container_str
|
|
10
|
+
from .model import Policy, Predicate, Rule
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Verdict(StrEnum):
|
|
16
|
+
ALLOW = "allow"
|
|
17
|
+
REQUIRE_APPROVAL = "require_approval"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_stringified_containers(value: Any) -> Any:
|
|
21
|
+
"""Recursively parse JSON-encoded object/array strings into real containers.
|
|
22
|
+
|
|
23
|
+
Some MCP client stacks (Claude Desktop stdio among them — see
|
|
24
|
+
``tools/util_helpers.py``'s ``JSON_STRING_COERCION``) pass model-emitted
|
|
25
|
+
stringified objects through unrepaired, e.g. sending
|
|
26
|
+
``{"selector": "{\\"domain\\": \\"light\\"}"}`` instead of a nested
|
|
27
|
+
object. Pydantic's ``JSON_STRING_COERCION`` ``BeforeValidator`` repairs
|
|
28
|
+
this, but only when the tool's own parameter validation runs — INSIDE
|
|
29
|
+
``call_next``, after policy evaluation. ``iter_path_values`` only
|
|
30
|
+
descends into ``dict``/``list``, so a still-stringified value makes a
|
|
31
|
+
predicate targeting a nested field (``args.selector.domain``,
|
|
32
|
+
``args.operations.*.entity_id``) silently yield nothing: no rule
|
|
33
|
+
matches, and — for ``ha_bulk_control`` — ``args.get("selector")`` still
|
|
34
|
+
sees a truthy string, so the selector fail-safe's own dynamic-call
|
|
35
|
+
detection still fires, but nothing inside it can be inspected either.
|
|
36
|
+
Applying the same repair here, once, before evaluation and hashing,
|
|
37
|
+
closes that gap for both plain predicate rules and the fail-safe, and
|
|
38
|
+
makes ``compute_args_hash`` key on the same logical value regardless of
|
|
39
|
+
which wire shape the client sent.
|
|
40
|
+
|
|
41
|
+
Malformed JSON that merely looks like a container is left as the raw
|
|
42
|
+
string (not raised): policy evaluation is not the place to surface a
|
|
43
|
+
JSON syntax error — the tool's own validation does that, with a
|
|
44
|
+
properly attributed parameter name.
|
|
45
|
+
|
|
46
|
+
Deeply-nested INPUT (real nested dicts/lists in the caller's own
|
|
47
|
+
``args`` -- NOT a stringified container decoding into deep nesting;
|
|
48
|
+
``loads_if_json_container_str`` already absorbs ``json.loads``'s own
|
|
49
|
+
``RecursionError``, so a still-stringified value can never reach this
|
|
50
|
+
function's own recursion at all) can exceed the interpreter's
|
|
51
|
+
recursion limit and raise ``RecursionError`` here. Unlike the
|
|
52
|
+
malformed-JSON case, this is deliberately NOT swallowed: silently
|
|
53
|
+
returning the unrepaired value would let this security gate evaluate
|
|
54
|
+
(and hash) unnormalized args and, for a rule scoped to a nested
|
|
55
|
+
selector-inspectable field, silently ALLOW a call that should have
|
|
56
|
+
required approval -- the identical "fail open on a degraded gate"
|
|
57
|
+
mistake ``PolicyMiddleware.on_call_tool`` already refuses to make for
|
|
58
|
+
a corrupt policy file. The caller (``on_call_tool``) is responsible
|
|
59
|
+
for catching this and failing the call closed with a structured,
|
|
60
|
+
logged error, matching that same precedent.
|
|
61
|
+
"""
|
|
62
|
+
return _normalize_stringified_containers(value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalize_stringified_containers(value: Any) -> Any:
|
|
66
|
+
"""Unbounded recursive worker for ``normalize_stringified_containers``.
|
|
67
|
+
|
|
68
|
+
Split out so the public function's ``RecursionError`` guard wraps a
|
|
69
|
+
single top-level call instead of needing a try/except at every
|
|
70
|
+
recursive frame.
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(value, str):
|
|
73
|
+
try:
|
|
74
|
+
return loads_if_json_container_str(value)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return value
|
|
77
|
+
if isinstance(value, dict):
|
|
78
|
+
return {key: _normalize_stringified_containers(v) for key, v in value.items()}
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
return [_normalize_stringified_containers(v) for v in value]
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def has_dynamic_selector_targets(name: str, args: dict[str, Any]) -> bool:
|
|
85
|
+
"""Return whether identical arguments can resolve to different targets later.
|
|
86
|
+
|
|
87
|
+
Single source of truth for the ``ha_bulk_control`` selector-mode predicate:
|
|
88
|
+
both ``PolicyMiddleware`` (approval-sharing/remembering gates) and
|
|
89
|
+
``evaluate()`` below (the operations fail-safe) must agree on exactly
|
|
90
|
+
which calls are "dynamic", or a future rename/extension of this check in
|
|
91
|
+
one place silently stops applying to the other.
|
|
92
|
+
"""
|
|
93
|
+
return name == "ha_bulk_control" and args.get("selector") is not None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def iter_path_values(args: dict[str, Any], path: str) -> Iterator[Any]:
|
|
97
|
+
"""Yield every value the dotted path resolves to.
|
|
98
|
+
|
|
99
|
+
The leading ``args`` segment is implicit and stripped. A ``*`` segment
|
|
100
|
+
fans out across the current node — across dict values for dicts,
|
|
101
|
+
across items for lists — so ``args.*`` yields every top-level
|
|
102
|
+
argument, ``args.config.*`` yields every leaf of the ``config``
|
|
103
|
+
sub-dict, and so on. Empty iterator = no match.
|
|
104
|
+
"""
|
|
105
|
+
parts = path.split(".")
|
|
106
|
+
if parts[0] == "args":
|
|
107
|
+
parts = parts[1:]
|
|
108
|
+
|
|
109
|
+
def walk(cur: Any, rest: list[str]) -> Iterator[Any]:
|
|
110
|
+
if not rest:
|
|
111
|
+
yield cur
|
|
112
|
+
return
|
|
113
|
+
head, tail = rest[0], rest[1:]
|
|
114
|
+
if head == "*":
|
|
115
|
+
if isinstance(cur, dict):
|
|
116
|
+
for v in cur.values():
|
|
117
|
+
yield from walk(v, tail)
|
|
118
|
+
elif isinstance(cur, (list, tuple)):
|
|
119
|
+
for v in cur:
|
|
120
|
+
yield from walk(v, tail)
|
|
121
|
+
return
|
|
122
|
+
if isinstance(cur, dict) and head in cur:
|
|
123
|
+
yield from walk(cur[head], tail)
|
|
124
|
+
|
|
125
|
+
yield from walk(args, parts)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _ci(x: Any) -> Any:
|
|
129
|
+
"""Lower-case strings for case-insensitive comparison; pass other
|
|
130
|
+
types through unchanged so type semantics (int != "1") survive.
|
|
131
|
+
Used on both sides of every string op — security gates should fire
|
|
132
|
+
whether the caller wrote 'Lock' or 'LOCK' or 'lock'."""
|
|
133
|
+
return x.lower() if isinstance(x, str) else x
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _contains_matches(val: Any, pv: Any) -> bool:
|
|
137
|
+
if isinstance(val, str) and isinstance(pv, str):
|
|
138
|
+
return pv.lower() in val.lower()
|
|
139
|
+
# Mirror the case-insensitive treatment that ``eq`` / ``in`` /
|
|
140
|
+
# ``not_in`` already apply: a rule listing ``["light.kitchen"]``
|
|
141
|
+
# must match an LLM passing ``"Light.Kitchen"``. Per-element
|
|
142
|
+
# ``_ci`` guards non-string entries so mixed-type collections
|
|
143
|
+
# (e.g. ``[1, "two"]``) keep their natural equality semantics.
|
|
144
|
+
return isinstance(val, (list, tuple, set)) and any(_ci(pv) == _ci(x) for x in val)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _numeric_matches(val: Any, op: str, pv: Any) -> bool:
|
|
148
|
+
try:
|
|
149
|
+
return bool(val > pv) if op == "gt" else bool(val < pv)
|
|
150
|
+
except TypeError:
|
|
151
|
+
# Numeric rule against a non-numeric arg value — log so
|
|
152
|
+
# users can tell their "temperature > 30" rule isn't
|
|
153
|
+
# silently never firing because the arg is a string.
|
|
154
|
+
logger.debug(
|
|
155
|
+
"policy: %s type-mismatch (val=%r pv=%r) — predicate skipped",
|
|
156
|
+
op,
|
|
157
|
+
val,
|
|
158
|
+
pv,
|
|
159
|
+
)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _op_matches(val: Any, op: str, pv: Any) -> bool:
|
|
164
|
+
"""Apply one op to one concrete value. Predicate dispatches over
|
|
165
|
+
the candidate values (which may be many for wildcard paths).
|
|
166
|
+
|
|
167
|
+
String comparisons are case-insensitive (security gates shouldn't
|
|
168
|
+
care whether the LLM lowercased its args). Non-string types
|
|
169
|
+
preserve their natural comparison semantics.
|
|
170
|
+
"""
|
|
171
|
+
match op:
|
|
172
|
+
case "eq":
|
|
173
|
+
return bool(_ci(val) == _ci(pv))
|
|
174
|
+
case "neq":
|
|
175
|
+
return bool(_ci(val) != _ci(pv))
|
|
176
|
+
case "in":
|
|
177
|
+
return _ci(val) in [_ci(x) for x in (pv or [])]
|
|
178
|
+
case "not_in":
|
|
179
|
+
return _ci(val) not in [_ci(x) for x in (pv or [])]
|
|
180
|
+
case "regex":
|
|
181
|
+
# `regex` is re.search (substring match). Anchor with ^...$
|
|
182
|
+
# for full-match. re.IGNORECASE so '^light\.' matches 'Light.x'.
|
|
183
|
+
return (
|
|
184
|
+
isinstance(val, str)
|
|
185
|
+
and isinstance(pv, str)
|
|
186
|
+
and re.search(pv, val, re.IGNORECASE) is not None
|
|
187
|
+
)
|
|
188
|
+
case "contains":
|
|
189
|
+
return _contains_matches(val, pv)
|
|
190
|
+
case "gt" | "lt":
|
|
191
|
+
return _numeric_matches(val, op, pv)
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def match_predicate(predicate: Predicate, args: dict[str, Any]) -> bool:
|
|
196
|
+
values = list(iter_path_values(args, predicate.path))
|
|
197
|
+
if predicate.op == "exists":
|
|
198
|
+
return bool(values)
|
|
199
|
+
if not values:
|
|
200
|
+
return False
|
|
201
|
+
# Existential semantics: a wildcard path matches if ANY value at the
|
|
202
|
+
# wildcard satisfies the op. For non-wildcard paths there's at most
|
|
203
|
+
# one value so the any() collapses to a single check.
|
|
204
|
+
return any(_op_matches(v, predicate.op, predicate.value) for v in values)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def match_rule(rule: Rule, tool_name: str, args: dict[str, Any]) -> bool:
|
|
208
|
+
if rule.tool_name not in ("*", tool_name):
|
|
209
|
+
return False
|
|
210
|
+
return all(match_predicate(p, args) for p in rule.when)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def find_matching_rule(
|
|
214
|
+
tool_name: str, args: dict[str, Any], policy: Policy
|
|
215
|
+
) -> Rule | None:
|
|
216
|
+
for rule in policy.rules:
|
|
217
|
+
if match_rule(rule, tool_name, args):
|
|
218
|
+
return rule
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _predicate_reaches_operations(path: str) -> bool:
|
|
223
|
+
"""Whether ``path`` can walk into ``args.operations`` under ``iter_path_values``.
|
|
224
|
+
|
|
225
|
+
``operations`` sits directly under ``args``, so only the first two
|
|
226
|
+
segments after stripping the implicit ``args`` prefix matter. A literal
|
|
227
|
+
``operations`` first segment obviously reaches it. A leading ``*`` reaches
|
|
228
|
+
it too — a wildcard segment fans out over EVERY value at that level (see
|
|
229
|
+
``iter_path_values``), landing on the `operations` list value exactly as
|
|
230
|
+
readily as any other top-level key — but ``operations`` is a *list*, so a
|
|
231
|
+
literal segment right after that wildcard (e.g. ``domain`` in
|
|
232
|
+
``args.*.domain``) can only ever match a *dict* value at that level (like
|
|
233
|
+
``selector``) — ``walk()`` requires ``isinstance(cur, dict)`` for a
|
|
234
|
+
literal head, so it silently yields nothing against a list and can never
|
|
235
|
+
reach an operation row. Only a SECOND wildcard (``args.*.*...``, as in
|
|
236
|
+
``args.*.*.entity_id``) or no further segment at all (bare ``args.*``,
|
|
237
|
+
which yields the raw ``operations`` list value itself) can actually reach
|
|
238
|
+
into the list. Any other concrete first segment (e.g. ``selector``) can
|
|
239
|
+
only ever address selector-inspectable fields and is precisely excluded.
|
|
240
|
+
"""
|
|
241
|
+
parts = path.split(".")
|
|
242
|
+
if parts and parts[0] == "args":
|
|
243
|
+
parts = parts[1:]
|
|
244
|
+
if not parts:
|
|
245
|
+
return False
|
|
246
|
+
if parts[0] == "operations":
|
|
247
|
+
return True
|
|
248
|
+
return parts[0] == "*" and (len(parts) == 1 or parts[1] == "*")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _rule_needs_resolved_operations(rule: Rule) -> bool:
|
|
252
|
+
"""Whether ``rule`` inspects fields only known after selector resolution.
|
|
253
|
+
|
|
254
|
+
A selector-mode ``ha_bulk_control`` call carries ``args.selector``, not
|
|
255
|
+
``args.operations`` — the leaf targets don't exist yet, they're resolved
|
|
256
|
+
inside the tool after this middleware runs. A rule predicate that can
|
|
257
|
+
reach ``args.operations`` (exact, prefixed, or via a leading wildcard —
|
|
258
|
+
see ``_predicate_reaches_operations``) can therefore never get a fair
|
|
259
|
+
match attempt against a selector call and needs the fail-safe below. A
|
|
260
|
+
rule whose predicates only ever address selector-inspectable fields
|
|
261
|
+
(e.g. ``args.selector.domain``) already got a fair, precise match
|
|
262
|
+
attempt in ``find_matching_rule`` and must not be broadened into an
|
|
263
|
+
unconditional gate.
|
|
264
|
+
"""
|
|
265
|
+
return any(_predicate_reaches_operations(p.path) for p in rule.when)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def evaluate(tool_name: str, args: dict[str, Any], policy: Policy) -> Verdict:
|
|
269
|
+
"""Decide whether one tool call requires approval under ``policy``.
|
|
270
|
+
|
|
271
|
+
A normal rule match (``find_matching_rule``) decides most calls. Two
|
|
272
|
+
fail-safes broaden approval beyond an exact predicate match, each only
|
|
273
|
+
when the operator has SOME rule that could plausibly apply: an
|
|
274
|
+
unmatched ``ha_call_service`` ``ws_command`` call (no ``domain``/
|
|
275
|
+
``service`` args for a domain/service-keyed rule to match), and an
|
|
276
|
+
``ha_bulk_control`` selector call whose matching rule needs fields
|
|
277
|
+
(``args.operations.*``) that don't exist yet at gate time, resolved
|
|
278
|
+
only later inside the tool. See the fail-safe blocks below for the
|
|
279
|
+
full reasoning behind each.
|
|
280
|
+
"""
|
|
281
|
+
if find_matching_rule(tool_name, args, policy) is not None:
|
|
282
|
+
return Verdict.REQUIRE_APPROVAL
|
|
283
|
+
# ha_call_service exposes a raw WebSocket escape hatch (``ws_command``) that
|
|
284
|
+
# carries no ``domain``/``service`` argument, so a rule keyed on
|
|
285
|
+
# ``args.domain``/``args.service`` cannot match it and it would otherwise slip
|
|
286
|
+
# through the fail-open default. If the operator has ANY rule that applies to
|
|
287
|
+
# ha_call_service -- one scoped to it by name, or a wildcard ``*`` rule, which
|
|
288
|
+
# ``match_rule`` treats as applying to every tool -- treat an unmatched
|
|
289
|
+
# ws_command call as require-approval (fail safe) so the escape hatch cannot
|
|
290
|
+
# sneak past that oversight. Blocking the call (require-approval) rather than
|
|
291
|
+
# silently allowing it is the safe error direction for a raw WS escape hatch,
|
|
292
|
+
# especially since the write-command blocklist is a deliberately
|
|
293
|
+
# non-exhaustive wrapper-bypass list that leans on this gate.
|
|
294
|
+
# Operators who want finer control can add a rule keyed on ``args.ws_command``.
|
|
295
|
+
if (
|
|
296
|
+
tool_name == "ha_call_service"
|
|
297
|
+
and args.get("ws_command")
|
|
298
|
+
and any(rule.tool_name in ("ha_call_service", "*") for rule in policy.rules)
|
|
299
|
+
):
|
|
300
|
+
return Verdict.REQUIRE_APPROVAL
|
|
301
|
+
# Structural selectors are resolved inside the tool, after this middleware.
|
|
302
|
+
# A pre-existing rule that inspects args.operations.* therefore cannot inspect
|
|
303
|
+
# the eventual leaf targets. Fail safe only for rules that actually depend on
|
|
304
|
+
# that unresolved data — a rule fully expressed over selector-inspectable
|
|
305
|
+
# fields (e.g. args.selector.domain) already had its precise shot at matching
|
|
306
|
+
# above, and broadening it here would defeat a deliberately conditional rule
|
|
307
|
+
# (a rule scoped to selector.domain == "lock" must not gate a "light" call).
|
|
308
|
+
if has_dynamic_selector_targets(tool_name, args) and any(
|
|
309
|
+
rule.tool_name in ("ha_bulk_control", "*")
|
|
310
|
+
and _rule_needs_resolved_operations(rule)
|
|
311
|
+
for rule in policy.rules
|
|
312
|
+
):
|
|
313
|
+
return Verdict.REQUIRE_APPROVAL
|
|
314
|
+
return Verdict.ALLOW
|