# 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=` -> 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 | --- ## 2026-08-14 — Session 3: 8 agents enrolled, repo transfer, KNEL org layout **Commits:** a96990d (org-buildout), 657939d/e3e5451 (KNELSecretsManager), c564531 (TSYSGroupAIOS), 87298df (aip) ### Decisions 1. **Repo ownership split**: org-buildout is docs-only (BW CLI source moved out); BW CLI lives in KNEL/KNELSecretsManager with ADR-002; agent-identity-provisioning transferred TSYSGroupCorporate -> KNEL, owned by VP TechOps. All 31 KNEL repos cloned to ~/projects/KNEL/. 2. **Gitea links commits to accounts by EMAIL.** Registries must use Cloudron-issued addresses (tsgstaff-coo-*@turnsys.com), not synthetic ones. Fixed in clone-as.sh/agent-profile.sh. 3. **tea credential helper** installed globally (vptechops token) -- clones over HTTPS work with clean URLs. NOTE: all pushes from this host to git.knownelement.com now authenticate as vptechops; watch attribution when pushing from TSGCOO-owned repos. ### Patterns proven this session **Invite acceptance (setupaccount.html):** fields #inputDisplayName, #inputPassword, #inputPasswordRepeat (username prefilled). Pankow forms require click + keyboard.type. Submit is
"Set up", disabled until form valid. **2FA on fresh accounts:** after acceptance the SPA sits on setupaccount.html; hash navigation cannot leave it -- goto panel root first. Forced-2FA screen says "Set up passkey" (profile says "Setup") -- match both. TOTP flow identical to session 2. ### Results (10 identities) | Agent | Cloudron | 2FA | Gitea | Discourse | Redmine | |---|---|---|---|---|---| | vp-techops | yes | yes | token | key | key | | vp-secops | yes | 2nd pass | BLOCKED (no app access) | acct created, retry | BLOCKED | | vp-techcompliance | yes | 2nd pass | BLOCKED (no app access) | acct created, retry | BLOCKED | | coo/svp-knel/svp-tctc | yes | yes | - (Q4 phase1) | - | - | | vp-investing/vp-trading | yes | yes | - | - | - | | vp-compliance | yes | yes | - | - | - | | vp-facilities | cleanup pass | cleanup pass | - | - | - | ### Open items for Charles - Grant Gitea/Redmine (and Discourse approval if required) app access for vp-secops and vp-techcompliance -- Gitea OIDC returns "The authorization request was denied." until granted (same class as the session-2 Redmine blocker). - vp-techops stale Gitea tokens cleanup still pending.