Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
edb051e
feat(screenshot): report theme changes instead of writing them back
kingpanther13 Aug 24, 2026
f6fe6e5
docs(beta): drop the stale theme-restore-bracket clause
kingpanther13 Aug 24, 2026
d5b7d10
fix(screenshot): guard the restore, bound guard sessions, unblock the…
kingpanther13 Aug 24, 2026
3eda95b
Merge remote-tracking branch 'upstream/master' into feat/theme-guard-…
kingpanther13 Aug 24, 2026
6dd2919
Merge remote-tracking branch 'upstream/master' into feat/theme-guard-…
kingpanther13 Aug 24, 2026
dd66232
fix(themes): coerce the engine-theme dict params from JSON strings
kingpanther13 Aug 24, 2026
341d23d
fix(screenshot): correct the read-only-mode claim and session cleanup
kingpanther13 Aug 24, 2026
a961bd2
fix(screenshot): shield the guard's close and import asyncio in its t…
kingpanther13 Aug 24, 2026
9febe9a
fix(themes): guard on an explicit null, and bound the shielded close
kingpanther13 Aug 24, 2026
49ea7d4
fix(themes): actually pass force through the tool dispatch
kingpanther13 Aug 24, 2026
f3b8c0b
test(themes): drive the force tests through the public dispatch
kingpanther13 Aug 24, 2026
efe9f1d
fix(themes): refuse engine-theme actions on an unidentifiable account
kingpanther13 Aug 24, 2026
aebc4bf
fix(screenshot): refuse cleartext to remote hosts, add e2e, widen per…
kingpanther13 Aug 24, 2026
e4cf519
fix(screenshot): parse private ranges as IPs, and repair my own e2e a…
kingpanther13 Aug 24, 2026
6f4fbcd
test(themes): deterministic engine-identity e2e, and own the disconne…
kingpanther13 Aug 24, 2026
9e2350b
test(themes): use one import form for theme_guard
kingpanther13 Aug 24, 2026
e7d6c35
fix(themes): never treat an unproven credential as the engine's account
kingpanther13 Aug 24, 2026
46faa6d
fix(screenshot): do not recommend a restore the tool will refuse
kingpanther13 Aug 24, 2026
3cf4fcc
fix(screenshot): allowlist the engine scheme, and quote the remedy as…
kingpanther13 Aug 24, 2026
3428242
fix(screenshot): bound the guard session at its construction site
kingpanther13 Aug 24, 2026
7477ae7
fix(screenshot): accurate refusal reasons, and a remedy that knows re…
kingpanther13 Aug 25, 2026
eb740d9
fix(screenshot): report the block that actually governs the restore
kingpanther13 Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions docs/beta.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,31 @@ token grants. If the token is missing or invalid, Puppet lands on the login
page and (by its design) restarts; ha-mcp surfaces this as a clear "set the
engine's access token" error rather than a silent failure.

