Commit Graph
95 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 8dce5b3caa fix(web-svc-collabora): name the WOPI host and follow the vhost's TLS state
The role restricted the WOPI hosts with `domain` and with
`--o:net.wopi_allowed_hosts`. The distroless image that #775 brought in
applies neither. `domain` was translated by the start script of the older
image, which this one does not have, and coolwsd knows no setting called
`net.wopi_allowed_hosts`. Its startup log listed that key among the
overrides and no trusted WOPI host, so the server ran with the policy the
image ships instead of the one the role declares.

The allowlist now goes through `aliasgroup1`, the variable coolwsd reads
itself. It carries the origin of the Nextcloud domain as the deployment
resolves it, with scheme and port, so it also holds in a Tor variant. The
old expression was built from DOMAIN_PRIMARY and would not have matched an
onion address.

No spec opened a document, which is why this went unnoticed. The
richdocuments spec now uploads a document, opens it and waits for the
`status:` frame on the editor websocket.

Its first run on the Tor variant found a second defect. `ssl.termination`
was fixed to true, so coolwsd advertised https URLs in its discovery while
an onion vhost only serves http, and the browser never reached the editor.
The setting now follows the TLS state of the Collabora vhost.

Closes #802

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:18:04 +02:00
Alejandro Roman 4b9a834546 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-21 03:25:20 +04:00
kevinveenbirkenbachandClaude Opus 5 27a7e8cceb test(nextcloud): retry the standalone login once inside the helper
Three Playwright workers run three spec files at once, and on a fresh instance the first OIDC callback is what provisions the Nextcloud account. Concurrent callbacks then race on INSERT INTO oc_users for the same uid: one wins, the losers answer HTTP 500. The exception is SQLSTATE[23000] 1062 Duplicate entry 'keycloak-<sub>' for key 'PRIMARY', raised from the sociallogin app's ProviderService through OC\User\Manager::createUser, and it is visible both in the CI rescue bundle of run 34653754175 and in the container log of a local variant-1 deploy.

The race is self-closing: once any insert wins, the window is shut for the rest of the run. It has never turned a job red, because Playwright's retries absorb it and a flaky-only run exits 0. What it costs is a rerun of the whole spec file, 1.8 minutes for fileslibreofficeedit against a 42 second first attempt, plus a flaky line in every report.

Retrying inside the helper keeps that cost to one login. The base entry point now makes a second attempt after a five second pause, which resolveTimeout stretches to twenty five over Tor, and the observed failures abort after thirty to forty two seconds against a nine hundred second budget, so the second attempt has room. A persistent failure still fails, and the first attempt's message is appended to the second rather than swallowed, which the old wrapper's bare catch did not do.

loginToStandaloneNextcloudWithRetry is gone: a helper of that name beside a base that retries is a trap, and its five callers now reach the same two attempts through the base. The other forty eight addon specs gain the retry without being touched.

Rejected alternatives, each against a written constraint: a serial globalSetup would have to live in the playwright.config.js that 87 roles share and would force ignoreHTTPSErrors into the central use block; PLAYWRIGHT_WORKERS=1 reverses the measured Tor tuning in playwright_workers.py and leaks through an unnamed include_vars into every later role; pre-creating the account collides with prevent_create_email_exists and turns a one-off flake into a permanent login refusal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 11:08:15 +02:00
kevinveenbirkenbachandClaude Opus 5 c215aca058 fix(nextcloud): route sociallogin through the tor proxy, fail fast on a refused login callback, stop leaking plugin config
Run 34421944328 Nextcloud#1 timed out "waiting for a signed-in Nextcloud
shell after the login redirect". 4164f37981 turned sso on in variant 1
while ldap stays off there, so that round logs in through sociallogin
instead of oidc_login - the first variant, on this branch or on main,
that runs sociallogin against an onion issuer. oidc_login goes through
Nextcloud's HTTP client, which honours the proxy in proxy.config.php;
sociallogin builds its own client and reads only
social_login_http_client['proxy'], so its token exchange never reached
the onion Keycloak. The template now sets the same tor SOCKS proxy for
it, inside the existing "sso on and the OIDC host is an onion" branch.

The spec only treated 5xx documents as a page failure, so a refused
callback surfaced as a 60s (x5 over tor) wait for a shell that could not
come. A 4xx on /apps/sociallogin/ or /apps/oidc_login/ now counts as
the page itself failing, reported without the query so no code leaks.

The plugin loop printed every item, including the OIDC client secret,
into the include line of a public CI log; it now labels by plugin key and
honours MASK_CREDENTIALS_IN_LOGS.

_shared.js grew past 250 lines; its page and modal helpers move to
_page.js and _shared.js re-exports them, with an identical export
surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 13:21:39 +02:00
Alejandro Roman 42f267407a fix(stalwart): fix fediwall, mailu and nexcloud issues in CI, all green now 2026-09-04 23:02:01 +04:00
kevinveenbirkenbachandClaude Opus 5 70feb319be feat(nextcloud): front the Context Agent with the MCP adapter
The hub serves 94 tools across 23 categories, write tools included, and enabling the ExApp switches every category on. That surface was published on the public vhost and every discovered client was handed an app password for it. svc-ai-mcp-adapter now sits in front in mcp_passthrough mode: clients present credentials.mcp_bearer to contextagentmcp and reach the nine read tools of calendar, contacts and files, which files/mcp/tools.json names and the sidecar enforces per call. Those three categories are the ones whose upstream is_available is unconditional, so the served set does not vary with which optional apps a deployment installs.

Enforcing it in the sidecar rather than in each client is what makes it hold: web-app-openwebui renders no per-tool filter at all, so a client-side include list would have bounded two consumers out of three.

06c13e7f3e withdrew the declared path from the edge for exactly this reason - "that surface is what the adapter exists to narrow". Nextcloud answers the same route under a second spelling, without the /index.php front controller, which try_files rewrites back onto the route after the edge has already declined to block it. The withdrawal becomes a regex covering both, and only where stripping the front controller changes the path, so the four adapter providers without one render unchanged.

The app password is minted after the stack renders, so it reaches the sidecar through upstream.env and rebuild.yml rather than the compose environment: compose up resolves env_file while creating the container, a restart does not. The sidecar is the last service in the template because a Jinja set has no block scope and its env_file would otherwise displace the one talk, whiteboard and talk_recording each carry.

Two suites cover what nothing measured: the vhost template rendered without a container, asserting both spellings are withdrawn, and every shipped tools.json loaded through the adapter own loader, so a bad contract fails in a second instead of as a probe timing out an hour into a deploy. The guest spec probed mcp.endpoint.path, which this change makes the proxy answer 404 for its own reasons; it now probes all three public paths and requires no MCP body.

Not yet deployed: the sidecar build, the upstream.env delivery and the hub answering exactly nine tools are unverified against a running stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 20:55:05 +02:00
kevinveenbirkenbachandClaude Opus 5 5d74e4f580 fix(nextcloud): assert the onlyoffice frames the connector actually mounts
The spec looked for the editor iframe at the top of the page and required its
src host to be the document server. Nextcloud does not put it there. Opening a
document mounts the connector's own frame, #onlyofficeFrame, which loads
/apps/onlyoffice/<fileid> with inframe=true from Nextcloud, and the document
server's editor is embedded a level deeper. The selector union matched nothing
at the top level, so the assertion described a page that never rendered and
the test failed while the round trip underneath it worked.

Assert what the working flow produces, at both levels: the connector frame
mounts, a frame at the Nextcloud host carries the inframe handover URL, and a
frame at the partner host serves the document editor. The JWT and download
error checks now read through the same nesting rather than a flat locator.

The coupling is still what decides the verdict. A document server that is
never reached produces no frame on the partner host, so the second poll times
out; only the frame that proves the handover can satisfy it.

This has been failing since the assertion was written in June, in both
deployment modes, which is what a wrong selector looks like and what a flaky
partner does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:52:11 +02:00
kevinveenbirkenbachandClaude Opus 5 de1efdf0d5 fix(nextcloud): let the Mastodon connect click survive the first-run wizard
The addon spec reached the per-user connected-accounts page, resolved the
"Connect to Mastodon" button and found it visible, enabled and stable, then
spent its whole 30s click timeout being refused:

    <div id="firstrunwizard" class="first-run-wizard modal-mask modal-mask--opaque">
    subtree intercepts pointer events

The single best-effort dismissal before the click is bounded and returns as
soon as two checks find no modal, so a wizard that appears late outlives it.
That the wizard, not the bridge, is the problem shows in the same job: the
spec passed eleven minutes later against the same stack, and passes in both
swarm rows.

The retry loop for exactly this interception already exists, written for the
user-menu click and named after it. Widen the name, take any locator, and use
it here. Nothing outside the shared module referenced the old name, so it is
renamed rather than aliased.

