RFC-0018: Home Assistant Read-Only Capability Mapping
Status: Accepted (2026-08-21, project creator) Author: Project creator and Claude Created: 2026-08-21 Reviewers: Project creator Target phase: Milestone 01.2, Local Conversation and Capabilities §11, Following Vertical Slice; ROADMAP.md Milestone 5, Home Automation Integration Supersedes: None
Summary
Register two read-only Home Assistant capabilities — home_assistant.list_entities and home_assistant.get_history — against RFC-0003's contract, reusing RFC-0017's confirmation pause/resume wire format unchanged. This is deliberately the roadmap's "first slice" only (discover and read allowlisted entities, answer state and history questions) — allowlisted mutating actions are explicit Non-Goal, left to a future RFC once that slice is actually built, matching this project's own precedent of splitting a read-only capability mapping from mutation (RFC-0006 did the same for Pi-hole).
The one genuinely new design question this RFC has to resolve that RFC-0006 and RFC-0017 didn't: Home Assistant is neither device-local (Pi-hole's loopback container) nor public-internet (SearXNG's federated search, an arbitrary fetched URL) — it is a separate, household-owned device on the same LAN. RFC-0003's network axis only has local/ external, and this RFC does not attempt to add a third tier by fiat (see Proposal, Classification). It also fixes the entity allowlist mechanism the roadmap already named but never specified, and where the Home Assistant connection's long-lived access token is stored, following this project's established credential-storage precedent.
Problem
docs/roadmap/01-2-local-conversation-capabilities.md §11 names the plan directly: "introduce Home Assistant through the same capability executor: 1. discover and read allowlisted entities; 2. answer state and history questions; 3. propose allowlisted actions; 4. require confirmation according to risk; and 5. preserve deterministic authorization outside the model." ROADMAP.md's Milestone 5 entry narrows the first slice further: "read-only Home Assistant entity discovery, state, and history. Later slices may propose allowlisted actions... The model never receives unrestricted Home Assistant, shell, Docker, or network access." RFC-0003 already anticipated this by name in its own Non-Goals ("Home Assistant control is explicit non-scope" for that milestone) and Compatibility section (a later capability "Home Assistant, Milestone 5" reuses its classification model), and RFC-0017's own Compatibility section built its confirmation wire format "intentionally general... so Milestone 5's future mutating Home Assistant capabilities reuse it rather than needing a second design."
None of that is a capability mapping yet. Four concrete gaps block items 1–2 above from being buildable, none hypothetical:
- No capability is registered.
sovereign_conversation.py'sbuild_registry()has no Home Assistant entry at all today (confirmed by direct code inspection). - No classification decision. RFC-0003's Confirmation Model table is keyed on
side_effect×network, andnetworkislocalorexternal, defined as "localcapabilities never leave the device." A request to a separate Home Assistant host genuinely leaves this device's own process boundary — but it never leaves the household's own network the wayweb.search/web.fetchdo. RFC-0003 doesn't contemplate this middle case, and nothing else in this project has needed to before (Pi-hole is a loopback-only container on the same device; SearXNG/llama.cpp are also device-local containers). This RFC has to decide how to classify a same-LAN, different-host, household-owned service under the existing two-value axis, not invent a third value unilaterally. - No allowlist mechanism. The roadmap says "allowlisted entities" twice without specifying where that allowlist lives, who populates it, or how a capability's
entity_idargument gets checked against it. Without this, a naive implementation would either expose every Home Assistant entity (locks, cameras, presence/person trackers — a real privacy and safety miss for a household product) or hardcode a household-specific list at build time, which is not renewable configuration. - No credential storage decision. Reading Home Assistant's state requires a bearer token (its own long-lived access token mechanism, confirmed directly against Home Assistant's REST API documentation — see Context and Evidence). This project already has an established, qualified pattern for exactly this class of problem (
pihole-admin-password), and a naive implementation would either reinvent it worse or skip the question entirely.
Goals
- Register
home_assistant.list_entitiesandhome_assistant.get_historyagainst RFC-0003's contract: schemas, classification, and bounds. - Decide how a same-LAN, different-host, household-owned service is classified under RFC-0003's existing
local/externalaxis, without silently amending that Accepted RFC's structural model. - Fix the entity allowlist: where it's stored, its shape, and exactly which pipeline stage rejects a non-allowlisted
entity_idand why that stage (before or after the confirmation prompt). - Fix where the Home Assistant base URL and access token are stored, matching this project's existing secret-storage precedent (
pihole-admin-password) rather than inventing a new one. - Resolve RFC-0017's own deferred Unresolved Question — "whether
/data/sovereign/capabilities/policy.jsonshould hold other future opt-in toggles... deferred until Milestone 5 or another feature needs its own toggle" — since that moment is now. - Reuse RFC-0017's confirmation pause/resume wire format unchanged, confirming in practice the generality that RFC's own Compatibility section already claimed for it.
Non-Goals
- Allowlisted mutating actions ("propose allowlisted actions; require confirmation according to risk" — roadmap items 3–4). This is explicitly the roadmap's next slice after the one this RFC covers, matching RFC-0006's own precedent of shipping Pi-hole read-only first and naming mutation as separate future work. A service-call capability (
POST /api/services/<domain>/<service>in Home Assistant's own REST API) genuinely changes physical household state (locks, switches, climate) and deserves its own RFC once this slice is stable, not a bundled afterthought here. - Amending RFC-0003's
networkclassification axis to add a distinct "local-network" tier betweenlocalandexternal. Real tension with that binary is named directly below (Proposal, Classification, and Drawbacks) but resolving it is a structural change to an Accepted RFC's contract, out of this mapping RFC's scope — exactly the posture RFC-0017 already took toward a different tension in the same table (see RFC-0017's Alternatives Considered, "auto-approve after first opt-in"). - Deploying, installing, or managing Home Assistant itself. Unlike Pi-hole, SearXNG, and llama.cpp — all embedded in Sovereign's own base image and lifecycle-managed by
sovereign-update— Home Assistant is a third-party, household-provided system this project does not run, version, or update. This RFC assumes a reachable instance already exists on the household LAN; provisioning one is entirely outside Sovereign's control and this RFC's scope. - The WebSocket API, Home Assistant's other real-time interface. RFC-0003's executor model is per-invocation request/response (
invoke()returns once); a persistent subscribed connection doesn't map onto that shape without a structural change to the executor itself, which this RFC doesn't propose. The REST API's/api/statesand/api/history/periodalready answer the roadmap's "state and history questions" scope without it. - Console's actual settings UI (the page where a household enters the base URL/token and picks allowlisted entities) — this RFC fixes the API contract that UI depends on, matching how RFC-0006/RFC-0017 fixed their own API contracts without building Console UI themselves. Necessary follow-up implementation, named in Acceptance Criteria, not designed here.
- Re-deriving the confirmation architecture or wire format — both already fixed by RFC-0004 and RFC-0017 respectively. This RFC only confirms both capabilities land on RFC-0003's existing
read_only/external/requiredrow, reusing the mechanism as-is.
Context and Evidence
- docs/roadmap/01-2-local-conversation-capabilities.md §11 and ROADMAP.md Milestone 5 — the direct source of this RFC's scope, quoted in Problem above.
- RFC-0003 (Accepted): the six-stage executor, the Confirmation Model table, and its own explicit anticipation of Home Assistant as a future capability reusing this contract, without specifying how.
- RFC-0017 (Draft, not yet formally accepted at the time of writing, but already implemented and hardware-smoke-tested per its own Acceptance Criteria strikethrough notes): the confirmation pause/resume wire format (
pending_confirmation/confirmationfields onPOST /api/v1/conversation/message) this RFC reuses unchanged, and its own deferred question aboutpolicy.json's scope, resolved here. image-builder/sovereign/appliance/lib/sovereign_pihole.py, read directly:PIHOLE_PASSWORD_PATH = /data/sovereign/secrets/pihole-admin-password, read bysovereign-conversation.serviceitself (not a separate root-run script) at call time — the direct precedent for how aDynamicUserservice in this project reads a credential.image-builder/sovereign/layer/sovereign-proof.rootfs-overlay/etc/systemd/system/sovereign-conversation.service, read directly:SupplementaryGroups=sovereign-pihole-secretsis how that credential read access is actually granted — a sysusers.d group created by a root-owned bootstrap step, not something the service grants itself.ReadWritePaths=/data/sovereign/capabilitiesis the existing, already-granted writable pathpolicy.jsonand the audit log already use.- Home Assistant's REST API, confirmed directly against its own developer documentation (
developers.home-assistant.io/docs/api/rest/): bearer-token authentication (Authorization: Bearer <token>), a long-lived access token created by the household through their own Home Assistant profile page (Sovereign never generates or manages this token, only stores what the household pastes in — the same posture this project already takes toward not touching signing/production keys it doesn't own, per ADR-0006's precedent for a different secret);GET /api/states(all entities, each withentity_id/state/attributes/last_changed);GET /api/states/<entity_id>(one entity);GET /api/history/period/<timestamp>?filter_entity_id=<id>(history, ISO timestamp, entity-scoped);POST /api/services/<domain>/<service>(service calls — the mutating-action surface this RFC explicitly does not map). Default port8123; base URL shapehttp://<host>:8123/api/. - Home Assistant's own local-network discovery: it advertises itself via Zeroconf/mDNS under the
_home-assistant._tcp.local.service type (confirmed via Home Assistant's developer network-discovery documentation and community references), the same class of mechanism this project already relies on for its own device (sovereign.local, viaavahi-daemon/libnss-mdns, per the Phase 01 image's own package set). This RFC treats discovery as a Console-UI convenience (pre-filling a base URL, not authenticating or trusting anything by itself) rather than a capability-executor concern — see Proposal. - Entity ID format (
domain.object_id, e.g.light.kitchen,sensor.living_room_temperature) is a stable, long-documented Home Assistant convention this RFC relies on for allowlist and argument validation.
Proposal
Classification
Both capabilities are read_only — neither changes Home Assistant state. For network, this RFC classifies them external, taking RFC-0003's existing definition at its word ("local capabilities never leave the device") rather than reading a same-household-LAN exception into it. A request to Home Assistant genuinely leaves this device's own process and network-namespace boundary to reach a different physical host — the same fact pattern RFC-0003's definition describes, even though the destination never leaves the household's own network the way web.search/web.fetch do. Per RFC-0003's Confirmation Model table, read_only/external is required confirmation — structurally derived, not a choice this RFC makes independently.
This is a deliberate, disclosed tradeoff, not an oversight: it means "what's the temperature in the living room," a fully local, household-owned, ambient query, costs the same per-invocation confirmation click as a query that reaches the public internet. The Alternatives Considered and Drawbacks sections below name this tension directly rather than resolving it by quietly stretching RFC-0003's local definition to cover a case it wasn't written for.
home_assistant.list_entities
- Purpose: answer "what is it" and "what's it doing right now" questions ("discover and read allowlisted entities") by returning the current state of every entity the household has allowlisted.
- Arguments: none. The full allowlisted set is small by construction (see Entity Allowlist below) and returning all of it in one call avoids needing a second "discover the allowlist" round trip before a "read state" round trip — the same reasoning RFC-0017 gave for not splitting
web.search's query and result-count into separate capabilities. - Result:
{ "entities": [ { "entity_id": string, "friendly_name": string, "domain": string, "state": string, "unit_of_measurement": string | null, "last_changed": timestamp } ], "retrieved_at": timestamp }. Fields mirror whatGET /api/statesalready returns per entity, narrowed to what a conversation needs — no rawattributesblob passed through unfiltered, since Home Assistant's ownattributesobject can contain fields well beyond what a household intends to expose conversationally (e.g. device identifiers, supported-feature bitmasks). - Classification:
read_only,external,requiredconfirmation. - Bounds:
timeout_seconds=8(a same-LAN request to a real second host — meaningfully faster than the public internet round tripweb.fetchbudgets for, but not the near-zero latency of a loopback containerpihole.summaryassumes),max_result_bytesleft atDEFAULT_MAX_RESULT_BYTES(64KiB — comfortably enough for a reasonably-sized allowlist; a household approaching this limit has an allowlist large enough to warrant its own review, not a bigger byte budget),max_invocations_per_turn=1, matching every other registered capability's default.
home_assistant.get_history
- Purpose: answer "what happened" and "when did X last change" questions for one specific allowlisted entity.
- Arguments:
{ "entity_id": string (required, must be in the allowlist), "period": "hour" | "day" | "week" (required) }. A bounded enum, not a free-form date range — the same "bounded, enumerated argument set" precedent RFC-0006 established for Pi-hole's ownperiodargument and RFC-0017 explicitly cited forweb.search'stime_range. A free-form range would need its own parsing/validation surface and could request an unbounded amount of history from Home Assistant's own database for no scope benefit this milestone's actual questions need. (periodmaps to/api/history/period/<timestamp>'s required timestamp by computing "now minus one hour/day/week" at invocation time — an implementation detail, not an architectural one, the same class of deferral RFC-0017 already made for its own extraction-method specifics.) - Result:
{ "entity_id": string, "period": string, "changes": [ { "state": string, "changed_at": timestamp } ], "retrieved_at": timestamp }.changesis capped at 50 entries — plenty for a conversational answer ("it's changed state 4 times today") without becoming a raw data export, the same "summary endpoint by design, not an export" bound RFC-0006 and RFC-0017 both already applied to their own list-shaped results. - Classification:
read_only,external,requiredconfirmation. - Bounds:
timeout_seconds=10(a real database query on the Home Assistant side, not a simple in-memory state read — deliberately longer thanlist_entities's budget),max_result_bytesleft atDEFAULT_MAX_RESULT_BYTES,max_invocations_per_turn=1.
Entity Allowlist
- A new persisted file,
/data/sovereign/capabilities/home-assistant.json, holds{ "enabled": bool, "base_url": string, "allowlisted_entities": [ string ] }. Kept separate frompolicy.json— this directly resolves RFC-0017's own deferred Unresolved Question.policy.jsonstays a flat single-flag file (web_search_enabled); Home Assistant's configuration is structurally bigger (a connection endpoint and a list, not one bool) and belongs in its own file rather than growingpolicy.jsoninto a general, ever-expanding device-config blob. Both files live under the same already-granted, already-lazily-created/data/sovereign/capabilities/directory (ReadWritePaths=unchanged from today) — no new systemd grant needed for this file. - Default:
enabled: false, empty allowlist. Matching every otherexternalcapability's conservative default in this project (web_search_enableddefaultsfalse) and the milestone plan's own "allowlisted" framing — a fresh device exposes nothing until the household explicitly configures it. - Allowlist membership is checked at RFC-0003's stage 3 (policy check), before any confirmation is generated and before any request reaches Home Assistant. This is a deliberate, different choice from
web.fetch's SSRF check, which happens at stage 5 (execute) because it needs a live DNS resolution to prevent a rebinding race between check and connect. Allowlist membership needs no network call at all — it's a pure comparison against already-loaded local configuration — so checking it as early as possible gives the best user experience: a proposal for a non-allowlistedentity_idis rejected immediately with a specificENTITY_NOT_ALLOWLISTEDerror, the model can tell the user why, and no confirmation prompt is ever generated for a request that was always going to fail. This mirrorsweb.search's own "policy off → rejected before confirmation" precedent exactly, applied to a per-argument check instead of a whole-capability toggle. - Populating the allowlist is a Console-settings concern, not a capability. The model never sees Home Assistant's full, unfiltered entity list — only whatever the household has already allowlisted. Building that allowlist requires a household member to browse the full set once, in Console's own settings UI, which needs its own authenticated proxy endpoint (
GET /api/v1/conversation/home-assistant/entities, forwardingGET /api/stateswith the stored token) distinct from the two model-facing capabilities above — named in Acceptance Criteria as necessary follow-up, not built here, matching how RFC-0006 and RFC-0017 both fixed their own settings-adjacent API shape without building the settings page itself.
Credential Storage
- The Home Assistant long-lived access token is stored at
/data/sovereign/secrets/home-assistant/access-token, not insidehome-assistant.jsonalongside the non-sensitivebase_url/allowlist fields — the same separation-of-secret-from-config this project already applies topolicy.jsonvs.pihole-admin-password. - Unlike
pihole-admin-password(generated by a root-runstart-piholescript, then read bysovereign-conversation.servicevia thesovereign-pihole-secretssysusers.d group), Home Assistant's token is not something Sovereign generates — the household creates it themselves in their own Home Assistant instance and submits it through Console. Sincesovereign-conversation.serviceis therefore both the writer (accepting the household's submission through its own authenticated endpoint) and the only reader, this needs no new sysusers.d group or root-run bootstrap step — a newReadWritePaths=/data/sovereign/secrets/home-assistantentry on the existing unit is sufficient, and the directory is lazily self-created the same way/data/sovereign/capabilities/already is (RFC-0017's own precedent, cited there against inventing atmpfiles.dentry). This is deliberately a separate directory from/data/sovereign/secrets/pihole-admin-password's own — scoping this service's new write access to only its own subdirectory, so a compromisedsovereign-conversation.serviceprocess gains no new ability to overwrite Pi-hole's credential, which it does not need write access to today and still won't. GET /api/v1/conversation/home-assistant(the config-read endpoint) never echoes the token value back — only{ "enabled": bool, "base_url": string, "has_access_token": bool, "allowlisted_entities": [string] }. The same "never log or return a secret verbatim" posture this project's audit-log design already applies to capability arguments.POST /api/v1/conversation/home-assistantacceptsbase_url,access_token(optional — omitted means "leave the stored token unchanged," so re-saving the allowlist doesn't require re-pasting the token every time), andallowlisted_entities, authenticated identically to/messageand/policy(delegating to console-auth'sverify-mutating).
Interfaces and Data Flow
Model proposes home_assistant.list_entities({}) [RFC-0004 flow]
-> RFC-0003 executor stages 1-3: resolve, validate arguments
(no arguments to validate for list_entities),
check policy (enabled must be true, read fresh from
/data/sovereign/capabilities/home-assistant.json each request)
-> stage 3 fails (disabled) -> rejection appended to context
-> stage 3 passes, confirmation required (structural, both
capabilities) -> confirmation_store.issue() -> round halts,
pending_confirmation returned [RFC-0017's existing wire format,
unchanged]
-> Console prompts using the same confirmation card RFC-0017 already
built: "home_assistant.list_entities wants to read your allowlisted
Home Assistant entities — this leaves your device. Approve?"
-> user approves -> client resubmits with
{"confirmation": {"token": "...", "approve": true}}
-> confirmation_store.consume() -> invoke() proceeds through stages
5-6: GET <base_url>/api/states with the stored bearer token,
filtered to allowlisted_entities, shaped to the declared
result_schema -> appended to context
home_assistant.get_history({"entity_id": "...", "period": "day"}):
-> stage 2 validates entity_id/period against their argument schema
(shape only, no allowlist knowledge)
-> stage 3 additionally checks entity_id against the allowlist
(ENTITY_NOT_ALLOWLISTED if absent) -- before stage 4's confirmation
gate is ever reached, so an out-of-allowlist request never
generates a prompt
-> stage 5: GET <base_url>/api/history/period/<timestamp>
?filter_entity_id=<entity_id>, shaped to the declared result_schemaSecurity and Privacy
- Every invocation of either capability requires per-invocation user confirmation, structurally, per RFC-0003's table — there is no automatic path, matching
web.search/web.fetch's own posture. A prompt-injected model can cause a proposal, never an execution. - The entity allowlist is the primary privacy control: a household that has a Home Assistant
person/device_trackerentity (presence, location), alock/alarm_control_panelentity, or a camera entity simply never adds it to the allowlist, and the model can never discover, name, or query it — not merely "the model is asked not to," but structurally absent from every result this capability can ever return. - The allowlist check happening before confirmation (see Entity Allowlist above) means a proposal for a non-allowlisted entity never even discloses that entity's existence in a confirmation prompt — the rejection is generic (
ENTITY_NOT_ALLOWLISTED), not "entity X exists but isn't allowlisted," which would itself leak information about the household's device inventory to anything reading the conversation transcript. - The access token is a real credential (bearer access to whatever the household's own Home Assistant instance permits, potentially much broader than the specific allowlisted entities this RFC's capabilities ever query) — stored outside conversation context entirely, never passed to or visible from the model, matching
pihole-admin-password's own handling. - Audit events follow RFC-0003 unchanged: the fact of invocation, classification, outcome, and which stage was reached — never entity state values or history content, which is real household activity data at least as sensitive as the search-query content RFC-0017 already refused to log.
- Unlike
web.fetch, the destination (base_url) is household-configured and never model-influenced, so no SSRF-style resolve-then-check policy is needed here — the only untrusted, model-supplied input isentity_id/period, both closed, bounded values checked against server-held state before any request is made.
Failure and Recovery
- Home Assistant unreachable or returning a non-2xx response: fails through RFC-0003's normal bounded-execution failure path, audited like any other capability failure, narrated by the model as a failure ("I couldn't reach Home Assistant right now") — not a fabricated empty-state answer.
enabled: truebutbase_url/access_tokennot yet configured (a household turned the feature on before finishing setup, or Console hasn't been used yet): rejected at stage 3 with a distinctCAPABILITY_NOT_CONFIGUREDcode, separate fromCAPABILITY_DISABLED, so the model can narrate the actual situation ("Home Assistant isn't set up yet") instead of a generic "disabled" message that would be misleading once the household believes they've turned it on.- An
entity_idthat was allowlisted but has since been removed or renamed on the Home Assistant side (a real, expected drift case — Home Assistant entities can be deleted or renamed independent of Sovereign) surfaces as a normal bounded-execution failure (Home Assistant's own 404 from/api/states/<entity_id>) once past the allowlist check — Sovereign's allowlist does not attempt to stay synchronized with Home Assistant's own entity registry in real time; this is a disclosed, accepted limitation of a locally-cached allowlist rather than a live query against Home Assistant on every check. - A confirmation token that expires while genuinely pending, or a
sovereign-conversationprocess restart invalidating an in-memory token, behaves identically to RFC-0017's own already-implemented and qualified handling — no new behavior, reusing the same mechanism.
Compatibility and Migration
No existing Home Assistant capability to migrate from. Both new capabilities and the home-assistant.json/credential-directory additions are purely additive: no change to POST /api/v1/conversation/message's wire format beyond what RFC-0017 already shipped (the pending_confirmation/confirmation fields), verified by the existing test_conversation_service.py suite continuing to pass unmodified. A future allowlisted-actions RFC can reuse this RFC's allowlist storage shape and confirmation reuse pattern rather than needing a third design.
Operations and Observability
home-assistant.json'senabled/base_url/allowlisted_entities(never the token) should be inspectable the same waypolicy.jsonalready is — a future Console settings panel reads and writes it, not a separate config path, per RFC-0003's own Operations precedent.- The capability audit log records how often each Home Assistant capability is proposed, approved, denied, or rejected by policy or allowlist — the same operational signal RFC-0017 already named for judging whether a confirmation-gated capability's UX is actually working in practice, applicable here without change.
Testing Strategy
- Contract-level tests mirroring RFC-0006/RFC-0017's pattern: argument validation rejects a malformed
entity_id(wrongdomain.object_idshape) and an invalidperiodvalue, both before any allowlist or network concern is reached. - Allowlist tests: an
entity_idabsent fromallowlisted_entitiesis rejected withENTITY_NOT_ALLOWLISTEDat stage 3, before a confirmation is generated — verified directly, the same structural proof RFC-0017 required forexternal_enabled: falserejecting before confirmation. - Policy-gate tests:
enabled: false(the real default) rejects both capabilities before confirmation;enabled: truewith an unsetbase_url/token producesCAPABILITY_NOT_CONFIGURED, distinctly fromCAPABILITY_DISABLED. - Confirmation wire-format tests confirming RFC-0017's existing mechanism is reused correctly for a second pair of capabilities — no new wire-format code path is exercised, only a second registration consuming the same one.
- Fixture-backed tests for both capabilities' Home Assistant-calling happy path (a stub HTTP server returning realistic
/api/states//api/history/periodshapes) without requiring a live Home Assistant instance for every test run — mirroring RFC-0006/RFC-0017's own testing strategy. - Credential-handling tests:
GET /api/v1/conversation/home-assistantnever includes the token value in its response body, verified directly against the raw JSON, not merely against a client that happens not to render it. - Real-hardware qualification is explicitly dependent on a real Home Assistant instance being reachable on the project's own household network — unlike Pi-hole/SearXNG/llama.cpp, this is not something Sovereign provisions itself as part of qualification, so this RFC cannot commit to a qualification timeline the way RFC-0017 could for a capability backed by an embedded, Sovereign-managed service.
Alternatives Considered
- Classify Home Assistant as
local, reasoning that "same household network" is functionally equivalent to "same device" for privacy purposes. Rejected: RFC-0003's own text defineslocalas "never leaves the device," not "never leaves the household" — stretching that definition here would be an undisclosed, ad hoc amendment to an Accepted RFC's structural contract, exactly the kind of scattered judgment call RFC-0003 itself rejected as an alternative during its own drafting ("let each capability define its own confirmation requirement"). If this classification proves too conservative in practice, RFC-0003 names its own Drawbacks section as the place to revisit it deliberately — not this mapping RFC. - Auto-approve after a first per-conversation opt-in, rather than per-invocation confirmation, to reduce the UX cost of the
externalclassification above. Rejected for the identical reason RFC-0017 rejected it forweb.search: RFC-0003's confirmation model already fixed "freshly-obtained... never a standing blanket approval" as structural. - Store the access token inside
home-assistant.jsonalongside the non-sensitive configuration, for a simpler single-file design. Rejected: this project already treats secrets and config as separately-stored, separately-permissioned classes of data (policy.jsonvs.pihole-admin-password); mixing them here would be a regression from established practice for no real simplification benefit. - Use the WebSocket API for a live-updating entity view. Considered, since it's Home Assistant's more idiomatic real-time interface, but rejected for this RFC: RFC-0003's executor is a per-invocation request/response model, and a persistent subscription doesn't fit it without a structural executor change this RFC doesn't propose. The REST API fully answers "discover and read" and "answer state and history questions" without needing a live connection.
- A free-form date-range argument for
get_historyinstead of a boundedperiodenum. Rejected for the same reason RFC-0006 and RFC-0017 both chose bounded enums over free-form ranges: it's real additional parsing/validation surface and an unbounded-size result risk for a milestone whose actual questions ("what happened today," "when did it last change") are all satisfied by a small closed set.
Drawbacks and Maintenance Cost
- The
externalclassification (see Alternatives Considered) means even the most mundane, fully-local, household-owned query — "what's the temperature" — costs the same confirmation round trip as a query that genuinely reaches the public internet. This is the single biggest, disclosed UX cost of this RFC's design, inherited directly from RFC-0003's existing structural model rather than introduced by this RFC. If real usage after this ships shows this makes the read-only slice too friction-heavy to be useful — the exact tension RFC-0003's own Drawbacks section already flagged in the abstract for "read-only external capabilities used often" — the right fix is a deliberate RFC-0003 amendment informed by that real evidence, not a workaround invented here. - A second, separate persisted-config file (
home-assistant.json) and a second, separate secrets directory add real, ongoing surface area beyond whatpolicy.json/pihole-admin-passwordalready established — justified here by genuinely different data shapes (a connection endpoint and a list, not one bool; a household-submitted token, not a Sovereign-generated one), not by convenience. - The locally-cached allowlist can drift from Home Assistant's own entity registry (see Failure and Recovery) — an accepted, disclosed limitation rather than building live allowlist-registry synchronization this milestone's actual scope doesn't need.
- Real-hardware qualification depends on external infrastructure (a household's own Home Assistant instance) this project doesn't control, unlike every prior capability's qualification path — a real, disclosed schedule risk, not a defect of this RFC's design.
Unresolved Questions
None of the following block acceptance of the mapping and design fixed above:
- Whether the
externalclassification (see Alternatives Considered) should eventually motivate a genuine RFC-0003 amendment introducing a distinct "local-network" tier — explicitly named as a candidate future revisit, not decided here, and not blocking this RFC. - The exact Console settings-page flow for building the allowlist (a live-browsed picker vs. a manually-entered list of entity IDs) is UI design, not architecture.
- Whether
home-assistant.jsonshould eventually support more than one Home Assistant instance (some households run more than one, e.g. a separate test instance) — no evidence this milestone's actual scope needs it; single-instance is the smallest design that answers the roadmap's real questions. - Real behavior when Home Assistant's own long-lived token expires or is revoked by the household from their own Home Assistant UI — expected to surface as a normal bounded-execution authentication failure, but unverified against a real instance.
- TLS certificate verification for
base_url, found during review and not addressed in the Proposal above. A household running Home Assistant withhttps://(its own self-signed certificate, or a LAN-internal CA — both common for local installs, unlikeweb.fetch's targets, which are public and hold ordinary publicly-trusted certs) may fail to connect under ordinary certificate verification, or — if the implementation instead disables verification to route around that — quietly lose any protection against a spoofed on-LAN response. Neither half of that tradeoff is decided here. This is implementation-level, not a change to this RFC's classification, allowlist, or credential design, so it does not block acceptance, but it must be resolved (most likely: support a plainhttp://default matching Home Assistant's own common local-network configuration, and require normal certificate verification if a household opts intohttps://, never a blanket bypass) beforePOST /api/v1/conversation/home-assistantships.
Acceptance Criteria
home_assistant.list_entitiesandhome_assistant.get_historyare registered against RFC-0003's contract with the schemas above, classifiedread_only/external/required./data/sovereign/capabilities/home-assistant.jsonis read fresh per request (not cached at process start), matchingpolicy.json's existing freshness behavior — verified by a test that toggling the file's content between requests changes the outcome without a service restart.- A proposal for an
entity_idoutsideallowlisted_entitiesis rejected withENTITY_NOT_ALLOWLISTEDbefore any confirmation is generated and before any Home Assistant request is attempted — verified directly, mirroring RFC-0017's own proof pattern forexternal_enabled: false. enabled: false(the real default on a device that has never touched this configuration) rejects both capabilities at the policy stage — verified directly, confirming the milestone's allowlist/opt-in requirement is structurally true on a fresh device.enabled: truewith no storedbase_url/token producesCAPABILITY_NOT_CONFIGURED, distinct fromCAPABILITY_DISABLED— verified directly.- The confirmation pause/resume flow for both capabilities reuses RFC-0017's existing
pending_confirmation/confirmationfields with no new wire-format code path — verified by extendingtest_conversation_service.py's existing confirmation tests to a second capability pair rather than writing a parallel mechanism. GET /api/v1/conversation/home-assistantnever returns the stored access token value — verified directly against the raw response body.- Existing tests (
test_conversation_service.py,test_capabilities.py, RFC-0017's own web-search/fetch tests) continue to pass unmodified, confirming this RFC's additions are additive to the existing turn loop and executor.
Explicitly not required for acceptance, named as necessary follow-up implementation matching this project's precedent of separating proposal from build-out:
- The capability implementation itself (
sovereign_homeassistant.pyor equivalent), thehome-assistant.json/credential-directory reading and writing, and the two new HTTP endpoints (GET/POST /api/v1/conversation/home-assistant, plus the entity-browsing proxy endpoint for the settings page). - Console's settings UI for entering the base URL/token and building the allowlist, and the confirmation-card copy for these two specific capabilities (the card mechanism itself is already built, per RFC-0017's Acceptance Criteria).
- Real-hardware qualification against an actual Home Assistant instance — dependent on one being available on the project's household network, per Testing Strategy's disclosed schedule risk.
Decision
Accepted (2026-08-21, project creator, reviewed by Claude at the project creator's direction). The home_assistant.list_entities/ home_assistant.get_history mapping, the external classification of Home Assistant traffic, the entity allowlist mechanism (checked at stage 3, before confirmation), and the credential-storage design are accepted as this milestone's platform contract. Review before acceptance found and fixed one real internal inconsistency (the Interfaces and Data Flow diagram misstated the allowlist check as stage 2 instead of stage 3, contradicting the Entity Allowlist section's own prose) and one genuine gap not previously addressed (TLS certificate verification behavior for base_url, now recorded as a named, non-blocking Unresolved Question with a concrete direction: support plain http:// for the common local case, and real certificate verification — never a blanket bypass — if a household opts into https://). Both are fixed in this revision. The Unresolved Questions above, including whether RFC-0003's network axis should eventually grow a distinct local-network tier, are accepted as non-blocking follow-ups, not gating conditions, matching RFC-0003's own precedent for its Unresolved Questions.