Files
computer-playbook/docs/requirements/033-web-app-homeassistant.md
kevinveenbirkenbachandClaude Opus 5 791eee7c9f refactor(mcp)!: prove the endpoint contract with a CLI test, not a deploy probe
The contract check ran as an Ansible task in the middle of every provider deploy, and the two client checks ran the same way. A test that is part of the deploy cannot be run without deploying, cannot be re-run on its own, and reports its verdict as a deploy failure. Twenty-seven task files lose 491 lines to this.

It is now what it always was. Each provider ships files/test/test.sh, templates/test.env.j2 and meta/tests.yml, and test-e2e-cli stages one shared harness beside it: run.sh drives contract.sh in a throwaway container on the provider's own network, then isolation.sh for adapter-fronted providers. The read probe each provider offers moved from the calling task into meta/mcp.yml next to the allowlist it must satisfy, and the schema now rejects a read probe naming a tool outside that allowlist.

The bearer the test presents is the one a client resolves through mcp.credential, via the new mcp_client_bearer lookup over resolve_credential, which moved to utils.roles.applications.mcp so it is the same function mcp_servers renders every client connection with. The probe presented whatever the calling role passed, which for the fifteen adapter-fronted providers is a different secret than any client holds.

Sidecar isolation is read from the engine rather than measured inside the container, so it no longer needs a delegation to the node holding the task: attachment is compared by network name, which drops the gwbridge special case, and a swarm worker says it cannot measure instead of skipping silently.

Retrying is bounded by wall clock against MCP_DEADLINE_SECONDS, which scales through the same lookup('timeout') as the harness budget it has to stay under. A flag that is neither true nor false aborts rather than reading as off.

mcp-provider-probe becomes mcp-provider-cli-test: an enabled provider must ship the test rather than include the task. mcp-probe-bearer becomes mcp-adapter-bearer, tying ADAPTER_BEARER to the declared credential key now that no probe include carries it. A new integration test renders the shared env template for every provider through Ansible's own templar, so a later change to it breaks visibly instead of poisoning twenty deploys quietly.

Hermes and OpenClaw keep their client-side proof as CLI tests of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 20:46:26 +02:00

7.3 KiB

033 - Home Assistant Role with MCP Server Surface

User Story

As a platform administrator of Infinito.Nexus, I want a web-app-homeassistant role that deploys Home Assistant and exposes its native MCP server, so that agent employees (Hermes on a Pi, 032) and other MCP clients can control home devices as scoped MCP tools without any host access, making the "isolated home assistant" scenario concrete.

Background

Home Assistant is the open-source home-automation platform. It ships a native Model Context Protocol Server integration (mcp_server) that implements Streamable HTTP and exposes Home Assistant's Assist API to MCP clients, with per-entity access control (only explicitly exposed entities are reachable). It can control lights, climate, covers, switches, sensors, media players, fans, locks, vacuums, and cameras.

This makes Home Assistant a natural 025 MCP server role and the concrete backend for the isolated-home-assistant scenario in 032: the agent stays kernel-isolated and reaches the home only through this MCP surface, never through host access. Home Assistant runs natively on arm64/Raspberry Pi, so it co-locates with the Pi agent story.

Confirmed Decisions

