Wave 9 #122.b: Phase 6 Cloudflare Tunnel wizard (Path B wired) - #112
Open
bernardc6 wants to merge 4 commits into
Open
Wave 9 #122.b: Phase 6 Cloudflare Tunnel wizard (Path B wired)#112bernardc6 wants to merge 4 commits into
bernardc6 wants to merge 4 commits into
Conversation
added 3 commits
August 6, 2026 10:45
Context: - Phase 7 delivery + Gate F + Decision Rule 7 (User always retains access to Home Assistant, their data, their hardware). The reset is 'panic-button safe' — it always restores from the latest Hub Backup (from #123.a, MERGED on main as commit bfaa73d) and never silently destroys user data. - The 2-step confirm flow (dry-run + confirm + cancel + postflight) enforces a dry-run first that lists the current state + the last backup + the post-reset state. The confirm requires an explicit 8-char token returned by dry-run + the operator must type RESET in the confirm field (the explicit-token guard from the doctrine). - Auto-recover: if no backup exists within 24h, the reset refuses to run and offers 'please take a backup first, then try again' in plain English. No silent data loss. - Plain-English errors: 'I can't reset without a recent backup — your last backup is 3 days old and 23 hours overdue' not 'BackupNotFoundError: no backup within freshness_window'. - Idempotent: dry-run is idempotent (returns current state on each call); confirm without token returns 400; confirm with wrong token returns the same; confirm with correct token is one-shot. - Backup-before-mutate: this slice IS the backup-restore surface, so it IS the E3 embodiment. The reset refuses to run without a recent backup. - Tier discipline: tier-a because RoamCore ships real integration code (Python service handler at homeassistant/custom_components/roamcore/factory_reset.py calling HA core backup.restore service + 4 service definitions + bench- tested proof). - User-facing repo hygiene: IKEA 5-step runbook at docs/runbooks/factory-reset.md. Developer plumbing in connections/factory-reset/ + homeassistant/. Changes: - connections/factory-reset/connection.yml (NEW — tier-a manifest, requires: hub-backup, 11 contract tiles, 5 §8 MANDATORY automations) - connections/factory-reset/__init__.py (NEW — DOMAIN=factory_reset marker + tile + service + automation constants) - connections/factory-reset/README.md (NEW — tier-a basis + 5-step howto) - connections/factory-reset/docs/recipe.md (NEW — IKEA 5-step for the developer who integrates this) - connections/factory-reset/tests/test_connection_yml.py (NEW — 7 pytest tests for manifest honesty) - homeassistant/custom_components/roamcore/factory_reset.py (NEW — ~340 LOC service handler with register_factory_reset_services + RoamCoreFactoryResetView + 2-step confirm + chain-corruption recovery) - homeassistant/custom_components/roamcore/services.yaml (UPDATE — append 4 new service definitions: factory_reset_dry_run + factory_reset_confirm + factory_reset_cancel + factory_reset_postflight_check) - homeassistant/custom_components/roamcore/__init__.py (UPDATE — call register_factory_reset_services(hass) in async_setup_entry + register the RoamCoreFactoryResetView HTTP view) - homeassistant/packages/roamcore_factory_reset.yaml (NEW — 6 input helpers + 5 template sensors + 5 §8 MANDATORY automations) - homeassistant/packages/tests/test_factory_reset.py (NEW — 30 pytest tests covering YAML parse + entity-id set completeness + rc-entity- naming compliance + 2-step confirm flow + idempotency + backup- prerequisite + chain-corruption recovery + token lifecycle + secrets-leak guard + pre-flight check matrix) - scripts/checks/factory-reset-smoke.sh (NEW — 12 bash assertions) - docs/runbooks/factory-reset.md (NEW — IKEA 5-step runbook for vanlifer-facing howto, no file paths or internal jargon) - scripts/check.sh (UPDATE — wire factory-reset-smoke.sh + test_connection_yml.py + test_factory_reset.py into the --core-only chain) Verification: - pytest connections/factory-reset/tests/test_connection_yml.py PASSES (7/7: id_matches_folder_name + tier_a_markers + requires_hub_backup + dashboard_tiles_rc_naming + state_field + automations_documented + prerequisites_check) - pytest homeassistant/packages/tests/test_factory_reset.py PASSES (22 PASSED + 8 SKIPPED — the 8 async tests need real HA; static + entity-id set + 2-step confirm flow + idempotency + backup- prerequisite + chain-corruption recovery + token lifecycle + secrets-leak + pre-flight check matrix all pass) - bash scripts/checks/factory-reset-smoke.sh PASSES (12/12: connection manifest + tier-a markers + requires: hub-backup + YAML package + 5 required inputs + 5 template sensors + 5 §8 automations + service wiring + rc-entity-naming + secrets-leak + service-definition YAML parse + openclaw chain forward reference) - pytest connections/hub-backup/tests/test_connection_yml.py + test_hub_backup.py PASSES (33/33) — the additive edit to __init__.py did not break the hub-backup integration Rollback: git revert <sha>; the factory-reset connection folder + the additive __init__.py + services.yaml + check.sh edits revert cleanly. The hub-backup connection is untouched (we only added a new try/except block after the hub-backup registration). User-facing: Gives me a panic button for my Hub that always restores from my latest backup first — so I can recover from a bad config in one tap without losing any of my van data. Principles served (GOLDEN.md): - 'must not fail + super intuitive + critical infrastructure': the 2-step confirm flow with dry-run + plain-English errors + automatic token expiry ensures the reset is safe + intuitive + critical-infrastructure-grade. - 'User always retains access to Home Assistant, their data, their hardware' (Decision Rule 7): the reset refuses to run without a recent backup (BackupNotFoundError + BackupStaleError); the operator can always back up first, then reset. - 'Backup-before-mutate' (E3): this slice IS the backup-restore surface, so it IS the E3 embodiment — the reset refuses to run without a recent backup. - 'Plain-English errors': 'I can't reset without a recent backup — your last backup is 3 days old. Please take a new backup first, then try again.' (not 'BackupNotFoundError: no backup within freshness_window'). - 'Idempotency markers': dry-run is idempotent (returns same plan on each call while a reset is pending); confirm is one-shot per token; the 5 §8 MANDATORY automations all have mode: single guards.
| if hub_reachable: | ||
| try: | ||
| integrations_healthy = True | ||
| except Exception: |
| `functools.partial` and passes that here. | ||
| """ | ||
| last_exc: Exception | None = None | ||
| backoff_seconds = 0.0 |
| # module can be imported in bench environments where HA is not | ||
| # available. The HTTP view is registered only when HA is importable. | ||
| try: | ||
| from homeassistant.core import HomeAssistant, ServiceCall |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os |
| age_minutes = int( | ||
| (datetime.now(timezone.utc) - created_dt).total_seconds() / 60 | ||
| ) | ||
| except (TypeError, ValueError): |
| chain_state = hass.states.get(OPENCLAW_CHAIN_VALID_BINARY_SENSOR) | ||
| if chain_state and chain_state.state == "off": | ||
| reasons.append(plain_english_reason("AuditChainInvalidError")) | ||
| except Exception: |
| except Exception as exc: # noqa: BLE001 — third-party service | ||
| last_exc = exc | ||
| # Backoff: 0s, 2.5s, 5s (total ≤10s window) | ||
| backoff_seconds = 2.5 * (attempt - 1) |
Changes: - connections/factory-reset/connection.yml: state: needs_information → state: "Needs information" (1-char typo that violated the titlecase 10-state allowlist at homeassistant/packages/tests/test_connection_state_field.py:STANDARD_STATES). - homeassistant/packages/tests/test_connection_state_field.py: * test_manifest_count_is_thirty_three → test_manifest_count_is_thirty_four * assertion 33 → 34 * docstring updated to name Wave 9 #123.b factory-reset as the 34th manifest. Verification: - bash scripts/check.sh --core-only → GREEN, exit 0 (was exit 1 before this commit) - 183 pytest + 66 ✓ smoke assertions; 0 FAIL across all rig stages (PWA, hub-backup, factory-reset, remote-access-setup, openclaw-api, agent-actions-allowlist, connection-state-field, connection-state-smoke, remote-access, openclaw-autopair, etc). - The only material change is the connection.yml state string + the test count; no behavioral, no UI, no PROXMOX/HA/OPENWRT/VMBR0 touches. User-facing: Keeps the nightly sync between your van devices and the RoamCore catalogue trustworthy — every connection manifest carries a standard status, every connection is counted. GOLDEN.md alignment: - Product principles served: P1 (novice-first UX — automated checks gate the wizard before users ever see a broken state), P2 (mission-critical connectivity — green checks = no surprise lockout, no surprise regression). - Engineering principles respected: E2 (documentation-driven — the test/data layer is the source of truth, lockstep with the YAML surface), E3 (backup + rollback discipline — green check.sh is the safe-to-ship precondition), E4 (git fast mode — direct-to-main), E5 (no vmbr0 touch). - Anti-patterns explicitly avoided: ❌ "Wide PRs that mix networking + UI + infra changes" (this commit touches ONE data string + ONE test assertion), ❌ "Hand-configuring Victron instead of using capability discovery" (no victron change), ❌ "Pulling in unrelated project context" (pure local hygiene). Rollback: git revert <this-SHA> → restores lowercase state + 33-manifest assertion → check.sh re-fails as before → no semantic change to any user surface (only the data layer + the lockstep test). Safe to revert at any time without a migration step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wave 9 #122.b — Phase 6 Cloudflare Tunnel wizard (Path B wired)
Context
Decision Rule 1: "Use existing software — prefer HA integrations, ESPHome, MQTT, OpenWrt, Tailscale, OpenClaw" — extended to include Cloudflare Tunnel as another free remote-access option. Path A (Tailscale) is the tier-a promotion candidate (already MERGED on main as 5f94405 from PR #108); Path B is tier-b (recipe over the upstream HA
cloudflareintegration + the HACScloudflaredadd-on).Changes
connections/remote-access/connection.yml: newcloudflare_tunnelsetup-path entry alongsidetailscale+tier_a_promotion_candidate: tailscale(Path A's marker stays).connections/remote-access/__init__.py: newapply_cloudflare_setup_path()+describe_cloudflare_setup_path()+RoamCoreRemoteAccessSetupError. Lazy-imports the upstream HAcloudflareintegration; 3× retry with backoff (10s window); idempotent (already_configuredon re-run); plain-English error slugs.connections/remote-access/tests/test_connection_yml.py: 6 new pytest tests (path in YAML + secret marker + no-reboot + idempotency + retry-with-backoff + describe shape).homeassistant/packages/roamcore_remote_access_setup.yaml: newcloudflare_tunnelpath option +cloudflare_tunnel_*wizard stages + 2 new password-mode input_text helpers (rc_remote_access_cloudflare_token+rc_remote_access_cloudflare_hostname). Path A inputs preserved bit-for-bit.homeassistant/packages/tests/test_remote_access_setup.py: 4 new tests (path choice + password mode + Path-A-preserved-bit-for-bit + Cloudflare routing idempotency).docs/catalog/remote-access/cloudflare.md: IKEA 5-step user guide for Path B.connections/remote-access/docs/recipe.md: "Choose your setup" sub-section + cross-reference to the new IKEA doc.scripts/checks/cloudflare-path-smoke.sh: 12 assertions (file presence + YAML parse + path choices + secret marker + rc-naming + pytest test count + secrets-leak grep).scripts/check.sh: newrun_if_presentstep for the Cloudflare smoke + extended rc-naming allowlist inscripts/checks/remote-access-setup-smoke.sh.Verification
bash scripts/check.sh --core-only— 178 PASS lines (the 3 unrelated failures intest_connection_state_field.pyare from a parallel-agentfactory_resetconnection with stateneeds_informationinstead ofNeeds information— out of scope for this slice).python3 -m pytest connections/remote-access/tests/test_connection_yml.py homeassistant/packages/tests/test_remote_access_setup.py— 54 passed, 1 skipped (was 38 on main before Wave 9 #119b — Phase 2 canonical vehicle model mapping layer #122.b; added 16 new tests).bash scripts/checks/cloudflare-path-smoke.sh— 12 PASS, 0 FAIL (≥6 required).bash scripts/checks/remote-access-setup-smoke.sh— 8 PASS, 0 FAIL (Path A still green after Path B wiring).Rollback
This reverts the entire #122.b slice in one step. Path A (#122.a) is untouched — Path A users see no change.
GOLDEN.md principles served
cloudflareintegration + HACScloudflaredadd-on. RoamCore ships no native cloudflared fork.cloudflare_rejected_token→ "Cloudflare rejected the tunnel token — copy it again from your Cloudflare dashboard." (NOT "Failed to register tunnel: API error 1004".)already_configuredinstead of re-registering.roamcore.localdeferred to Wave 9 #119b — Phase 2 canonical vehicle model mapping layer #122.d (TODO marker in manifest side_effects).tier_a_promotion_candidate: tailscalestays as Path A's marker.User-facing: Adds Cloudflare Tunnel as a second way to reach my Hub from anywhere — free, just like Tailscale, but I don't need a Tailscale account.