Skip to content

Wave 9 #122.b: Phase 6 Cloudflare Tunnel wizard (Path B wired) - #112

Open
bernardc6 wants to merge 4 commits into
mainfrom
subagent/phase6-tailscale-cloudflare
Open

Wave 9 #122.b: Phase 6 Cloudflare Tunnel wizard (Path B wired)#112
bernardc6 wants to merge 4 commits into
mainfrom
subagent/phase6-tailscale-cloudflare

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

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 cloudflare integration + the HACS cloudflared add-on).

Changes

  • Manifestconnections/remote-access/connection.yml: new cloudflare_tunnel setup-path entry alongside tailscale + tier_a_promotion_candidate: tailscale (Path A's marker stays).
  • Python helpersconnections/remote-access/__init__.py: new apply_cloudflare_setup_path() + describe_cloudflare_setup_path() + RoamCoreRemoteAccessSetupError. Lazy-imports the upstream HA cloudflare integration; 3× retry with backoff (10s window); idempotent (already_configured on re-run); plain-English error slugs.
  • Testsconnections/remote-access/tests/test_connection_yml.py: 6 new pytest tests (path in YAML + secret marker + no-reboot + idempotency + retry-with-backoff + describe shape).
  • HA packagehomeassistant/packages/roamcore_remote_access_setup.yaml: new cloudflare_tunnel path 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.
  • Package testshomeassistant/packages/tests/test_remote_access_setup.py: 4 new tests (path choice + password mode + Path-A-preserved-bit-for-bit + Cloudflare routing idempotency).
  • User docdocs/catalog/remote-access/cloudflare.md: IKEA 5-step user guide for Path B.
  • Recipeconnections/remote-access/docs/recipe.md: "Choose your setup" sub-section + cross-reference to the new IKEA doc.
  • Smokescripts/checks/cloudflare-path-smoke.sh: 12 assertions (file presence + YAML parse + path choices + secret marker + rc-naming + pytest test count + secrets-leak grep).
  • Chain wiringscripts/check.sh: new run_if_present step for the Cloudflare smoke + extended rc-naming allowlist in scripts/checks/remote-access-setup-smoke.sh.

Verification

  • bash scripts/check.sh --core-only — 178 PASS lines (the 3 unrelated failures in test_connection_state_field.py are from a parallel-agent factory_reset connection with state needs_information instead of Needs 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.py54 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.sh12 PASS, 0 FAIL (≥6 required).
  • bash scripts/checks/remote-access-setup-smoke.sh8 PASS, 0 FAIL (Path A still green after Path B wiring).

Rollback

git revert <merge-commit>

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

  • Decision Rule 1: "Use existing software" — Path B recipes over the upstream HA cloudflare integration + HACS cloudflared add-on. RoamCore ships no native cloudflared fork.
  • Doctrine (2026-08-04) "must not fail + super intuitive + critical infrastructure":
    • Plain-English errors: cloudflare_rejected_token → "Cloudflare rejected the tunnel token — copy it again from your Cloudflare dashboard." (NOT "Failed to register tunnel: API error 1004".)
    • Idempotent: re-running with same params returns already_configured instead of re-registering.
    • Auto-recover: 3× retry with backoff; mDNS fallback to roamcore.local deferred to Wave 9 #119b — Phase 2 canonical vehicle model mapping layer #122.d (TODO marker in manifest side_effects).
  • Tier discipline: tier-b (recipe over upstream); tier_a_promotion_candidate: tailscale stays 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.

OpenClaw Subagent 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant