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.
Files changed (303) hide show
  1. {ha_mcp_dev-8.4.0.dev2510/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.1.dev2516}/PKG-INFO +1 -1
  2. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/pyproject.toml +1 -1
  3. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/en.json +2 -2
  4. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.html +2 -2
  5. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_search.py +42 -21
  6. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/enforcement.py +270 -39
  7. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/model.py +58 -11
  8. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/resolver.py +147 -81
  9. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/LICENSE +0 -0
  11. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/README.md +0 -0
  13. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/setup.cfg +0 -0
  14. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_rich_compat.py +0 -0
  18. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/__init__.py +0 -0
  19. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/requirements.txt +0 -0
  20. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
  21. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
  22. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
  23. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
  24. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
  25. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
  26. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
  27. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
  28. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
  29. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
  30. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
  31. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
  32. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
  33. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/client.py +0 -0
  34. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
  35. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
  36. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
  37. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
  38. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
  39. {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
  40. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
  41. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
  42. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
  43. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
  44. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
  45. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
  46. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
  47. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
  48. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
  49. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
  50. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
  51. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
  52. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
  53. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
  54. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
  55. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
  56. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/server.py +0 -0
  57. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
  58. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
  59. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
  60. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
  61. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
  62. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
  63. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
  64. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
  65. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
  66. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
  67. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
  68. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
  69. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
  70. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
  71. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
  72. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
  73. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
  74. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
  75. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_vendor/websockets/version.py +0 -0
  76. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/_version.py +0 -0
  77. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/__init__.py +0 -0
  78. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/consent_form.py +0 -0
  79. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/oidc_compat.py +0 -0
  80. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/auth/provider.py +0 -0
  81. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/backup_manager.py +0 -0
  82. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/browser_landing.py +0 -0
  83. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/__init__.py +0 -0
  84. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/rest_client.py +0 -0
  85. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/supervisor_client.py +0 -0
  86. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/websocket_client.py +0 -0
  87. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/client/websocket_listener.py +0 -0
  88. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/config.py +0 -0
  89. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  90. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  91. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  92. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  93. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  94. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  95. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/errors.py +0 -0
  96. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/hacs_auto_refresh.py +0 -0
  97. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/llm_exposure.py +0 -0
  98. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/log_filters.py +0 -0
  99. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/__init__.py +0 -0
  100. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/approval_queue.py +0 -0
  101. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/editing.py +0 -0
  102. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/evaluator.py +0 -0
  103. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/handlers.py +0 -0
  104. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/middleware.py +0 -0
  105. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/model.py +0 -0
  106. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/persistence.py +0 -0
  107. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/policy/value_sources.py +0 -0
  108. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/py.typed +0 -0
  109. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/read_only.py +0 -0
  110. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/redaction.py +0 -0
  111. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/renamed_tools.py +0 -0
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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
  117. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  118. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  119. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  120. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  121. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {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
  128. {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
  129. {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
  130. {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
  131. {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
  132. {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
  133. {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
  134. {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
  135. {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
  136. {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
  137. {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
  138. {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
  139. {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
  140. {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
  141. {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
  142. {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
  143. {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
  144. {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
  145. {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
  146. {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
  147. {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
  148. {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
  149. {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
  150. {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
  151. {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
  152. {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
  153. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/server.py +0 -0
  154. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/__init__.py +0 -0
  155. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  156. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  157. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  158. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  159. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  160. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  161. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  162. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
  163. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  164. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  165. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_theme.py +0 -0
  166. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  167. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  168. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
  169. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  170. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
  171. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/es.json +0 -0
  172. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
  173. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/it.json +0 -0
  174. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
  175. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
  176. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
  177. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  178. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
  179. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
  180. {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
  181. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.css +0 -0
  182. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/settings_ui/settings.js +0 -0
  183. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/smoke_test.py +0 -0
  184. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  185. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/strict_bps.py +0 -0
  186. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/__init__.py +0 -0
  187. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/auto_backup.py +0 -0
  188. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/backup.py +0 -0
  189. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  190. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_result.py +0 -0
  191. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/best_practice_variables.py +0 -0
  192. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/bulk_selector.py +0 -0
  193. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_api.py +0 -0
  194. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_config_entries.py +0 -0
  195. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_config_reads.py +0 -0
  196. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_devices.py +0 -0
  197. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_registries.py +0 -0
  198. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  199. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  200. {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
  201. {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
  202. {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
  203. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_identity.py +0 -0
  204. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
  205. {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
  206. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/device_control.py +0 -0
  207. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/error_log_parsing.py +0 -0
  208. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/hacs_registration.py +0 -0
  209. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/helpers.py +0 -0
  210. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
  211. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_common.py +0 -0
  212. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_sources.py +0 -0
  213. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
  214. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/__init__.py +0 -0
  215. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/base.py +0 -0
  216. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/matter.py +0 -0
  217. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/thread.py +0 -0
  218. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  219. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/radio/zwave.py +0 -0
  220. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/reference_validator.py +0 -0
  221. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/registry.py +0 -0
  222. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
  223. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  224. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  225. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  226. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  227. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  228. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  229. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
  230. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  231. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  232. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  233. {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
  234. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_addons.py +0 -0
  235. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_areas.py +0 -0
  236. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
  237. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  238. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  239. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_calendar.py +0 -0
  240. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_camera.py +0 -0
  241. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_categories.py +0 -0
  242. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_code.py +0 -0
  243. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  244. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  245. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  246. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  247. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  248. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  249. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_dev.py +0 -0
  250. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_energy.py +0 -0
  251. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_entities.py +0 -0
  252. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  253. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_groups.py +0 -0
  254. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_hacs.py +0 -0
  255. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_history.py +0 -0
  256. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_integrations.py +0 -0
  257. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_labels.py +0 -0
  258. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_logs.py +0 -0
  259. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_radio.py +0 -0
  260. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_registry.py +0 -0
  261. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_resources.py +0 -0
  262. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_security_policy.py +0 -0
  263. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_service.py +0 -0
  264. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_services.py +0 -0
  265. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_system.py +0 -0
  266. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_themes.py +0 -0
  267. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_todo.py +0 -0
  268. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_traces.py +0 -0
  269. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_updates.py +0 -0
  270. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_utility.py +0 -0
  271. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  272. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  273. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  274. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/tools_zones.py +0 -0
  275. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/util_helpers.py +0 -0
  276. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/tools/validation_middleware.py +0 -0
  277. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/__init__.py +0 -0
  278. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/categorized_search.py +0 -0
  279. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  280. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/transport_security.py +0 -0
  281. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/update_check.py +0 -0
  282. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/__init__.py +0 -0
  283. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/config_hash.py +0 -0
  284. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/config_write_lock.py +0 -0
  285. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/data_paths.py +0 -0
  286. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/domain_handlers.py +0 -0
  287. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/entity_membership.py +0 -0
  288. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  289. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  290. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/operation_manager.py +0 -0
  291. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/python_sandbox.py +0 -0
  292. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/skill_loader.py +0 -0
  293. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/utils/usage_logger.py +0 -0
  294. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/__init__.py +0 -0
  295. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp/visibility/persistence.py +0 -0
  296. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  297. {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
  298. {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
  299. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  300. {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
  301. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/tests/__init__.py +0 -0
  302. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/tests/test_constants.py +0 -0
  303. {ha_mcp_dev-8.4.0.dev2510 → ha_mcp_dev-8.4.1.dev2516}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 8.4.0.dev2510
3
+ Version: 8.4.1.dev2516
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "8.4.0.dev2510"
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
@@ -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 the filter inverts – only entities matching an allowlist stay visible, everything else is hidden (including entities added later). Leave all three empty to disable.",
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. Adds two websocket reads per search while enabled.",
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 the filter inverts – only entities matching an allowlist stay visible, everything else is hidden (including entities added later). Leave all three empty to disable.</p>
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 (&ldquo;conversation&rdquo;) voice assistant, mirroring the Settings &gt; Voice assistants &gt; Expose list. Adds two websocket reads per search while enabled.</p>
531
+ <p class="a11y-section-help" data-i18n="visibility.assist.help">Hide entities not exposed to Home Assistant's Assist (&ldquo;conversation&rdquo;) voice assistant, mirroring the Settings &gt; Voice assistants &gt; 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, exactly
2400
- # as the legacy path does — so a visibility-active install can still take
2401
- # the fast path. Without the capability an active filter stays on the
2402
- # legacy path; with no active filter the plain ``search`` route runs with
2403
- # no ``visibility`` param (old components keep working). ``ha_get_overview``
2404
- # needs no analogous gate — it re-applies the filter server-side over the
2405
- # component's raw slices. Checked only when the component would otherwise
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, dict[str, Any] | None]:
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`` capability + config serialized →
2441
- ``(True, <wire dict>)``: the component applies the hide dimensions
2442
- in-process.
2443
- - filter active without the capability, or the config could not be loaded
2444
- → ``(False, None)``: the legacy path applies the filter server-side
2445
- before the counts/pagination (fail-closed to legacy on a bad config,
2446
- matching ``visibility_state_and_wire``'s fail-closed pairing).
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") and visibility is not None:
2456
- return True, visibility
2457
- return False, None
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: dict[str, Any] | None = None,
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: dict[str, Any] | None,
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: dict[str, Any] | None = None,
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
- The whole result is refused rather than token-redacted because adjacent values
21
- may still be the hidden entity's data, and generic mutation could corrupt an
22
- arbitrary tool schema or diagnostic payload.
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 typing import TYPE_CHECKING, Any, NoReturn
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
- # Tool deliberately outside the visibility barrier unless the operator opts in.
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 in hidden:
224
+ if matcher.exact(text):
195
225
  if exact is None:
196
226
  exact = text
197
- elif regex is not None and regex.search(text):
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 _hidden_and_regex(
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
- hidden, regex = await self._hidden_and_regex(config)
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, hidden, regex)
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
- hidden: set[str],
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], hidden, regex)
487
- raw_exact, raw_embedded = _scan_value(args, hidden, regex)
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
- _hidden, regex = await self._hidden_and_regex(config)
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, regex
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
- regex: re.Pattern[str] | None,
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 regex is not None and regex.search(_tool_error_text(exc)):
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
- if regex is not None and regex.search(_serialize_result(result)):
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._hidden: set[str] | None = None
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._hidden is not None and self._key == key and now < self._expiry:
616
- return self._hidden, self._regex
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._hidden is not None and self._key == key:
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._hidden, self._regex
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
- regex = _build_hidden_regex(hidden)
634
- self._hidden = hidden
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 hidden, regex
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
- _hidden, regex = await _hidden_cache.get(config, client)
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(