ha-mcp-dev 7.12.3.dev1882__tar.gz → 7.12.3.dev1885__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 (165) hide show
  1. {ha_mcp_dev-7.12.3.dev1882/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1885}/PKG-INFO +2 -2
  2. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/README.md +1 -1
  3. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/config.py +18 -0
  5. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/errors.py +6 -0
  6. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/server.py +64 -9
  7. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/settings_ui/__init__.py +97 -39
  8. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/settings_ui/settings.css +34 -12
  9. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/settings_ui/settings.js +62 -9
  10. ha_mcp_dev-7.12.3.dev1885/src/ha_mcp/strict_bps.py +272 -0
  11. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_config_automations.py +4 -0
  12. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_config_dashboards.py +4 -0
  13. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_config_helpers.py +4 -0
  14. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_config_scenes.py +4 -0
  15. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_config_scripts.py +4 -0
  16. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_yaml_config.py +4 -0
  17. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/kill_signal_diagnostics.py +13 -2
  18. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -2
  19. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  20. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/LICENSE +0 -0
  21. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/MANIFEST.in +0 -0
  22. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/setup.cfg +0 -0
  23. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/__init__.py +0 -0
  24. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/__main__.py +0 -0
  25. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/_pypi_marker +0 -0
  26. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/_version.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/auth/__init__.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/auth/consent_form.py +0 -0
  29. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/auth/provider.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/backup_manager.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/browser_landing.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/client/__init__.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/client/rest_client.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/client/supervisor_client.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/client/websocket_client.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/client/websocket_listener.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/llm_exposure.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/__init__.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/approval_queue.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/evaluator.py +0 -0
  44. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/handlers.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/middleware.py +0 -0
  46. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/model.py +0 -0
  47. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/persistence.py +0 -0
  48. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/policy/value_sources.py +0 -0
  49. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/py.typed +0 -0
  50. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/read_only.py +0 -0
  51. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  52. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  53. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  54. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  55. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  56. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  57. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  60. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  63. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  66. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  71. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  72. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  73. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  74. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  75. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  76. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/settings_ui/settings.html +0 -0
  77. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/smoke_test.py +0 -0
  78. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  79. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/__init__.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/auto_backup.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/backup.py +0 -0
  82. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  83. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/component_api.py +0 -0
  84. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/device_control.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/enhanced.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/helpers.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/__init__.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/base.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/matter.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/thread.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/radio/zwave.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/reference_validator.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/registry.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_addons.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_areas.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_calendar.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_camera.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_categories.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_code.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_dev.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_energy.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_entities.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_groups.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_hacs.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_history.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_integrations.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_labels.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_radio.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_registry.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_resources.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_search.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_service.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_services.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_system.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_themes.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_todo.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_traces.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_updates.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_utility.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/tools_zones.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/util_helpers.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/tools/validation_middleware.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/transforms/__init__.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/transforms/categorized_search.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/transport_security.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/update_check.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/__init__.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/config_hash.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/data_paths.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/domain_handlers.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/operation_manager.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/python_sandbox.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/skill_loader.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/utils/usage_logger.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/visibility/__init__.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/visibility/model.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/visibility/persistence.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp/visibility/resolver.py +0 -0
  159. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  162. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  163. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/tests/__init__.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/tests/test_constants.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1882 → ha_mcp_dev-7.12.3.dev1885}/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: 7.12.3.dev1882
3
+ Version: 7.12.3.dev1885
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
@@ -268,7 +268,7 @@ Spend less time configuring, more time enjoying your smart home.
268
268
  | **HACS** | `ha_get_hacs_info`, `ha_manage_hacs` |
