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>
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.
web-app-homeassistantdeploys Home Assistant in Container mode. The role uses the officialhome-assistant/home-assistantcontainer (no Supervisor, no add-on store, not privileged), following the standardweb-app-*contract (meta/*). Add-on management is out of scope; themcp_servercore integration does not need the Supervisor.- MCP surface is the native
mcp_serverintegration under the 025 contract. The role'smeta/services.ymldeclaresmcpwithdirection: 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. - 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.
- Consumers discover it via 025. Agent clients (032) and other 025 client roles reach it through the
roles_with_servicelookup; the endpoint is not hard-coded in any consumer. - 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 inmeta/secrets.ymlcredentials:; 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. - arm64/Raspberry Pi is supported. The role runs on
arm64so it can sit next to the Pi agent from 032. - 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 asvc-*role instead. The hub's own auth provider guards that surface. The MCP endpoint staysexposure: internal: it is reachable in-cluster only, an unauthenticated probe of the public origin is refused, andfiles/playwright/test-mcp-guest.jsasserts 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-homeassistantrole 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_serverintegration is enabled and declared inmeta/services.ymlasdirection: 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.ymlcredential.source: token_store, written bysys-token-store— never in a README, an env file or a Playwright trace. (Revised: 025 and 035 moved MCP bearers out ofmeta/secrets.ymlinto 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-hermesfrom 032) discovers the role viaroles_with_serviceand reads an exposed entity's state through MCP. - The role is added to 025's MCP audit as a
serverrole. - 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
- MCP contract: 025-mcp-role-integration.md
- Agent that consumes it (isolated home-assistant scenario): 032-agent-employees-firecracker.md
- Home Assistant MCP Server: home-assistant.io/integrations/mcp_server