feat: consolidate proven Discourse/Redmine flows into provision-agent.py

Replaced the stale session-1 provision_discourse/provision_redmine with
the flows proven on vp-techops this session. Both are now parameterized
by the agent manifest (username derived from name, hyphens stripped,
overridable via username: field).

Discourse: login modal -> OpenID button -> signup on first login ->
RSA User API key flow (PKCS1v15 decrypt, JSON payload).

Redmine: KNEL Cloud SSO button -> consent -> Show/Reset on the API
access key section via targeted DOM traversal.

Added docs/JOURNAL.md with all working selectors, flows, gotchas, and
verification results so future sessions do not rediscover them.
This commit is contained in:
TSYS Group COO
2026-08-14 08:23:37 -05:00
parent f414b0b7ff
commit 6d89f16610
3 changed files with 318 additions and 129 deletions
+84
View File
@@ -0,0 +1,84 @@
# JOURNAL.md — Agent Identity Provisioning
> Append-only decision & pattern log. One section per change. Never delete or reorder.
---
## 2026-08-13 — Session 2: BW sync fix, 2FA, all four systems proven
**Commits:** f633a10, be2f607, 2d01a9f, 2258e1b, fcd484f, f414b0b
### Decisions
1. **BW sync lifecycle**: `BitwardenHelper.login()` must end with `bw sync`.
Root cause of session-1 "vanishing items": the container's local cache
was never synced after login. `list_items()` also syncs before reading.
2. **Container UID/GID**: provision user is 1002:1002, matching the host
TSGCOO account, so bind-mount state files are owned by the invoking user.
During rapid iteration `:latest` tagging with overwrite is acceptable.
3. **Source mounted read-only** into the container (bw_helper.py,
provision-agent.py, test files) so selector iterations do not require
image rebuilds.
4. **One-off scripts kept**: the exploration scripts (dump-cloudron-dom.py,
enable-cloudron-2fa.py, etc.) remain in the repo as proven references;
their flows have been consolidated into provision-agent.py.
### Patterns (selectors and flows that WORK)
**Cloudron panel (Pankow/Vue):**
- Login: `#inputUsername` / `#inputPassword`, type via `page.keyboard.type()`
(never `fill()`), submit via `[role="button"]:has-text("Log in")`.
- 2FA prompt on OIDC login: `#inputTotpToken` + `#totpTokenSubmitButton`
(NOT `#inputTotp`).
- 2FA enrollment: `#/profile` -> click `text=Setup` -> click
`text=switchToTotp` (Cloudron defaults to Passkey) -> secret is base32
text on the page (regex `[A-Z2-7]{16,}`) -> enter code in
`#totpTokenInput` -> click Enable.
**Gitea (proven session 1):**
- SSO button: `a[href*="oauth2/cloudron"]` at `/user/login`.
- Token page `/user/settings/applications`: fill `#name` via JS evaluate,
scopes are radio buttons, extract 40-hex from `.ui.info.message`.
**Discourse:**
- Login modal via `.login-button`, then `button:has-text("OpenID")`.
- First SSO lands on `/signup` with email pre-authenticated: fill
`#new-account-username`, click Sign Up.
- User API key: RSA keypair -> `/user-api-key/new?...&public_key=<PEM>` ->
click Authorize -> capture POST response -> decrypt with **PKCS1v15**
(not OAEP) -> payload JSON `{"key": "..."}`.
- API auth header is `User-Api-Key` (admin keys use `Api-Key`).
**Redmine:**
- SSO button: `#login-oauth-submit-1` ("Continue with KNEL Cloud").
- Prereq: Cloudron admin must grant the user access to the Redmine app,
otherwise OIDC shows "You do not have access" and redirects back.
- API key: `/my/account` -> click Show in `.api-key-actions` -> read
`#api-access-key` (40-hex). If absent, click the Reset link found by
DOM traversal from `#api-access-key` (generic `a:has-text("Reset")`
clicks the wrong section and logs you out).
### Username derivation
Manifest `name` is hyphenated (vp-techops); app usernames are not
(vptechops). Default: `agent.get("username", name.replace("-", ""))`.
Override with an explicit `username:` field in agents.yaml.
### Gotchas
- Em dashes (U+2014) break Python source; use `--`.
- Python f-string interpolation inside JS template literals does not work;
build JS strings with plain concatenation inside evaluate().
- Discourse admin API keys page is admin-only; User API keys are the
self-service path.
- Gitea token page needs `wait_until="domcontentloaded"` (networkidle
times out).
### Verification results (vp-techops)
| System | Credential | Verified via |
|---|---|---|
| Cloudron | password + TOTP | full login round-trip |
| Gitea | 40-char token | `GET /api/v1/user` -> vptechops |
| Discourse | 32-char user key | `GET /latest.json` with User-Api-Key |
| Redmine | 40-char API key | `GET /users/current.json` -> id 11 |