The push trigger matched any branch, so pushing this feature branch ran the
refresh pipeline from the branch: it built and pushed ci-claude:2.1.218 and
opened a bump PR (#379) prematurely. Restrict the push event to branch: master
so only master ever builds/pushes images or opens bump PRs; cron and manual
are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bump credential is a claude-bot PAT scoped write:repository only (a
dedicated Gitea user needs admin to create, which claude-bot lacks; the
scoped-down PAT is the same least-privilege at the token level). Token minted
and stored as the ci_claude_bump_bot_token Woodpecker secret (events:
push/cron/manual). Commit author set to claude-bot accordingly. Only the cron
registration remains, to be done after this merges to master.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ci-claude image installed a fixed, pinned Claude Code version and every
consumer pipeline pinned a stale dated image tag. Two problems:
- The npm install layer changed on every build (install-time mtimes + the
npm cache), so it never deduped in the Gitea registry — the single largest
orphan pattern behind #336 (~260 MB orphaned per no-op rebuild).
- A frozen version silently rots: old Claude Code releases eventually stop
working against the API.
Containerfile: default to @latest (npm auto-selects the musl native binary on
this alpine base) and make the install layer byte-stable — clean the npm cache
and normalize install-time mtimes on the installed tree and the directories the
RUN touches. Two independent --no-cache builds now produce a byte-identical npm
layer, so a rebuild of the same version dedups instead of orphaning a copy.
New ci-claude-refresh.yml owns the image lifecycle: weekly (and on-demand) it
resolves the latest published version, and only when it differs from what the
consumers pin does it build ci-claude:<version> (+:latest) and open a PR
rewiring the 5 consumer pipelines. Version tags make "did it change?" a
race-free string compare and keep the cron from opening no-op PRs. Nothing
auto-merges. build-ci-images.yml no longer builds ci-claude so the two never
fight over tags.
Requires owner setup before it does anything (documented in the pipeline
header): a dedicated ci-claude-bump-bot identity (write:repository only) as the
ci_claude_bump_bot_token secret, and a cron named "ci-claude-refresh".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review blocker: my previous filename fix used bash substring syntax
${TALOS_SCHEMATIC:0:12}, but the apply step runs under busybox ash
(Alpine /bin/sh) which errors 'Bad substitution' — the repo already uses
cut for this exact reason. Replaced with $(echo "$TALOS_SCHEMATIC" |
cut -c1-12). The HCL substr() in talos_image.tf is unaffected (not shell).
Also finishes the comment sweep the prior commit claimed but didn't: 4
more stale 'eth1' host-NIC references in the matter/otbr/music-assistant
NAD headers are now ens19. Grep confirms zero eth1 left in the v30iot
NAD/deployment files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
The step exports REVIEW_TOKEN (write:repository PAT) for its own curl/gitea_post
calls, but that also placed it in the environment of the claude triage+review
agents, which process attacker-controlled PR content — a prompt-injected diff
could read /proc/self/environ and exfiltrate it into the public report. The
agents never call Gitea (the shell does, via $AUTH), so run them under
'env -u REVIEW_TOKEN'. Defence in depth alongside the settings.json /proc deny.
Caught by the review bot; matches the fix applied to the other fleet repos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up (optional): the shelly/matter/otbr NAD header comments
still described the old eth1 probe-order naming, contradicting this PR's
own root-cause writeup. Updated to the stable ens19 predictable name and
dropped the stale k8s-edge/probe-order narration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Review blocker: the Proxmox image-exists check derived TALOS_FILENAME
from version only, so bumping the schematic left the cached filename
unchanged — a future VM (re)provision would find the stale old-schematic
image and skip the download, silently reintroducing net.ifnames=0. Embed
the schematic hash in the filename (pipeline + opentofu/talos_image.tf,
kept in sync) so a schematic bump forces a fresh download.
Also reconciles stale comments the rename left behind: the MA reconcile
docstring (hostNetwork-era eth1 explanation), cluster.yaml kubelet nodeIP
(eth1->ens19), and the matter NAD's k8s-edge claim (edge isn't migrated
and isn't a current node; the pod is pinned off it anyway).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Root cause of the IoT NIC name shuffle: the nocloud platform driver
unconditionally injects net.ifnames=0, disabling systemd predictable
naming, so eth0/1/2 followed driver-probe order and renamed across
reboots (a k8s-3 reboot put the v30iot NIC on eth2 and mgmt on eth1,
silently mis-targeting the v6-disable sysctl onto mgmt and attaching
macvlan children to the Server VLAN). LinkAliasConfig cannot fix this
(it only sets a cosmetic IFLA_IFALIAS, invisible to sysctl/CNI).
Fix: re-enable predictable naming via a new Image Factory schematic
(extraKernelArgs: -net.ifnames=0, net.ifnames=1). Because the cluster
boots sd-boot/UKI, the cmdline is baked into the image — machine.install
extraKernelArgs is silently ignored — so this MUST be delivered as a
schematic + talosctl upgrade, not a config patch. The busPath-derived
names are topology-stable and identical across the three identical VMs:
0000:06:13.0 (v30iot) -> ens19
0000:06:12.0 (mgmt) -> ens18
0000:02:00.0 (ceph) -> enp2s0np1
Re-keys everything that referenced the volatile eth1 onto the stable
ens19: the four v30iot macvlan NADs (matter/otbr/shelly/music-assistant)
and the per-node v6-disable sysctl. Bumps the schematic id in the deploy
pipeline. Applied live via rolling talosctl upgrade (one node at a time,
full Ceph recovery between each); verified ens19 identical on all three,
v30iot NIC addressless v4+v6, macvlan children retain their own v6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
The earlier edit that removed the stale suspend-comment block also dropped the
'spec:' line, leaving suspend/schedule/jobTemplate nested under metadata: — an
invalid CronJob that would be rejected by the API server (kustomize build does
not validate against the k8s schema, so it passed). Re-add spec: and sync the
top-of-file doc comment to the reconciled infrastructure/claude#setup-token
path. Caught by the review bot on this PR; verified with kubectl --dry-run=server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate zigbee2mqtt.home.renner.uno behind the same passkey-step-up Authentik
proxy-outpost pattern used for esphome/shelly-manager/vscode. The Z2M frontend
has no auth of its own (anyone on :8080 controls the whole Zigbee network), so
Traefik only ever routes the hostname to a dedicated outpost that validates the
session before forwarding.
New:
- apps/home-assistant/zigbee2mqtt-gate-service.yaml — ExternalName mirror to
ak-outpost-zigbee2mqtt-gate in the authentik ns (:9000).
- apps/home-assistant/zigbee2mqtt-ingress.yaml — zigbee2mqtt.home.renner.uno →
the outpost; cert-manager ingress-shim issues zigbee2mqtt-tls.
- apps/authentik/zigbee2mqtt-gate-egress.yaml — CNP letting the outpost egress
to the zigbee2mqtt pod on :8080.
Edited:
- apps/authentik/blueprints-configmap.yaml — 5-entry blueprint block (group /
proxyprovider / application / policybinding / outpost) named zigbee2mqtt-gate;
internal_host carries the explicit :8080 (Service listens on 8080, unlike
esphome's :80). Authentik spawns the ak-outpost-zigbee2mqtt-gate Deployment
+Service from the outpost entry.
- infrastructure/network-policies/home-assistant-ns.yaml — fromEndpoints rule
admitting the zigbee2mqtt-gate outpost to :8080 (shared 8080 with vscode/
shelly-manager; the fromEndpoints selector + the authentik-side egress scope
it to the zigbee2mqtt pod).
- the two kustomizations registering the new files.
DNS (k8s-gateway watches Ingress) and TLS (cert-manager) are automatic. Access
limited to the "Zigbee2MQTT Managers" group (k.renner). Verified: kustomize
builds, dry-run OK, blueprint YAML parses with the new block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin both CronJobs to the built secret-rotator image that carries woodpecker-secret
mode (sha256:828bf486…), flip suspend:false, and reconcile the claude-oauth
BAO path to homelab/infrastructure/claude#setup-token (where the OAuth value is
actually seeded) — with the matching policy update (applied out-of-band).
Bootstraps + policies/roles are already applied and the org secrets are live, so
the rotators now maintain them on schedule: review-bot mints+syncs review_bot_token
quarterly; claude-oauth syncs claude_code_oauth_token daily.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The coordinator_backup.json approach did NOT make herdsman adopt the existing
network. herdsman's zstack manager compares the CONFIG network params against
the adapter's NV and refuses on mismatch; a backup only prevents accidental
re-commissioning, it does not supply the network to an already-commissioned
adapter. So Z2M sat on defaults (PAN 0x1a62 / channel 11) vs the real network
(PAN 0x8b2b / channel 25) and correctly refused — no re-pair, no damage.
Set the real network params explicitly (values are the adapter's own, read from
herdsman's own mismatch log, cross-checked against the OpenBao backup which
matches exactly: key fe0b..2d, pan 8b2b, ext fc7a735d08c15476, ch 25):
advanced:
channel: 25
pan_id: 0x8B2B
ext_pan_id: [0xfc,0x7a,0x73,0x5d,0x08,0xc1,0x54,0x76] # natural order
network_key: "!secret network_key"
Byte order pinned against zigbee-herdsman v10.6.1 source (determineStrategy):
ext_pan_id is reversed only when WRITING to NV, but compared/logged
non-reversed, and the network key is never reversed — so both go in natural
order matching the displayed adapter hex.
network_key stays secret: the init container extracts it from the OpenBao
coordinator backup (jq + sed -> natural-order int array) into
/app/data/secret.yaml, referenced by the config as a quoted "!secret" ref.
channel/pan_id/ext_pan_id are non-secret and live in the ConfigMap.
Verified: kustomize builds, deployment dry-run OK, extraction produces the
correct 16-byte array, config schema types valid. The final config<->adapter
match is confirmed at startup, where the coordinator_backup.json guarantees a
safe refusal (never a re-commission) if anything is still off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unblocks the node-unaddressing change: MA ran hostNetwork and its
reconcile initContainer fails closed if no 10.2.30.0/24 address exists
on a host interface — removing node host addressing would crash-loop it
on every node it can schedule to (no edge toleration).
MA now attaches net1 via macvlan on v30iot (DHCP-leased), exactly like
matter-server/otbr/shelly-manager: its own IoT address, direct L2
adjacency for Snapcast/SlimProto/Sendspin audio sync (macvlan bridge
mode puts frames straight on the wire — same property hostNetwork gave,
without the node holding an IoT address). The reconcile script is
unchanged in logic: it scans the pod's interfaces for the 10.2.30.0/24
address and now finds net1 (Multus attaches it with its lease before any
container runs) instead of the host eth1. bind_ip stays 0.0.0.0 -> binds
eth0 + net1; publish_ip = net1's leased IP. NAD omits matter's Thread
accept_ra tuning (MA has no Thread); keeps sbr (leased default gateway
would otherwise blackhole cluster-internal replies).
Folded into this PR so the change is atomic: on merge Flux migrates MA
to its own IoT lease before the manual node-unaddressing rollout removes
the host addresses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Root cause of Z2M refusing to start, verified against Z2M's own validator: the
MQTT password was written as a bare YAML tag `password: !secret mqtt_password`,
and Z2M 2.x does NOT register a `!secret` YAML tag — js-yaml throws
"unknown scalar tag !<!secret>", which surfaces as the generic
"configuration is invalid (use yaml-validator)" message.
Z2M's secret mechanism is parseValueRef (settings.ts read()): it matches the
regex `!<file> <key>` against the STRING VALUE of mqtt.user/password/server,
advanced.network_key, frontend.auth_token. So the reference must be a QUOTED
string, not a tag:
password: "!secret mqtt_password"
Reverted the interim env-override attempt: ZIGBEE2MQTT_CONFIG_* is applied only
in write()/onboarding, not on a plain read of an existing config, so it did NOT
set mqtt.password (confirmed: mqtt.password was empty). The quoted-!secret +
init-rendered /app/data/secret.yaml path is the one Z2M actually resolves on
read.
Verified end-to-end on the REAL rendered ConfigMap via `settings.validate()` in
the running image: VALIDATE_ERRORS: [], mqtt.password resolves from secret.yaml,
31 devices parsed.
Post-merge: the PVC still holds the pre-fix seeded configuration.yaml (init only
seeds when absent), so it must be cleared once so it re-seeds from this fixed
ConfigMap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups (non-blocking):
- The disable_ipv6 sysctl keys on the literal 'eth1' while the interface
is selected by busPath; a rename would no-op the sysctl SILENTLY and
leave the host v6-addressed on IoT. Document the dependency and that
the rollout must verify eth1 is addressless per node.
- shelly-manager-deployment comment said 'master: eth2' — the NAD uses
eth1. Last lingering eth2-means-v30iot reference, corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Review on PR #363:
- Suspend both CronJobs at merge (review-bot quarterly, claude-oauth daily):
the pinned image is still :latest and predates woodpecker-secret mode, so an
un-suspended merge would fail on schedule with 'unknown mode'. The image-pin
follow-up flips them on. (suggestion 2)
- GITEA_VERIFY_REPO: note shelly-manager verified private 2026-07-23 (unauth
404 + Woodpecker SCM PRIVATE=yes) and the degradation-if-public risk, so the
pull-only gate's assumption is documented. (suggestion 1)
- make-before-break header: caveat that the 'sync-fail keeps working' property
holds for one cycle; a persistent sync failure must be fixed before the next
quarterly run reaps the still-referenced token. (suggestion 3)
Left as-is per the review: the woodpecker.rs defensive else-arms (mirror
bao.rs) and no ureq-mocking test (repo convention).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security + routing. The k8s nodes held a DHCP-leased v4 address and an
RA-derived v6 address on the IoT VLAN — a network that must be treated
as compromised. That gave every IoT device an L3-reachable path to the
node (kubelet, SSH-equivalent apid, any host service) and, as a side
effect, installed a connected 10.2.30.0/24 route so node replies to
IoT-subnet clients bypassed the UDM entirely — breaking every UDM-DNATed
flow (the hairpin NAT: un-translated SYN-ACK -> client kernel RST,
verified live 2026-07-23).
The NIC (busPath 0000:06:13.0) stays up as the macvlan PARENT for the
v30iot pod attachments (matter-server, otbr, shelly-manager), whose
children do their own DHCP in their own netns — host addressing was
never needed for that. dhcp: false drops the v4 address+route;
sysctl net.ipv6.conf.eth1.disable_ipv6=1 drops v6 including link-local,
so the host has no addressable presence on the L2 at all. The old
comment's claimed purpose (Cilium L2 announce for an IoT LB pool) does
not exist — no L2AnnouncementPolicy, BGP-only pool.
RESIDUAL (not addressed here): the NIC remains a macvlan parent, so the
node kernel still processes frames from the IoT L2. Full node isolation
from IoT would require relocating how those three pods reach IoT devices
— a separate, larger change.
Same suppression pattern the repo already uses (edge-host 10-vlan40,
HA macvlan /32) rather than policy routing (Talos 1.12 has no
RoutingRuleConfig; that is 1.13+).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Live cutover fix. Two changes:
1. Remove the top-level `permit_join: false` from the Z2M config. It was
REMOVED in Zigbee2MQTT 2.0 — a config containing it fails schema validation
and Z2M refuses to start (serves the failure page). Joining is runtime-only
now (default off; toggled via frontend or the bridge/request/permit_join
topic), which is what we want anyway. Verified the other 2.x-sensitive keys
(homeassistant.enabled, frontend.enabled, mqtt.*) against the current docs —
they were already correct.
2. Flip replicas 0 -> 1. The cutover is committed: ZHA is disabled, the
coordinator-backup Secret is populated in OpenBao, and Z2M is now the live
Zigbee stack. Flux owns this field, so it must be 1 in git (a manual
`kubectl scale` gets reverted on the next reconcile). Rollback = set 0 and
re-enable ZHA.
Note: the running PVC still holds the pre-fix seeded configuration.yaml; the
init container only seeds when absent, so after merge that stale file must be
cleared once (delete it + restart the pod) so it re-seeds from this fixed
ConfigMap. Tracked as the immediate post-merge step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a woodpecker-secret mode to secret-rotator that upserts an OpenBao-held
value into a Woodpecker ORG-level CI secret (create if absent, else patch
value+events), and wire two rotators that use it — moving review_bot_token and
claude_code_oauth_token off hand-loading into the rotation toolchain and onto
org-level secrets every repo inherits.
secret-rotator:
- woodpecker.rs: org-id lookup, existence probe (404=absent), create/patch an
org secret via a dedicated woodpecker-bot bearer token. Never logs the value.
- main.rs: woodpecker-secret mode reads a value from BAO_PATH#BAO_FIELD (errors
if missing/empty, so a blank never overwrites a working secret) and the
woodpecker-bot token from the bootstrap, then upserts the org secret with the
configured events (must include pull_request for the PR-review pipeline).
rotators (kubernetes/infrastructure/openbao/rotation/):
- review-bot.yaml: one CronJob, init step mints review-bot's PAT (gitea-token
mode, pull-verified against a private repo it collaborates on; push not
verified — it's a read collaborator that posts reviews via the API), main step
syncs it to org secret review_bot_token (events cron,manual,pull_request).
- claude-oauth.yaml: sync-only (the OAuth token is interactive setup-token, not
mintable) — daily push of an owner-seeded OpenBao value to org secret
claude_code_oauth_token.
- policies/rotator-{review-bot,claude-oauth}.hcl: least-privilege OpenBao
policies + documented hand-seeded bootstraps (review-bot password,
woodpecker-bot token, seeded OAuth/token fields).
netpol: rotator-{review-bot,claude-oauth}-egress (openbao-ns) +
gitea-allow-review-bot-rotator + a new woodpecker-allow-rotators, so the
rotators reach gitea:3000 / woodpecker:8000 through apps default-deny.
Deploy prerequisites (can't be done from git): pin the image @sha256 once a
build including woodpecker-secret mode lands; apply the two OpenBao policies/
roles; seed the bootstraps. After the org secrets exist, drop homelab's
repo-level claude_code_oauth_token / review_bot_token copies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Woodpecker runs its ${...} variable substitution over the ENTIRE pipeline YAML
including comments, so the literal dollar-brace in the triggering comment
(added when documenting the dropped manual path) failed to parse — 'unable to
parse variable name' — erroring every pull_request pipeline before any step
ran. Reword the comment to avoid the glyph; the live homelab pipeline was down
since #362 merged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The triage + review agents run with REVIEW_TOKEN and CLAUDE_CODE_OAUTH_TOKEN in
their environment, process attacker-controlled PR title/body/diff, and their
output is posted verbatim as a public PR comment. Without a Read sandbox a
prompt-injected diff could read /proc/self/environ (the tokens) or .git/config
(Woodpecker's clone credential) and get it published. Every other agent
pipeline already denies this via --settings; pr-review had none.
Add ci/agents/pr-review/settings.json (mirrors issue-fix/update-audit, gitea
#197) denying Read on /proc,/root,/home,/etc,.git, and pass --settings
$PROMPTS/settings.json to both claude invocations (resolves alongside the
prompts, so it works for the repos that fetch prompts from the homelab clone).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correct the cutover mechanism before it ever runs. The prior approach hand-set
advanced.pan_id/ext_pan_id/network_key in the Z2M config from the ZHA values.
That is the risky path: the extended PAN ID byte order is REVERSED between ZHA
(zigpy) and Z2M (zigbee-herdsman), so loading the ZHA value as-is makes herdsman
see a mismatch and FORM A NEW network — dropping every device (the explicit
non-goal). Mixing a manual ext_pan_id with a backup also conflicts (Z2M ignores
the reversed value).
Instead restore an Open Coordinator Backup, the shared zigpy<->herdsman
interchange format, which pins byte orders in-spec and handles the reversal on
import:
- New zigbee2mqtt-coordinator-backup ExternalSecret (OpenBao) holds the full
backup JSON (contains the network key -> OpenBao only, never git), replacing
zigbee2mqtt-network-externalsecret.yaml.
- Init container seeds /app/data/coordinator_backup.json on first run only
(guarded on database.db too, so Z2M's own state is never overwritten), and
now renders only the mqtt password into secret.yaml.
- Config drops advanced.pan_id/ext_pan_id/network_key/channel entirely; the
network comes from the restored backup.
Runbook updated: Step 1 exports the Open Coordinator Backup from zigbee.db via
zigpy (non-disruptive — no second radio session); Step 2 stores it in OpenBao;
added an explicit device re-interview step (devices stay joined but Z2M's
database starts empty), and a PVC-clear note in rollback.
Verified live: coordinator has pan 0x8b2b, ext_pan_id fc7a735d08c15476,
channel 25; zigpy emits a valid open-coordinator-backup for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a pull_request event Woodpecker's built-in clone already checks the repo out
at the PR head in the workspace, so the agents read that directly for context.
Restores full-context review (the prior commit over-corrected to diff-only)
while keeping the manual, token-bearing clone gone. Prompts point at the
working-directory checkout instead of ./work/target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clone existed only to hand review_bot_token to git, which persisted it in
work/target/.git/config — a file the review agent (Read/Glob/Grep, output
posted publicly) could read. Nothing needed the checkout that the API-fetched
diff + metadata don't already provide, so drop the clone entirely: no token on
disk, no work/target, no cleanup. The PR + diff are still read over the API
with ephemeral curl Authorization headers (never written to disk).
Prompts updated to match: the agents review the diff alone (no ./work/target),
and are told to flag, not guess, when a judgement needs code they can't see.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull the live ZHA device registry (31 devices + coordinator) and pre-seed each
device's friendly_name in the Z2M config, keyed by IEEE. Devices then come up as
"EG Küche Tischlampe Oben" instead of "0x001788010856bb17" the moment Z2M
re-interviews them on the adopted network — which is what keeps the HA-side
entity reconciliation small and mechanical. Two devices that were both bare
"Deckenlampe" in ZHA are disambiguated by area (Z2M requires unique friendly
names): EG Galerie (Hue LTC001) and DG Lager (LUMI N1 switch).
Add zigbee2mqtt-entity-reconciliation.md: an audit of exactly which ZHA entities
the git-tracked config references, so the cutover knows what must be re-pointed.
Key result — the HA-side change is FAR smaller than "rewire everything":
- 216 ZHA entities exist, but only 16 distinct entity_ids are referenced in
the repo (200 are unreferenced diagnostics/config entities).
- 14 of the 16 are physical-device entities, all mapping cleanly into the
31-device inventory (no orphans).
- The remaining 2 are the `light.texas_instruments_coordinator_*` entities —
now identified as ZHA *groups* on the coordinator (not physical devices),
accounting for 14 references. These don't come from Z2M device discovery and
must be re-created as Z2M groups and re-pointed by hand.
Scope note captured in the doc: only git-tracked YAML/JSON was audited; live
.storage Lovelace dashboards and UI-created automations/helpers must be
reconciled separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the credential-in-URL clone (which git wrote into
work/target/.git/config, readable by the review agent whose output is posted
publicly) with a tokenless clone that supplies auth via an HTTP header through
GIT_CONFIG_* env. The persisted remote is tokenless, so no repo file holds the
PAT — fixing the leak at the source instead of deleting .git after the fact.
Supersedes the previous rm -rf .git stopgap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clone embedded REVIEW_TOKEN in the URL, which git persists in
work/target/.git/config. The review agent runs next cd'd into the workspace
with Read/Glob/Grep and its report is posted verbatim as a public PR comment,
so the write:repository PAT was readable and could be exfiltrated via prompt
injection from the reviewed PR (caught by the review bot on PR #180). The
agent needs only the working tree, so drop .git entirely after checkout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay down Zigbee2MQTT (Z2M) in the home-assistant namespace as the eventual
replacement for the ZHA integration, wired to drive the SAME coordinator ZHA
uses today (SMLIGHT SLZB-06 / TI CC2652, tcp://slzb-zigbee.renner.uno:6638).
Devices are joined to the network on the coordinator chip, so if Z2M comes up
with the existing network parameters (pan_id, ext_pan_id, network_key, channel)
zigbee-herdsman ADOPTS that network instead of forming a new one — every device
stays joined, no re-pairing.
Ships INERT and safe to merge:
- Deployment is replicas: 0. Reconciling this PR starts nothing.
- The ZHA network params come from the `zigbee2mqtt-network` ExternalSecret
(OpenBao), populated only at cutover. Until then the ExternalSecret is
NotReady and the pod cannot start, so it can never accidentally form a new
network (which would drop every device).
The cutover is a manual, documented, reversible procedure — see
zigbee2mqtt-migration-runbook.md.
Contents:
- zigbee2mqtt-{pvc,config,deployment,service}.yaml — the app. Config template
is seeded to the PVC once; secrets (mqtt password + ZHA network params) are
injected via /app/data/secret.yaml (rendered each start by an init
container) and referenced with Z2M `!secret`, keeping the network key out
of git and out of the ConfigMap.
- zigbee2mqtt-network-externalsecret.yaml — OpenBao-backed network params,
populated at cutover from a ZHA network backup.
- New `zigbee2mqtt` Mosquitto broker user: bootstrap Job
(mosquitto-bootstrap.yaml) + passwd/ACL/env (mosquitto-deployment.yaml),
scoped to zigbee2mqtt/# plus write homeassistant/# for HA MQTT discovery.
- zigbee2mqtt-migration-runbook.md — the cutover + rollback procedure.
No new egress CiliumNetworkPolicy is needed: the namespace-wide default-deny
policy already grants same-namespace (mosquitto), DNS, and world (the IoT-VLAN
coordinator) to every pod.
Follow-up (separate homeassistant-repo PR): re-point automations/dashboards
from ZHA entity ids to the new Z2M MQTT-discovery entities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Woodpecker exposes --var values to YAML ${...} substitution and when-filters
only, not as a step's shell environment, so the documented
'--var PIPELINE=pr-review --var PR=<n>' re-review read an unset shell var and
silently no-opped with a green build (caught by the review bot on PR #49).
Trigger on pull_request only; re-review by pushing a commit (new head sha
clears the dedup marker). Resolve the PR from CI_COMMIT_PULL_REQUEST alone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>