269
269
  | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_remove_helpers_integrations` |
270
270
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
271
- | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration_enabled` |
271
+ | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration` |
272
272
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
273
273
  | **Matter** | `ha_manage_radio` |
274
274
  | **Scenes** | `ha_config_get_scene`, `ha_config_remove_scene`, `ha_config_set_scene` |
@@ -235,7 +235,7 @@ Spend less time configuring, more time enjoying your smart home.
235
235
  | **HACS** | `ha_get_hacs_info`, `ha_manage_hacs` |
236
236
  | **Helper Entities** | `ha_config_list_helpers`, `ha_config_set_helper`, `ha_remove_helpers_integrations` |
237
237
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
238
- | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration_enabled` |
238
+ | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration` |
239
239
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
240
240
  | **Matter** | `ha_manage_radio` |
241
241
  | **Scenes** | `ha_config_get_scene`, `ha_config_remove_scene`, `ha_config_set_scene` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.12.3.dev1882"
7
+ version = "7.12.3.dev1885"
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
@@ -237,6 +237,18 @@ class Settings(BaseSettings):
237
237
  # regardless of the per-call param or BP warnings. Default on.
238
238
  enable_mandatory_bps: bool = Field(True, alias="ENABLE_MANDATORY_BPS")
239
239
 
240
+ # Strict best-practices gate (issue #1779) — child flag of
241
+ # ``enable_mandatory_bps``. When effective, the six write tools are
242
+ # HARD-BLOCKED unless the call carries the acknowledgment key that is
243
+ # published only inside the best-practices skill content served by
244
+ # ``ha_get_skill_guide`` (modeled on the Hubitat MCP acknowledgment
245
+ # gate). Default ON so strict mode is active whenever the parent is on;
246
+ # inert when the parent is off — that cascade is enforced at the
247
+ # consumption site (``strict_bps.strict_bps_effective``), not here,
248
+ # because this flag is deliberately NOT a beta sub-flag and there is no
249
+ # config-level parent gate for non-beta flags.
250
+ enable_strict_mandatory_bps: bool = Field(True, alias="ENABLE_STRICT_MANDATORY_BPS")
251
+
240
252
  # Filesystem tools — read/write/delete/list under the HA config dir.
241
253
  # Previously gated by a direct ``os.getenv`` call in
242
254
  # ``tools/tools_filesystem.py`` so callers (and the settings UI)
@@ -656,6 +668,12 @@ FEATURE_FLAG_FIELDS: tuple[FeatureFlagField, ...] = (
656
668
  # gated by the beta master) nor in ADVANCED_SETTINGS_FIELDS (registries
657
669
  # are name-disjoint per _validate_registries()).
658
670
  FeatureFlagField("enable_mandatory_bps", "ENABLE_MANDATORY_BPS", bool),
671
+ # Child flag of enable_mandatory_bps (#1779). Non-beta like its
672
+ # parent, so it belongs here and NOT in BETA_FEATURE_FIELDS; kept out
673
+ # of ADVANCED_SETTINGS_FIELDS too (registries are name-disjoint).
674
+ FeatureFlagField(
675
+ "enable_strict_mandatory_bps", "ENABLE_STRICT_MANDATORY_BPS", bool
676
+ ),
659
677
  FeatureFlagField("enable_yaml_config_editing", "ENABLE_YAML_CONFIG_EDITING", bool),
660
678
  FeatureFlagField("enable_yaml_edit_confirm", "ENABLE_YAML_EDIT_CONFIRM", bool),
661
679
  # Per-key sub-gates beneath enable_yaml_config_editing. Nested in
@@ -100,6 +100,12 @@ class ErrorCode(StrEnum):
100
100
  # because the server-wide Read Only Mode toggle is on.
101
101
  READ_ONLY_MODE = "READ_ONLY_MODE"
102
102
 
103
+ # Strict mandatory best-practices gate (#1779). A gated write tool was
104
+ # blocked because strict mode is effective and the call did not carry
105
+ # the acknowledgment key published only in the best-practices skill
106
+ # content. Fail-closed: nothing was changed.
107
+ BPS_ACKNOWLEDGMENT_REQUIRED = "BPS_ACKNOWLEDGMENT_REQUIRED"
108
+
103
109
  # Mandatory auto-backup (#1579). A write that requires a pre-write
104
110
  # snapshot was blocked because the snapshot could not be captured
105
111
  # (fetch failed, the snapshot write failed — e.g. disk full — or the
@@ -257,6 +257,13 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
257
257
  # queues a pointless approval request.
258
258
  self._apply_read_only_middleware()
259
259
 
260
+ # Strict mandatory best-practices gate (#1779) — always installed,
261
+ # self-no-ops at call time. Registered immediately AFTER the
262
+ # read-only middleware so a read-only rejection wins over the
263
+ # strict-BPS one (a write blocked by read-only mode should not be
264
+ # asked for an acknowledgment key it could never usefully supply).
265
+ self._apply_strict_bps_middleware()
266
+
260
267
  # Wire tool security policies middleware (#966) — opt-in via
261
268
  # ENABLE_TOOL_SECURITY_POLICIES. Must come last so the middleware
262
269
  # wraps the final tool surface (including the search proxies).
@@ -1041,6 +1048,41 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
1041
1048
 
1042
1049
  self.mcp.add_middleware(ReadOnlyMiddleware(list_tools=_list_all_tools))
1043
1050
 
1051
+ def _apply_strict_bps_middleware(self) -> None:
1052
+ """Install the strict best-practices gate (#1779).
1053
+
1054
+ Always installed — the middleware self-no-ops at call time
1055
+ (consults ``strict_bps_effective()`` per call), so a toggle applies
1056
+ live in standalone-HTTP/embedded mode like ``read_only_mode``
1057
+ (other modes pick it up on restart).
1058
+
1059
+ Also emits a startup WARNING when the child flag is on while its
1060
+ parent (``enable_mandatory_bps``) is off: strict mode is inert in
1061
+ that configuration, and env-var users (Docker, uvx, pip) would
1062
+ otherwise have no signal that their strict toggle does nothing.
1063
+ Precedent: the lite-docstrings warning.
1064
+ """
1065
+ from .strict_bps import StrictBpsMiddleware
1066
+
1067
+ if self.settings.enable_strict_mandatory_bps and (
1068
+ not self.settings.enable_mandatory_bps
1069
+ ):
1070
+ logger.warning(
1071
+ "ENABLE_STRICT_MANDATORY_BPS is on but ENABLE_MANDATORY_BPS is "
1072
+ "off — strict best-practices mode is INERT. The strict gate only "
1073
+ "engages when the mandatory-BPS master switch is also on. Enable "
1074
+ "ENABLE_MANDATORY_BPS to activate the acknowledgment gate."
1075
+ )
1076
+
1077
+ # Unfiltered catalog lookup so the gate can pass through calls to
1078
+ # gate-map tools that never registered (e.g. ha_config_set_yaml with
1079
+ # yaml editing off) — those get FastMCP's unknown-tool error instead
1080
+ # of a key-fetch misdirection (#1820 review).
1081
+ async def _list_all_tools() -> Any:
1082
+ return await self.mcp.local_provider._list_tools()
1083
+
1084
+ self.mcp.add_middleware(StrictBpsMiddleware(list_tools=_list_all_tools))
1085
+
1044
1086
  # Shared action-phrased keyword block for retrieval. Some MCP clients
1045
1087
  # (Claude Code, others) rank candidate tools by token-overlap between
1046
1088
  # the user's natural-language query and each tool's `description`
@@ -1295,14 +1337,13 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
1295
1337
  ),
1296
1338
  ] = None,
1297
1339
  ) -> dict[str, Any]:
1298
- # ``skills_dir`` is captured from the enclosing scope at
1299
- # registration time. The current ``_get_skills_dir()`` is
1300
- # effectively static per process (it inspects an on-disk
1301
- # path that doesn't change), so the closure is fine. If a
1302
- # future change makes the skills location dynamic (env-var
1303
- # override, etc.), the closure won't pick that up — re-read
1304
- # via ``self._get_skills_dir()`` here instead.
1305
- return self._handle_skill_guide_call(skills_dir, skill, file)
1340
+ # Re-read the skills dir live on every call (#1820 review):
1341
+ # the strict-BPS gate also reads it live, so a registration-time
1342
+ # capture deadlocks the one recovery path — vendor absent at
1343
+ # boot (gate fails open), operator runs `git submodule update
1344
+ # --init` in place, the gate flips ON, but a captured None here
1345
+ # would keep the acknowledgment key unobtainable until restart.
1346
+ return self._handle_skill_guide_call(self._get_skills_dir(), skill, file)
1306
1347
 