These choices are settled at requirement creation time and bound the implementation. Re-opening any of them MUST be recorded in the implementing PR.

  1. web-app-homeassistant deploys Home Assistant in Container mode. The role uses the official home-assistant/home-assistant container (no Supervisor, no add-on store, not privileged), following the standard web-app-* contract (meta/*). Add-on management is out of scope; the mcp_server core integration does not need the Supervisor.
  2. MCP surface is the native mcp_server integration under the 025 contract. The role's meta/services.yml declares mcp with direction: server, implementation: native, transport: streamable_http, served under the role's canonical origin per 025. No third-party MCP add-on is used when the native integration covers the need.
  3. Access is entity-scoped and read-mostly by default. Only entities explicitly exposed to the Assist API are reachable via MCP. Mutating/actuating tools (locks, covers, switches) are off by default and require operator opt-in, per 025's mutating-tools rule.
  4. Consumers discover it via 025. Agent clients (032) and other 025 client roles reach it through the roles_with_service lookup; the endpoint is not hard-coded in any consumer.
  5. Auth caveat is documented. Home Assistant uses its own auth (users + long-lived tokens + trusted_proxies), not full OIDC. The MCP endpoint is authenticated with a Home Assistant credential stored in meta/secrets.yml credentials:; if platform SSO in front of the HA UI is wanted, the trusted-proxy header approach and its limits MUST be documented in the role README.
  6. arm64/Raspberry Pi is supported. The role runs on arm64 so it can sit next to the Pi agent from 032.
  7. The UI is published, the MCP surface is not. (Revised during implementation; the original decision was internal-only for both.) Home Assistant follows the web-app-* contract and serves its own UI on a canonical domain through the reverse proxy, because every part of that contract — meta/domains.yml, meta/csp.yml, the guest Playwright persona — assumes a public vhost, and a role without one would have to be shaped as a svc-* role instead. The hub's own auth provider guards that surface. The MCP endpoint stays exposure: internal: it is reachable in-cluster only, an unauthenticated probe of the public origin is refused, and files/playwright/test-mcp-guest.js asserts exactly that. A platform-SSO gate in front of the UI remains a documented follow-up.

Architecture

flowchart LR
    user([Household]) -->|UI / voice| ha
    subgraph node["Node (arm64 / Pi ok)"]
        ha["web-app-homeassistant<br/>+ native mcp_server (025)"]
        devices["exposed entities:<br/>lights, climate, locks,<br/>cameras, sensors..."]
        ha --- devices
    end

    agent["web-app-hermes (032)<br/>isolated MCP client"]
    agent -->|"MCP over Streamable HTTP (025)"| ha

Where each guarantee is enforced

Guarantee Enforced by
Only Assist-exposed entities are reachable ensure_mcp_entry binds the MCP config entry to llm_hass_api: ["assist"] in provision_mcp.py
Mutating tools stay off the MCP long-lived token belongs to a dedicated account pinned to Home Assistant's system-read-only group; ensure_service_account re-asserts group_ids through config/auth/update on every run, so an account promoted by hand is demoted again
The endpoint refuses an anonymous caller test-mcp-guest.js
The stored bearer still authenticates tasks/utils/mcp/probe.yml, which hard-fails the deploy when the hub rejects it
The advertised tool contract matches what the hub serves files/test/test.sh, which drives the shared MCP contract harness inside the provider's own network
The pinned image runs on arm64 test_arm64_images.py

Acceptance Criteria

  • A web-app-homeassistant role deploys Home Assistant in Container mode (no Supervisor/add-ons, not privileged) and it comes up healthy; its MCP endpoint is not published, per the revised Decision 7.
  • The native mcp_server integration is enabled and declared in meta/services.yml as direction: server, implementation: native, transport: streamable_http, conforming to the 025 schema and lint.
  • The MCP endpoint is authenticated; an unauthenticated probe is rejected, and the credential lives in the platform token store — meta/mcp.yml credential.source: token_store, written by sys-token-store — never in a README, an env file or a Playwright trace. (Revised: 025 and 035 moved MCP bearers out of meta/secrets.yml into the token store after this criterion was written.)
  • Only entities explicitly exposed to Assist are reachable via MCP; mutating tools are off unless the operator opts in.
  • An MCP client (e.g. web-app-hermes from 032) discovers the role via roles_with_service and reads an exposed entity's state through MCP.
  • The role is added to 025's MCP audit as a server role.
  • The role runs on arm64.
  • The role comes up green in both compose and swarm modes.
  • A Playwright spec exercises the Home Assistant surface and is green.

Cross-linking

  • Implementing PR: to be linked.

See Also