This does not turn the job green: it also fails the onlyoffice addon, which
is a separate cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:59:06 +02:00
kevinveenbirkenbachandClaude Opus 5 f2f261b9fd fix(nextcloud): match the WOPI field, not the initial-state carrier
The richdocuments spec matched with a three-branch union whose last branch,
input[id*='wopi' i], is a substring search across every element id. Nextcloud
renders its initial state as hidden inputs, and one of them is
`initial-state-richdocuments-wopi_callback_url` -- it contains "wopi", so .first()
took it before the real field was ever considered.

The artifact says it outright:

    119 × locator resolved to <input value="IiI=" type="hidden"
                              id="initial-state-richdocuments-wopi_callback_url"/>
    Expected: visible   Received: hidden   Timeout: 60000ms

Nothing was wrong with the deployment. The same error-context a11y snapshot shows
"Collabora Online server is reachable." and the real textbox carrying
https://collabora.infinito.example.

The narrowed selector accepts both real shapes and neither matches the hidden id,
which is neither equal to wopi_url nor carries that name attribute. Dropping the
substring branch also immunises the spec against every future initial-state field.

.filter({ visible: true }) would work too and is the house idiom
(web-app-mailu/files/playwright/playwright.spec.js:136), but a selector that cannot
match the wrong element is deterministic rather than visibility-dependent.

Scope is one variant: only variant 0 enables collabora, 1-13 set
collabora.enabled: false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 09:22:21 +02:00
Alejandro Roman 333079cc2a feat(UI): fix migration, errors happening in local deploy and swarm integraton + merged main 2026-08-30 21:39:52 +04:00
kevinveenbirkenbachandClaude Opus 5 7d8e7e87ff test(mcp): prove a switched-off surface is actually gone
Every role-local MCP spec gates itself on the service flag and skips when the surface is off, so nothing ever checked that switching it off removes it. A surface still serving after the flag went false was indistinguishable from one that was never deployed: both read as a green skip. The gating helper had no inverse at all, so there was no way to assert what a disabled service must not do.

A shared assertion now probes the endpoint with both a GET and a POST while the flag is false and fails on any answer that shows something listening. It counts 401 and 403 as listening, which is the point: a refused credential still means the surface is there. The check is transport-agnostic because the roles differ in their enabled-state probes, over SSE, Streamable HTTP or plain JSON-RPC, while an endpoint that is gone answers none of them. Each role passes its endpoint URL as a thunk, since some resolve it only once a test is running.

The coverage lint requires the registration, and derives the role set from meta/mcp.yml rather than from a filename convention, which is how the tenth role surfaced: its spec is not named like the other nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:40:15 +02:00
kevinveenbirkenbachandClaude Opus 5 7162b7bf4c Merge fork/feature/svc-net-tor into feature/ai-agent-employees
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.

The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.

Resolutions that were not unions:

Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.

plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.

compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.

tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.

Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.

01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.

Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.

roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.

config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.

READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.

Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:32:30 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00
Alejandro Roman 7332bd66ae Merge upstream/main into feature/role-stalwart-mail-swarm 2026-08-18 01:42:23 +04:00
kevinveenbirkenbachandClaude Opus 5 42449ebf0a fix(e2e): let the checks that needed a wait actually wait
Sixteen role specs decided something immediately after an action that had not settled, because locator.isVisible() discards the timeout it is handed. Three shapes: an assertion that could never fire, a login outcome read off the DOM before the app re-rendered, and a branch that selected a locator by whether an iframe had mounted yet.

The ones that could never fire are the worst of them. taiga test-seaweedfs polled for a rejection toast right after saveAction.click(), so a real upload rejection was swallowed and misreported as a SeaweedFS storage failure; friendica ldapauth decided whether to run its wrong-password test at the instant a fresh context finished goto, so a slow render skipped a security check silently. wordpress test-discourse-roundtrip picked titleBox by whether the Gutenberg iframe had mounted and, when it had not, bound the wrong one and then burned the full budget failing with a misleading message.

Every conversion keeps the control flow: the .catch(() => false) fallback stays, so a waitFor that throws still yields the boolean the caller expects. Twenty of the twenty-two waits use base 2000, one 3000, one 5000. resolveTimeout multiplies by five on an onion target, so the common case is ten seconds there. Across thirteen onion traces the largest wait of any kind that ever succeeded was 790 ms, so ten seconds is a twelvefold margin; the bases started higher and were pulled down because a swallowed deadline is a duration in disguise - where the optional element is normally absent, the full timeout is paid on every run, including green ones.

Not exercised: no spec was run. Three of these make a test stricter, so a job that turns red on them is surfacing a real failure that was previously hidden, not regressing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:01:43 +02:00
kevinveenbirkenbachandClaude Opus 5 642055a20a style(e2e): drop the timeout argument isVisible discards
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored: both return immediately and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.

Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.

