Skip to content

Releases

v0.29.0 — nodered and neonplan namespaces, entities_remove_entity, tool output fixes

Choose a tag to compare

@Fistacho Fistacho released this 12 Nov 05:57

354 tools across 32 domains (was 330 / 30): new nodered and neonplan namespaces, entities_remove_entity.

Security: the Node-RED write tools are off by default. They appear only after you turn on the add-on option nodered_write_enabled — and turning it on means full trust: a deployed flow runs any code inside the Node-RED add-on, which holds a Supervisor token with the manager role (it could change nexus's own options, e.g. lift read_only) and can read Node-RED's stored node secrets. Reading flows works either way (ADR-0007).

Added

  • nodered namespace (12 new tools; the 5 write tools only with the new
    add-on option nodered_write_enabled, see below) for the
    community Node-RED add-on (slug *_nodered). Nexus talks to
    Node-RED's Admin API on 127.0.0.1:46836, the loopback address the add-on
    itself pins in its config.js (with adminAuth off — the add-on leaves
    login to Home Assistant's ingress and its own nginx). Nexus reaches it
    thanks to host_network: true, so no Home Assistant password and no
    new permission are needed. The Supervisor API is
    only used to find the add-on and check that it is started.

    • Read: nodered_list_flows (tabs and subflows with node counts and the
      deploy rev), nodered_get_flow (one tab with all its nodes),
      nodered_search_nodes (by type and/or name), nodered_find_entity_usage
      (which nodes and flows reference a Home Assistant entity — exact
      selector, template/function text, substring and regex selectors),
      nodered_get_info (Node-RED version, settings, run state, installed
      palette modules), nodered_get_diagnostics (Node-RED's diagnostics
      report), nodered_list_backups.
    • Write: nodered_create_flow, nodered_update_flow (replaces the
      flow's nodes; values hidden by the placeholder are restored from the
      current flow), nodered_delete_flow, nodered_set_flow_enabled,
      nodered_restore_backup. Each write first saves all flows to nexus's
      private /data/nodered_backups/ (newest 30 kept) and then deploys the
      whole configuration with the rev it read, so Node-RED refuses the
      write (rev_conflict) when someone deployed in the meantime; an
      optional expected_rev checks against the rev you read earlier. Only
      changed flows restart. A failed backup stops the write.
    • Safety checks before anything is deployed: node types must be installed
      in Node-RED (an unknown type would stop every flow), node ids must not
      clash with other flows, and a new node must not contain the redaction
      placeholder. All five write tools are destructive, so read_only
      mode hides them too. nodered_restore_backup also checks the backup's
      node types and that it was taken from this add-on.
    • New add-on option nodered_write_enabled (default off, ADR-0007):
      until it is on, the five write tools are hidden and refused like in
      read_only. Turning it on means full trust: a deployed flow runs
      any code inside the Node-RED add-on, which holds a Supervisor token
      with the manager role (it can change nexus's own options, e.g. lift
      read_only) and can read Node-RED's stored node secrets
      (credential_secret, flows_cred.json). Reading flows works either way.
    • Secrets: Node-RED never returns node credentials. Every answer
      replaces with the placeholder: fields whose key looks secret, env and
      HTTP header values whose name looks secret or is Authorization,
      Bearer/Basic values, user:pass@ URLs, and whole JSON strings
      holding any of these (header names keyType/keyValue stay visible).
      nodered_update_flow refuses (secret_redirect_blocked) to change the
      type of a node whose hidden values it restores, or the host/url/
      broker/port/server/proxy of a node with hidden values or stored
      credentials, unless the secret is given explicitly; a node with stored
      credentials cannot change its type. A destination chosen at runtime
      (msg.url, function code) is out of reach of this check. Secrets typed into
      function code or free text are not detected.
    • nodered_find_entity_usage skips selector regexes over 200 characters
      or with nested quantifiers (skipped_patterns); the nodes argument
      is limited to 4 MiB of JSON.
  • neonplan namespace (11 new tools, 32 namespaces) for
    the HACS integration NeonPlan 3D (neonplan3d, built against 3.0.6):
    the AI can read, check and edit the 3D house plan through Home
    Assistant's WebSocket API. No add-on option or permission is needed;
    saving needs an admin token, as in the NeonPlan editor.

    • Read: neonplan_get_status (installed/loaded, version, whether a
      license is active, packs, plans, open NeonPlan screens),
      neonplan_list_plans, neonplan_get_plan (whole building or a summary
      of floors, rooms with area_id, openings and bound entities),
      neonplan_validate_plan, neonplan_list_restore_points,
      neonplan_suggest_bindings (which entities of a room's area fit which
      plan field: blinds, contacts, lights, climate sensors, TV, vacuum,
      cameras, locks — a proposal, nothing is written).
    • neonplan_validate_plan checks what NeonPlan's server does not: the
      structure its schema requires, duplicate ids, an opening's room_id,
      edge and position on its wall, entity fields, items outside every
      room, and against the live Home Assistant every area_id and entity
      id. It warns about null entity fields that NeonPlan binds
      automatically from the area.
    • Write (destructive, hidden in read_only mode): neonplan_save_plan
      (whole building), neonplan_patch_plan (JSON Patch operations — the
      usual way to edit; an operation on path "", the whole plan, needs
      overwrite=true like save_plan), neonplan_create_restore_point,
      neonplan_restore_restore_point, neonplan_upload_background_image
      (png/jpeg/webp from /config or base64, up to the 8 MiB limit; the
      integration deletes an image no plan references at its next start).
    • NeonPlan's own save replaces the whole plan and has no conflict check.
      Saves through nexus therefore validate first (errors block), refuse to
      replace a plan with floors unless overwrite=true, check an optional
      expected_revision, take a restore point, check the revision again
      right before saving, and read the plan back. An editor left open with
      unsaved changes can still overwrite a save: neonplan_get_status lists
      the open screens.
    • Without the integration every tool answers neonplan_not_installed.
      On a version other than 3.0.x reads add a warning and writes are
      refused (neonplan_version_unsupported) unless force=true.
  • entities_remove_entity (1 new tool, 354 in total) — removes an
    entity's entry from the entity registry (config/entity_registry/remove).
    An entity its integration still provides (a state that is not a restored
    placeholder) is refused with entity_still_provided unless force=true,
    because the integration would re-create the entry at its next reload.

Changed

  • devices_remove_device now removes the whole device
    (config/device_registry/remove, Home Assistant 2026.x), not only its
    link to one config entry as the description used to say. In practice the
    old command (config_entries/remove_device) does not exist in Home
    Assistant 2026.11, so the tool never worked there. It checks first that
    the device belongs to config_entry_id, falls back to
    config/device_registry/remove_config_entry on an older Home Assistant,
    and returns {"removed": true, ...} or device_not_found /
    config_entry_not_in_device / ws_error.

Fixed

  • history_get_state_history and history_get_logbook with hours above
    24 returned only the first day: without end_time Home Assistant ends
    the window one day after its start. Both now send end_time = now.
  • areas_delete_area deleted the area but then failed with
    "structured_content must be a dict or None. Got str: 'success'": Home
    Assistant answers that command with the plain string "success". It now
    returns {"deleted": true, "area_id"}.
  • The same kind of failure after the work was done, found by a new
    contract test: blueprints_delete_blueprint, voice_delete_pipeline and
    voice_set_preferred_pipeline (Home Assistant answers with no payload,
    which the MCP client rejected as missing structured output), and
    system_restart_ha, system_stop_ha, system_create_backup and
    themes_set_active_theme (a service call answers a list of changed
    states). They now return {"deleted": true, ...},
    {"preferred_pipeline"} or {"service", "changed_states"}.
  • New test tests/test_tool_output_shapes.py: every tool declared to
    return an object may pass a Home Assistant answer through unchanged only
    when that answer is known to be an object.

v0.28.1 — hacs_* tools on the built-in Marketplace (HA 2026.11)

Choose a tag to compare

@Fistacho Fistacho released this 06 Nov 12:05

Fixed

  • hacs_* tools on Home Assistant 2026.11+. HA 2026.11 builds HACS into
    core as the Marketplace and the hacs/* WebSocket commands are gone:
    live on HA 2026.11.0 hacs_list_hacs_critical_updates answered
    WS error: {'code': 'unknown_command'}, and so did every other hacs_*
    tool. Each tool now sends the Marketplace command first
    (marketplace/repositories/list, marketplace/repository/info,
    marketplace/repository/install, marketplace/repository/uninstall,
    marketplace/repository/refresh, marketplace/repositories/add) and
    falls back to the hacs/* command only when HA answers
    unknown_command, so HA before 2026.11 with HACS keeps working. Tool
    names and parameters are unchanged.
  • Marketplace errors come back as {"error", "code", "backend", "hint"}
    instead of a raw WebSocket error. Until the Marketplace's first-run
    warning is accepted (per user: the one that owns nexus's token),
    installs, updates and new custom repositories answer
    warning_not_accepted with the hint to open Settings → Marketplace and
    accept it. Nexus never accepts it for you.
  • Add-on mode: nexus reaches HA through the Supervisor proxy as the
    Supervisor system user, not as the owner, and the Marketplace keeps the
    warning acceptance per user. That user cannot accept it in the UI, so
    install, update and add custom repository through nexus answer
    warning_not_accepted even after the owner accepted it. The hint says so
    and names what works: doing it in Settings → Marketplace or Settings →
    Updates, or, for an installed repository, an automation that runs
    update.install on its update.* entity (automations run without a
    user, so the owner's acceptance counts). Nexus prepares or runs such an
    automation only when the owner explicitly asks for it, never on its own
    after a warning_not_accepted answer. Listing and reading work.
    Standalone nexus with a user's token is not affected. Adding a custom repository
    also needs a GitHub account connected to the Marketplace
    (github_not_connected). A repository that replaces a built-in
    integration (replaces_built_in) has to be installed from the UI. When
    neither the Marketplace nor HACS answers, the tools say so instead of
    returning unknown_command.
  • hacs_install_hacs_repository, hacs_uninstall_hacs_repository,
    hacs_update_hacs_repository and hacs_add_custom_repository return
    {"status": "ok", "backend": "marketplace"|"hacs"} on success, on both
    backends. The Marketplace answers these commands with result: null, and
    a bare null made the MCP call fail output validation ("Invalid
    structured content") after HA had already done the work. With HACS they
    used to return {}.
  • A timeout or connection error is no longer reported as a rejection by
    the Marketplace: code is null, backend is null unless already
    established, and the hint says to check the repository with
    hacs_get_hacs_repository before retrying (an install may still be
    running in HA). A probe that gets no answer is never re-sent as a
    hacs/* command.
  • hacs_install_hacs_repository and hacs_update_hacs_repository wait up
    to 120 s for the install to finish (both backends) instead of the 10 s
    every other WebSocket command gets. HA answers an install only after the
    download (up to 100 MB in the Marketplace) is written and the integration
    reloaded, so with 10 s they would usually have reported "No answer"
    while HA was still installing. hacs_uninstall_hacs_repository gets the
    same 120 s: with GitHub connected the Marketplace first re-fetches the
    repository. Other hacs_* commands keep 10 s. An MCP client with a
    shorter timeout of its own may still give up first; the tool
    descriptions say to check with hacs_get_hacs_repository then.
  • Only connection, timeout and protocol failures (RuntimeError,
    OSError, websockets errors, a bad JSON frame) are reported as "No
    answer from Home Assistant". Other exceptions, i.e. bugs in nexus, are
    no longer hidden behind that message.

Changed

  • ha_client: a failed WebSocket command raises HAWebSocketError, a
    RuntimeError subclass carrying HA's error code and message. The
    message text is unchanged.
  • ha_client._ws_call_timeout(msg_type, timeout, /, **fields) — _ws_call
    with its own time bound; timeout is positional-only, so a command field
    of that name still reaches HA.
  • dashboards_add_dashboard_resource description: Marketplace resources
    live under /local/community/ (/hacsfiles/ was HACS).

v0.28.0 — areas_update_area (rename areas)

Choose a tag to compare

@Fistacho Fistacho released this 31 Oct 21:54

Added

  • areas_update_area (1 new tool, 330 in total) — renames an area (room)
    or changes its icon, floor or voice aliases through HA's area registry.
    Only the fields you pass are sent, so everything else keeps its current
    value. The area_id does not change, so devices and entities assigned to
    the area stay assigned. Calling it with no field to change is rejected
    before anything reaches Home Assistant. The icon and floor cannot be
    cleared with this tool.

Changed

  • areas_create_area "Not for" now points to areas_update_area instead
    of saying that areas cannot be updated.

v0.27.0 — adguard filter-list tools

Choose a tag to compare

@Fistacho Fistacho released this 25 Oct 08:05

Added

  • Filter-list management in the adguard namespace (3 new tools, 329 in
    total):
    • adguard_add_filter — adds a blocklist or allowlist by https:// URL
      and waits up to 180 s while AdGuard downloads and parses it. HA's
      adguard.add_url service gives up after 10 s, so large lists (for
      example a 12 MB / 560k-rule list) were never added. A URL that is
      already on either list returns already_exists without touching
      AdGuard. http:// URLs, local file paths and URLs with credentials are
      rejected.
    • adguard_remove_filter — removes one list by its exact URL; an unknown
      URL returns not_found (AdGuard itself would answer OK and do nothing).
    • adguard_set_filter_enabled — turns one list on or off, keeping its
      name and URL; enabling also waits up to 180 s for the download.
    • All three return the list after the change and the sum of rules in the
      enabled lists. They are write tools, so read_only mode hides them.

Changed

  • adguard_set_dns_config "Not for" now points to the new filter tools
    instead of HA's adguard services.

v0.26.0 — adguard namespace

Choose a tag to compare

@Fistacho Fistacho released this 25 Oct 06:53

Added

  • New adguard namespace (3 tools) for the AdGuard Home add-on
    (hassio-addons, slug *_adguard). HA's own adguard integration only
    offers filter-list services and switches; these tools reach the DNS
    settings too:
    • adguard_get_config — status, DNS settings (dns_info), filter lists
      (custom rules as a count plus the first 20), safe search, access lists
      and dhcp on/off + interface (no leases or MAC addresses).
    • adguard_set_dns_config — partial update of an allowlist of DNS fields
      (upstream/fallback/bootstrap/PTR resolvers, upstream mode, rate limit,
      cache, dnssec_enabled, blocking mode, upstream timeout). Any other key
      is rejected. When resolvers change, AdGuard's own upstream test runs
      first and nothing is written unless every server answers OK. Returns
      the settings before and after the write.
    • adguard_check_host — whether a domain is filtered, and by which rule
      and list.
  • How it connects: the AdGuard add-on serves its API on
    127.0.0.1:45158 (host network), the same address HA Core's adguard
    integration uses. nexus already runs with host_network: true
    (ADR-0004), so this needs no new add-on permission or role. The
    Supervisor is only asked where the add-on is and whether it is running.

v0.25.1

Choose a tag to compare

@Fistacho Fistacho released this 28 Sep 12:36

Fixed

  • HACS write tools (hacs_update_hacs_repository, hacs_install_hacs_repository,
    hacs_uninstall_hacs_repository, hacs_add_custom_repository) were sending
    WebSocket commands HACS doesn't implement (hacs/repository/install,
    hacs/repository/update, hacs/repository/uninstall — all answered
    {"code": "unknown_command"} when called live against HA 2026.9.3). They now
    use HACS's real API: hacs/repository/download (install and update — HACS
    has no separate command for either), hacs/repository/remove, and
    hacs/repositories/add for custom repositories. hacs_get_hacs_repository
    now sends the repository_id field hacs/repository/info actually expects
    instead of repository. Update is now hacs/repository/refresh (re-fetch)
    followed by hacs/repository/info (read available_version) and
    hacs/repository/download with that version. Tool names and parameters are
    unchanged.

v0.25.0

Choose a tag to compare

@Fistacho Fistacho released this 28 Sep 06:21

Added

  • Pre-built, cosign-signed OCI image on ghcr.io (ADR-0005) — config.yaml
    now declares image: "ghcr.io/fistacho/nexus-agent" (single multi-arch
    manifest tag, no {arch} placeholder), so Supervisor pulls the add-on
    instead of building it locally. .github/workflows/publish-image.yml
    builds, pushes and cosign-signs both the per-arch source images
    ({arch}-nexus-agent) and the combined manifest on every vX.Y.Z tag,
    then verifies every one of them is actually pullable before the run is
    allowed to go green. See Docker Image for the
    docker run/cosign verify commands.
  • server.json — an MCP Registry server descriptor (schema-validated by
    .github/workflows/mcp-registry.yml on every tagged release from day
    one; actual publication to the registry is deliberately held back until
    nexus 1.0.0, since the registry itself is still in preview). Its OCI
    package declares the same ghcr.io image and tag as config.yaml.
  • DOCS.md — full operational reference (add-on options, security posture,
    ESPHome) split out of README so the README itself stays focused on
    getting started.
  • apparmor.txt.draft — a draft AppArmor profile for the add-on; not yet
    active (no apparmor: true in config.yaml), kept as a draft until it's
    been exercised against a real install.
  • A literal LABEL io.modelcontextprotocol.server.name in the Dockerfile —
    the MCP Registry's ownership-verification mechanism for the pushed image,
    matching server.json's own name.

Changed

  • services_call_service gains a confirm parameter (default False,
    ADR-0006): required to actually run one of eight guarded HA/Supervisor
    services — homeassistant.restart/stop, hassio.host_reboot/
    host_shutdown/restore_full/restore_partial, update.install,
    group.remove — closing the gap where a model told
    confirmation_required by a dedicated tool (e.g. system_restart_ha)
    could simply reach the same effect through the generic dispatcher
    instead. Without confirm=True, the call performs no I/O and returns a
    confirmation prompt naming the dedicated tool to prefer, when one exists.
    Ignored for every other service.
  • ws_call_service now outright refuses those same eight guarded services
    ({"error": "guarded_service_refused", ..., "use": "services_call_service"})
    instead of accepting confirm itself — none of them return a response
    payload under return_response=True anyway (HA's own ServiceRegistry
    rejects that combination before the handler runs).
  • domain/service in services_call_service and ws_call_service must
    now match [A-Za-z0-9_]+ — refused with {"error": "invalid_service_name"} and zero I/O otherwise. Closes a case-folding/
    path-injection gap (ADR-0006 F2–F4) where "HASSIO"/"ADDON_STOP", or a
    service value containing ?, # or a .. segment, could reach a
    different Supervisor operation than nexus's own self_protection guard
    was comparing against.
  • areas_control_area's action is now restricted to turn_on/
    turn_off/toggle (previously passed through unchecked) and
    services_send_notification's dotted target must now name the
    notify domain — both were side doors that could reach an arbitrary
    domain.service call under cover of a narrower-looking tool (ADR-0006
    D6).
  • Breaking: the add-on now ships as a pre-built image for aarch64 and
    amd64 only. armhf/armv7/i386 have no supported, CI-buildable path
    to a signed image (the build tooling used for ghcr.io only has native
    runners for the two architectures above) — users on those architectures
    keep working on 0.24.x, which still builds locally via the Dockerfile,
    but cannot update past it.

Security

  • Every path segment ha_client.py splices into an HA REST URL from a
    caller-supplied value (entity_id, automation/script config IDs, event
    types, history/logbook timestamps) is now validated or percent-encoded
    before the request is built — entity_id against HA Core's own
    valid_entity_id pattern, everything else against a generic
    disallow-list (/, \, ?, #, literal .., checked both raw and
    percent-decoded once). Same treatment for the add-on/backup slug
    argument on all 12 supervisor_* tools that take one, checked before
    self_protection/confirm. Closes the same class of bug as the
    services_call_service name-validation fix below (a value containing
    ?, # or .. could otherwise resolve to a different endpoint than the
    one named).
  • self_protection.blocked_hassio_service_call now compares domain/
    service case-insensitively, closing the self-protection bypass where
    "HASSIO"/"ADDON_STOP" reached the same Supervisor operation as
    "hassio"/"addon_stop" without tripping the guard.
  • The add-on's HTTP transport now validates the Origin header on every
    request per the MCP spec's Streamable HTTP security requirements: an
    Origin that doesn't match nexus's own known LAN origins (or the
    operator's NEXUS_ALLOWED_ORIGINS allow-list) gets 403 Forbidden, as
    does a missing-but-null or repeated Origin header. A request with no
    Origin header at all is allowed (most non-browser MCP clients never send
    one), and both HA's ingress proxy and GET /health are exempt (ingress
    is already a trusted, peer-verified channel; /health is the Supervisor
    watchdog's unauthenticated probe).

Fixed

  • run.sh's standalone mode (no Supervisor, e.g. the ghcr.io image run via
    plain docker run) now honors a caller-supplied -e HA_URL/
    -e HA_CONFIG_PATH instead of silently discarding them — every release
    through 0.24.0 unconditionally forced HA_URL=http://supervisor/core
    regardless of mode.
  • Several WebSocket-level ESPHome Device Builder tests that drove a real
    local websockets.serve server against the client on a worker thread
    (a source of intermittent CI flakiness) now run against an in-process
    fake connection instead, for every case that doesn't specifically need
    real concurrent client/server behaviour (a genuine timeout, an actual
    TCP refusal).

v0.24.0

Choose a tag to compare

@Fistacho Fistacho released this 27 Sep 19:49

Added

  • Optional tool_mode add-on option (full default, search; env
    NEXUS_TOOL_MODE) — ADR-0003 P3. search replaces tools/list with
    discover's three lookup tools (discover_tool_search,
    discover_get_tool_doc, discover_list_namespaces) plus one call-by-name
    proxy per ADR-0002 annotation class (discover_call_read_tool /
    _write_tool / _destructive_tool) — a much smaller catalogue for
    context-constrained clients. Every hidden tool still works if called
    directly by name, and stays subject to read_only/disabled_namespaces
    exactly as in full mode. discover can no longer be listed in
    disabled_namespaces while tool_mode=search — the add-on refuses to
    start rather than leave tools/list empty. Requires a restart to take
    effect.
  • discover_tool_search/discover_get_tool_doc now also return an
    "access" field ("read"/"write"/"destructive", from the tool's own
    MCP annotations) alongside the existing fields.

Security

  • supervisor_get_addon now redacts secrets in options instead of
    returning them in full to any caller with the manager hassio_role
    (nexus's own): schema-declared password-typed fields, a key-name
    heuristic (password/passwd/pass/pwd/secret/token/api_key/
    credential/private/key — plural forms included, e.g. credentials,
    secrets, tokens, passwords, api_keys — exact-token matched so
    passenger/bypass_cache are not flagged), and an in-value scan for URLs
    with inline credentials. Redacted fields are listed in a new
    redacted_fields: [{"path", "reason"}] on the result.
  • supervisor_set_addon_options preserves previously-stored secrets when
    the caller echoes the **REDACTED** placeholder back for a field, via a
    read-modify-write round trip; a placeholder inside a list whose length no
    longer matches the stored list is refused outright rather than guessed,
    and a subsequent Supervisor error's detail is sanitized once any
    placeholder was resolved this way (Supervisor can otherwise echo the
    just-restored secret straight back in a rejected payload's detail).
  • Self-protection of the add-on (self_protection.py, ADR-0004 D-2):
    supervisor_set_addon_options, supervisor_uninstall_addon and
    supervisor_stop_addon refuse to target nexus's own add-on (fail-closed,
    by the real slug resolved at startup, or the literal "self" alias
    Supervisor itself resolves to the calling add-on). The same guard blocks
    hassio.addon_stop/app_stop/addon_stdin/app_stdin in
    services_call_service and ws_call_service, which reach the identical
    Supervisor operations without ever calling tools/supervisor.py.
    supervisor_restart_addon (and hassio.addon_restart/app_restart) stay
    unblocked — restarting nexus with unchanged options is not privilege
    escalation. This guard only covers direct invocation; a script,
    automation, YAML file, blueprint or Jinja template that reaches the same
    hassio.* service indirectly through HA Core's own engine is an accepted
    residual risk (owner sign-off 2026-09-27) — the actual security boundary
    is the add-on's own read_only/disabled_namespaces options, which none
    of those indirect paths can change. See ADR-0004 "Zaakceptowane ryzyko
    szczątkowe (0.24.0)".

Fixed

  • websockets.connect(...)'s default 10s close_timeout no longer adds an
    extra ~10s on top of a WebSocket call's own timeout when the peer never
    completes the closing handshake — both ha_client.py and
    tools/websocket.py now cap it at 1s.
  • The HA WebSocket auth handshake (auth_required -> auth -> auth_ok)
    in ha_client.py is now bounded by the same deadline as the rest of the
    call, instead of blocking forever against a peer that never completes it.
  • stdio mode no longer writes its startup log lines to stdout — only valid
    MCP JSON-RPC messages belong there per the MCP transports spec; nexus's
    own startup lines now go to stderr, same as its existing startup-abort
    messages.

Changed

  • E2E (e2e.yml) is now a required status check, promoted out of
    "informational" after being observed green for several releases in a row.
  • hacs.json removed. Nexus is not, and was never validly, a HACS
    integration (no custom_components/, would not pass HACS's "one
    integration per repository" validation) — see ADR-0005: the add-on
    channel plus a future OCI image and MCP Registry entry replace the
    previously assumed HACS channel.

Internal

  • CI's test job now runs under pytest-xdist (-n auto); tests/ test_ws_timeouts.py marks its real-socket cases slow so the fast dev
    loop can exclude them.

v0.23.0

Choose a tag to compare

@Fistacho Fistacho released this 27 Sep 16:22

Changed (important for operators)

  • The add-on now uses host_network: true (ADR-0004). ESPHome
    compile/validate/OTA upload only work through the Device Builder add-on's
    trusted-peer ingress site, reachable at 127.0.0.1:<its ingress_port> — the
    same path Home Assistant Core itself uses — and that address is only
    reachable this way when nexus shares the host's network namespace.
    Consequence: the add-on's security rating drops by one point, and Docker no
    longer maps ports: from config.yaml — nexus now reconstructs the Network
    tab's LAN-port semantics itself at startup (default 0.0.0.0:7123; pick a
    different port in the Network tab and nexus opens the LAN on that port
    instead, ingress stays fixed at 7123 either way; disable the port there and
    only ingress remains reachable, with no LAN socket at all). The port
    option itself is unchanged and still scheduled for removal in 1.0.0 — do not
    work around any of this by editing it.
  • The ESPHome Device Builder's ingress URL is now discovered automatically
    through the Supervisor (ingress_port) instead of a fixed guess. The
    previous default ESPHOME_DASHBOARD_URL=http://homeassistant.local:6052 has
    been removed — standalone (non-add-on) installs must now set it explicitly
    for esphome_* compile/validate/upload tools to work.
  • esphome_* tools now return stable error codes instead of ad-hoc messages:
    esphome_not_configured, esphome_unreachable, esphome_auth_required,
    esphome_protocol_error, esphome_command_failed, esphome_unsupported_option,
    timeout. esphome_unreachable/esphome_auth_required now carry a
    diagnosis block (mode, dashboard URL, slug, advice) instead of leaving the
    caller to guess what to check next.
  • esphome_upload_device now requires confirm=True (new confirm parameter,
    default false) — OTA-flashing a device is not undoable the way a config
    edit is. Without it, the tool performs no I/O and returns
    {"error": "confirmation_required", "message", "action"}.
  • esphome_compile_device(only_generate=True) now returns
    {"error": "esphome_unsupported_option", ...} instead of silently compiling
    anyway — the Device Builder's ingress site has no equivalent of ESPHome
    CLI's --only-generate.
  • A compile/upload call that times out now returns
    job_may_still_be_running: true — the underlying ESPHome job is not
    cancelled by the timeout, so retrying blind can race a job already running;
    check esphome_get_addon_logs first.

Added

  • esphome_write_config now returns warning + external_sources when the
    written config references remote external_components/packages — those
    are fetched and executed at compile time, so review them before calling
    esphome_compile_device.

Security

  • New SSRF guard on card_builder_upload_image_from_url: resolves the target
    hostname once, rejects loopback/link-local/unspecified/multicast/reserved
    destinations, and connects to the resolved IP literal (not the hostname
    again) on every redirect hop, closing the DNS-rebinding window. Relevant
    because under host_network, a request to loopback from this add-on now
    has the same peer identity Home Assistant Core has — exactly what some
    ingress-fronted add-ons (e.g. the Device Builder) trust without
    authentication.
  • esphome_* tools refuse to read or write secrets.yaml/secrets as a
    device name — ESPHome's per-device secrets file is not a device.
  • In add-on mode nexus no longer trusts the loopback peer or the
    host_network bridge gateway (172.30.32.1) as if it were Supervisor's
    ingress proxy: /mcp still requires the API key, and the Setup UI still
    only unlocks over genuine ingress (verified peer IP + X-Ingress-Path) or
    a valid Authorization: Bearer token.

Internal

  • New esphome_dashboard.py (Device Builder ingress client) and
    addon_network.py (ListenPlan — reconstructs the Network tab's LAN-port
    semantics from the Supervisor's own add-on info under host_network), plus
    their test suites.
  • ADR-0003 P3 (tool-search mode) is reserved for 0.24.0, not this release —
    policy.py's tool_mode stays fixed at "full".

v0.22.2

Choose a tag to compare

@Fistacho Fistacho released this 27 Sep 12:38

Fixed

  • esphome_ping_dashboard reported port_6052_mapped: true (and advised checking
    the firewall) when the Supervisor's add-on info returned "network": {"6052/tcp": null} — port 6052 declared but not published. The check now only counts the port
    as mapped when its value is an int, and when it isn't (ingress-only), the advice
    points at publishing port 6052 in the add-on's own Network settings with
    authentication enabled. Found on a live HA instance, 2026-09-27.