Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2026.10.6.1 — 2026-10-06

- Restyle the OAuth pairing page with the connector logo, a responsive card and automatic light/dark themes.
- Permit only the exact static stylesheet using a SHA-256 CSP hash; scripts and external assets remain blocked.
- Preserve all five languages, HTML escaping, count-only anonymous disclosure and the existing pairing flow.
- Add regressions that independently hash the served stylesheet in every language and check the inline logo for active content or external references.

Updating preserves configured permissions and OAuth grants. This release passed 91 Python tests, 5 JavaScript tests and package checks. See the [focused security review](docs/SECURITY-ASSESSMENT-2026-10-06.md); no live HA deployment or Grok/Tesla acceptance test was performed by the maintainer for this release.

## 2026.10.5.2 — 2026-10-05

- Expose action tools with readable ASCII names derived from their configured names, such as `otevrit_branu`, while retaining their original titles and descriptions.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ This is an independent integration, not the official Home Assistant MCP Server o
Install the published release through HACS:

1. In HACS, open **Custom repositories** and add `https://github.com/Jenik5/GrokConnectorForHomeAssistant` as an **Integration**.
2. Download **2026.10.5.2** or a later stable release. Stable releases do not require beta versions to be enabled in HACS.
2. Download **2026.10.6.1** or a later stable release. Stable releases do not require beta versions to be enabled in HACS.
3. Restart Home Assistant to load the newly installed Python integration.
4. Open **Settings → Devices & services → Add integration → Grok Connector**.
5. Enter your public HTTPS origin, for example `https://your-instance.ui.nabu.casa` or `https://ha.example.org:8125`, and choose the MCP language. Enter only the origin, without credentials, a path, query or fragment.
Expand Down Expand Up @@ -146,3 +146,7 @@ python tools/check_package.py
Tests use simulated states/actions and adapters for HA/aiohttp. They never connect to a house or control devices. GitHub workflows additionally run hassfest and HACS validation after publication. [Release procedure](docs/RELEASING.md) · [MIT license](LICENSE).

Home Assistant and Grok names/marks belong to their respective owners. No endorsement or affiliation is implied.

## Pairing page appearance