A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:49:18 +02:00
kevinveenbirkenbachandClaude Opus 5 8ad639d7f8 test(nextcloud): record the Mastodon authorize handoff instead of racing it
The spec polled the live URL until it read /oauth/authorize?, a state that cannot last: Mastodon answers that endpoint with a 302 to /auth/sign_in for any session-less browser, in 5.39 ms per its own log, while expect.poll samples roughly every 100 ms. The test was chasing something it is structurally unable to observe, and reported the bridge as broken when it was working.

The listener is registered on the context rather than the page so the popup is covered too, and after every preceding navigation so no earlier request is captured. currentUrl now prefers the recorded request and otherwise behaves exactly as before. All four downstream assertions - host is not Nextcloud, host is the partner, client_id present, response_type=code - are untouched and now read a complete URL instead of one that has already gone.

Not a masked SSO defect: the partner Mastodon has OIDC_ENABLED, OMNIAUTH_ONLY and ONE_CLICK_SSO_LOGIN all true, and its log shows discovery, token, certs and userinfo each returning 200 over the onion with a successful biber login.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:58:53 +02:00
kevinveenbirkenbachandClaude Opus 5 c14b1d13d6 fix(nextcloud): compare the Mastodon partner host against the deployed one
integration_mastodon asserted the provisioned oauth_instance_url resolved to
the literal "microblog.infinito.example". That value is not fixed: the addon
hook writes lookup('tls','web-app-mastodon','url.base') into it
(meta/addons/integration_mastodon.yml:12), so on an onion node the plugin
correctly received http://microblog.<onion>/ and the spec failed for being
right. The product was never wrong; the assertion was pinned to a clearnet
name.

The partner URL now reaches the spec the way every sibling bridge spec already
gets it, from the same lookup the hook uses:

  playwright.env.j2   MASTODON_BASE_URL, gated on services.mastodon.enabled
  _shared.js          read and exported as mastodonBaseUrl
  the spec            compares host against host, not against a literal

web-app-fediwall already wires MASTODON_BASE_URL from this exact lookup
(templates/playwright.env.j2:12), so the name and the source are the
established ones; this adds the gate, so the variable renders empty instead of
naming an undeployed partner.

The guard in front of the comparison is not decoration: without it an empty
MASTODON_BASE_URL would reach new URL("") and abort the spec with a TypeError
instead of an assertion anyone can read.

The evidence that this is the whole difference: integration_peertube runs the
identical shape against shared.env.peertubeBaseUrl and PASSED in the same red
run, on the same onion node where integration_mastodon failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 02:02:28 +02:00
kevinveenbirkenbachandClaude Opus 5 a11a30ddaa fix(web-app-nextcloud): read the authorize request, not the settled URL
The connect control replaces the location with the partner's authorize
endpoint, and GitLab bounces the unauthenticated browser on to its sign-in
page, so the spec asserted against the wrong URL and the redirect_to escape
hatch existed to paper over it. Capturing the request is what the Zammad
spec beside it already does. The README claimed the app password still
belongs to the administrator, which stopped being true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:49:40 +02:00
kevinveenbirkenbachandClaude Opus 5 8c299f8e34 Merge PR #495 (nextcloud image bump + NC34 fixes) into feature/svc-net-tor
Covered by PR #380 rather than merged on its own. This branch already carries
the Tor onion-service provider across the whole role set, and #495 touches the
same nextcloud Playwright specs; landing them separately would mean resolving
the same conflicts twice, once here and once on main. Reviewing #495 as part of
#380 keeps the resolution in one place.

One conflict, in the external-sites addon spec. Nextcloud 34 renamed the admin
section from #external to #external-vue and replaced the add-site control with
a 'New site' button, so the incoming selectors win — retargeting them is what
the PR is for. This branch's resolveTimeout wrappers stay on both assertions.

make quality green on the merge result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:47:00 +02:00
kevinveenbirkenbachandClaude Opus 5 e96c752549 Merge fix/playwright/env-template-gate into feature/svc-net-tor
75 commits: the socialhome role reimplemented against the project it actually
deploys, native-login specs for funkwhale and shopware, a window.open IdP
handoff and a settings-link fallback in the shared persona helpers, a swarm
converge budget that gives up on a terminal task, and a long tail of role
fixes.

