fix: critical bugs in agent identity provisioning [#442]

Email domain bug (would have caused all provisioning to fail):
- Cloudron email default was tsys-cloudron.knel.net (the dashboard host)
  instead of turnsys.com (the actual identity domain). Fixed in 3 places.
- Added explicit cloudron_email field to all agents in agents.yaml.example.

Other fixes:
- STATE_DIR.mkdir() moved from module level to main() so --dry-run and
  --help work outside the container.
- IndexError guard: password_inputs[0] crashes if zero fields found.
- State file save moved to finally block so partial results survive
  provisioning failures.
- Exception in provision_agent no longer re-raised (was preventing state
  file write and summary reporting).
- BW item_exists no longer swallows network/session errors as 'not found'
  (was causing duplicate credential creation).
- Redundant -u flag in bw generate (-uluns → -ulns).
- Dockerfile: npx install with || true → npm install -g (silent failure
  would cause runtime 'bw: command not found').
- Added .dockerignore to prevent .env/agents.yaml/state from entering image.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
TSYS Group COO
2026-08-13 11:21:38 -05:00
parent 8ce279276f
commit 9b4502f55d
5 changed files with 42 additions and 14 deletions
+18 -10
View File
@@ -50,7 +50,6 @@ DISCOURSE_URL = os.environ.get("DISCOURSE_URL", "https://community.turnsys.com")
REDMINE_URL = os.environ.get("REDMINE_URL", "https://projects.knownelement.com")
STATE_DIR = Path("/app/state")
STATE_DIR.mkdir(exist_ok=True)
# ---------------------------------------------------------------------------
# Cloudron enrollment
@@ -77,8 +76,9 @@ def enroll_cloudron(
item_name = f"{name} Cloudron"
if bw.item_exists(item_name):
log.info(f"[{name}] Cloudron credential already exists in Bitwarden — skipping")
cloudron_email = agent.get("cloudron_email", f"{name}@turnsys.com")
return {
"username": agent.get("cloudron_email", f"{name}@tsys-cloudron.knel.net"),
"username": cloudron_email,
"password": bw.get_item_password(item_name),
}
@@ -97,8 +97,10 @@ def enroll_cloudron(
if len(password_inputs) >= 2:
password_inputs[0].fill(password)
password_inputs[1].fill(password)
else:
elif len(password_inputs) == 1:
password_inputs[0].fill(password)
else:
raise RuntimeError(f"[{name}] No password field found on Cloudron invite page")
# Set display name if field exists
name_field = page.query_selector('input[name="displayName"], input[name="name"]')
@@ -117,7 +119,7 @@ def enroll_cloudron(
totp_secret = enable_cloudron_2fa(page, agent, bw)
# Store credential in Bitwarden
cloudron_email = agent.get("cloudron_email", f"{name}@tsys-cloudron.knel.net")
cloudron_email = agent.get("cloudron_email", f"{name}@turnsys.com")
bw.create_item(
name=item_name,
username=cloudron_email,
@@ -235,14 +237,14 @@ def sso_login(page: Page, system_url: str, agent: dict, bw: BitwardenHelper) ->
# Fill Cloudron SSO login form
cloudron_item = f"{name} Cloudron"
username = agent.get("cloudron_email", f"{name}@tsys-cloudron.knel.net")
cloudron_email = agent.get("cloudron_email", f"{name}@turnsys.com")
password = bw.get_item_password(cloudron_item)
user_input = page.query_selector('input[name="username"], input[type="email"], input[name="email"]')
pass_input = page.query_selector('input[type="password"]')
if user_input:
user_input.fill(username)
user_input.fill(cloudron_email)
if pass_input:
pass_input.fill(password)
@@ -569,14 +571,17 @@ def provision_agent(
except Exception as e:
log.error(f"[{name}] Provisioning failed: {e}")
raise
results["error"] = str(e)
finally:
page.close()
# Save state
# Save state (in finally so partial results survive failures)
state_file = STATE_DIR / f"{name}.json"
with open(state_file, "w") as f:
json.dump(results, f, indent=2)
try:
with open(state_file, "w") as f:
json.dump(results, f, indent=2)
except OSError:
log.warning(f"[{name}] Could not write state file: {state_file}")
return results
@@ -590,6 +595,9 @@ def main():
parser.add_argument("--headed", action="store_true", help="Show browser (debugging)")
args = parser.parse_args()
# Ensure state directory exists (deferred from module level so --dry-run works)
STATE_DIR.mkdir(parents=True, exist_ok=True)
# Load manifest
agents = load_manifest(args.manifest)
if args.agent: