ha-mcp-dev 7.4.1.dev445__tar.gz → 7.4.1.dev446__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 (108) hide show
  1. {ha_mcp_dev-7.4.1.dev445/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev446}/PKG-INFO +2 -1
  2. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/pyproject.toml +4 -1
  3. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/config.py +31 -0
  4. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/errors.py +8 -0
  5. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/server.py +13 -1
  6. ha_mcp_dev-7.4.1.dev446/src/ha_mcp/tools/tools_code.py +1293 -0
  7. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/categorized_search.py +25 -2
  8. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
  9. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  10. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/requires.txt +1 -0
  11. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/LICENSE +0 -0
  12. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/README.md +0 -0
  14. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/_version.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/auth/provider.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/__init__.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/rest_client.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_client.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/client/websocket_listener.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/py.typed +0 -0
  27. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  28. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  29. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  30. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  38. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  44. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  46. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  47. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/settings_ui.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/backup.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/device_control.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/enhanced.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/helpers.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/reference_validator.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/registry.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/smart_search.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_addons.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_areas.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_energy.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_groups.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_hacs.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_history.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_integrations.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_labels.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_registry.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_resources.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_search.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_service.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_services.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_system.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_todo.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/tools_zones.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/tools/util_helpers.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/transforms/__init__.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/__init__.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/config_hash.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/data_paths.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/domain_handlers.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/operation_manager.py +0 -0
  101. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/python_sandbox.py +0 -0
  102. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp/utils/usage_logger.py +0 -0
  103. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  106. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/tests/__init__.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/tests/test_constants.py +0 -0
  108. {ha_mcp_dev-7.4.1.dev445 → ha_mcp_dev-7.4.1.dev446}/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.4.1.dev445
3
+ Version: 7.4.1.dev446
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
@@ -25,6 +25,7 @@ Requires-Dist: python-dotenv==1.2.2
25
25
  Requires-Dist: truststore==0.10.4
26
26
  Requires-Dist: websockets==16.0
27
27
  Requires-Dist: cryptography==47.0.0
28
+ Requires-Dist: pydantic-monty==0.0.9
28
29
  Dynamic: license-file
29
30
 
30
31
  > **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.4.1.dev445"
7
+ version = "7.4.1.dev446"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "truststore==0.10.4",
32
32
  "websockets==16.0",
33
33
  "cryptography==47.0.0",
34
+ "pydantic-monty==0.0.9",
34
35
  ]
35
36
 
36
37
  [project.urls]
@@ -76,6 +77,8 @@ explicit_package_bases = true
76
77
  module = [
77
78
  "fastmcp.*",
78
79
  "jq",
80
+ "pydantic_monty",
81
+ "pydantic_monty.*",
79
82
  ]
80
83
  ignore_missing_imports = true
81
84
 
@@ -113,6 +113,37 @@ class Settings(BaseSettings):
113
113
  # supervisor UI rejects out-of-range values before they reach env vars.
114
114
  tool_search_max_results: int = Field(5, ge=2, le=10, alias="TOOL_SEARCH_MAX_RESULTS")
115
115
 