From **2026.10.6.1**, the OAuth pairing page displays the connector logo and follows the browser's light/dark preference in a responsive layout. The pairing process and all five translations are unchanged. Only the fixed stylesheet is allowed by its CSP hash; no scripts or external assets are introduced. See the [focused security review](docs/SECURITY-ASSESSMENT-2026-10-06.md).
2 changes: 1 addition & 1 deletion custom_components/grok_connector/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Independent paths and bounds of the community connector."""
DOMAIN = 'grok_connector'
VERSION = '2026.10.5.2'
VERSION = '2026.10.6.1'
MCP_PATH = '/api/grok_connector/mcp'
OAUTH_PATH = '/api/grok_connector/oauth'
RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource' + MCP_PATH
Expand Down
4 changes: 2 additions & 2 deletions custom_components/grok_connector/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .gateway import PROTOCOLS
from .diagnostics import category, emit
from .security import OAuthError, allowed_callback, digest
from .oauth_page import render_authorization, transaction_cookie_name
from .oauth_page import STYLE_SOURCE, render_authorization, transaction_cookie_name
from .i18n import browser_language, text

NO_CACHE = {"Cache-Control": "no-store", "Pragma": "no-cache", "X-Content-Type-Options": "nosniff"}
Expand All @@ -41,7 +41,7 @@ def form_headers_for_callback(callback):
raise OAuthError("invalid_client")
origin = "https://" + host + (":443" if parsed.port == 443 else "")
return {**FORM_HEADERS, "Content-Security-Policy":
"default-src 'none'; form-action 'self' " + origin +
"default-src 'none'; style-src " + STYLE_SOURCE + "; form-action 'self' " + origin +
"; base-uri 'none'; frame-ancestors 'none'"}


Expand Down
2 changes: 1 addition & 1 deletion custom_components/grok_connector/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
"issue_tracker": "https://github.com/Jenik5/GrokConnectorForHomeAssistant/issues",
"requirements": [],
"single_config_entry": true,
"version": "2026.10.5.2"
"version": "2026.10.6.1"
}
42 changes: 36 additions & 6 deletions custom_components/grok_connector/oauth_page.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
# custom_components/grok_connector/oauth_page.py
"""No-script localized consent document with per-transaction secure cookies."""
import base64
import hashlib
import html
import re
from pathlib import Path

from .const import OAUTH_PATH
from .i18n import browser_language, text

# Brand icon inlined: the page CSP forbids scripts and external images, inline SVG needs neither.
LOGO = (Path(__file__).parent / 'brand/icon.svg').read_text(encoding='utf-8')
# Light by default, dark via the browser preference; HA blue is the accent colour.
STYLE = '''
:root{color-scheme:light dark;--bg:#f4f6f8;--card:#fff;--fg:#1c2733;--muted:#5f6b7a;--line:#dbe1e8;--accent:#03a9f4;--accent-fg:#fff;--soft:#e8f6fd}
@media(prefers-color-scheme:dark){:root{--bg:#111518;--card:#1c2228;--fg:#e6ebf0;--muted:#9aa6b4;--line:#333d47;--accent:#18bcf2;--accent-fg:#06202b;--soft:#15303d}}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
main{width:100%;max-width:440px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:32px 28px;box-shadow:0 4px 24px rgba(0,0,0,.08)}
.logo{text-align:center}.logo svg{width:84px;height:84px}
h1{font-size:1.35rem;line-height:1.3;text-align:center;margin:12px 0 16px}
.badge{background:var(--soft);border-radius:10px;padding:10px 14px;font-size:.92rem;margin:0 0 14px}
.muted{color:var(--muted);font-size:.9rem;margin:0 0 14px}
.muted strong{display:block;color:var(--fg);font:.85rem ui-monospace,monospace;word-break:break-all;margin-top:2px}
label{display:block;font-weight:600;margin:18px 0 6px}
input{width:100%;padding:12px 14px;font:1.1rem ui-monospace,monospace;letter-spacing:.06em;color:var(--fg);background:var(--bg);border:2px solid var(--line);border-radius:10px}
input:focus{outline:none;border-color:var(--accent)}
button{width:100%;margin-top:16px;padding:13px;font-family:inherit;font-weight:600;font-size:1rem;color:var(--accent-fg);background:var(--accent);border:0;border-radius:10px;cursor:pointer}
button:hover{filter:brightness(1.08)}button:focus-visible{outline:3px solid var(--fg);outline-offset:2px}
footer{margin-top:20px;text-align:center;color:var(--muted);font-size:.8rem}
'''
# CSP source allowing exactly this stylesheet (no 'unsafe-inline'); the page CSP is otherwise default-src 'none'.
STYLE_SOURCE = "'sha256-" + base64.b64encode(hashlib.sha256(STYLE.encode()).digest()).decode() + "'"


def transaction_cookie_name(transaction):
if not isinstance(transaction,str) or not re.fullmatch(r'[A-Za-z0-9_-]{43}',transaction):
Expand All @@ -21,11 +48,14 @@ def t(key):
read_count=len(runtime.policy.readable),control_count=len(runtime.policy.actions))
# Do not publish the home's entity IDs, names or states on an anonymous page.
page = f'''<!doctype html><html lang="{language}"><head><meta charset="utf-8">
<meta name="referrer" content="same-origin"><meta name="viewport" content="width=device-width">
<title>{t('page_title')}</title></head><body><main><h1>{t('page_title')}</h1>
<p>{html.escape(count)}</p><p>{t('callback')} <strong>{html.escape(callback)}</strong></p>
<p>{t('pair_help')}</p><form method="post" action="{OAUTH_PATH}/authorize">
<meta name="referrer" content="same-origin"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark"><title>{t('page_title')}</title>
<style>{STYLE}</style></head><body><main><div class="logo">{LOGO}</div><h1>{t('page_title')}</h1>
<p class="badge">{html.escape(count)}</p>
<p class="muted">{t('callback')}<strong>{html.escape(callback)}</strong></p>
<p class="muted">{t('pair_help')}</p><form method="post" action="{OAUTH_PATH}/authorize">
<input type="hidden" name="transaction" value="{html.escape(transaction)}">
<label>{t('pair_label')} <input type="password" name="pairing_secret" required autocomplete="off" maxlength="64"></label>
<button type="submit">{t('approve')}</button></form><p>{t('community')}</p></main></body></html>'''
<label for="pairing_secret">{t('pair_label')}</label>
<input id="pairing_secret" type="password" name="pairing_secret" required autocomplete="off" maxlength="64" autofocus>
<button type="submit">{t('approve')}</button></form><footer>{t('community')}</footer></main></body></html>'''
return page
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The reading list is a data-disclosure boundary, not an action-target boundary. A

`security.py` owns an independent authority; it never accepts or forwards HA credentials. Resource/issuer URLs are bound to the administrator-configured public HTTPS origin, including any nonstandard port. Both Nabu Casa and custom DNS hostnames are accepted. URL syntax validation rejects ambiguous authorities and browser IPv4 representations; it performs no DNS lookup or outbound request. Public reachability and valid TLS are the operator's responsibility. Temporary registrations, authorization transactions and pairing secrets expire. Approved client metadata and hashed grants are stored with HA's supported Store API. See the [custom-origin security assessment](SECURITY-ASSESSMENT-2026-10-05.md).

The browser form uses per-transaction Secure/HttpOnly/SameSite=Lax cookies, same-origin referrer metadata, HTML escaping and script-free CSP. `form-action` includes only the verified registered callback origin because Chrome applies this policy to the OAuth redirect as well. Authorization POST rejects a missing or mismatching transaction cookie and a foreign Origin. The server permits native/server clients that omit Origin; possession of the cookie and the single-use pairing secret is still required.
The browser form uses per-transaction Secure/HttpOnly/SameSite=Lax cookies, same-origin referrer metadata, HTML escaping and script-free CSP. `form-action` includes only the verified registered callback origin because Chrome applies this policy to the OAuth redirect as well. The page's single inline stylesheet (brand logo, light/dark theme via `prefers-color-scheme`) is allowed by a `style-src` SHA-256 hash computed from the exact stylesheet text, so the CSP stays `default-src 'none'` without `unsafe-inline`. Authorization POST rejects a missing or mismatching transaction cookie and a foreign Origin. The server permits native/server clients that omit Origin; possession of the cookie and the single-use pairing secret is still required.

Each grant lasts at most 30 days, with short access tokens and refresh rotation. Pairing/authorization codes cannot be reused. A refresh-token replay invalidates the token family. Changing the origin also prevents stored credentials from matching the new resource.

Expand Down
22 changes: 22 additions & 0 deletions docs/SECURITY-ASSESSMENT-2026-10-06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# OAuth pairing-page styling security assessment

Date: 2026-10-06. Scope: [PR #5](https://github.com/Jenik5/GrokConnectorForHomeAssistant/pull/5), original head `69e48ad2c3fcf5da3aec89ce50db1df47c26c87f`, against `c0d96e78ad0cef25135ede6b00f5bc73922cb114` (2026.10.5.2).

## Decision

Accept the change. The reviewed diff adds presentation without broadening Grok's permissions or weakening authentication. The CSP adds one SHA-256 stylesheet hash, a deliberate narrow exception to the previous no-style policy. It does not introduce `unsafe-inline`, a stylesheet host allowlist or script permission. This is a focused source and regression review, not a full penetration test.

## Reviewed boundaries

- The stylesheet is a fixed packaged string. Its hash is computed from that string and emitted in the authorization form's CSP. It contains no imports or external resource references, no user input and no secrets. Hash authorization follows the [CSP element/source matching algorithm](https://www.w3.org/TR/CSP3/#match-element-to-source-list).
- `default-src 'none'`, `base-uri 'none'`, `frame-ancestors 'none'` and the verified callback origin in `form-action` remain in place. Scripts, external images, fonts, frames and network connections gain no permission. Error responses retain their original stricter CSP.
- The SVG comes from the fixed local `brand/icon.svg` path, with no request-controlled filename or remote fetch. The reviewed file contains only SVG paths/groups, drawing attributes and solid colors; no scripts, event handlers, links, external references or embedded HTML.
- Callback and transaction values and translations remain HTML escaped. The public page still discloses entity/action counts only, never their identifiers, names or states. No third-party assets, analytics or new logging are added.
- POST target, password input, pairing-secret requirements, transaction cookies, Origin checks, callback/resource binding, PKCE, token issuance/rotation and anti-replay behavior are unchanged. Authorization success and rejection regressions remain in the full suite.
- Entity/action selection, fixed action arguments, policy revocation and grant-bound MCP sessions are unchanged. Updating this release does not itself require fresh pairing.

## Verification and limits

Two added regressions independently check the served style hash and complete CSP in every supported language, and parse the served logo to prevent active markup or external references. Existing escaping, disclosure, cookie/Origin, OAuth, replay, policy and session tests remain covered. Local result: **91 Python tests**, **5 JavaScript tests**, package checks and JavaScript syntax validation passed.

The tests run shipped modules with framework adapters and simulated actions. The contributor reports headless Chrome light/dark rendering with the real CSP and a live custom-origin hash check; these were not independently repeated by the maintainer. No live HA deployment, TLS/proxy audit, physical command or Grok/Tesla client acceptance test was performed in this review. Browser/layout acceptance remains a separate check.
8 changes: 8 additions & 0 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,11 @@ PR [#4](https://github.com/Jenik5/GrokConnectorForHomeAssistant/pull/4) replaces
The maintainer fix reserves status and legacy names, avoids all literal slugs during suffix allocation, and rechecks generated names until unique. Retry identity uses the immutable action ID. The fixed implementation passed **89 Python tests**, **5 JavaScript tests** and package checks. Coverage includes all 64 configured actions with empty/non-ASCII/truncated/reserved names, calling each action through readable and legacy names, mixed-alias concurrent retries, authorization and argument rejection, and private HTTP diagnostic categories. These names follow the [MCP tool naming guidance](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names).

The tests use actual connector modules with framework adapters and simulated action runners. No household action, live HA update or Grok/Tesla display test was performed for this release preparation. The contributor's report of live use is separate from maintainer-verified evidence. Existing OAuth, origin/port checks, policy revocation and session-isolation regressions remain covered.

## Styled OAuth pairing page — 2026-10-06 / 2026.10.6.1

PR [#5](https://github.com/Jenik5/GrokConnectorForHomeAssistant/pull/5) adds the packaged logo, responsive layout and automatic light/dark themes. The focused security review found no change to authentication, authorization, redirect validation, token handling, entity/action permissions or session boundaries. See the [assessment](SECURITY-ASSESSMENT-2026-10-06.md).

The maintainer added two regressions: one independently hashes the served stylesheet and checks the exact CSP and privacy headers in all five languages; the other parses the inline SVG and permits only passive geometry, without scripts, event handlers, style attributes or external references. The full local suite passed **91 Python tests**, **5 JavaScript tests**, package checks and JavaScript syntax validation.

Tests exercise shipped connector modules with framework adapters and simulated actions. No maintainer live HA update, browser rendering test, physical action or Grok/Tesla acceptance test was performed for this release. The contributor's reported headless Chrome rendering and live custom-origin test remain separate evidence. Updating alone keeps existing configuration and grants because neither their schemas nor the policy/origin handling changed.
Loading
Loading