v0.2.0-beta.1
Pre-release
Pre-release
First post-flip beta. Major work since v0.1.0:
- Trust-On-First-Use TLS pinning for both AGL hosts (closes AP-1
fromsecurity/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-levelpermissions: 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.manufacturerno 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 shipsaiohttp==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'srequires-pythonand CI matrix follow.
Net effect: 13 of the 21 Dependabot alerts auto-close asstate=fixed
on the next scan; the remaining 8 were already dismissed as not-used
on 2026-05-03. Bumpshacs.json:homeassistantto2026.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.toml—requires-python>=3.14.2,homeassistant>=2026.4.4,
aiohttp>=3.13.4,pytest-homeassistant-custom-component>=0.13.325,<0.13.326.
Rufftarget-version = "py314"; mypypython_version = "3.14"..github/workflows/ci.yml— matrixpython-version: ["3.14"].- Three
test_config_flowtests now patchasync_setup_entryto a
no-op stub. pytest-HA 0.13.325 is stricter about socket use during
flow.CREATE_ENTRYteardown; 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
DeviceInfono longer claims AGL authorship.sensor.pypreviously
setmanufacturer="AGL Australia"andmodel="AGL Energy API", which
HA's "Service info" card rendered asAGL Energy API by AGL Australia
— implying the integration was an official AGL product. It is not.
Updated tomanufacturer="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 fromresp.connection.transportafter
the response context entered, but aiohttp had already released the
Connection back to its pool —resp.connectionwasNoneand the
capture silently degraded to no-pin (verified empty in liveentry.data
on 2026-05-03). Replaced with aHagglePinningConnector(TCPConnector
subclass) that overrides_wrap_create_connectionto capture the SPKI
during the TLS handshake itself, before any response object exists.
The connector exposesobserved[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 populatesobservedcorrectly — 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 inasync_unload_entry.
Added
SECURITY.mdat the repo root — disclosure path
(security@naanya.bizand 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 toAGENTS.mdfor AGL-specific contribution
steps.CODE_OF_CONDUCT.md— Contributor Covenant 2.1 with private
reporting viasecurity@naanya.biz..github/CODEOWNERS—@naanyabizowns everything; auto-routes
PR review requests once branch protection requires them..github/workflows/codeql.yml— Python CodeQL scan on every push
tomain, every PR, and weekly on Monday. Uses the
security-extended,security-and-qualityquery 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 withgh attestation verify. - AGENTS.md "GitHub Issues Workflow" section documenting when to
open an issue vs PR, theCloses #Nconvention, 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.auand
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
modulecustom_components/haggle/agl/pinning.py. Closes AP-1 from
security/2026-05-02T04-43Z/.
Fixed
coordinator.pyuses UTC for the fetch range instead of the OS local
date. AGLdateTimeslots 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_codeand_fetch_contractsuse HA's shared
aiohttp client (async_get_clientsession(hass)) instead of creating a
throwawayaiohttp.ClientSession()per call. Inherits HA's TCP connector
pool. (#28)__init__.py::async_setup_entryusesasync_create_clientsession(hass);
the manualsession.close()inasync_unload_entryis gone — HA owns the
session lifecycle now. Thesessionfield onHaggleRuntimeDatais
removed since callers no longer need to reach for it. (#29)
Removed
- Three never-called
AglClientmethods:async_get_servicehub,
async_get_usage_daily,async_close. (#32) CONF_ACCESS_TOKENandCONF_ACCESS_TOKEN_EXPIRYconstants from
const.pyand the empty-string/zero values that were being written into
entry.dataon creation. AGENTS.md already prohibited persisting access
tokens; the code was a footgun for future contributors. (#26)beautifulsoup4runtime dependency. Zero call sites in
custom_components/; was a dead dep that every HACS installer downloaded for
no reason. Removed frommanifest.jsonandpyproject.toml(also drops
types-beautifulsoup4from dev deps and thesoupsieve/ typed-stub
transitives fromuv.lock). Resolves SCA noise plus HACS-posture B1/B2
fromsecurity/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
aiohttpCVE coverage: the 9 CVEs againstaiohttp==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 with2026.4.0. HA
2026.4.0also bumps the Python floor to3.14.2, so a hardaiohttp>=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
HA2026.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
onhacs/action@mainandhome-assistant/actions/hassfest@master. - Add
permissions: read-alltoci.yml,hacs.yml,hassfest.yml.
release.ymlretains its job-levelcontents: write. - Remove
continue-on-error: truefrom the HACS validation step. The
underlying validation passes (All (8) checks passedconfirmed against
main); the suppression flag was hiding real failures from CI. - Switch
release.ymltobody_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_idin 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 usessha256(refresh_token)[:16]. (SAST-001) - Use the return value of
AglAuth.async_force_refreshon the 401-retry
path inAglClient._getinstead 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.debugonly — they no longer surface in HA Persistent
Notifications or the defaulthome-assistant.log. Affects token refresh,
the_geterror path, and the config-flow_fetch_contractsoverview call.
(SAST-003, SAST-004) - Consolidate the
auth0-clientSDK identity blob to a single
AGL_AUTH0_CLIENTconstant inconst.py. Previously the same JSON shape was
base64-encoded twice with different field ordering — once inconst.py,
once inagl/client.py— risking inconsistent headers per call site.
(SAST-006) - Allowlist plan-rate fields in
parse_planinstead of forwarding the
raw API rate dict viadict(rate). Onlykind,type,title,price
propagate intoPlanRates.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.pyandcoordinator.py. Non-finite (inf,nan) and negative
values clamp to0.0with a warning so adversarial AGL responses can no
longer poison the recorder viaasync_add_external_statistics. (SAST-008) - Trigger reauth on persist failure: if
async_update_entryraises while
saving a rotated refresh token,__init__.py::_persist_refresh_tokennow
callsentry.async_start_reauth(hass)immediately instead of silently
continuing in split-brain state. (SAST-009)