fix: Cloudron enrollment + 2FA for fresh invite accounts

Invite acceptance (verified live on 8 agents): Pankow forms need
click + keyboard.type (fill() silently no-ops), submit button is
<div role="button"> "Set up" which starts disabled until the form
is valid.

2FA enablement: fresh accounts land on setupaccount.html ("Your
account is ready") and hash navigation cannot leave that page --
load the panel root first. Also match "Set up" (forced-enrollment
screen says "Set up passkey", profile page says "Setup").

Verified in the 8-agent run: coo/svp-knel/svp-tctc/vp-investing/
vp-trading/vp-compliance enrolled with TOTP. vp-secops and
vp-techcompliance ran pre-fix and need the --enable-2fa second pass.
This commit is contained in:
2026-08-14 10:26:05 -05:00
parent 5e1d043890
commit 87298df1ba
3 changed files with 81 additions and 15 deletions
+30 -15
View File
@@ -119,28 +119,33 @@ def enroll_cloudron(
log.info(f"[{name}] Generated password ({len(password)} chars)")
# Navigate to invite link
# Proven selectors (session 3 DOM dump of setupaccount.html):
# #inputUsername (prefilled from invite), #inputDisplayName,
# #inputPassword, #inputPasswordRepeat,
# submit = <div role="button"> "Set up" (disabled until form valid)
page.goto(invite_url, wait_until="networkidle")
page.wait_for_timeout(2000)
# Fill in the invite acceptance form using real Cloudron selectors
page.wait_for_selector('#inputPassword', timeout=15000)
page.wait_for_selector("#inputPassword", timeout=15000)
page.fill('#inputPassword', password)
page.fill('#inputPasswordRepeat', password)
# Pankow/Vue forms need click + keyboard.type, never fill()
page.click("#inputDisplayName")
page.keyboard.type(display_name)
page.click("#inputPassword")
page.keyboard.type(password)
page.click("#inputPasswordRepeat")
page.keyboard.type(password)
# Set display name if field exists
display_field = page.query_selector('#inputDisplayName')
if display_field:
display_field.fill(display_name)
# Submit — Cloudron uses a button with class btn-primary or type submit
submit = page.query_selector('button[type="submit"], button.btn-primary, button:has-text("Setup"), button:has-text("Create"), button:has-text("Accept")')
if submit:
submit.click()
# Submit button is a div[role=button]; starts disabled, enables on valid input
setup_btn = page.locator('[role="button"]:has-text("Set up")')
page.wait_for_timeout(1000)
if setup_btn.count() > 0 and setup_btn.first.is_visible():
setup_btn.first.click()
else:
page.keyboard.press('Enter')
log.warning(f"[{name}] Set up button not found/clickable, pressing Enter")
page.keyboard.press("Enter")
page.wait_for_load_state("networkidle")
page.wait_for_timeout(5000)
log.info(f"[{name}] Invite accepted")
# Enable 2FA
@@ -177,6 +182,12 @@ def enable_cloudron_2fa(page: Page, agent: dict, bw: BitwardenHelper) -> str:
name = agent["name"]
log.info(f"[{name}] Enabling 2FA on Cloudron account")
# Fresh accounts sit on setupaccount.html ("Your account is ready") --
# hash navigation cannot leave that page. Load the panel root first.
if "setupaccount" in page.url:
page.goto(f"{CLOUDRON_BASE}/", wait_until="networkidle", timeout=20000)
page.wait_for_timeout(3000)
page.evaluate('() => window.location.hash = "#/profile"')
page.wait_for_timeout(3000)
@@ -198,11 +209,15 @@ def enable_cloudron_2fa(page: Page, agent: dict, bw: BitwardenHelper) -> str:
return ""
# Step 1: Click "Setup" to start 2FA enrollment
# (forced-enrollment screen says "Set up passkey"; profile says "Setup")
setup_clicked = False
for selector in [
'text=Setup',
'text=Set up',
'[role="button"]:has-text("Setup")',
'[role="button"]:has-text("Set up")',
'button:has-text("Setup")',
'button:has-text("Set up")',
'a:has-text("Setup")',
]:
loc = page.locator(selector)