Eleven conflicts. The shared persona helpers (keycloak.js, logout.js) take the
incoming structure — they gained real behaviour this branch never had — with
this branch's resolveTimeout convention re-applied over it and
confirmKeycloakLogoutIfPrompted re-attached at every return point; without it
the Keycloak SSO session outlives the logout. playwright.config.js keeps this
branch's version: both sides set actionTimeout, but ours routes through an env
var and 15s is too short behind an onion. socialhome's spec takes the incoming
content — 2dbd6eb10 found the role described the wrong upstream project
entirely — and keeps gotoOnion and expectHstsWhenTls, since demanding HSTS over
a plaintext onion is a false failure. Both READMEs were regenerated rather than
merged by hand.

Three collisions arrived with no conflict marker at all, because each side
changed a different place in a different file:

- group_vars/all/18_swarm.yml declared swarm_stack_converge_retries twice.
  Same value, so nothing breaks today; editing the documented one would have
  silently changed nothing.
- svc-net-tor and web-app-socialhome both claimed 192.168.74.0/24. Socialhome,
  the newer role, moves to 192.168.75.0/24.
- web-app-socialhome/meta/schema.yml arrived as a new file, so ef5ea7cb5's
  rename never saw it. Its credential would not have resolved and the role
  would have deployed with an unusable administrator account.

The incoming specs also needed this branch's conventions: raw timeouts wrapped,
four page.goto calls routed through gotoOnion, a request.get given an
onion-aware timeout, and 21 roles marked playwright-service-flag over the tor
block this branch added to them.

make quality green on the merge result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:20:19 +02:00
kevinveenbirkenbachandClaude Opus 5 9430b25006 fix(nextcloud): let the server-side HTTP client trust the internal CA
integration_mastodon's connect returned 401 with
'cURL error 60: unable to get local issuer certificate ... for
https://microblog.infinito.example/api/v1/apps', byte-identical across
attempt and retry. The CA plumbing the repo already injects - the CA
mount, SSL_CERT_FILE, CURL_CA_BUNDLE, php.ini curl.cainfo - cannot reach
that call: OC\Http\Client\Client pins Guzzle's VERIFY to the
certificate manager's own bundle, which defaults to the shipped
resources/config/ca-bundle.crt.

occ security:certificates:import is the only lever that reaches it.
Verified in nextcloud:34: the command is registered as
'security:certificates:import' taking a required path, and
addCertificate() calls createCertificateBundle(), which writes the merged
rootcerts.crt that getAbsoluteBundlePath() then returns. Public TLS keeps
working because the import appends to the shipped bundle. Every
integration_* app talking to an internal host over HTTPS was broken the
same way, not just Mastodon.

Registers the GitLab OAuth application with the scopes the client
actually requests: integration_gitlab/src/utils.js:26 sends
'read_user read_api read_repository', and Doorkeeper's ScopeChecker does
plain string-set containment, so 'api' does not subsume 'read_api' and
consent would have been refused. Also least privilege.

The gitlab spec sampled the URL the page settled on after GitLab's 302,
but GitLab keeps the return path in session[:user_return_to], so that URL
carries no query and the assertion looked for evidence GitLab structurally
does not provide. It now captures the authorize request itself and
additionally asserts redirect_uri points back at this Nextcloud and that
the partner answered below 400.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:12:30 +02:00
kevinveenbirkenbachandClaude Opus 5 a14a347625 fix(nextcloud): attach the 500 detector before the login navigation
waitForVisibleCandidate can report a failed document response instead of
a bare selector timeout, but it attached the listener itself, and its
first call comes after loginToStandaloneNextcloud's goto has already
consumed both the redirect and the response. Playwright delivers only
events that arrive after attachment, so the detector was structurally
blind to the one navigation it exists for.

Attach it before the goto. trackServerErrors is idempotent through its
WeakMap, so the later call is a no-op and the existing evaluation in the
wait loop is untouched.

A 500 login page now fails in ~130ms naming the endpoint, rather than
after 60s of waiting for selectors that cannot appear on an error page --
which is what pointed the recent OIDC investigation at the proxy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:50:49 +02:00
kevinveenbirkenbachandClaude Opus 5 a0f0c9c967 fix(nextcloud): retarget selectors dropped by NC34
The external app's admin UI became a Vue app in v9.0.1 — the only
release declaring min/max-version 34, and the one CI installs. Its
template is now just <div id="external-vue"></div>, so the two hard
assertions on #external and #add_external_site (v8.0.1 markup) could
only time out. Match the mount point and the NcButton label instead;
both stay sharp, since an unmounted #external-vue has zero height and
the button exists only once the component ran.

