Skip to content

v0.2.0-beta.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 May 14:20
fcd2a2d

First post-flip beta. Major work since v0.1.0:

  • Trust-On-First-Use TLS pinning for both AGL hosts (closes AP-1
    from security/2026-05-02T04-43Z/).
  • 14-finding SAST sweep addressing AP-2/AP-4/AP-6 chains.
  • Supply-chain hardening: all GitHub Actions SHA-pinned;
    workflow-level permissions: read-all; HACS validation gate
    un-suppressed; release.yml hardened against changelog interpolation.
  • Repo posture files: SECURITY.md, CODEOWNERS,
    CONTRIBUTING.md, CODE_OF_CONDUCT.md, weekly CodeQL scan,
    build-provenance attestations on release.
  • Platform bump to Python 3.14.2 / HA 2026.4.4 — closes the 13
    outstanding aiohttp/cryptography/orjson CVEs at the source.
  • Branding correction: DeviceInfo.manufacturer no longer
    claims AGL Energy authorship.

This is the first version intended for HACS submission. Marked beta
until it has soak-time on independent installs.

Changed

  • Platform floor bumped to Python 3.14.2 + Home Assistant 2026.4.4.
    HA 2026.4 ships aiohttp==3.13.5 (closes 9 CVEs in SCA-M01..M04 +
    SCA-L01..L06), cryptography>=46.0.7 (closes SCA-M05 + SCA-L08), and
    orjson>=3.11.7 (closes SCA-H02). HA 2026.4 itself requires Python
    3.14.2, so the integration's requires-python and CI matrix follow.
    Net effect: 13 of the 21 Dependabot alerts auto-close as state=fixed
    on the next scan; the remaining 8 were already dismissed as not-used
    on 2026-05-03. Bumps hacs.json:homeassistant to 2026.4.4, so HACS
    refuses to install on older HA. CHANGELOG previously deferred this to
    v0.2.x; the live install verified clean on 2026-05-03 so we bring it
    forward.
  • pyproject.tomlrequires-python>=3.14.2, homeassistant>=2026.4.4,
    aiohttp>=3.13.4, pytest-homeassistant-custom-component>=0.13.325,<0.13.326.
    Ruff target-version = "py314"; mypy python_version = "3.14".
  • .github/workflows/ci.yml — matrix python-version: ["3.14"].
  • Three test_config_flow tests now patch async_setup_entry to a
    no-op stub. pytest-HA 0.13.325 is stricter about socket use during
    flow.CREATE_ENTRY teardown; previously the coordinator's first
    refresh leaked through. Patch is harmless — the tests assert
    flow-level state (entry data shape, PKCE secret zeroing), not setup
    behaviour.

Fixed

  • DeviceInfo no longer claims AGL authorship. sensor.py previously
    set manufacturer="AGL Australia" and model="AGL Energy API", which
    HA's "Service info" card rendered as AGL Energy API by AGL Australia
    — implying the integration was an official AGL product. It is not.
    Updated to manufacturer="Haggle" and
    model="AGL smart-meter (unofficial integration)". Added a regression
    test that asserts neither field contains the AGL name. README and
    info.md already carry the "unofficial / not affiliated with AGL"
    disclaimer; this fix brings the in-app HA UI into line with the docs.
  • TOFU SPKI pinning was silently broken on every install. The first
    attempt (#45) extracted the SPKI from resp.connection.transport after
    the response context entered, but aiohttp had already released the
    Connection back to its pool — resp.connection was None and the
    capture silently degraded to no-pin (verified empty in live entry.data
    on 2026-05-03). Replaced with a HagglePinningConnector (TCPConnector
    subclass) that overrides _wrap_create_connection to capture the SPKI
    during the TLS handshake itself, before any response object exists.
    The connector exposes observed[host] and an optional
    on_new_connection(host, spki) callback for warn-on-mismatch validation.
    New regression tests stand up a real local TLS server with a known cert
    and assert the connector populates observed correctly — these would
    have caught the lifecycle bug. The integration owns its own
    aiohttp.ClientSession(connector=HagglePinningConnector) rather than
    HA's shared session (HA's shared connector cannot be subclassed); the
    session is closed in async_unload_entry.