116
+ # Code Mode — sandboxed Python execution via pydantic-monty.
117
+ # Provides an "escape hatch" tool (ha_manage_custom_tool) that lets LLMs write
118
+ # custom one-off Python code when no existing tool covers the request.
119
+ # Disabled by default due to the inherent risk of LLM-generated code.
120
+ # Range bounds reject zero/negative values that would silently break the
121
+ # tool and clamp upper bounds at sane safety margins (5 min wall-clock,
122
+ # 256 MB memory, 10k recursion, 10k API/tool calls per execution).
123
+ enable_code_mode: bool = Field(False, alias="ENABLE_CODE_MODE")
124
+ code_mode_max_duration: float = Field(
125
+ 30.0, ge=1.0, le=300.0, alias="CODE_MODE_MAX_DURATION"
126
+ )
127
+ code_mode_max_memory: int = Field(
128
+ 10_485_760, ge=1_048_576, le=268_435_456, alias="CODE_MODE_MAX_MEMORY"
129
+ ) # 10 MB default; 1 MB floor, 256 MB ceiling
130
+ code_mode_max_recursion: int = Field(
131
+ 100, ge=1, le=10_000, alias="CODE_MODE_MAX_RECURSION"
132
+ )
133
+ code_mode_max_invocations: int = Field(
134
+ 100, ge=1, le=10_000, alias="CODE_MODE_MAX_INVOCATIONS"
135
+ )
136
+ # Path to a JSON file for persisting saved custom tools across restarts.
137
+ # Empty string disables persistence (saved tools live in process memory
138
+ # and are lost on restart). The addon sets this to /data/saved_tools.json
139
+ # by default so saved tools survive addon restarts (the /data directory
140
+ # is mapped per-addon by Supervisor and is preserved across addon
141
+ # updates).
142
+ code_mode_saved_tools_path: str = Field(
143
+ "", alias="CODE_MODE_SAVED_TOOLS_PATH"
144
+ )
145
+
146
+
116
147
  @property
117
148
  def env_file_name(self) -> str:
118
149
  """Get the current environment file name."""
@@ -81,6 +81,14 @@ class ErrorCode(StrEnum):
81
81
  # Component errors
82
82
  COMPONENT_NOT_INSTALLED = "COMPONENT_NOT_INSTALLED"
83
83
 
84
+ # Code-mode sandbox errors. The sandbox is a separate execution
85
+ # context; runtime failures inside it map cleanly to one of these
86
+ # three buckets so the LLM can self-recover instead of seeing every
87
+ # failure as INTERNAL_ERROR.
88
+ SANDBOX_LIMIT_EXCEEDED = "SANDBOX_LIMIT_EXCEEDED"
89
+ SANDBOX_SYNTAX_UNSUPPORTED = "SANDBOX_SYNTAX_UNSUPPORTED"
90
+ SANDBOX_RUNTIME_ERROR = "SANDBOX_RUNTIME_ERROR"
91
+
84
92
 
85
93
  # Default suggestions for common error codes
86
94
  DEFAULT_SUGGESTIONS: dict[ErrorCode, list[str]] = {
@@ -590,6 +590,11 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
590
590
  )
591
591
  pinned.extend(getattr(self, "_skill_tool_names", []))
592
592
 
593
+ # Pin code mode tool so it gets individual permission gating
594
+ # rather than being hidden behind the BM25 search proxy.
595
+ if self.settings.enable_code_mode:
596
+ pinned.append("ha_manage_custom_tool")
597
+
593
598
  # The client may not support resources or server instructions — add
594
599
  # skills hint to the search tool description (the one place the LLM
595
600
  # is guaranteed to see).
@@ -609,12 +614,19 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
609
614
  max_results=self.settings.tool_search_max_results,
610
615
  always_visible=pinned,
611
616
  search_tool_description=description,
617
+ # Pinned tools must be excluded from the proxy's
618
+ # category sets when code mode is on; otherwise sandbox
619
+ # code can launder a recursive ``ha_manage_custom_tool``
620
+ # invocation through ``ha_call_write_tool``. See the
621
+ # docstring on ``_rebuild_category_cache``.
622
+ enable_code_mode=self.settings.enable_code_mode,
612
623
  )
613
624
  )
614
625
  logger.info(
615
- "Tool search transform applied (%d pinned tools, max_results=%d)",
626
+ "Tool search transform applied (%d pinned tools, max_results=%d, code_mode=%s)",
616
627
  len(pinned),
617
628
  self.settings.tool_search_max_results,
629
+ self.settings.enable_code_mode,
618
630
  )
619
631
  except Exception:
620
632
  logger.exception("Failed to apply tool search transform")