Drop #app-content from the shared shell candidates as well: NC34 emits
that id in apps/settings/templates/help.php alone, a route no spec
visits. Dead alternatives inside an OR-list are how the login breakage
stayed invisible — #header-start__appmenu sat there long after Vue
started replacing its mount node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 16:53:47 +02:00
kevinveenbirkenbachandClaude Opus 5 6e0b219079 fix(nextcloud): detect the NC34 shell after login
Nextcloud 34 moved the app links into the waffle popover, so
`a[href*="/apps/files"]` is absent until the menu is opened, and
`#header-start__appmenu` is replaced by `nav.app-menu` on mount in
33 and 34 alike. Both post-login markers were therefore gone and
every spec died in `loginToStandaloneNextcloud`, although the login
itself succeeded (POST /login 303, GET /apps/dashboard/ 200).

Match on the header chrome and the dashboard root instead — present
in both versions, absent from the guest layout, so the post-logout
assertion in test-login-biber-ldap.js stays sharp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:46:26 +02:00
kevinveenbirkenbach 342de43111 Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
# Conflicts:
#	roles/svc-ai-ollama/tasks/01_manager_ops.yml
#	roles/web-app-decidim/templates/compose.yml.j2
#	roles/web-app-joomla/templates/compose.yml.j2
#	roles/web-app-matrix/files/flavor/ansible/runner/bootstrap.service
#	roles/web-app-matrix/templates/flavor/compose/services.yml.j2
#	roles/web-app-nextcloud/files/playwright/_shared.js
#	roles/web-svc-cdn/tasks/01_assets.yml
#	scripts/tests/deploy/swarm/utils/trigger_units.sh
#	scripts/tests/deploy/utils/filesystem/docker_dataroot.sh
2026-08-05 02:32:58 +02:00
kevinveenbirkenbach 0b192dac99 Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/ai-agent-employees 2026-08-05 01:48:14 +02:00
kevinveenbirkenbach 56e4792ca5 test(nextcloud): fail fast on document 5xx 2026-08-04 08:37:39 +02:00
kevinveenbirkenbachandClaude Opus 5 a2be4f081a feat(nextcloud): serve MCP through the Context Agent ExApp
Nextcloud carries no MCP server of its own, so the role runs the Context
Agent as an ExApp: render its container while MCP is enabled, wait for
its heartbeat, register it with AppAPI, add the MCP route, enable it, and
only then mint the app password clients present.

Split the credentials by origin. The shared secret between the AppAPI
proxy and the ExApp is generated by this deployment and stays a vault
credential; the app password is minted by Nextcloud through occ and is
persisted through sys-token-store instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:24:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 66a1259842 refactor(web-app-nextcloud): point integration_openai at the shared gateway
Nextcloud reached its OpenAI-compatible backend through Flowise, which
only forwarded to the LiteLLM sidecar that Flowise no longer runs. The
consumer flag, the activation condition and the endpoint now bind to
svc-ai-litellm directly, one hop shorter.

The endpoint switches from Flowise' public TLS URL to the internal
gateway address plus /v1, so a container-to-container call no longer
travels through the reverse proxy.

The api_key stops crossing a role boundary: it was read from
web-app-flowise credentials.llm_master_key, handing Nextcloud
administrative access to the whole gateway, and is now this role's own
per-consumer virtual key.

The litellm variant pin sits on variant 7 rather than 3 because adding
the gateway to variant 3 pushed that round past the matrix memory budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:16:52 +02:00
Alejandro Roman ac101d2efa Merge kevinveenbirkenbach/feature/swarm-nfs into stalwart integration branch 2026-07-21 10:00:17 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6fd8a499f3 fix(nextcloud): addon spec locators match the custom_apps webroot
drawio, fileslibreofficeedit and metadata are appstore/upstream apps, so Nextcloud serves their frontend bundles from /custom_apps/<app>/, not the read-only shipped /apps/<app>/. The specs asserted the bundle with a leading-slash locator script[src*="/apps/<app>/"], which cannot substring-match /custom_apps/<app>/ (the char before 'apps' is '_'), so the enabled-app bundle check deterministically returned count 0 across every variant.

Drop the leading slash (script[src*="apps/<app>/"]): it matches both /apps/ and /custom_apps/ webroots, so it is correct for shipped and appstore apps alike. These are the only 3 addon specs in the suite that assert a bundle asset; the served URLs (/custom_apps/drawio/js/main.js etc.) are captured in the CI trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:13:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5e194d885d test(web-app-nextcloud): onion-harden the Playwright suite
Scale the login helper's waitForVisibleCandidate call-sites and the Sign In / Log out clicks through resolveTimeout, and wait for network idle after logout so the slow keycloak end-session redirect chain over Tor settles before the logged-out state is probed.

