4a56b4c19c set services.element.force_verification to false only for
the SSO variants 0 and 2, on the reasoning that only the SSO specs log
the same account in twice. The deploy's second pass logs the variant 1
administrator (ansible flavor, SSO off, password login) in again. PASS 1
bootstraps the account's cross-signing identity, so in PASS 2 Element
stops on "Confirm your digital identity" ("Use another device" / "Can't
confirm?" / "Remove this device", no skip), and the password-login spec
times out at _shared.js:436 (fork run 40979252521, job 123823820263,
Discourse#1 with web-app-matrix variant 1, all three retries).
Variant 1 now sets force_verification to false like the other two. The
services.yml default stays true, so production keeps forced
verification. No variant exercises true end to end any more; the
follow-up issue restores one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Element v1.12.33 (automated bump a8fba45d60, on main) ships element-web
#35364, which turns force_verification on by default. From the second
OIDC login of an account on, Element stops on "Confirm your digital
identity" with no skip button, so the Matrix SSO specs that log the same
account in twice (test-dm-admin-biber.js, test-design.js) never reach the
signed-in UI: "administrator: authenticated Element UI (user menu / room
list / welcome) must render" fails after 120 s (fork run 40625679270,
Matrix#2 and the co-deployed Moodle#2).
services.element.force_verification now carries the platform default,
true as upstream, and both flavors render it (the compose element config
and the MDAD element configuration extension). Only variants 0 and 2,
the SSO rounds, set it to false. Production keeps forced verification.
Cherry-picked from e042c9dbf9 on feature/store-i18n. main has no Element
branding block yet, so the MDAD flavor carries only the force_verification
key in its configuration extension.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An earlier commit on this branch turned the three off because the ansible flavor ran
them without a published port, proxy entry, domain or certificate. They
now stay on and are wired end to end.
Element Call: MDAD's LiveKit server and JWT service get their own
entities in meta/services.yml with resource limits, local ports and a
public media port (TCP and UDP). The runner container publishes them
through container_ports, and the Synapse vhost proxies /livekit-server/
(websocket) and /livekit-jwt-service/ to them. The JWT service checks
each OpenID token against the user's homeserver over federation, and
federation was not reachable in this flavor: Synapse served it only on
a plain-HTTP listener that the runner published directly on 8448.
Synapse now serves the client and federation resources on one listener,
OpenResty serves the Synapse vhost on 443 and as default_server on the
federation port, the direct 8448 publish is gone, and the role writes
/.well-known/matrix/server as the compose flavor does. With a
self-signed CA the JWT container mounts the CA bundle as Synapse does.
Jitsi: a new services.jitsi entity follows the shared-service contract.
It is enabled in the ansible flavor off tor nodes. When web-app-jitsi is
in the inventory, Element points at that shared Jitsi and MDAD's own
Jitsi stays off. Otherwise MDAD runs its own Jitsi on jitsi.<domain>
with a front-proxy vhost, its web port published to the runner and its
videobridge on a public UDP port. The entity lists the jitsi domain, so
the web health probe skips it whenever Matrix does not serve it. The
jitsi_*_container_network overrides were dropped: MDAD reads none of
them, Jitsi always ran on its own network. Variant 0 covers the shared
case, variant 1 the internal one. On an onion-primary deploy both call
stacks stay off, so Element never advertises a clearnet call surface.
MDAD's Prosody keeps an account's first password, so after a credential
rotation jicofo failed SASL with the new one. The role now derives the
jicofo and videobridge passwords from the macaroon secret and recreates
both accounts on every deploy.
Heisenbridge stays on by default. It runs in the nested host network,
where MDAD's container-name URLs do not resolve, so its homeserver URL
and the appservice URL in its registration point at 127.0.0.1.
A bootstrap that a previous deploy left failed (for example after a
Docker Hub rate limit) is now reset and restarted, so a mode=update
deploy converges instead of failing on the stale unit state.
Playwright proves each path: the RTC spec fetches a LiveKit token from
the JWT service and opens the SFU signalling websocket, the Jitsi spec
checks Element's preferred domain and joins a conference (signing in
through Keycloak first when the shared Jitsi sits behind SSO), and the
Heisenbridge spec opens a DM with the bot and waits for its answer to
"help".
Closes#1165
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fourteen roles repeated the same two lines to say they use the gateway: a
`shared` flag gated on group_names, and an `enabled` flag conjoining that gate
with LITELLM_CHAT_MODEL_SERVED. Both are now single-sourced.
`shared` becomes literal true. The flag means the provider is a shared instance
rather than one deployed per consumer, and a central gateway is always that;
the dynamic form only said "shared if deployed", which `enabled` already
decides. The 50 shared pins under `litellm:` leave meta/variants.yml with it.
The tree had already noticed: 20 of those 50 lines carried
`# nocheck: variants-static-override`, two of them with copy-pasted text about
container_backup. This narrows e5de4e085a for one service key rather than
reversing it; every other dynamic flag keeps the group_names form.
`enabled` becomes `{{ LITELLM_USABLE }}`, defined in group_vars/all/16_ai.yml.
That shorthand is only possible because is_explicit_truth learns about it.
The resolver reads a dependency out of the flag TEXT — "in group_names" in
value — so a constant hides the role id and the provider silently leaves the
static closure of every role referencing it. Measured: the old expression
resolves True, `{{ LITELLM_USABLE }}` resolves False. GROUP_GATED_CONSTANTS
names the constants whose own definition carries the gate, and the docstring
states the obligation: a SPOT of this kind that is not listed there drops its
provider without a word.
The wider fix, accepting any Jinja as dynamic, was measured and rejected: 35
other flags carry a Jinja value without the marker (MCP sidecars, javascript
and SSO gates, recaptcha), and they would all enter closures they are not in
today.
A literal shared flag is no longer dynamic, so test_dynamic_flags wants the
`# nocheck: dynamic-flag` its own message documents for legitimate exceptions;
where a marker already existed the rule list is extended rather than a second
comment added.
Behaviour is unchanged where it counts. The closure keeps litellm with the same
app counts (wordpress 26, openwebui 23, hermes 23, mediawiki 22, flowise 23),
unrelated Jinja still resolves False, and the truth table of the new enabled
form matches the old one in all four served/deployed combinations. Three roles
resolve without litellm, and `git show HEAD:` proves that predates this change:
zammad pins enabled false in variant 0 at line 39, nextcloud at line 97.
Six unit tests cover the resolver, including one asserting that every
registered constant is actually defined in group_vars, so an entry cannot
become a dead promise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Matrix was the one role that already shipped an AI credential and pointed it at
the vendor, and four separate defects kept it from reaching the gateway. Each
one hid the next, so fixing fewer than all four still leaves a surface that
reports green and answers nothing.
The schema rejected the gateway by construction.
`chatgpt_bridge_openai_api_key` validated `^sk-[a-zA-Z0-9]{40,}$` -- OpenAI's own
key format -- with algorithm `plain`. A LiteLLM virtual key cannot satisfy that
pattern, so no amount of configuration could have worked. It is `sk_prefixed`
with a length check now, and a `litellm_api_key` entry joins it in the same
shape the other eight consumers use.
The bridge reached Synapse over the public ingress. MATRIX_HOMESERVER_URL was
MATRIX_SYNAPSE_URL, an https URL on the canonical domain, so every login left
the container network, crossed openresty and came back. A 502 taken while
Synapse was still starting still consumes an rc_login.address slot, and once
that bucket empties the bridge dies on M_LIMIT_EXCEEDED and restarts into the
same wall: measured at 139 restarts on the local stack. MATRIX_SYNAPSE_LOCAL_URL
addresses the container directly over plain http on the internal port.
MATRIX_SYNAPSE_URL stays public where it has to be -- public_baseurl, Element,
and the mautrix bridges, which are a separate concern and untouched here.
Nothing ordered the bridge behind Synapse. Synapse itself depends on the mautrix
bridges, but matrix-chatgpt-bot is not one of them, so it was free to start
first and spend its login quota on a homeserver that was not listening. It now
waits for `service_healthy`.
The specs could not read their own configuration. templates/playwright.env.j2
renders MATRIX_PLUGINS_JSON through dotenv_quote, and both
test-litellm-chatgpt.js and test-bridge-roster.js called JSON.parse on the raw
value. The parse returned a string rather than an object, so `plugins.chatgpt`
was undefined: the litellm spec aborted after 0.99s claiming the plugin was off,
and the roster spec quietly downgraded itself to a skip. Both decode through
decodeDotenvQuotedValue first.
00_core.yml verifies the shared gateway contract before dispatching to either
flavor, so an empty virtual key or a base URL that resolves outside the
deployment fails ahead of any container work.
Verified: the decode fix by execution -- the spec previously died at 0.99s on
plugins.chatgpt and now runs through to the invite poll at line 202. The
contract assertions and the credential ran ok in a variant-2 deploy, which also
exercises the schema change end to end.
Not verified: the local URL and the health gate have not deployed yet, so
whether they break the crash loop is still open. Behind them sits a separate
defect this commit does not address -- matrix-synapse's process does not trust
the platform CA (openresty logs TLS alert 48 from its address) and
RegularPolicyForHTTPS caches one SSL context for the process lifetime, which
breaks every outbound HTTPS call Synapse makes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72fd9c870 required the onion in every variant but exempted variant 1, on the
argument that the minimal round is the one place the stack runs without it. That
argument does not hold: the onion is part of the deployment under test, not a
feature bolted onto the full shape. A round that skips it is not a smaller
deployment, it is a different one.
So the exemption is gone and 95 variant-1 entries pin the onion on. Two
exclusions remain, both read from data rather than a comment: a role with no tor
block has no onion to pin, and a role whose meta/services.yml pins tor.enabled to
a literal false has already declared it has none.
That put the rule in conflict with test_coverage, which requires both polarities
of every dynamic flag: with tor on everywhere, the false side has no variant left
to live in. 94 roles take the exemption that contract already provides for
exactly this - "flags whose group-derived value is a hard invariant that variants
must never override", the same one 4e6ef2542 used for the seaweedfs frontend.
web-app-nextcloud needs none: its variant 8 pairs the clearnet-only PeerTube and
keeps the false polarity.
Measured, because the declaration is not what CI runs. assign_tor gives the onion
to every second capable entry with the run number as the parity switch, so more
capable variants do not mean proportionally more onion jobs: across five roles
and 21 entries the count goes 8 -> 10 on an even run and 9 on an odd one. The
clearnet path also survives - checkmk reports v1=false on run 0 and v1=true on
run 1, so both states are still covered across two consecutive runs. What the
94 markers give up is a declarative guarantee in variants.yml, not the coverage
itself.
The real cost is weight, not job count. web-app-nextcloud variant 1 goes from
3.09GB reservation / 7.27GB limit to 18.26GB / 43.01GB once svc-net-tor and its
closure join, so the cheap round is no longer cheap. Every variant stays under
the 32/64GB budget, and the same effect applies to the other 94 unmeasured.
Verified: tests/lint 336 passed / 307 subtests, tests/integration/roles/meta/
variants plus the role-meta lints 39 passed. The rule was checked in both
directions - it reported the variant-1 offenders before the flips and none after.
Not measured: what the heavier rounds do to wall-clock, against swarm jobs capped
at 355 minutes and one that has hit that ceiling before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7a70d27b7 made a variant that switches SSO on also switch the onion on, because
that pairing is where the platform breaks in ways clearnet never shows. The same
argument reaches further than SSO: a Secure cookie is dropped, an https identity
provider refuses to embed, and crypto.subtle is undefined for any feature on a
non-secure origin, not only for a login. A variant that never runs the onion is
not testing the deployment users get.
So the rule now covers every variant of a role that declares a tor block.
Variant 1 stays exempt - it is the minimal, partner-free round every role
declares and the one place the stack is exercised without the onion at all - and
a role whose meta/services.yml pins tor.enabled to a literal false is skipped
outright, because that declaration already says the role has no onion and needs
no comment repeating it.
That found 137 variants. 101 were variant 1. Of the rest, 28 are the LDAP-only
auth-isolation rounds, which gain real coverage from running LDAP over the onion
and are flipped on here. Four cannot: svc-net-tor is the provider itself, and
nextcloud's variant 8 pairs the clearnet-only PeerTube. Both carry a marker
naming the reason; opentalk and coturn need none, since their services.yml pins
the literal false the rule now reads.
Flipping them exposed a contract that contradicted itself. test_auth_isolation
and test_non_baseline_explicit_disables demanded `shared: false` beside
`enabled: false`, while variants-static-override flagged that same line as a
static override - which is why all 133 of them carried a nocheck marker. The
demand only makes sense where `shared` is dynamic: a literal base value cannot
leak through deep-merge, so restating it protects nothing. Both contracts now
ask for it only in that case, and the 133 lines are gone.
The result is a stricter rule with fewer lines: 189 insertions against 193
deletions.
Verified: tests/lint 336 passed / 307 subtests, tests/integration/roles 107
passed / 610 subtests. The rule is checked in both directions - it reported 137
offenders before the resolution and none after.
Not measured: 28 additional onion rounds cost matrix wall-clock, against swarm
jobs already capped at 355 minutes and one that has hit that ceiling before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SSO over a plaintext onion is where this platform actually breaks, and none of
it shows on clearnet: a framework's Secure session-cookie default makes Chromium
drop the cookie, an https identity provider cannot be embedded in an http page,
and crypto.subtle is undefined because an onion is no secure context. Pixelfed,
Taiga, Joomla and WordPress each cost a red job over exactly that. The
combination is only exercised if a variant deploys both, so the lint requires
it: services.sso.enabled true implies services.tor.enabled true.
69 variants enable SSO; 59 already complied. Of the ten that did not, seven are
flipped on because the mechanism is proven there - matrix and nextcloud both run
SSO behind the onion in variant 0 already (20ddda8c1), and their later variants
were pinned false only by the blanket rule from 1d9d355bc. Nextcloud variant 1
stays false: SSO is off there, and it is what gives the coverage test its
false-pinning variant.
Three roles opt out, each for a structural reason named in the marker. jitsi and
bigbluebutton pin tor.enabled false in meta/services.yml because the conference
needs getUserMedia, which no plaintext onion can offer. seaweedfs gates its own
sso flag on 'svc-net-tor not in group_names', so on a tor node there is no login
flow to test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Variant 0 pins the backup consumer flag enabled so the matrix baseline exercises the in-group branch; every other variant pins it disabled (shared: false carries the static-override suppression since the base value stays literally true). The matrix and wordpress variant entries move their services mapping to the list-item head so the static-override line parser resolves the override paths; yaml semantics are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Indentation/whitespace normalization emitted by make autoformat across role meta/services.yml, meta/variants.yml and a task file. No semantic changes (verified: git diff --ignore-all-space is empty for every file). Kept separate from the Tor egress-router feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the tor.enabled true/false rounds (shared:false + nocheck on the disabled round) so the dynamic tor flag is covered on these roles, and reorder each variant services-first so the tor nocheck registers. Each variant keeps its exact keys (credentials / addons / server.domains preserved, values unchanged) — mapping key order only; no cross-variant moves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Legacy mautrix-python facebook/instagram bridges are EOL and fail at runtime (appservice bot masquerade 403). Replace both with the bridgev2 mautrix-meta addon: config, schema credential, variant-2 wiring, and a profile spec with a positive @metabot probe plus an un-bridged-MXID negative control.
playwright.env.j2 now emits MAUTRIX_<bridge>_ADDON_ENABLED only for bridges that are actually deployed, so each bridge spec runs iff its bridge is up (closes the skip-as-green gap) and is telegram-drop aware.
Verified: compose variant-2 deploy failed=0; @metabot registers on the live Synapse; bridge suite green (meta/signal/whatsapp/slack), telegram skips (no API creds).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six mautrix network bridges declared as mechanism: bridge, enabled: false, with DB creds referenced from meta/schema.yml. MATRIX_BRIDGES derived from addons; vars/bridges.yml, filter_plugins/bridge_filters.py and its unit test removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch the ansible flavor over to a forked
github.com/kevinveenbirkenbach/matrix-docker-ansible-deploy that
supports container_network='host'. Inside the matrix-mdad DiD
container, every homeserver-stack service (synapse, element, coturn)
runs with --network=host so it shares matrix-mdad's network
namespace, which is attached to the outer compose's `postgres` and
`openldap` networks. Synapse/bridges therefore resolve `database`,
`openldap` via outer Docker DNS — no inner postgres or LDAP needed.
meta/services.yml:
- upstream.repo -> the fork; ref kept at `master` (the host-network
patches live there). The pinned-commit story comes once the fork
push lands.
- postgres.enabled stays `true` (matrix always needs a DB); postgres.shared
becomes `'svc-db-postgres' in group_names` again — central when the
DB role is up, dedicated per-app when it isn't. Both paths reach
matrix-mdad's NS via the outer Docker daemon's DNS now that we run
Synapse on --network=host.
- coturn becomes a first-class shared-service toggle: enabled+shared
follow `'web-svc-coturn' in group_names`. When the central role is
present, MDAD's bundled coturn stays off and Synapse's TURN URIs
point at the central coturn (`turn:HOST:STUN_PORT` + tls variant)
with the shared static_auth_secret pulled from web-svc-coturn
credentials.
meta/variants.yml:
- V1 (ansible all-on): postgres.shared=true, coturn.shared=true.
- V2 (ansible no-auth): postgres.shared=false (dedicated path),
coturn off — exercises both polarities for the variant-coverage
lint and the dedicated DB code path.
- V3 (compose all-on): postgres.shared=true, coturn.shared=true.
vars/main.yml:
- MATRIX_COTURN_SHARED + MATRIX_COTURN_{HOST,STUN_TURN_PORT,STUN_TURN_TLS_PORT,AUTH_SECRET}
lookups (gated on shared so dedicated runs don't try to read
credentials that are not in scope).
templates/flavor/ansible/vars.yml.j2:
- matrix_homeserver_container_network / matrix_synapse_container_network
/ matrix_client_element_container_network / matrix_coturn_docker_network
all pinned to `host`. Hits the fork's `when: var not in ['', 'host']`
task guards (no 403 on docker_network create) and the
`{% if var not in ['', 'host'] %}` systemd-template guards (no
ExecStartPre=docker network connect against an additional bridge,
which is invalid on host networking).
- When central coturn is shared: matrix_coturn_enabled=false +
matrix_synapse_turn_uris (udp/tcp + turns udp/tcp) +
matrix_synapse_turn_shared_secret wired to MATRIX_COTURN_AUTH_SECRET.
Tests: 186/186 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements docs/requirements/021-sso-flavor-migration.md atomically:
* Schema: every role's meta/services.yml replaces the parallel
`oauth2:` / `oidc:` top-level blocks with a single `sso:` block
carrying `enabled`, `shared`, and a `flavor` discriminator
(`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
entirety; its 5 sidecar templates move under
roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
`is_oidc_native`) and the oauth2 sub-values
(`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
Three lookup plugins consume the resolver: `sso` (single-app
predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
and plugins/filter/compose_volumes.py all gate on
`lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
`enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
→ `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
`OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
`oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
`OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
`SSO_SERVICE_ENABLED`; Playwright gating helpers
(isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
`flavor: oauth2` with their existing `oauth2.acl.blacklist`
(/user/login) and `oauth2.acl.whitelist` (federation + discovery
endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
(tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
test_acl_mutual_exclusive.py) are deleted per Decision #10;
test_oauth2_contract.py is renamed and rewritten to
test_sso_contract.py, asserting that `flavor: oauth2` roles carry
`oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
(web-app integration) now requires the unified `sso:` block with an
explicit flavor or a `# nocheck: sso` opt-out; new
tests/lint/repository/no_legacy_sso_paths/ guards the source tree
against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
docs/contributing/actions/testing/suppression.md are updated to the
unified SSO naming.
Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).
BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two-line block
# variant pinning every dynamic services.yml flag to false so the
# matrix also exercises the out-of-group_names branch.
sat inside the variant-0 (true-pinning) block of 46 variants.yml files —
inverted relative to its content. Removed across the board along with
the now-orphaned blank line that used to separate it from the
preceding service entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the leading `---` document marker and align top-level comments with
the indentation of the YAML block they describe, across 9 web-app role
variant files. No semantic changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Decouples the CSS service concept from its underlying transport
(currently `web-svc-cdn`) so the flag becomes opt-in via inventory
group membership instead of staying literal-`true` everywhere.
- `roles/web-svc-css/`: new role. Marker / wrapper role that owns
the `css` entry in the service registry, declares `web-svc-cdn` as
the upstream that serves the actual bytes, and 301-redirects its
own canonical domain to the cdn domain so health probes return a
valid response.
- `roles/web-svc-cdn/meta/services.yml`: drop the `css` block — the
canonical owner is now web-svc-css; cdn keeps `javascript`.
- 51 consumer roles (web-app-* and web-svc-{coturn,file,html,
libretranslate,logout,onlyoffice}) migrated their `css:` block from
literal `{ enabled: true, shared: true }` to dynamic
`{ enabled: "{{ 'web-svc-css' in group_names }}", shared: ... }`,
with matching matrix coverage added to each role's
`meta/variants.yml` (true-pinning + false-pinning entries) so the
dynamic-flag variant lint stays green.
- 41 `# nocheck: playwright-service-flag` rationale strings updated
from the now-incorrect "canonical:cdn alias of static-asset
provider" to "css consumer, no Playwright surface (provider:
web-svc-css)".
Bundled refactor: `roles/web-svc-asset/tasks/01_core.yml` now
includes `web-opt-rdr-domains` directly with a single-entry mapping
to redirect its canonical domain to web-svc-file, instead of having
that wiring live in the global `tasks/stages/01_constructor.yml`
constructor. The `add_redirect_if_group` filter
(`plugins/filter/redirect_filters.py`) and its unit test are dropped
as dead code — both call sites are now role-local.
Dashboard's consumption of the new pattern is intentionally not part
of this commit and follows separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* scripts/lint/autoformat.sh:
- rename run_sort_claude_settings worker to run_claude (matches the
renamed scripts/lint/claude.sh).
- drop the EXIT trap that wiped status_dir; cleanup happens explicitly
after `wait` returns, so a parallel worker that interrupts the
parent never races a vanished status dir.
- each worker now captures its tool's exit code and writes one of
OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
(markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
real failures count as FAIL.
- all workers run to completion regardless of individual tool failures
(so the user always sees the full picture); after the summary, the
script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
(history preserved via git mv); update test_claude_settings_sorted.py
fix-hint accordingly.
Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleans up the lint debt the main->feature/web-app-kix merge surfaced.
Each change addresses a real failure surfaced by `make test`; no
``# nocheck`` blanket suppressions added.
Migrations to the shared cache helpers:
- ~25 test files swap raw ``yaml.safe_load`` / ``yaml.safe_dump`` /
``yaml.dump`` for ``utils.cache.yaml.{load_yaml_any,load_yaml_all,
load_yaml_str,dump_yaml,load_yaml_all_str}``.
- ~30 test files swap raw ``glob.glob`` / ``os.walk`` / ``Path.rglob``
for ``utils.cache.files.iter_project_files`` (or
``iter_non_ignored_files`` for the dependabot fallback walker, or
per-role ``Path.iterdir`` / ``Path.glob`` where the scope is a
single subtree).
- 3 ``Path.read_text`` lint helpers now route through
``utils.cache.files.read_text``.
Variant + service-flag fixes:
- web-app-kix: shift its subnet from 192.168.105.80/28 to
192.168.105.128/28 to clear the collision with web-app-hugo and
svc-prx-openresty.
- web-app-fediwall: add the missing ``mastodon`` / ``friendica``
service flags referenced by ``meta/variants.yml``; pin the css /
logout / dashboard / matomo / prometheus literals with
``# nocheck: dynamic-flag`` and add a third "everything off"
variant so the variant-coverage matrix sees both true and false
pinning for every dynamic flag.
- web-app-magento, web-app-shopware, web-svc-xmpp: switch the
``run_after: web-app-keycloak`` matcher from a literal-false
oauth2/oidc opt-out to the ``"{{ 'web-app-keycloak' in group_names }}"``
dynamic form, and extend each role's ``meta/variants.yml`` with the
matching true/false (and shopware's LDAP-only) coverage.
- web-app-fusiondirectory, web-app-matrix: declare the
``matomo`` flag so their ``run_after: web-app-matomo`` resolves
through an explicit services entry; add matomo true/false to both
variants files.
- web-app-fediwall, web-app-mini-qr: add the
``# nocheck: logout`` opt-out directly above ``logout:`` so the
logout-dashboard guard accepts the legitimately-disabled state.
- ~30 ``services.yml`` files: insert
``# nocheck: dynamic-flag`` directly above the literal
``css`` / ``redis`` / ``view`` / ``worker`` / ``email`` / ``html``
/ ``mariadb`` / ``opensearch`` / ``talk`` / ``logout`` blocks that
the dynamic-flag guard surfaced.
Code-quality follow-ups uncovered by ruff:
- utils/cache/yaml.py: collapse ``load_yaml_all_str`` to a one-line
return + on-call ``# nocheck: direct-yaml`` so the lint accepts
the cache module's own ``yaml.safe_load_all`` invocation.
- utils/templating.py: replace the ``Optional[Any]`` import that
slipped in from main with the modern ``Any | None``; rewrite the
cleanup ``try/except/pass`` blocks as
``contextlib.suppress(Exception)`` so the imported
``contextlib`` module is consumed consistently.
- 7 new-from-main lint test files (test_inline_script_block_size,
test_no_inline_multiline_php_in_sh, test_no_lookup_config_jinja_default,
test_no_inline_multiline_sql, test_web_role_no_web_dependency,
test_shared_primary_entity_run_once, test_fediwall_active): drop
their local ``Path(__file__).parents[N]`` PROJECT_ROOT
computations and import the package-level constant instead.
- roles/web-app-moodle/templates/playwright.env.j2: drop the
unreferenced ``KEYCLOAK_REALM_NAME`` env key that the
playwright-env-keys-used guard flagged.
Two new integration lints under tests/integration/roles/meta/:
* test_variants_coverage.py — every services.yml flag declared via
the dynamic ``"{{ '<role>' in group_names }}"`` form MUST be
exercised by meta/variants.yml: at least one variant entry pins
it to literal ``true`` AND at least one pins it to ``false``.
Pairs may share variant entries — the test only checks the union
of all variants covers both polarities for every dynamic flag.
* test_services_no_self_reference.py — a role's own primary entity
(matching ``get_entity_name(role)``) MUST NOT use the dynamic form
for ``enabled`` / ``shared``. Both MUST be literal ``true``: the
role's own services.yml is loaded only when the role itself is in
group_names, so a ``'<self>' in group_names`` Jinja is tautological.
The dynamic-flag test now skips the role's own entity to avoid a
contradiction with this new guard.
Self-reference fixes (Jinja → literal ``true: true``) on 19 roles:
svc-ai-ollama, svc-db-{mariadb, openldap, postgres}, web-app-{
dashboard, discourse, keycloak, mailu, matomo, prometheus},
web-svc-{asset, cdn, collabora, coturn, file, libretranslate, logout,
onlyoffice, simpleicons}.
Variants coverage rollout (62 new + 17 merged meta/variants.yml):
* Roles without a pre-existing variants.yml get a new file with two
entries — one pinning every dynamic flag true, one pinning every
dynamic flag false. No ``- {}`` baseline (matrix runs only the
two coverage variants).
* Roles with a pre-existing variants.yml (akaunting/baserow/bluesky/
bookwyrm/flowise/fusiondirectory/jenkins/jira/joomla/keycloak/lam/
minio/phpmyadmin/postmarks/wordpress/web-svc-xmpp) keep their
matrix variants (OIDC vs LDAP, Single-Site vs Multisite, dev creds
+ TOTP-disabled realm, autologin, etc.); the all-true coverage is
deep-merged into variant 0 and the all-false coverage into
variant 1, so each role still ships exactly two matrix rounds.
* svc-ai-ollama keeps its single CI image-shrink variant; after the
self-reference fix it has no remaining dynamic flags so no extra
coverage entry is required.
* svc-db-{mariadb, openldap, postgres} drop variants.yml entirely
for the same reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>