You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add opt-in enforce mode to the entity visibility filter (#2024)
* feat: add opt-in enforce mode to the entity visibility filter
Fixes#2015. When "enforce": true, the visibility filter's hidden set
becomes a refuse-on-contact read boundary across every tool: direct
reads of a hidden entity are concealed as not-found before the tool
runs, content reads whose arguments or output would surface a hidden
entity_id are refused, and unscannable surfaces (sandbox execution,
screenshots) are refused outright. Enforcement resolves the hidden set
via a new strict resolver mode that fails closed on degraded data, with
last-known-good fallbacks for both the config and the hidden set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* fix: close ha_search config-body leak and round-1 CI findings
The e2e failure exposed a real gap: ha_search's config-body branch
(automation/script/scene/helper/dashboard records) surfaced a hidden
helper's entity_id, so the outbound scan refused the whole search. In
enforce mode those records are collection reads and are now OMITTED via
a scrub that shares the middleware's TTL-cached hidden set.
Codex round 1: the last-known-good hidden set is no longer reused when
the config key changed (a different denylist is a different policy —
fail closed instead); a device-registry payload an active area/label
dimension needs is validated under strict mode instead of failing open
to empty maps; ha_config_set_dashboard(return_screenshot=true) is
refused as an unscannable pixel surface like the get-side screenshot.
Also: CodeQL quality findings (explicit string concat, unnecessary
lambdas), haos_inaddon skip-ceiling bump for the new external_only e2e
test, and the e2e cleanup now drops enforce before removing the probe
so a mid-test failure cannot be masked by a refused cleanup call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* fix: scrub component-path config records in enforce mode
Round-2 e2e showed the leak again on the component fast path: when the
filter is active, ha_search still routes through ha_mcp_tools/search,
whose search_visibility wire applies the hide dimensions to ENTITY
results only — its config-body records can still carry a hidden
entity_id, so the outbound scan refused the whole search. Mirror the
legacy deep_search scrub after _shape_component_search_response,
decrementing config_total_matches/count by the dropped records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* fix: share the hidden-set cache at module scope, not per middleware instance
Round-3 CI pinned the remaining leak: the scrub reached the hidden set
through a module singleton pointing at a STALE server instance's
middleware — cold cache plus a closed httpx client ('Cannot send a
request, as the client has been closed'), so active_hidden_regex
fail-softed to None and the scrub never ran. A process can hold several
server instances (the e2e suite does), so the singleton design was
wrong: the TTL cache now lives at module scope shared by all
middleware instances and scrub callers, and active_hidden_regex takes
the CALLER's live client for any refresh. Same TTL / same-key
last-known-good / fail-closed semantics, now instance-independent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* fix: run inbound concealment before the approval queue; validate device entries
Codex round 2. The enforcement middleware is split in two: the inbound
conceal/refuse gate now registers BEFORE the read-only guard and
PolicyMiddleware, so a call naming a hidden entity is concealed as
not-found before its raw arguments can be stored in the approval queue
(rendered in the settings UI) or answered with an approval-pending
response that would confirm existence. The outbound result scan stays
registered last (innermost, raw tool output). A wiring test pins the
registration order.
The strict device-registry validation now also checks per-entry shape:
a degenerate success payload like {"result": [null]} passed the shape
check but parsed to empty device maps, silently dropping device-
inherited area/label denies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* docs: state the camera exemption; fix boundary note and refusal list; pin wiring by call order
Patch76 review round. The camera-image exemption is now stated rather
than silent: FAQ and SECURITY.md scope the enforce boundary to HA
entity data (a visible camera returns physical-world imagery; hide the
camera itself when its view is sensitive) and the 'never flows' claim
is qualified to tool reads. The FAQ refusal list gains
ha_config_set_dashboard(return_screenshot), which the code already
refused. The settings-UI boundary warning is scoped to 'without
enforce mode' in the HTML fallback and all four locale catalogs so it
no longer contradicts the enforce section below it. The middleware-
order wiring test now records actual call order on a stubbed
_initialize_server run instead of scanning source text, so an inline
add_middleware appended after the outbound half fails the test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
* test: fold Patch76 re-review notes into the wiring test
The module docstring now describes the recorded-call-order mechanism
(the source-order wording described the approach this test replaced),
and the tail assertion is a bare emptiness check so a middleware-
registering helper that doesn't follow the _apply_ naming cannot slip
in behind the outbound scan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2nScAXT4Q2tCECQusr2j4
---------
Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
"visibility.intro": "Verstecke Entitäten mit geringem Wert (automatisch generierte Diagnose- oder Helfer-Entitäten oder eine explizite Liste) aus den Sammlungs-Lese-Tools <code>ha_search</code> und <code>ha_get_overview</code>, sodass Such- und Übersichtsergebnisse weniger überladen sind. Standardmäßig ausgeschaltet; Änderungen gelten beim nächsten Tool-Aufruf.",
85
-
"visibility.boundary": "Rauschreduzierung, keine Zugriffskontrolle: Eine versteckte Entität wird weiterhin durch ein direktes Lesen ihrer entity_id zurückgegeben und erscheint weiterhin in Automatisierungs-, Dashboard- und Template-Inhalten. Verlasse dich nicht darauf als Sicherheitsgrenze.",
85
+
"visibility.boundary": "Ohne den Erzwingungsmodus (unten) ist dies Rauschreduzierung, keine Zugriffskontrolle: Eine versteckte Entität wird weiterhin durch ein direktes Lesen ihrer entity_id zurückgegeben und erscheint weiterhin in Automatisierungs-, Dashboard- und Template-Inhalten. Verlasse dich nicht allein auf den Filter als Sicherheitsgrenze.",
"visibility.enforce.help": "Erweitert den Filter vom Aufräumen von Suche und Übersicht auf jeden Werkzeug-Lesezugriff. Ein direkter Lesezugriff auf eine versteckte Entität liefert „nicht gefunden“, und ein Inhalts-Lesezugriff (Dashboard, Template, Automatisierung, Trace, Log oder Datei), der eine versteckte Entität offenlegen würde, wird verweigert. Bestmögliche Verschleierung gegen versehentliche Offenlegung, keine gehärtete Sicherheitsgrenze.",
89
+
"visibility.enforce.only_enforced": "Versteckte Entitäten für alle Werkzeuge unlesbar machen (nicht nur für die Suche)",
87
90
"visibility.categories.title": "Nach Kategorie verstecken",
88
91
"visibility.categories.help": "Home Assistant markiert automatisch generierte Entitäten als Diagnose oder Konfiguration.",
"visibility.intro": "Hide low-value entities (auto-generated diagnostic or helper entities, or an explicit list) from the collection read tools <code>ha_search</code> and <code>ha_get_overview</code>, so search and overview results are less cluttered. Off by default; changes apply on the next tool call.",
85
-
"visibility.boundary": "Noise reduction, not access control: a hidden entity is still returned by a direct read of its entity_id and still appears in automation, dashboard, and template content. Do not rely on this as a security boundary.",
85
+
"visibility.boundary": "Without enforce mode (below), this is noise reduction, not access control: a hidden entity is still returned by a direct read of its entity_id and still appears in automation, dashboard, and template content. Do not rely on the filter alone as a security boundary.",
"visibility.enforce.help": "Extends the filter from decluttering search and overview to every tool read. A direct read of a hidden entity returns “not found”, and a content read (dashboard, template, automation, trace, log, or file) that would surface a hidden entity is refused. Best-effort concealment against incidental exposure, not a hardened security boundary.",
89
+
"visibility.enforce.only_enforced": "Make hidden entities unreadable across all tools (not just search)",
87
90
"visibility.categories.title": "Hide by category",
88
91
"visibility.categories.help": "Home Assistant tags auto-generated entities as diagnostic or config.",
89
92
"visibility.categories.legend": "Entity categories to hide",
0 commit comments