-
Notifications
You must be signed in to change notification settings - Fork 200
fix(screenshot): re-arm the theme guard for themed captures only #2255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d223ba9
3036483
78e43fa
d1d9f23
39c05a1
6b5db05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| A batch that requests a theme (``theme`` or ``dark_mode``) is bracketed by | ||
| a :class:`ThemeGuard` that restores the engine user's saved frontend theme | ||
| afterwards, because such renders make Puppet write it (issue #1909). | ||
| Unthemed batches issue no writes at all. Snapshot and restore failures | ||
| surface through ``capture_warnings``. A theme-lock timeout rejects the | ||
| themed capture to prevent an unserialized render. | ||
| """ | ||
| path = _validate_dashboard_path(dashboard_path) | ||
| options = validate_capture_parameters( | ||
|
|
@@ -888,16 +885,48 @@ 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. | ||
| guard = ThemeGuard.for_capture(engine_target.addon_credential, client) | ||
| # await guard.take_snapshot() | ||
| # ThemeGuard bracket, armed only for renders that request a theme. | ||
| # | ||
| # Puppet dispatches a ``settheme`` event into the authenticated frontend, | ||
| # which Home Assistant persists to the engine user's real profile and | ||
| # syncs to that user's live sessions (#1909). Upstream stopped the | ||
| # no-parameter dispatch (balloob/home-assistant-addons#89), but that fix | ||
| # is scoped by its own title — "don't dispatch settheme when no | ||
| # theme/dark was requested" — so an explicit ``theme=``/``dark`` render | ||
| # still writes, on every engine version. | ||
| # | ||
| # Arming only that path keeps unthemed captures free of any write, which | ||
| # is what makes ``readOnlyHint: True`` honest for the overwhelmingly | ||
| # common case (#1991, PR #2014). | ||
| theme_requested = options.theme is not None or options.dark_mode | ||
| guard = ThemeGuard.for_capture( | ||
| engine_target.addon_credential, client, armed=theme_requested | ||
| ) | ||
| # Bound the wait by the caller's own render budget: they are already | ||
| # willing to wait that long, and a themed render can easily hold the | ||
| # bracket longer than a short fixed timeout. | ||
| await guard.take_snapshot(lock_timeout=options.render_timeout_seconds) | ||
| if guard.lock_timed_out: | ||
| # Rendering unserialized here would reintroduce exactly the interleave | ||
| # the lock exists to prevent (this batch would snapshot the other | ||
| # batch's transient theme and restore it afterwards), so fail instead. | ||
| raise_tool_error( | ||
| create_error_response( | ||
| ErrorCode.INTERNAL_ERROR, | ||
| "Another themed dashboard screenshot is still in progress for " | ||
| "this screenshot-engine account.", | ||
| details=( | ||
| "Themed captures are serialized so their theme " | ||
| "snapshot/restore brackets cannot interleave and strand " | ||
| "the account on the wrong theme." | ||
| ), | ||
| suggestions=[ | ||
| "Retry once the in-flight capture finishes.", | ||
| "Omit theme/dark_mode to capture without the bracket.", | ||
| ], | ||
| context={"path": path}, | ||
| ) | ||
| ) | ||
| batch_error: ToolError | None = None | ||
| try: | ||
| async with httpx.AsyncClient( | ||
|
|
@@ -976,8 +1005,26 @@ async def capture_dashboard_images( | |
| # Held (not re-raised here) so the restore in ``finally`` runs first | ||
| # and its outcome can be attached to the error payload below. | ||
| batch_error = exc | ||
| except Exception as exc: | ||
| # A non-ToolError failure (e.g. raised while entering the HTTP client | ||
| # context) would otherwise bypass _reraise_with_guard_warnings and | ||
| # reach the caller's generic handler with the guard's warnings | ||
| # dropped. It must become a *structured* ToolError: the merge helper | ||
| # parses the message as JSON and re-raises unchanged when that is not | ||
| # a dict, so wrapping the bare text would still lose the warnings. | ||
| logger.exception("Dashboard capture batch failed unexpectedly") | ||
| batch_error = ToolError( | ||
| json.dumps( | ||
| create_error_response( | ||
| ErrorCode.INTERNAL_ERROR, | ||
| "Dashboard screenshot capture failed unexpectedly.", | ||
| details=f"{exc.__class__.__name__}: {exc}", | ||
| context={"path": path}, | ||
| ) | ||
| ) | ||
| ) | ||
| finally: | ||
| # await guard.restore() # ThemeGuard bracket disabled (#1991) — see above. | ||
| await guard.restore() | ||
| if capture_warnings is not None: | ||
| capture_warnings.extend(guard.warnings) | ||
|
Comment on lines
1028
to
1029
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the render block raises a non- Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-enabling
guard.restore()meansha_get_dashboard_screenshotcan now issuefrontend/set_user_data, and the documented concurrent-user case can overwrite a real theme change, yet the tool remains annotated withreadOnlyHint: Trueintools_dashboard_screenshot.py. Update the affected safety annotation so clients are not told that this path cannot modify Home Assistant state.AGENTS.md reference: AGENTS.md:L619-L624
Useful? React with 👍 / 👎.