Added

  • SECURITY.md at the repo root — disclosure path
    (security@naanya.biz and GitHub private security advisories), threat
    model summary including the TOFU pinning behaviour, scope, and
    coordinated-disclosure expectations.
  • CONTRIBUTING.md — dev loop, commit conventions, branch and PR
    workflow, and a pointer to AGENTS.md for AGL-specific contribution
    steps.
  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1 with private
    reporting via security@naanya.biz.
  • .github/CODEOWNERS@naanyabiz owns everything; auto-routes
    PR review requests once branch protection requires them.
  • .github/workflows/codeql.yml — Python CodeQL scan on every push
    to main, every PR, and weekly on Monday. Uses the
    security-extended,security-and-quality query packs.
  • Build-provenance attestation in release.yml — every tag-triggered
    GitHub Release now ships a Sigstore-rooted attestation against the
    custom_components/haggle/ source tree via
    actions/attest-build-provenance. Verify with gh attestation verify.
  • AGENTS.md "GitHub Issues Workflow" section documenting when to
    open an issue vs PR, the Closes #N convention, and that issues are
    the canonical place for "do this next round" items (closes #22).

Added (previous)

  • Trust-On-First-Use TLS certificate pinning for secure.agl.com.au and
    api.platform.agl.com.au. The SHA-256 SPKI hash of each AGL host's leaf
    certificate is captured during the initial PKCE config flow and persisted
    to the config entry; every subsequent token refresh and BFF request is
    observed and compared. Mismatches surface as a HA persistent notification
    (haggle_pin_mismatch_<host>) plus a WARNING log — they do not block
    the request, so a legitimate AGL cert rotation cannot brick HACS users.
    Re-pin via the standard Reconfigure flow on the integration card. New
    module custom_components/haggle/agl/pinning.py. Closes AP-1 from
    security/2026-05-02T04-43Z/.

Fixed

  • coordinator.py uses UTC for the fetch range instead of the OS local
    date. AGL dateTime slots are UTC; date.today() on a non-UTC HA host
    could fetch tomorrow's empty data or skip yesterday entirely around
    midnight. (#31)
  • config_flow._exchange_code and _fetch_contracts use HA's shared
    aiohttp client
    (async_get_clientsession(hass)) instead of creating a
    throwaway aiohttp.ClientSession() per call. Inherits HA's TCP connector
    pool. (#28)
  • __init__.py::async_setup_entry uses async_create_clientsession(hass);
    the manual session.close() in async_unload_entry is gone — HA owns the
    session lifecycle now. The session field on HaggleRuntimeData is
    removed since callers no longer need to reach for it. (#29)

Removed

  • Three never-called AglClient methods: async_get_servicehub,
    async_get_usage_daily, async_close. (#32)
  • CONF_ACCESS_TOKEN and CONF_ACCESS_TOKEN_EXPIRY constants from
    const.py and the empty-string/zero values that were being written into
    entry.data on creation. AGENTS.md already prohibited persisting access
    tokens; the code was a footgun for future contributors. (#26)
  • beautifulsoup4 runtime dependency. Zero call sites in
    custom_components/; was a dead dep that every HACS installer downloaded for
    no reason. Removed from manifest.json and pyproject.toml (also drops
    types-beautifulsoup4 from dev deps and the soupsieve / typed-stub
    transitives from uv.lock). Resolves SCA noise plus HACS-posture B1/B2
    from security/2026-05-02T04-43Z/.

Security

  • PKCE verifier and challenge are zeroed after a successful exchange.
    The flow object can persist in memory across multi-step retries; a stale
    one-shot verifier is one less secret to leak. (#27)
  • Note on aiohttp CVE coverage: the 9 CVEs against aiohttp==3.13.3
    flagged in the security review (SCA-M01, SCA-M04, SCA-L01..L06) are fixed in
    aiohttp>=3.13.4, which Home Assistant bundles starting with 2026.4.0. HA
    2026.4.0 also bumps the Python floor to 3.14.2, so a hard aiohttp>=3.13.4
    pin would force a Python platform bump for every HACS user. Deferred to a
    v0.2.x release that promotes the platform floor deliberately. Users on
    HA 2026.4.0+ already receive the patched runtime.
  • Pin all GitHub Actions to commit SHAs across ci.yml, hacs.yml,
    hassfest.yml, release.yml. Closes the supply-chain branch-poisoning vector
    on hacs/action@main and home-assistant/actions/hassfest@master.
  • Add permissions: read-all to ci.yml, hacs.yml, hassfest.yml.
    release.yml retains its job-level contents: write.
  • Remove continue-on-error: true from the HACS validation step. The
    underlying validation passes (All (8) checks passed confirmed against
    main); the suppression flag was hiding real failures from CI.
  • Switch release.yml to body_path: instead of interpolating
    ${{ steps.changelog.outputs.body }} directly into the release body, removing
    the shell-context injection vector.
  • Hash refresh-token before using it as a fallback unique_id in the config
    flow. The HA entity registry is plaintext JSON on disk; previously the first
    16 chars of the live OAuth2 refresh token landed there when contracts were
    unavailable at setup time. Now uses sha256(refresh_token)[:16]. (SAST-001)
  • Use the return value of AglAuth.async_force_refresh on the 401-retry
    path in AglClient._get instead of reading _auth._token_set.access_token
    via private-attribute access. The retry path no longer silently masks auth
    failures. (SAST-002)
  • Redact AGL/Auth0 response bodies from exceptions that propagate to
    ConfigEntryAuthFailed / UpdateFailed. Bodies and PII-bearing URLs now go
    to _LOGGER.debug only — they no longer surface in HA Persistent
    Notifications or the default home-assistant.log. Affects token refresh,
    the _get error path, and the config-flow _fetch_contracts overview call.
    (SAST-003, SAST-004)
  • Consolidate the auth0-client SDK identity blob to a single
    AGL_AUTH0_CLIENT constant in const.py. Previously the same JSON shape was
    base64-encoded twice with different field ordering — once in const.py,
    once in agl/client.py — risking inconsistent headers per call site.
    (SAST-006)
  • Allowlist plan-rate fields in parse_plan instead of forwarding the
    raw API rate dict via dict(rate). Only kind, type, title, price
    propagate into PlanRates.unit_rates — closes the open-schema vector that
    a MITM AGL response could exploit to inject keys into coordinator state.
    (SAST-007)
  • Bound numeric API values with a shared _safe_float() helper in
    agl/parser.py and coordinator.py. Non-finite (inf, nan) and negative
    values clamp to 0.0 with a warning so adversarial AGL responses can no
    longer poison the recorder via async_add_external_statistics. (SAST-008)
  • Trigger reauth on persist failure: if async_update_entry raises while
    saving a rotated refresh token, __init__.py::_persist_refresh_token now
    calls entry.async_start_reauth(hass) immediately instead of silently
    continuing in split-brain state. (SAST-009)