Skip the Talk-admin and SeaweedFS-filer specs on onion targets (WebRTC HPB/STUN/TURN and a clearnet-only filer UI). Assert the XWiki partner host against XWIKI_BASE_URL from the env instead of a hard-coded clearnet host, so the check holds on both clearnet and onion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:50:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 453ddb9170 fix(web-app-bigbluebutton): do not load BigBlueButton on an onion node
BigBlueButton is WebRTC (SRTP/UDP with ICE/STUN) which Tor cannot carry. Guard the role's core include on the resolved Keycloak domain not being an onion (alongside the existing run_once guard), disable the co-installed cloud_bbb Nextcloud app on onion at runtime, and skip the BBB Playwright specs on onion targets.

Relax the run_once schema test (test_service_core_first_task_run_once) to accept a when-list that contains the run_once guard, so a role can add extra include conditions while staying protected against duplicate execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:50:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 35dee215c8 test(playwright): route all timeouts through resolveTimeout for onion-compat
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:38:06 +02:00
Alejandro Roman f9319863cf removed guras and make local/CI DinD use .test isntead of .example for the whole stack 2026-07-08 10:11:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc77d9d265 fix(security): resolve all open code-scanning alerts and guard against recurrence
Resolve the 80 open CodeQL/code-scanning alerts on this branch.

Security-sensitive fixes:
- cli/administration/deploy/development: break the unsafe import cycle by
  extracting the env/arg helpers into a leaf module `env.py`, giving the
  load-time DAG compose -> common -> deps -> env.
- web-app-bluesky login-broker server.js: prototype-pollution guard on
  cookie parsing, CR/LF + control-char log sanitisation, HTML-escaped
  exception text, and a ReDoS-bounded handle regex.
- web-app-baserow SSO: harden the open-redirect `next` handling.
- network diagnose probes: require TLS >= 1.2.
- actionlint installer: validate tar members against path traversal.
- decidim/apt-purge lint regexes: rewrite to avoid catastrophic backtracking.
- 6x Playwright URL regexes anchored; crs-k/stale-branches pinned to a commit SHA.

Mechanical cleanups: unused imports/globals/locals, empty-except comments,
import-and-import-from, implicit-string-concat, multiple-definition,
unreachable/useless-assignment.

False positives are intentionally left in place (Django settings patches,
Odoo __manifest__, side-effect imports, cross-module symbols).

Recurrence guards:
- import-linter contract (pyproject + tests/lint) locks the dev-deploy
  package acyclic; a new cycle now fails CI instead of only CodeQL.