Puppet's theme and dark-mode renderer controls used to dispatch Home
Assistant's `settheme` event on every cold render, which Home Assistant
persisted on the frontend profile of the user whose token the engine runs with
— and synced to that user's real web and mobile sessions, flipping a dark-mode
user's whole UI to light on every screenshot (#1909). Recent Puppet versions
fixed that cold-render dispatch, so ha-mcp's snapshot/restore bracket around
each capture is now disabled (#1991); the guard code is retained so it can be
switched back on if a future engine regression reintroduces the write. If you
run an older Puppet build, update the app (or your self-hosted sidecar
image) — older engines still persist the theme selection and will keep
flipping it. A dedicated Puppet account remains a sound belt-and-suspenders
setup. Language selection is local to Puppet's browser session.
Puppet dispatches Home Assistant's `settheme` event on cold renders, which
Home Assistant persists on the frontend profile of the user whose token the
engine runs with — and syncs to that user's real web and mobile sessions,
flipping that user's whole UI on every screenshot (#1909). Upstream stopped
the dispatch for renders that request no theme
(balloob/home-assistant-addons#89), but by its own title only for that case,
and it is unreleased as of Puppet 2.6.0 — so on current releases every render
writes.

The screenshot and dashboard-get tools **detect** this and report it, but
never write: they read the engine account's saved theme before and after the
render and, when it changed, emit a warning naming the previous value. Undoing
it is a separate, explicitly write-annotated call —
`ha_manage_theme(action="set_engine_theme", value=..., expected_current=...)`
— passing both values from the warning, so a theme changed in the meantime is
refused rather than overwritten — so these tools stay
honestly `readOnlyHint: True` (#1991). `ha_manage_theme(action=
"get_engine_theme")` inspects the same value. Note this is the engine
account's *per-user* profile, a different layer from the backend default that
`action="set"` changes.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Give the engine its own Home Assistant user and long-lived token** and the
problem stops mattering: the write lands on an account nobody looks at, so no
real session is disturbed. The warning is still emitted — ha-mcp has no signal
telling it an account is dedicated — but it becomes safe to ignore. Language selection is local to Puppet's browser session.

To change the Puppet engine app's own options (such as `keep_browser_open`)
or to restart it, use `ha_manage_app`; the screenshot tools only render and
Expand Down Expand Up @@ -262,10 +275,15 @@ render failure to a warning so it never breaks a write that already committed.
`include_screenshot` (get) does not commit a dashboard/config write, and the
screenshot *is* the requested payload, so a total render failure surfaces as
an error (matching the standalone `ha_get_dashboard_screenshot` tool) rather
than a warning a caller might miss. Because Puppet can persist theme/dark
preferences (and the theme-restore bracket writes frontend user data to undo
that), screenshot operations are blocked in server Read Only Mode; ordinary
dashboard get/list/search calls remain available.
than a warning a caller might miss. Screenshot operations stay **available** in server Read
Only Mode: both entry points are `readOnlyHint: True`, so the transform does
not hide them and the middleware does not block them (#1991 removed the
exemption that used to block them, pinned by
`test_dashboard_config_screenshot_now_passes`). Rendering still makes Puppet
persist theme/dark preferences on the engine account, and ha-mcp still reports
that in warnings — so in Read Only Mode you get the warning but cannot act on
it, since `ha_manage_theme` is exempted only for its read actions
(`get_engine_theme` inspects the value; `set_engine_theme` stays blocked).

**Raw rendered paths remain constrained.** `ha_get_dashboard_screenshot`
validates legacy `dashboard_path` values (rejects URLs, query strings,
Expand Down
33 changes: 14 additions & 19 deletions src/ha_mcp/dashboard_screenshot/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,15 +855,12 @@ async def capture_dashboard_images(
preset's native orientation. ``full_page`` is a compatibility alias for
requesting the engine's native ``WIDTHxauto`` viewport.

The batch used to be bracketed by a :class:`ThemeGuard` that restored the
engine user's saved frontend theme when a cold render changed it (issue
#1909). That bracket is currently disabled (#1991): upstream Puppet no
longer dispatches ``settheme`` on cold renders, so there is nothing to undo.
The guard construction and the ``client`` / ``capture_warnings`` plumbing
are retained so the bracket can be re-enabled by uncommenting the
snapshot/restore calls if a future engine regression reintroduces the write;
while disabled ``capture_warnings`` simply stays empty and never affects the
captures themselves.
Each batch is bracketed by a :class:`ThemeGuard`, which reads the engine
user's saved frontend theme before and after rendering. It never writes:
when the render changed the theme it reports the previous value through
``capture_warnings`` so the agent can restore it with the write-annotated
``ha_manage_theme``, keeping this path honestly read-only (#1909, #1991).
Guard failures are non-fatal.
"""
path = _validate_dashboard_path(dashboard_path)
options = validate_capture_parameters(
Expand All @@ -888,16 +885,14 @@ async def capture_dashboard_images(
mime_type = _MIME_TYPES[options.image_format]
captures: list[DashboardImageCapture] = []

# ThemeGuard bracket — currently DISABLED (#1991). Stock Puppet used to
# dispatch a theme write into the authenticated frontend on cold renders,
# which Home Assistant persisted to the engine user's real profile (#1909);
# ha-mcp snapshotted before and restored after to undo it. Upstream Puppet
# has since fixed the cold-render settheme dispatch, so the bracket is no
# longer needed. The guard is still constructed (and the snapshot/restore
# calls kept below, commented out) so it can be re-enabled by uncommenting
# if a future engine regression reintroduces the write.
# ThemeGuard bracket. Puppet's settheme dispatch persists onto the engine
# token user's profile and syncs to that user's live sessions (#1909).
# Both reads here; the guard reports the change rather than undoing it, so
# this tool issues no writes and stays read-only (#1991). Armed for every
# capture, not just themed ones: the upstream fix that would spare the
# no-parameter case is unreleased as of Puppet 2.6.0.
Comment thread
kingpanther13 marked this conversation as resolved.
guard = ThemeGuard.for_capture(engine_target.addon_credential, client)
# await guard.take_snapshot()
await guard.take_snapshot()
Comment thread
kingpanther13 marked this conversation as resolved.
Comment thread
kingpanther13 marked this conversation as resolved.
batch_error: ToolError | None = None
try:
async with httpx.AsyncClient(
Expand Down Expand Up @@ -977,7 +972,7 @@ async def capture_dashboard_images(
# and its outcome can be attached to the error payload below.
batch_error = exc
finally:
# await guard.restore() # ThemeGuard bracket disabled (#1991) — see above.
await guard.detect_change()
Comment thread
kingpanther13 marked this conversation as resolved.
if capture_warnings is not None:
capture_warnings.extend(guard.warnings)

Expand Down
Loading
Loading