1307
1348
  self.mcp.tool(
1308
1349
  name=SKILL_TOOL_NAME,
@@ -1560,10 +1601,24 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
1560
1601
  # parsing the (potentially large) content body. Scoped to the
1561
1602
  # best-practice skill because that's the one the write-tool
1562
1603
  # MandatoryBPS param gates; other skills (if any) are unrelated.
1604
+ from .strict_bps import strict_bps_ack_line, strict_bps_effective
1563
1605
  from .tools.util_helpers import _HA_BEST_PRACTICES_SKILL_NAME
1564
1606
 
1607
+ is_best_practices = skill == _HA_BEST_PRACTICES_SKILL_NAME
1608
+
1609
+ # Publish the acknowledgment key inside the best-practices content
1610
+ # itself (#1779) when strict mode is effective — this Tier-3 read is
1611
+ # the ONLY caller-facing surface that carries the key. Prepended to
1612
+ # the content body so a model that reads the guide obtains the key
1613
+ # it must pass as BestPracticeKey on gated writes. The skill://
1614
+ # resource surface reads raw files and does NOT get this line; that
1615
+ # is accepted — resource-preferring clients recover via the block
1616
+ # error's suggestion, which points them back at this tool.
1617
+ if is_best_practices and strict_bps_effective():
1618
+ content = f"{strict_bps_ack_line()}\n\n{content}"
1619
+
1565
1620
  response: dict[str, Any] = {}
1566
- if skill == _HA_BEST_PRACTICES_SKILL_NAME:
1621
+ if is_best_practices:
1567
1622
  response["skill_content_hint"] = _SKILL_GUIDE_MANDATORYBPS_HINT
1568
1623
  response.update(
1569
1624
  {
@@ -19,7 +19,7 @@ import os
19
19
  import re
20
20
  import time
21
21
  import uuid
22
- from collections.abc import Awaitable, Callable
22
+ from collections.abc import Awaitable, Callable, Container
23
23
  from pathlib import Path
24
24
  from typing import TYPE_CHECKING, Any, Literal, NamedTuple, NotRequired, TypedDict
25
25
 
@@ -1248,6 +1248,49 @@ def _schedule_supervisor_self_restart(
1248
1248
  task.add_done_callback(_BACKGROUND_RESTART_TASKS.discard)
1249
1249
 
1250
1250
 
1251
+ def _reject_child_flags_without_parent(
1252
+ raw_flags: dict[str, Any],
1253
+ parent_field: str,
1254
+ child_fields: Container[str],
1255
+ message: Callable[[list[str]], str],
1256
+ suggestions: list[str],
1257
+ ) -> JSONResponse | None:
1258
+ """Reject a feature-flag save that enables child flags while their
1259
+ parent stays off after the merge.
1260
+
1261
+ A child flag is only valid when ``parent_field`` is truthy AFTER the
1262
+ merge. The post-merge parent is derived from the payload (if present),
1263
+ else the live ``Settings`` value — the same value the runtime gate
1264
+ will see. A child turned truthy against an
1265
+ off parent would be forced back off at runtime, so reject it now
1266
+ rather than let the user learn the save was a no-op at next startup.
1267
+ Turning the parent off alone is NOT rejected: children absent from the
1268
+ payload keep their persisted value and the runtime gate handles them.
1269
+
1270
+ Returns a 409 ``JSONResponse`` — ``message`` receives every offending
1271
+ child, which is also echoed in ``context["rejected"]`` — or ``None``
1272
+ when there is nothing to reject.
1273
+ """
1274
+ rejected = [k for k in raw_flags if k in child_fields and bool(raw_flags[k])]
1275
+ effective_parent = bool(
1276
+ raw_flags.get(
1277
+ parent_field,
1278
+ getattr(get_global_settings(), parent_field),
1279
+ )
1280
+ )
1281
+ if rejected and not effective_parent:
1282
+ return JSONResponse(
1283
+ create_error_response(
1284
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
1285
+ message(rejected),
1286
+ suggestions=suggestions,
1287
+ context={"rejected": rejected},
1288
+ ),
1289
+ status_code=409,
1290
+ )
1291
+ return None
1292
+
1293
+
1251
1294
  def build_settings_handlers(
1252
1295
  server: HomeAssistantSmartMCPServer | None,
1253
1296
  *,
@@ -1943,14 +1986,11 @@ def build_settings_handlers(
1943
1986
  status_code=400,
1944
1987
  )
1945
1988
 
1946
- # Master beta-gate check: a sub-flag write is only valid
1947
- # when the master ``enable_beta_features`` is on AFTER the
1948
- # merge. Derive the post-merge master from the payload (if
1949
- # present), otherwise fall back to the live ``Settings`` value.
1950
- # Reject sub-flag writes that try to enable a beta when the
1951
- # resulting master state would still be off — the runtime gate
1952
- # would force them False anyway and the user should know the
1953
- # save was a no-op rather than learning at next startup.
1989
+ # Master beta-gate check: a beta sub-flag may only be enabled
1990
+ # when the master ``enable_beta_features`` is on AFTER the merge
1991
+ # (see ``_reject_child_flags_without_parent`` for the post-merge
1992
+ # derivation and why a parent-off-only save is not rejected). All
1993
+ # offending sub-flags are listed in the rejection.
1954
1994
  #
1955
1995
  # Applied in BOTH standalone and addon mode.
1956
1996
  # The earlier "skip in addon mode" carve-out existed because
@@ -1959,43 +1999,61 @@ def build_settings_handlers(
1959
1999
  # one-cycle legacy bridge. On dev addon, start.py writes the
1960
2000
  # master env var from the schema-bound options key. On stable
1961
2001
  # addon, the master is not in schema and the standalone
1962
- # web-UI master path remains the gate (the gate read below
2002
+ # web-UI master path remains the gate (the gate read
1963
2003
  # falls through to the override-file value). Either way the
1964
2004
  # gate is sound to apply uniformly.
1965
2005
  from ..config import (
1966
2006
  BETA_FEATURE_FIELDS as _BETA_SUB,
1967
2007
  )
1968
2008
 
1969
- effective_master = bool(
1970
- raw_flags.get(
1971
- "enable_beta_features",
1972
- getattr(get_global_settings(), "enable_beta_features", False),
1973
- )
2009
+ beta_rejection = _reject_child_flags_without_parent(
2010
+ raw_flags,
2011
+ "enable_beta_features",
2012
+ _BETA_SUB,
2013
+ lambda rejected: (
2014
+ "Cannot enable beta sub-flag(s) "
2015
+ f"{', '.join(rejected)} while the master "
2016
+ "'Enable beta features' toggle is off. Include "
2017
+ "enable_beta_features=true in the same save, or "
2018
+ "flip the master on first."
2019
+ ),
2020
+ [
2021
+ "Include enable_beta_features=true in the same save "
2022
+ + "payload as the sub-flag(s).",
2023
+ "Or turn on the master 'Enable beta features' toggle "
2024
+ + "first, then enable the sub-flag(s).",
2025
+ ],
1974
2026
  )
1975
- beta_sub_writes = [
1976
- k for k in raw_flags if k in _BETA_SUB and bool(raw_flags[k])
1977
- ]
1978
- if beta_sub_writes and not effective_master:
1979
- return JSONResponse(
1980
- create_error_response(
1981
- ErrorCode.VALIDATION_INVALID_PARAMETER,
1982
- (
1983
- "Cannot enable beta sub-flag(s) "
1984
- f"{', '.join(beta_sub_writes)} while the master "
1985
- "'Enable beta features' toggle is off. Include "
1986
- "enable_beta_features=true in the same save, or "
1987
- "flip the master on first."
1988
- ),
1989
- suggestions=[
1990
- "Include enable_beta_features=true in the same save "
1991
- + "payload as the sub-flag(s).",
1992
- "Or turn on the master 'Enable beta features' toggle "
1993
- + "first, then enable the sub-flag(s).",
1994
- ],
1995
- context={"rejected": beta_sub_writes},
1996
- ),
1997
- status_code=409,
1998
- )
2027
+ if beta_rejection is not None:
2028
+ return beta_rejection
2029
+
2030
+ # Strict-mandatory-BPS dependency gate: strict mode
2031
+ # (``enable_strict_mandatory_bps``) is a child of
2032
+ # ``enable_mandatory_bps`` and is inert unless the parent is on.
2033
+ # Same post-merge derivation and no-parent-off-rejection as the
2034
+ # beta gate above (see ``_reject_child_flags_without_parent``).
2035
+ strict_rejection = _reject_child_flags_without_parent(
2036
+ raw_flags,
2037
+ "enable_mandatory_bps",
2038
+ ("enable_strict_mandatory_bps",),
2039
+ lambda _rejected: (
2040
+ "Cannot enable strict best-practices mode "
2041
+ "('enable_strict_mandatory_bps') while the parent "
2042
+ "'Attach best-practice skills on writes' "
2043
+ "(enable_mandatory_bps) toggle is off. Strict mode is "
2044
+ "a child of that toggle and has no effect without it. "
2045
+ "Include enable_mandatory_bps=true in the same save, or "
2046
+ "turn the parent on first."
2047
+ ),
2048
+ [
2049
+ "Include enable_mandatory_bps=true in the same save "
2050
+ + "payload as enable_strict_mandatory_bps.",
2051
+ "Or turn on the parent 'Attach best-practice skills on "
2052
+ + "writes' toggle first, then enable strict mode.",
2053
+ ],
2054
+ )
2055
+ if strict_rejection is not None:
2056
+ return strict_rejection
1999
2057
 
2000
2058
  # Build the validated override dict. Reject unknown fields and
2001
2059
  # env-locked fields up front so the user gets a precise error
@@ -504,18 +504,40 @@
504
504
 
505
505
  .feature-row.beta-master-row { font-weight: 600; padding-top: 20px; border-top: 2px solid var(--border); margin-top: 8px; }
506
506
  .feature-row.beta-master-row .feature-help { font-weight: 400; }
507
- .feature-row.beta-sub { padding-left: 32px; position: relative; }
508
- .feature-row.beta-sub::before { content: ""; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: var(--border); }
509
- .feature-row.beta-sub.dimmed { opacity: 0.38; }
510
- .feature-row.beta-sub.dimmed input { cursor: not-allowed; }
511
- .feature-row.codemode-sub { padding-left: 56px; position: relative; }
512
- .feature-row.codemode-sub::before { content: ""; position: absolute; left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
513
- .feature-row.codemode-sub.dimmed { opacity: 0.38; }
514
- .feature-row.codemode-sub.dimmed input { cursor: not-allowed; }
515
- .feature-row.yaml-packages-sub { padding-left: 56px; position: relative; }
516
- .feature-row.yaml-packages-sub::before { content: ""; position: absolute; left: 36px; top: 0; bottom: 0; width: 2px; background: var(--border); }
517
- .feature-row.yaml-packages-sub.dimmed { opacity: 0.38; }
518
- .feature-row.yaml-packages-sub.dimmed input { cursor: not-allowed; }
507
+ /* Nested sub-flag rows share one guide-bar treatment; only the indent
508
+ depth differs (see the depth-1 / depth-2 blocks below). */
509
+ .feature-row.beta-sub,
510
+ .feature-row.codemode-sub,
511
+ .feature-row.yaml-packages-sub,
512
+ .feature-row.mandatory-bps-sub { position: relative; }
513
+ .feature-row.beta-sub::before,
514
+ .feature-row.codemode-sub::before,
515
+ .feature-row.yaml-packages-sub::before,
516
+ .feature-row.mandatory-bps-sub::before {
517
+ content: ""; position: absolute; top: 0; bottom: 0; width: 2px; background: var(--border);
518
+ }
519
+ .feature-row.beta-sub.dimmed,
520
+ .feature-row.codemode-sub.dimmed,
521
+ .feature-row.yaml-packages-sub.dimmed,
522
+ .feature-row.mandatory-bps-sub.dimmed { opacity: 0.38; }
523
+ .feature-row.beta-sub.dimmed input,
524
+ .feature-row.codemode-sub.dimmed input,
525
+ .feature-row.yaml-packages-sub.dimmed input,
526
+ .feature-row.mandatory-bps-sub.dimmed input { cursor: not-allowed; }
527
+
528
+ /* Depth 1 — nested directly under a top-level toggle (beta-sub under the
529
+ master; strict-mode under the non-beta enable_mandatory_bps row). */
530
+ .feature-row.beta-sub,
531
+ .feature-row.mandatory-bps-sub { padding-left: 32px; }
532
+ .feature-row.beta-sub::before,
533
+ .feature-row.mandatory-bps-sub::before { left: 12px; }
534
+
535
+ /* Depth 2 — nested under a beta-sub parent (yaml packages under
536
+ enable_yaml_config_editing; code-mode numerics under enable_code_mode). */
537
+ .feature-row.codemode-sub,
538
+ .feature-row.yaml-packages-sub { padding-left: 56px; }
539
+ .feature-row.codemode-sub::before,
540
+ .feature-row.yaml-packages-sub::before { left: 36px; }
519
541
 
520
542
  /* ── ADVANCED SETTINGS ───────────────────────────────────────────────── */
521
543
  .adv-section-title {
@@ -1555,6 +1555,10 @@ const FEATURE_META = {
1555
1555
  label: "Attach best-practice skills on writes",
1556
1556
  help: "Master switch for the write-tool skill content delivery feature (issue #1182). When enabled (default), the six config write tools (automations, scripts, scenes, helpers, dashboards, raw YAML) attach the canonical Home Assistant best-practice reference files under skill_content on every successful write, plus auto-embed any reference sections cited by best-practice warnings. Each tool also exposes a per-call MandatoryBPS parameter the agent can set to false on subsequent calls once it has the content. When this master switch is off, NO skill_content goes out regardless of the per-call parameter or BP warnings. Leave on if your LLM benefits from inline guidance; turn off to minimise tokens when using an LLM that has the best-practice files indexed via skills or another retrieval path. Requires restart to take effect.",
1557
1557
  },
1558
+ enable_strict_mandatory_bps: {
1559
+ label: "Strict best-practices mode",
1560
+ help: "Strict mode: prevents the client from using the tool until it can prove that it read the best practices. While on, the six best-practice write tools (automations, scripts, scenes, helpers, dashboards, raw YAML) are blocked and return an error directing the client to read the best-practices skill via ha_get_skill_guide and pass back the acknowledgment key it obtains there. Nested under \"Attach best-practice skills on writes\" above and inert while that parent toggle is off. Requires restart to take effect (applies live in standalone HTTP mode).",
1561
+ },
1558
1562
  // Master beta toggle — gates the 5 sub-flags below at runtime
1559
1563
  // (see config.py:_apply_feature_flag_overrides master gate). UI
1560
1564
  // dims sub-rows when this is off and re-renders live on flip.
@@ -1613,7 +1617,7 @@ let BETA_SUB_FLAGS = new Set();
1613
1617
  // Sub-flags of ``enable_yaml_config_editing`` (confirm-flow toggle +
1614
1618
  // per-key packages gates). They ARE in BETA_SUB_FLAGS (it mirrors
1615
1619
  // config.BETA_FEATURE_FIELDS verbatim), but the main render pass skips
1616
- // them via the includes() guard below, and renderYamlPackagesSubRows
1620
+ // them via the includes() guard below, and renderSubFlagRows
1617
1621
  // re-renders them nested beneath their parent — so they never appear
1618
1622
  // as standalone beta-sub rows.
1619
1623
  const YAML_PACKAGES_SUB_FLAGS = [
@@ -1623,6 +1627,16 @@ const YAML_PACKAGES_SUB_FLAGS = [
1623
1627
  'enable_yaml_packages_scene',
1624
1628
  ];
1625
1629
 
1630
+ // Sub-flag of ``enable_mandatory_bps`` (strict best-practices mode).
1631
+ // Unlike YAML_PACKAGES_SUB_FLAGS this is NON-beta — it is NOT in
1632
+ // BETA_SUB_FLAGS and its only gate is the parent toggle. The main
1633
+ // render pass skips it via the includes() guard below, and
1634
+ // renderSubFlagRows re-renders it nested beneath its parent so
1635
+ // it never appears as a standalone top-level row.
1636
+ const MANDATORY_BPS_SUB_FLAGS = [
1637
+ 'enable_strict_mandatory_bps',
1638
+ ];
1639
+
1626
1640
  // Cached add-on flag. Each settings endpoint (/api/settings/features,
1627
1641
  // /api/settings/advanced, /api/settings/backup-config) returns
1628
1642
  // ``is_addon`` so the env-locked banner copy can adapt — the addon
@@ -1733,9 +1747,12 @@ function renderFeatureFlags(flags) {
1733
1747
  const f = flags[fieldName];
1734
1748
  if (!f) return;
1735
1749
  // Skip yaml-packages sub-rows in the main pass — they're rendered
1736
- // by renderYamlPackagesSubRows below right after their parent so
1750
+ // by renderSubFlagRows below right after their parent so
1737
1751
  // the nesting reads in source order.
1738
1752
  if (YAML_PACKAGES_SUB_FLAGS.includes(fieldName)) return;
1753
+ // Same for the strict-mode sub-row — rendered by
1754
+ // renderSubFlagRows right after its enable_mandatory_bps parent.
1755
+ if (MANDATORY_BPS_SUB_FLAGS.includes(fieldName)) return;
1739
1756
  const meta = FEATURE_META[fieldName];
1740
1757
  const isMaster = fieldName === 'enable_beta_features';
1741
1758
  const isBetaSub = BETA_SUB_FLAGS.has(fieldName);
@@ -1820,6 +1837,18 @@ function renderFeatureFlags(flags) {
1820
1837
  renderFeatureFlags(_lastFeatureFlags);
1821
1838
  }
1822
1839
  }
1840
+ // Re-render on enable_mandatory_bps flip so the strict-mode
1841
+ // sub-row dims/undims immediately. Same live-cache pattern as
1842
+ // the master and yaml-config flips above.
1843
+ if (fieldName === 'enable_mandatory_bps') {
1844
+ if (_lastFeatureFlags[fieldName]) {
1845
+ _lastFeatureFlags[fieldName] = {
1846
+ ..._lastFeatureFlags[fieldName],
1847
+ value: input.checked,
1848
+ };
1849
+ renderFeatureFlags(_lastFeatureFlags);
1850
+ }
1851
+ }
1823
1852
  saveFeatureFlag(fieldName, input.checked);
1824
1853
  });
1825
1854
  const slider = document.createElement('span');
@@ -1864,7 +1893,20 @@ function renderFeatureFlags(flags) {
1864
1893
  // itself is off.
1865
1894
  if (fieldName === 'enable_yaml_config_editing') {
1866
1895
  const parentOn = !!f.value;
1867
- renderYamlPackagesSubRows(flags, targetBody, masterOn, parentOn);
1896
+ renderSubFlagRows(flags, targetBody, YAML_PACKAGES_SUB_FLAGS, {
1897
+ cssClass: 'yaml-packages-sub',
1898
+ lockedByGate: !masterOn || !parentOn,
1899
+ });
1900
+ }
1901
+ // After the enable_mandatory_bps parent row, inject its strict-mode
1902
+ // sub-row. This whole group is non-beta, so the only gate is the
1903
+ // parent toggle (no master beta involved).
1904
+ if (fieldName === 'enable_mandatory_bps') {
1905
+ const parentOn = !!f.value;
1906
+ renderSubFlagRows(flags, targetBody, MANDATORY_BPS_SUB_FLAGS, {
1907
+ cssClass: 'mandatory-bps-sub',
1908
+ lockedByGate: !parentOn,
1909
+ });
1868
1910
  }
1869
1911
  // After the enable_filesystem_tools row, inject the custom-directories
1870
1912
  // editor (issue #1567). Dimmed when either the master beta is off or
@@ -1877,14 +1919,23 @@ function renderFeatureFlags(flags) {
1877
1919
  });
1878
1920
  }
1879
1921
 
1880
- function renderYamlPackagesSubRows(flags, parentEl, masterOn, parentOn) {
1881
- YAML_PACKAGES_SUB_FLAGS.forEach(fieldName => {
1922
+ // Shared renderer for bool sub-flag rows nested under a parent toggle
1923
+ // (yaml-packages under enable_yaml_config_editing, strict mode under
1924
+ // enable_mandatory_bps). Each row is a checkbox+slider whose change
1925
+ // handler syncs _lastFeatureFlags and POSTs via saveFeatureFlag, dimmed
1926
+ // + input-disabled when ``lockedByGate`` is true. Callers precompute
1927
+ // lockedByGate from whatever gates apply to their group (yaml-packages:
1928
+ // master beta AND parent; mandatory-bps: parent only) and pass the CSS
1929
+ // class that carries the group's indent/guide-bar depth. The number/text
1930
+ // code-mode sub-numerics are NOT rendered here — they save through
1931
+ // commitAdvancedEdit and live in renderCodeModeSubRows.
1932
+ function renderSubFlagRows(flags, parentEl, subFieldNames, { cssClass, lockedByGate }) {
1933
+ subFieldNames.forEach(fieldName => {
1882
1934
  const f = flags[fieldName];
1883
1935
  if (!f) return;
1884
1936
  const meta = FEATURE_META[fieldName] || { label: fieldName, help: '' };
1885
- const lockedByGate = !masterOn || !parentOn;
1886
1937
  const row = document.createElement('div');
1887
- row.className = 'feature-row yaml-packages-sub' + (lockedByGate ? ' dimmed' : '');
1938
+ row.className = 'feature-row ' + cssClass + (lockedByGate ? ' dimmed' : '');
1888
1939
 
1889
1940
  const info = document.createElement('div');
1890
1941
  info.className = 'feature-info';
@@ -1900,7 +1951,7 @@ function renderYamlPackagesSubRows(flags, parentEl, masterOn, parentOn) {
1900
1951
  `${escapeHtml(ORIGIN_INFO_NOTE[f.origin])}</div>`
1901
1952
  : '';
1902
1953
  info.innerHTML =
1903
- `<div class="feature-name">${escapeHtml(meta.label)}</div>` +
1954
+ `<div class="feature-name" id="label-feature-${fieldName}">${escapeHtml(meta.label)}</div>` +
1904
1955
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
1905
1956
  lockedNote + infoNote;
1906
1957
 
@@ -1913,6 +1964,7 @@ function renderYamlPackagesSubRows(flags, parentEl, masterOn, parentOn) {
1913
1964
  input.name = 'feature:' + fieldName;
1914
1965
  input.checked = !!f.value;
1915
1966
  input.disabled = !f.editable || lockedByGate;
1967
+ input.setAttribute('aria-labelledby', 'label-feature-' + fieldName);
1916
1968
  input.addEventListener('change', () => {
1917
1969
  // Keep the cached flag value in sync (parity with the parent/master
1918
1970
  // row handlers) so a later parent flip — which re-renders from
@@ -1980,7 +2032,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
1980
2032
  `<div class="feature-locked-note">${envLockedNoteHtml(f.env_var, f.field)}</div>`;
1981
2033
  }
1982
2034
  info.innerHTML =
1983
- `<div class="feature-name">${escapeHtml(meta.label)}</div>` +
2035
+ `<div class="feature-name" id="label-feature-${f.field}">${escapeHtml(meta.label)}</div>` +
1984
2036
  `<div class="feature-help">${escapeHtml(meta.help)}</div>` +
1985
2037
  lockedNote;
1986
2038
 
@@ -2003,6 +2055,7 @@ function renderCodeModeSubRows(parentEl, masterOn, codeModeOn) {
2003
2055
  inputEl.disabled = disabled;
2004
2056
  inputEl.dataset.advField = f.field;
2005
2057
  inputEl.name = 'adv:' + f.field;
2058
+ inputEl.setAttribute('aria-labelledby', 'label-feature-' + f.field);
2006
2059
  inputEl.addEventListener('change', () => {
2007
2060
  let v;
2008
2061
  if (f.type === 'int') v = parseInt(inputEl.value, 10);