Commit Graph
1055 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 57d6ece9b0 feat(cdn): serve the CDN same-origin on every front-proxy vhost
Values an app renders once (env files, branding config, web manifests)
cannot follow the network a page is served on, so onion pages loaded
logos and icons from the clearnet CDN host (Mobilizon, Wazuh, Mini-QR in
run 40243434327). Like the object-store route from 40b178d9a4, every
front-proxy vhost now aliases the CDN directory under /_infinito/cdn/,
with nosniff and a sandboxing CSP so a mirrored HTML or SVG file cannot
script the app's origin. lookup('cdn') and lookup('design') add
relative_urls for consumers that want host-relative URLs; the absolute
urls stay for server-side users. The include lives outside the location
snippets, so test_vhost_cross_cutting_includes requires it of every
override, the compose synapse vhost included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 03:29:40 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c37715e5a2 fix(sys-ctl-hlth-webserver): expect 301 on every sibling of a redirect source
web-opt-rdr-domains renders a `return 301` vhost for each network sibling
of a redirect source (network_redirects), but the health expectations only
forced the clearnet source to [301]. A handover app's onion sibling kept
its [302] handover expectation, so Bluesky's disabled view domain failed
"view.bluesky.<onion>: ERROR: Expected [302]. Got 301." and
hlth-webserver aborted the deploy. Every expected domain whose clearnet
sibling is a redirect source now expects [301].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 03:18:57 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b87bf5f979 fix(ci): disable partners a tor-only node serves no network of
A variant that pairs a clearnet-only partner with the onion (Matrix
variant 2 with web-svc-coturn) could draw the tor-only network mode, and
the deploy's domain map then aborted with "web-svc-coturn: allows
['clearnet'] but the node runs in 'tor' mode". disabled_services() now
also emits every deployed partner with domains and a Tor bond whose
networks the node mode does not serve, mirroring utils/cache/domains.py,
so the inventory provision drops it the same way it drops Tor on
clearnet rows. Measured across every role: 8 tor-only rows change, all of
which failed at that point before; clearnet and multi rows are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 03:16:53 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b27aaf0c11 Merge branch 'fix/web-app-seaweedfs/writeacp-grant' into feature/store-i18n
The WriteAcp grant lands in roles/web-svc-seaweedfs/templates/s3.json.j2,
where store-i18n keeps the server templates; git's rename detection moved
the hunk there and roles/web-app-seaweedfs/templates/s3.json.j2 stays
absent. roles/web-svc-seaweedfs/README.md lists WriteAcp like the
web-app-seaweedfs README the hotfix updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:25:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ec7d4c9042 fix(web-app-seaweedfs): grant consumers WriteAcp on their bucket
SeaweedFS 4.49 (seaweedfs/seaweedfs#11592) authorizes a PutObject that
carries x-amz-acl as s3:PutObjectAcl as well. The consumer identities
only held Read/Write/List/Tagging, so every app that sends an ACL on
upload (Nextcloud, Fider and Pixelfed with private, Mastodon, PeerTube,
BookWyrm and Baserow with public-read) got AccessDenied after the 4.48 -> 4.51
bump in #1226. Main run 40401200083 failed Fider#0, BookWyrm#0,
Baserow#0, Pixelfed#0 and Nextcloud#0/#4/#8 on it.

Add WriteAcp to the s3.json identities and to the sidecar s3.configure
grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:28:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 99123762df Merge branch 'feature/updater' into feature/store-i18n
16 conflicts, resolved per hunk:

- sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop;
  the single stack_host_contents write of http.conf plus vhost and the
  front_proxy_vhost_changed gate move into tasks/vhost.yml, with the
  http.conf decided per sibling domain so an onion vhost gets no HTTPS
  redirect.
- sys-front-tls: 01_http.yml is gone as the updater intends; the
  cert-renew include in main.yml gates on the vhost domain's TLS.
- sys-svc-compose: the updater's resolve_host_cid without the remote
  echo and the prepared pending flag in up.yml, written with
  IS_SWARM_MODE and the repeated-when block grouping.
- web-app-nextcloud: the updater's post-upgrade gate and pending-only
  plugin configuration, which already carry the pinned-plugin handling.
- web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and
  style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap,
  the S3 blob store stays off there as TestBlobstoreRenderOrder demands.
- roles_with_service: onion swap and dashboard card key combined, the
  canonical domain is the canonical URL's host in both branches.
- sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together;
  run_checker forwards its options to build_docker_cmd, keeping the
  script under the 500-line limit.
- web-app-matrix: Element branding and web_client_location side by side.
- web-app-stalwart/README.md: the generated Cosmos section is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:47:07 +01:00
kevinveenbirkenbachandClaude Opus 5.5 0359d04c82 perf(sys-ctl-hlth-csp): probe four vhosts in parallel with csp-checker 3.2.0
csp-checker 3.2.0 checks every URL in its own browser context and takes --concurrency for a worker pool. The role pins 3.2.0, sets HEALTH_CSP_CONCURRENCY to 4, and the systemd unit hands it through script.py to the container command ahead of the -- URL separator, for the clearnet and the onion batch alike. TimeoutStartSec keeps its sequential sizing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 22:21:12 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f3e9f3de17 fix(web-app-discourse): set become once through apply on the legacy-checkout include
test_include_uniform_apply_keyword flagged legacy_checkout.yml after 11c2d8dcd3 moved its guard onto the include: both tasks repeated become: true. The include in 03_launcher.yml now applies become to the file and keeps the guard; the order test reads include_tasks in its mapping form too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 22:11:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 2244cc691b perf(svc-bkp-volume-2-local): skip baudolo-seed when databases.csv already holds the consumer's row
Every database consumer called baudolo-seed through the seed task, 37 times and always 'changed' in a full production run (54.4 s), although databases.csv rarely changes. The first seed of a play now reads the file once (05_read-seeded-databases.yml) into svc_bkp_volume_2_local_seeded; baudolo_seed_current compares the consumer's row with it and baudolo_seed_apply advances the cached state the way baudolo-seed writes the file, so the command runs only for a missing or differing row. A file baudolo did not write byte for byte (hand-edited, CRLF, short rows) falls back to one call per consumer; 02_reset forgets the cache with the file.

Replay harness old vs new: databases.csv byte-identical in fresh, empty, converged, drift and reset-mid-play scenarios; converged seeds 37 -> 0, drift 38 -> 2; the negative control without reset invalidation diverges. About 44 s per unchanged redeploy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:35:14 +01:00
kevinveenbirkenbachandClaude Opus 5.5 333b09cdca perf(web-app-nextcloud): configure all queued plugins in one exec, writing only the values Nextcloud lacks
Each plugin ran its own occ config:app:set per configuration value right after its install or enable, and a redeploy rewrote every value. Install and enable now queue the plugin; the configure stage runs once for the queue: it loads every queued addon spec, reads occ config:list once, keeps only the values Nextcloud does not hold exactly (nextcloud_plugin_config_pending; typed, masked and structured values are always kept), and runs the hooks in queue order afterwards.

Combined with store-i18n's 7f72e53a58: the pending occ config:app:set calls go on the stdin of one container exec -i ... sh -s, so no value (Talk, OnlyOffice, whiteboard, BBB secrets) reaches a host argv; a failing call names its appid/configkey.

Replay harness old vs new: identical final state, updated keys and hook order in first-deploy, converged, drift and failure scenarios (failure stops at the same key); converged config:app:set calls 19 -> 3. Merge note: resolve 03_configure.yml with this side against 7f72e53a58.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:29:48 +01:00
kevinveenbirkenbachandClaude Opus 5.5 73a16bef09 perf(sys-svc-compose): prepare the compose instance in one module call
Every app's compose pass ran three remote tasks before compose up: create the directories, compose ps for running containers, and stat the pending marker (53 apps x ~2.7 s in a production run). compose_instance_prepare does all three in one call from 02_stack_host_prep.yml, sharing the directory creation with directory_batch through utils/directory_batch.py; compose ps runs only in compose mode. up.yml evaluates the registered result, named compose_instance_before_files because it predates 04_files: the running check is skipped whenever the compose, env or override file changed in this run (those changes notify compose-up themselves).

Replay harness old vs new: rendered files and the compose-up, marker and swarm handlers identical in every scenario; the only difference is the skipped running check when only the override changed, where compose-up still runs. About 85 s per full production run.

Merge note: store-i18n's a4c7fda974 hoists the removed marker stat into a block; resolve up.yml with this side.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:22:32 +01:00
kevinveenbirkenbachandClaude Opus 5.5 d45d1f07af fix(web-app-discourse): clone the launcher into services/discourse_docker and retire the URL-named checkout
DISCOURSE_REPOSITORY_DIR appended the repository URL to the services directory, so the launcher lived under services/https:/github.com/discourse/discourse_docker.git/. It now lives under services/discourse_docker. Before the clone, launcher/legacy_checkout.yml removes the URL-named tree (computed with relpath against the services directory) and prunes its emptied parents; a guard skips the cleanup when that path would escape the services directory or equal the new checkout. The cleanup must precede the clone: git otherwise resolves the old relative path as a URL and aborts with 'protocol ... is not supported'. Discourse data stays in the launcher's named volume and /var/discourse/shared; the first deploy rebuilds once because the bootstrap digest lived in the old containers/ directory.

relpath joins the builtin filters test_defined knows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:12:53 +01:00
kevinveenbirkenbachandClaude Opus 5.5 afb82f57a1 feat(web-app-dashboard): let a role name the canonical domain its card opens
The dashboard card of every app opened its primary domain, so Matrix led to the Synapse status page and Stalwart to its admin UI. A role can now declare services.dashboard.card: <key of meta/domains.yml canonical>; docker_cards builds the card URL with lookup('canonical_url', app, key), the Apps menu reuses it, and roles_with_service('dashboard') hands the same domain to the tile e2e spec. Matrix sets card: element, Stalwart card: webmail. Without card nothing changes, and the prometheus/matomo target lists are untouched. A lint requires the key to resolve in the role's own domains.canonical.

The Matrix ansible flavor also sets Synapse's web_client_location to Element through matrix_synapse_configuration_extension_yaml, as the compose flavor already does, so matrix.<domain>/ redirects there. Merge note: feature/iam-profile-rbac defines the same extension variable inside the OIDC block; the next merge must combine both into one block (agreed with that session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 acad754cda fix(group_vars): evaluate the CAPTCHA flags only for a configured provider
A host that defines CAPTCHA with one provider replaces the group_vars mapping (no hash_behaviour merge), so galaxyserver, which configures only hCaptcha, left CAPTCHA.RECAPTCHA undefined. RECAPTCHA_ENABLED then failed to render, the applications render fallback returned the expression as text, and Keycloak's '| bool' coerced it to False with a deprecation warning that ansible-core 2.23 turns into an error. Both flags now check that their provider mapping is defined first; the effective values stay the same for every existing inventory.

test_captcha_enabled renders every role's services.recaptcha/hcaptcha.enabled through the real templar for defaults, both providers, only the other provider and only this provider, and fails on the raw text (negative control with the old expression: 4 subtests).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:58:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 d6ebec8488 perf(web-app-nextcloud): run the post-upgrade pass only after a version change, take app updates from the daily update check otherwise
Every redeploy ran occ maintenance:repair --include-expensive (43.5 s, maintenance mode on) and listed the appstore updates through Tor (49.7 s) although the version had not changed. 03_upgrade.yml now reads occ status and files/mimetype_version after occ upgrade; nextcloud_needs_post_upgrade decides whether the pass is due (version changed in this deploy, or the expensive repair has not run for the running version). A due deploy includes 03_upgrade/01_post_upgrade.yml with today's app update and repair; an unchanged one includes 03_upgrade/02_cached_updates.yml, which updates only the unpinned enabled apps whose release stored by the updatenotification background job is newer (nextcloud_cached_app_updates, PHP version_compare semantics). NEXTCLOUD_PLUGINS_PINNED is the single source of the pinned check.

Verified against echoserver's live occ output (status, marker, updatenotification cache, app:list, with debug log lines): unchanged redeploy is not due and finds no update. Scenario harness 24/24 incl. negative controls on the old file. About 86 s per unchanged redeploy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:52:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 d26b6ea110 Merge branch 'feature/updater' into feature/store-i18n
Conflict in roles/sys-stk-front-proxy/tasks/main.yml: store-i18n moved
the per-vhost tasks into the network-sibling loop of tasks/vhost.yml, so
main.yml keeps HEAD and the deferred probe lands in vhost.yml instead:
every sibling domain is queued in front_proxy_deferred_probes, and the
inline probe of an unchanged vhost is gone in favour of the batched one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:35:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 afaade9bbd perf(sys-stk-front-proxy): probe every queued vhost in one parallel batch per stage
Each vhost used to run its own sequential reachability probe. The role now queues the vhost and deferred_probe.yml checks the whole queue with one uri_probe_batch call after the shared-service preload and after the server roles. A vhost answering like a stale config restarts OpenResty once; one still unserved afterwards fails the deploy by name. uri_probe_batch takes follow_redirects explicitly so the probe sees the redirect status itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 18:16:04 +01:00
kevinveenbirkenbachandClaude Opus 5.5 9df6619903 Merge branch 'feature/updater' into feature/store-i18n
Conflicts resolved:
- sys-front-inj-matomo: inject.yml keeps the store-i18n split; the cached
  failed site list load (5b7284c4a4) is ported into resolve_site.yml next
  to its token guard, and test_site_cache.py reads that file.
- web-app-bluesky: platform_administrator from store-i18n with the
  prefix-free handle and its i8- reserved variant from feature/updater;
  the broker CONFIG keeps siteTitle, storePath and chooseHandlePath.
- web-app-stalwart README: the managed Quick Setup section is dropped,
  the docs role renders it.
- README.md: the role overview is regenerated from the merged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:19:41 +01:00
kevinveenbirkenbachandClaude Opus 5.5 35722aa3bc fix(lint): shorten the Matomo cache reset name, read the Keycloak vars path from the SPOT
5b7284c4a4 named its reset task with 81 characters, one over the limit of
test_name_emoji_and_length, and 754d35c4de built the path to
roles/web-app-keycloak/vars/main.yml with a pathlib chain that test_file_spot
requires to come from ROLE_FILE_VARS_MAIN. Both commits went in without the
lint suite. The task keeps its behaviour under a 70-character name, the test
reads the same file through the constant. make test-lint: 577 passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 13:39:42 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b2350162a5 fix(web-app-keycloak): pass kcadm payloads on stdin, not in a heredoc
keycloak_kcadm_update built `cat <<'JSON' | kcadm update|create ... -f -`
with the JSON payload inside the command it hands to sh -c, and the
reCAPTCHA task did the same in its shell script. Client secrets, the SMTP
password, the LDAP bindCredential and the reCAPTCHA secret therefore sat
in a host process argument (#1213).

The library now sends the payload as subprocess input, also on its lock
and dead-container retries, and the reCAPTCHA task uses the shell
module's stdin. Every caller's kcadm_exec already runs `container exec -i`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:03:38 +01:00
kevinveenbirkenbachandClaude Opus 5.5 a758ce71f9 fix(web-app-prometheus): pick each probe target's module by its own TLS mode
While the private CA is missing from the prometheus stack, a self-signed
target has to use http_ready_private_ca. The blackbox job chose that
module once per application from lookup('tls', app, 'mode'), which aligns
to one sibling domain. For web-app-docs that sibling was the onion domain
(TLS off), so its clearnet https target kept the verifying http_ready and
failed with "x509: certificate signed by unknown authority" (CI run
40131272595: n8n#1, and hidden behind the first failure in API#1 and
Mattermost#1).

Every served domain now gets its own target group whose module follows
lookup('tls', probe_domain, 'mode'). Series labels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:57:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 754d35c4de fix(web-app-keycloak): bring SAML clients to existing realms too
SAML clients were created only by the realm import, and the import runs only
for a missing realm. echoserver keeps its legacy realm cymais.cloud and ran
SuiteCRM behind oauth2-proxy before v16, so the SuiteCRM SAML client
https://<suitecrm>/saml/metadata never existed there: the SAML hop showed
Keycloak's Invalid Request page and Keycloak logged LOGIN_ERROR
error=client_not_found reason=Cannot_match_source_hash.

The existing-realm reconcile now also loops over the clientId of every SAML
client in the rendered realm dictionary, the same source the import uses,
and creates or updates it through kcadm like the application-declared OIDC
clients. The same path already converged the stalwart-webmail and
stalwart-webui clients in echoserver full-10.

Same change as 2b4db90576 on feature/store-i18n, so a later merge of the two
lines resolves without a conflict, plus a unit test that pins the SAML client
selection and its place in the reconcile loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 12:30:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5b7284c4a4 perf(sys-front-inj-matomo): cache a failed site list load so later vhosts skip the dead API
On a host whose Matomo was not deployed yet, every vhost injected before
web-app-matomo asked SitesManager.getSitesWithAtLeastViewAccess again and
waited out uri_retry (30 retries x 2 s): 6 vhosts, 84 to 98 s each, 8.7
minutes in galaxyserver full-2. matomo_site_cache only kept a loaded list.

inject.yml now caches every executed load: the list on success, none on a
failed request or an error mapping. A cache of none skips the cached-id step
and getSitesIdFromSiteUrl, so the existing skip task turns the injection off
for the vhost as before, without the retry wait. web-app-matomo resets the
cache of every play host (delegate_facts, swarm workers included) after
07_config and before 08_reinject_consumers, so the re-injection and every
later vhost load the list from the now running Matomo.

Evidence: the real inject.yml with the real uri_retry against a closed port
and a stub Matomo, 11 vhosts in 4 scenarios: the first probe took 68.6 s, the
next two 0.4 and 0.3 s with matomo off; after the reset a known site came
from the cache, an unknown one went getSitesIdFromSiteUrl -> addSite and was
appended, a later vhost of that domain hit the cache. HEAD inject.yml took
68 s per vhost with the same outcome. The swarm reset reached a second host.
ansible-lint passes; tests/unit/python/roles/sys-front-inj-matomo pins the
gates and the reset order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 12:26:05 +01:00
kevinveenbirkenbachandClaude Opus 5.5 bc14298f93 fix(i18n): remove the staging file when a catalogue write fails
write_catalog writes the rendered catalogue to .<name>.po.tmp and moves it
over the target. When the write fails, the staging file stayed behind: the
translation run that hit a full disk left eleven empty .docs.po.tmp files
untracked in locale/. Any failure between the write and the move now
deletes the staging file and re-raises, so the target keeps its old bytes
and nothing is left in the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:49:50 +01:00
kevinveenbirkenbach 1e4124b0d7 Merge branch 'feature/updater' into feature/store-i18n 2026-10-25 11:28:33 +01:00
kevinveenbirkenbachandClaude Opus 5.5 7b2dfcfac7 fix(tls): serve a redirect to another host through the redirect role
resolve_term follows a redirect source to its target so an alias inherits the TLS settings of the application it points at. A redirect to a domain another host serves ends that chain on a domain this host does not know, and the tls and cert lookups failed: galaxyserver full-1 stopped in web-opt-rdr-domains on 'tls: domain project.infinito.nexus not found' for its project.veen.world -> project.infinito.nexus redirect.

When the chain ends outside the host's domains and web-opt-rdr-domains is deployed, the source now resolves to web-opt-rdr-domains with the source itself as primary domain, which is the role that serves its vhost and whose certificate bundle names it. Only the outermost call falls back, so the source asked for stays the primary domain across a longer chain; without the redirect role, or for a target this host serves, nothing changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 11:04:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ae833f1559 fix(docs): quote the country placeholder in the deploy instructions
The README template wrote COUNTRY=<your-country> unquoted since 558784e6ca. The web-app-docs CLI test checks the published Production commands with bash -n before it fills the placeholders, and bash reads <your-country> as a redirection: 'syntax error near unexpected token newline' for ERPNext's page (ERPNext#2, run 40131272595). It is quoted now like every other placeholder; the later sed substitution still matches inside the quotes.

test_generate also runs bash -n over every bash block of the generated Production section, so the next unquoted placeholder fails as a unit test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:57:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 dc26b941c1 fix(web-svc-translate): answer a batch per string instead of failing it whole
One string that every engine refused or mangled raised out of the batch route's gather, so the gateway answered the whole batch with a 422 and the client lost the other nineteen translations. Gateway.translate_each now owns the bounded fan-out; with partial=true it answers (None, None) for a string that ran into a GatewayError and keeps the rest. Non-gateway exceptions still raise, and a NoBackendError on every string still fails the batch as the outage it is.

The i18n client asks for partial batches and counts a null answer as a refused entry with its reason, so the re-ask and damage checks continue per string. A caller that does not send the flag gets the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:56:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 de29b99a89 fix(i18n): keep letter-digit names like S3 and OAuth2 whole
PROTECTED masked identifiers with an underscore and bare numbers, so S3, OAuth2, POP3 and HTML5 went to the engines as a word plus a separately protected digit. Engines tore them ('Amazon S ilə uyğun ...' for 'S3-compatible') or glued the digit onto another word ('Distribuid3-compatible'); the masking audit over core found 159 such entries.

A letter-digit token is now one protected span, ahead of the bare-number alternative. The client masks it whole, and damage.reason() condemns a translation that lost or moved it, so prune and retranslation pick those entries up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:49:19 +01:00
kevinveenbirkenbach 2c04cce617 Merge branch 'feature/updater' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	plugins/lookup/deployment.py
2026-10-25 09:32:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 69871cbfad fix(web-app-flowise): stop retrying the MCP reconcile when Flowise refuses the key
The reconcile task retried every failure thirty times ten seconds apart, so a permission the key lacks surfaced only after five minutes: rdr-3 spent 331 s on a 403 that no retry could change. The reconciler now ends with exit code 3 when Flowise answers 401 or 403, and the task stops retrying on that code while its failed_when still fails the run under MCP_RECONCILE_STRICT.

Only 401 and 403 count as final: authorize may answer a provider that is still starting with another 4xx, and that wait is what the retries are for. Every exit that reports an HTTP status goes through fail(), which keeps the message on the exception, so the existing tests read it unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:29:43 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ca4f53d0e4 fix(mcp): keep a persistent host's providers through a partial deploy
mcp_servers narrowed the MCP providers to application_closure(deployment.whitelist), which on a persistent host drops every provider an earlier run deployed and a partial run does not touch. sys-svc-mcp-reconcile then converges each client present on the host against that narrowed set: on echoserver the --id web-opt-rdr-domains run rdr-3 removed the Prometheus registration from Flowise.

deployment now reports ephemeral (runtime act or github), and the closure narrows only there, where a whitelist is all a run deploys; a persistent host narrows nothing, as a full deploy already did with its empty whitelist, so its clients keep every provider it serves. CI keeps its narrowing unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:24:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f8385c1cca test(web-app-flowise): import PROJECT_ROOT in the mint key test
test_mint_mcp_api_key.py computed PROJECT_ROOT itself from Path(__file__).parents[6], which test_project_root_import forbids outside an __init__.py. The files package now exports PROJECT_ROOT the way its reconcile subpackage does, and the test imports it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:20:43 +01:00
kevinveenbirkenbachandClaude Opus 5.5 a6bb6c5240 fix(web-app-flowise): let the MCP registry key delete its stale probe flows
The reconciler prunes the managed fixture chatflows of providers it no longer probes, but the scoped API key it gets was minted without chatflows:delete, so the first prune ended in 403 Forbidden after thirty retries; on echoserver rdr-3 stopped on 'FAILED deleting stale infinito:fixture:web-app-prometheus'. Adding the permission alone would not have reached that host, because api_key() reused an existing key unchanged.

KEY_PERMISSIONS now carries chatflows:delete, and api_key() reuses the managed key only while its permissions equal KEY_PERMISSIONS; otherwise it deletes the key through DELETE /api/v1/apikey/<id> and mints a new one, which the reconciler receives in the same run. The script gains its mirrored unit test, so its mirrored-unit-test exemption goes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:08:15 +01:00
kevinveenbirkenbachandClaude Opus 5.5 2597ce1b90 fix(ca_injected): read an application's own TLS on its canonical domain
Asked by application id, lookup('tls') aligns the domain to the vhost in `domain`. sys-svc-cdn reloads the compose handlers inside every sibling of the vhost loop, so a handler flushed after the loop still saw the application's onion sibling there: on prometheus' second stack deploy (the MCP adapter rebuild) ca_injected answered False, the swarm CA override was skipped and compose_file_args dropped it from docker stack deploy. Blackbox lost the CA mount and the docs healthz probe failed with an unknown authority (API#1, Jenkins#1), and the spec flip recreated every task at once, so the sso-proxy reached central redis before its sidecar registered and the converge gate failed on NOAUTH (OnlyOffice#1, Nextcloud#1, GitLab#1, Mattermost#1; run 40131272595).

The CA mount belongs to the containers, not to a vhost, so the own-TLS check now asks with the canonical domain, which no alignment touches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:44:25 +01:00
kevinveenbirkenbach f69170274c Merge branch 'feature/updater' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	roles/web-opt-rdr-domains/README.md
#	roles/web-opt-rdr-domains/tasks/main.yml
#	roles/web-svc-design/tasks/00_core.yml
#	tasks/stages/01_constructor.yml
#	tasks/stages/02_server.yml
#	tests/unit/python/plugins/lookup/test_current_play_redirect_domains.py
2026-10-25 08:31:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 6bb1dafcc5 refactor(web-opt-rdr-domains): read host-wide redirects from the role config
Host-wide redirects such as a legacy zone came from the top-level inventory variable redirect_domain_mappings, which only the role default declared. Once the role input moved to web_opt_rdr_domains_mappings nothing declared it any more and the inventory validation refused the deploy, because it does not see the lookup's vars_ read. The redirects now live where the role's other settings are: services.rdr-domains.redirects in meta/services.yml, empty by default and set per inventory under applications.web-opt-rdr-domains, and current_play_redirect_domains reads them through the config lookup. They still win over the application aliases, and a caller that includes the role without its own web_opt_rdr_domains_mappings still renders the merged CURRENT_PLAY_REDIRECT_DOMAINS.

Inventories move their redirect_domain_mappings list unchanged to applications.web-opt-rdr-domains.services.rdr-domains.redirects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 08:11:33 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c691abe8a8 fix(swarm-test): drop the backup host's mirrored network mode before it deploys
The VPN variants reset credentials with --mirror --backup in the mesh prologue, which copies the manager's host_vars onto the backup host again, NETWORK_MODE: multi included, after extend_inventory had stripped it. The backup deploy then failed in lookup('tls'): "NETWORK_MODE 'multi' needs svc-net-tor with a node onion on this node" (Meta#0, run 40131272595). The strip is now a helper that also runs right before the backup host deploys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:50:10 +01:00
kevinveenbirkenbachandClaude Opus 5.5 3044ae7a2e perf(web-app-nextcloud): plan the plugin pipeline from one app:list
Every addon ran through the install, enable and configure stages on its own, and each stage asked occ for the plugin's state again. The role now reads occ app:list --output=json once, and the new nextcloud_plugin_plan filter replays the addon order (an enabled entry switches its incompatible_plugins off and itself on, a disabled entry switches itself off, the later entry wins) and splits the app ids into disable, install, enable and configure. One app:disable call takes every id that has to go off; the install, enable and configure includes loop only over the ids that need that stage, so an already enabled plugin skips straight to its configuration. A plugin shipped as a pinned archive still goes through install on every run.

On echoserver the plugin phase took 608 s in full-9 for 51 addons; a repeat deploy is derived at about 195 s instead of about 550 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 07:39:16 +01:00
kevinveenbirkenbachandClaude Opus 5.5 28ce14ffe7 style(web-app-keycloak,web-app-openproject): apply autoformat to the no-verify commits
quality-high's autoformat pass rewrote three files that 9301220d05,
49aa920034 and c78d5f906c committed without the hooks. ruff drops three
`noqa: E402` markers as unused (RUF100): E402 does not fire on an import
that follows sys.path.insert. The formatter wraps the long
KEYCLOAK_RBAC_GROUP_LOGIC_DIGEST line and the OpenProject admin runner's
`sh -c` line; the double-quoted scalar and the `>-` block both fold the
break back into a space, and a parse against HEAD gives equal values.

On merge: feature/store-i18n 2bcf59109e carries the same four Keycloak
hunks byte for byte; they merge without a conflict.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 23:02:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 2bcf59109e style(web-app-keycloak): apply autoformat to the merged keycloak files
ruff drops three noqa: E402 markers as unused (RUF100): E402 does not fire on an import that follows sys.path.insert. The formatter also wraps the long KEYCLOAK_RBAC_GROUP_LOGIC_DIGEST line; the double-quoted scalar folds the break back into a space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:31:20 +02:00
kevinveenbirkenbachandClaude Opus 5.5 086abc6338 fix(playwright): reach other applications through lookup('landing_url')
The playwright env renders once per network pass. lookup('tls', app, 'url.base') aligns another application's URL to the pass network even where that application hands the network over, so a spec expected a host the browser is redirected away from (fediwall on its onion pass expected the Mastodon and Friendica onion siblings).

lookup('landing_url', app) returns the aligned URL, or the canonical one behind a handover. Every cross-application URL in the 79 playwright.env.j2 templates goes through it now, fediwall's inline handover expressions included, and a lint rejects both the raw tls form and the inline one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:25:39 +02:00
kevinveenbirkenbach db17bd5d4a Merge branch 'feature/updater' into feature/store-i18n 2026-10-24 21:52:15 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e0e2c0c944 fix(lookup): fan a redirect out only to the networks its target is served in
network_redirects gave every network sibling of a source the mapping, and redirect-domain.conf.j2 falls back to the canonical target when the target has no sibling in the source's network. Since e4fd4552f0 schedules the clearnet apex entry without web-opt-rdr-domains in the round, its fan-out rendered a vhost for the bare node onion pointing at the clearnet homepage in every multi-network round without an onion dashboard: the CSP health check of API#1 and OnlyOffice#1 in run 40109131688 failed with '<node onion>: left .onion: ended at https://dashboard.<domain>/'.

The canonical sibling of a source keeps its mapping as before; a further sibling is fanned out only when the target is served in that sibling's network. A dropped onion source has no vhost and the default server answers it with 404 inside the onion instead of redirecting to clearnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:19:45 +02:00
kevinveenbirkenbachandClaude Opus 5.5 49aa920034 perf(web-app-keycloak): mark user emails verified in one REST call
The emailVerified update looped over every non-reserved user, uploaded a
shell script per user and ran kcadm with a JVM login each time, and it
always reported changed: 7 users took 23.8 s on echoserver.

email_verified.py uses the shared keycloak_admin client through
tasks/utils/rest_script.yml: it looks each user up by exact username,
skips users already verified, and writes emailVerified with a GET plus a
full-representation PUT, as kcadm update did. The task reports changed
only when a user was updated, and a failed lookup now fails the task
instead of being treated as a missing user. The old lookup piped
kcadm -q username=X into head -n1, which could have hit a longer username
sharing the prefix.

Evidence (local compose, slot 104): summary updated=0 unchanged=8
missing=0 failed=0 in 0.60 s, task ok, staging dir removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 19:39:53 +02:00
kevinveenbirkenbachandClaude Opus 5.5 9301220d05 perf(web-app-keycloak): reconcile per-app RBAC over the admin REST API
The per-app RBAC groups and LDAP group mappers were reconciled by four
kcadm shell scripts that started a JVM per call and re-synced every app
on every deploy: 582 s for the mappers plus 324 s for the orphan purge on
echoserver (39 apps).

rbac_reconcile.py now talks to the admin REST API from inside the
Keycloak network namespace (nsenter --net) with the admin credentials
read from /proc/<pid>/environ, so nothing secret reaches argv or output.
It drops the shared ldap-roles mapper, purges root groups of undeployed
apps, ensures each app group and mapper, and syncs a mapper only when the
group is new, the mapper config drifted, or the fingerprint stored on the
group (mapper payload, app rbac and domains, group-logic digest) differs.

The per-app payload no longer carries groups.path.group: Keycloak 26.8.1
does not store that key, so a payload with it never matched the stored
mapper and every deploy re-synced every app.

The admin client lives in keycloak_admin.py; tasks/utils/rest_script.yml
stages it with the script in a 0700 temp dir on KEYCLOAK_HOST_NODE, runs
it and removes the dir in an always block.

Evidence (local compose, slot 104): matrix iteration green in both
variants and passes; update runs show synced=1 in 1.1 s, then
synced=0 unchanged=1 in 0.81 s through the final delivery path, no
staging dir left behind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 19:28:31 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4eb4c0fe22 fix(inventory): bake the variant a pin forces, as the round's include already does
services_overrides_for_round applied the # variant-pin: markers when it resolved a round's include set, while plan_dev_inventory_matrix built round_variants, which the inventory bake, the deploy CLI and the swarm matrix read, by clamping the round index alone. Once docs variant 0 pinned web-app-dashboard#reciprocal, round 2 of a sweep resolved dashboard variant 1 (SSO off, no web-app-keycloak in the inventory) but baked variant 0 (SSO on): Keycloak came up without its own bake, defaulted totp_enabled to true, and every dashboard login stopped at CONFIGURE_TOTP while the Keycloak card was missing (11 jobs of run 40042690056).

active_variants_for_round holds the clamp-plus-pins logic once; services_overrides_for_round and the planner both call it, and the planner keeps the primary apps' indices on top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:18:53 +02:00
kevinveenbirkenbachandClaude Opus 5.5 5a1761f218 fix(web-app-openclaw): admit the Control UI origin of every network the gateway serves
On a multi-network host one gateway serves the clearnet domain and its onion sibling, but allowedOrigins carried only the canonical URL. The Tor pass opens http://claw.ai.<onion>, so the gateway closed every Control UI websocket with 4008 before the pairing step and the UI reported origin-not-allowed (142 onion origins, 120 closes in the Funkwhale#1 rescue log of run 40042690056).

OPENCLAW_CONTROL_UI_ORIGINS takes lookup('network_urls', OPENCLAW_URL): one URL per network sibling with that sibling's scheme, canonical first, and the URL alone on a single-network host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:13:55 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d30425c0ac fix(i18n): refuse a translation that adds lines or comes back in another script
A chat engine prefixed the Vietnamese catalog with its own preamble ("...Vietnamese:" followed by a blank line) and wrote Urdu in Devanagari. Neither tripped the damage predicate: vi is Latin script, so the script checks were blind, and untranslated only measures the Latin share, so a non-Latin language answered in another non-Latin script passed.

added_lines refuses a translation that breaks more lines than its source, and the gateway's _mangled carries the same check so such an answer falls through to the next engine instead of being cached. wrong_script refuses a non-Latin language when less than half of the letters it writes in another language's endonym script belong to its own; the script per language is derived from the endonym in meta/languages.yml, and letters are counted over runs of letters plus combining marks so Indic and vocalised Arabic words stay whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:09:00 +02:00
kevinveenbirkenbachandClaude Opus 5.5 372142848b test(web-app-stalwart): decide the IMAP parallelism instead of racing it
The parallel-sessions test asserted that more than one of four sessions
appended something, which a loaded host where one worker drains the
shared iterator fails with nothing broken. The sessions now meet at a
barrier before their first append, so a serialised pool still fails the
assertion while a concurrent one always passes.

(cherry picked from commit 657015852a)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 18:01:19 +02:00