- new lint test requires every bandit (S###) `noqa` to carry a justification.
- BLE001 (blind-except) added to ruff as a ratchet (tests/roles exempt,
  current production offenders grandfathered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:42:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f887bcc164 test(web-app-nextcloud): assert the Zammad client_id is plaintext, not an ICrypto ciphertext
The request-capture assertion proves the authorize request carries a client_id but not that the partner accepted it, so it would pass even with a garbage (encrypted) client_id - masking an NC_ENC_MODE/encryption regression. Add a cheap guard: the captured client_id must not contain a '|', which is the ICrypto ciphertext separator (cipher|iv|hmac|version) and never appears in a valid OAuth uid. No effect on valid couplings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:37:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f2fa81e01f test(web-app-nextcloud): capture the Zammad OAuth authorize request, not the post-login redirect
The integration_zammad connect control is a button that runs window.location.replace(<partner>/oauth/authorize?client_id=...&response_type=code). Zammad's SPA immediately bounces the unauthenticated test browser to its login at the root path, so the settled page URL is no longer the authorize endpoint and the assertion saw the root. Capture the /oauth/authorize request itself (page.waitForRequest) - it is the coupling signal that carries the provisioned client_id to the partner - instead of the URL the browser ends up on after Zammad's redirect.

Verified: the spec passes against the live variant-4 coupling (client_id and response_type present, partner host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:56:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c4206db8e3 fix(web-app-nextcloud): dismiss the firstrunwizard modal before the Files New-menu click
On a freshly provisioned instance the firstrunwizard what's-new modal mounts together with the Files app (after the post-goto dismissal already ran) and overlays the New button, so files_linkeditor's newMenuButton.click() was swallowed and the test hit its 120s timeout (proven by the failure screenshot: the modal sits on top of the visible New button). Re-dismiss once the app has rendered, right before driving the menu. The helper is a no-op when no modal is present, so the added call is idempotent: verified passing both on a fresh deploy with the modal and on a reused stack without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:56:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dba4b2a5d1 test(web-app-nextcloud): harden integration_discourse spec timeouts for deploy-load
When the spec runs during a full variant-3/4 deploy the host is under heavy provisioning load, so the Discourse Vue components render slowly and the cumulative test exceeded its 120s budget at the instance-address field. Raise the overall timeout to 240s and the URL-field and connect waits to 60s. Idle the spec passes in ~24s; this only adds margin for the loaded path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 00:34:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8023e4b8b2 fix(web-app-mattermost): repair nextcloud OAuth coupling probe and gate user-access-tokens on nextcloud presence
The nextcloud->mattermost OAuth provisioning failed because the partner-running probe used 'container exec <mm> true' (rc 127 — no true binary in the mattermost image), which gated provisioning off and left the OAuth app unprovisioned (spec failed on oauth_instance_url=null). Replace it with 'container inspect -f {{.State.Running}}' gated on stdout-trimmed == true, provision the OAuth app via a controller-run script using a short-lived admin PAT, and rewrite the spec as a behavioral connect-redirect check.

Mattermost personal access tokens are now enabled only when the consumer web-app-nextcloud is deployed (services.mattermost.user_access_token_enabled renders from 'web-app-nextcloud' in group_names) instead of being hardcoded on.

Verified: probe live in the DiD stack (inspect running -> rc0, missing -> graceful skip); the user-access-token chain renders True/False via a real Templar (nextcloud present -> True, absent -> False); service-flag integration tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:41:19 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c1b41418d feat(nextcloud): zammad OAuth provisioning + strengthen peertube addon spec
Provision a Doorkeeper OAuth application on the partner Zammad and write its client_id/client_secret into integration_zammad (Nextcloud-encrypted), so the per-user OAuth connect flow has the credentials the generic SPOT cannot supply. Register both the pretty-URL and /index.php oauth-redirect variants, and drop the hashed-secret fallback so a missing plaintext secret fails loudly instead of persisting an unusable hash.

Strengthen the integration_peertube spec: pin the admin 'instances' value to the exact deployed partner host (new PEERTUBE_BASE_URL) instead of merely 'not Nextcloud', and add a functional OCS search-providers check that fails when the PeerTube provider is not registered. PeerTube has no per-user OAuth (verified upstream: no client_id/oauth_instance_url config, no /oauth/authorize route), so the removed Connect->/oauth/authorize assertion tested a non-existent flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:50:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 87f2ffc745 fix(test): notification-2FA spec asserts registration, not the enable control
The personal-security enable control for twofactor_nextcloud_notification
only renders once the provider is activatable, which needs a SECOND active
session to receive the approval notification — impossible in a single-session
E2E (the provider state is correctly false there). The addon hook already
proves registration via occ twofactorauth:state; the browser-observable
equivalent is that the provider state element is surfaced, so assert
toBeAttached, not toBeVisible. Verified in-container: nextcloud v1 suite
31 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:36:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1a1a02a2d6 fix(web-app-nextcloud): drop dead popup reference in openproject spec
Leftover 'if (popup) await popup.close()' referenced a variable removed
together with the connect-button/popup OAuth flow -> eslint no-undef ->
lint-javascript red -> make test red. make test is green again. No change
to the spec's assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 01f0b8686a fix(web-app-nextcloud): assert openproject admin OAuth coupling, not a connect button
OpenProject's integration_openproject renders no 'Connect to OpenProject'
button on connected-accounts (unlike discourse/gitlab/mattermost); the
connection happens via the Files smart-picker / the OpenProject side. The
spec's connect-button + OAuth-authorize popup flow tested non-existent UI.

Assert the real provisioned bidirectional coupling instead, read from the
admin initial-state config: openproject_instance_url (partner host),
openproject_client_id, openproject_client_secret, authorization_method=oauth2.

NOTE: NOT yet verified green live -- the compose-playwright verify run was
OOM-killed (exit 137) before finishing. High confidence by logic only: in the
prior run this spec already passed the instance-URL check (same JSON.parse
path), failing only on the now-removed connect-button assertion, and all
asserted fields are confirmed present in the decoded admin config. Re-run a
scoped verify to confirm before relying on it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6fea999e93 fix(tests): make make test green (eslint + env order + autoformat)
- discourse-activity-pub, integration_matrix specs: drop unused catch
  binding and useless json init (eslint no-unused-vars/no-useless-assignment)
- openwebui playwright.env.j2: order OLLAMA_SERVICE_ENABLED after
  OIDC_ISSUER_URL (env keys must be ASC, test_alphabetical_sort)
- test_addon_env_flags.py: autoformat

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:16 +02:00