All 10 agents enrolled in Cloudron with TOTP 2FA (vp-secops and vp-techcompliance via the --enable-2fa second pass after the setup page fix). Gitea tokens generated and verified for both Q3 VPs. Remaining blockers are Charles-side: Redmine app access grants and a Discourse signup/approval investigation.
6.0 KiB
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
- BW sync lifecycle:
BitwardenHelper.login()must end withbw sync. Root cause of session-1 "vanishing items": the container's local cache was never synced after login.list_items()also syncs before reading. - 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
:latesttagging with overwrite is acceptable. - Source mounted read-only into the container (bw_helper.py, provision-agent.py, test files) so selector iterations do not require image rebuilds.
- 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 viapage.keyboard.type()(neverfill()), submit via[role="button"]:has-text("Log in"). - 2FA prompt on OIDC login:
#inputTotpToken+#totpTokenSubmitButton(NOT#inputTotp). - 2FA enrollment:
#/profile-> clicktext=Setup-> clicktext=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#namevia JS evaluate, scopes are radio buttons, extract 40-hex from.ui.info.message.
Discourse:
- Login modal via
.login-button, thenbutton:has-text("OpenID"). - First SSO lands on
/signupwith 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 useApi-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(generica: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
- 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/.
- 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.
- 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
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.