Commit Graph
607 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 43bd6583e6 test(web-app-docs): reuse StopBuilderError in the timeout loop test and merge its nested with
Gate pass 9 on the merged store-i18n stopped at ruff N818 on the local Stop exception that the builder-timeout test defined next to the module's StopBuilderError from the build-before-clone tests, plus SIM117 on the nested with in the healthy-mirror test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:44:51 +01:00
kevinveenbirkenbach 373330d040 Merge branch 'fix/web-app-docs/builder-timeout-and-prepared-glob' into feature/store-i18n
# Conflicts:
#	tests/unit/python/roles/web-app-docs/files/infinito_docs/test_build_queue.py
2026-10-31 17:18:02 +01:00
kevinveenbirkenbachandClaude Opus 5.5 1e804115fc fix(web-app-docs): keep the builder alive through git timeouts and never wipe a healthy mirror
fetch() leaked subprocess.TimeoutExpired, which ended the builder thread while it kept the builder lock, so no replica built again. A clone killed at its timeout also left a HEAD that never resolves, and the HEAD-file check ran remote update on it forever. The loop and refs() now catch SubprocessError, fetch() re-clones when HEAD does not resolve, and the clone lands in repo.git.partial and replaces the mirror only after it succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:07:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 9f36a7ac6b perf(web-app-docs): build the deployed snapshot while the first mirror clone runs
run_builder fetched the git mirror before it claimed any build, so the deployed site, which copies the snapshot baked into the image and reads no mirror, waited for the first git clone --mirror (about 60-150 s on CI, 967 MiB with 1113 refs/pull/* heads). The builder now claims and submits DEPLOYED inside the pool before the first fetch; latest and the tags still wait for it, so none builds from an outdated head. The README no longer claims the fetch waits for a running build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:59:25 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4592f2bf78 test(web-app-taiga): pass a neutral config-append marker in the static URL test
test_used_keys_declared reads every INFINITO_* literal in code as an environment key, so the marker string copied from vars/main.yml failed the lint without a registry entry. The marker only lands in the append's header comment, which the test does not inspect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:07:46 +01:00
kevinveenbirkenbach c976f51504 Merge branch 'fix/web-app-weblate/swarm-static-mountpoint' into feature/store-i18n 2026-10-31 13:27:52 +01:00
kevinveenbirkenbach d5a8997ff4 Merge branch 'fix/web-app-taiga/onion-design' into feature/store-i18n 2026-10-31 13:27:46 +01:00
kevinveenbirkenbach ffcbd49883 Merge branch 'fix/web-app-docs/languages-404-duration' into feature/store-i18n 2026-10-31 13:27:39 +01:00
kevinveenbirkenbachandClaude Opus 5.5 880eeeac5e fix(web-app-taiga): serve Django admin static files from the vhost's own origin
taiga-back's image settings build STATIC_URL from TAIGA_URL, the canonical clearnet domain, so the Django admin on the onion vhost linked base.css and its scripts to the clearnet host and the onion CSP blocked them (header background rgba(0, 0, 0, 0) in the design spec). STATIC_URL is now the same-origin path below FORCE_SCRIPT_NAME. The Jinja static() global of the e-mail engine keeps the canonical absolute URL, because a mail client resolves no same-origin path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:08:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f0a70b8164 fix(web-app-docs): answer 404 for an untranslated language before the deployed build
The server answered 202 for every /deployed/<code>/ path until the deployed Sphinx build finished (about 80 min with -j1), so test-languages.js:61 outwaited its poll budget on attempt one and passed only on retry. The untranslated decision now reads the snapshot's meta/languages.yml and docs catalogs, the same files the build writes into the language index, so the 404 set is identical before and after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:58:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 299aa8e707 fix(svc-db-openldap): name the user whose record lacks LDAP entry fields
A user key that no role declares, such as an inventory leftover, gets no username, firstname, lastname, uid or gid derived. openldap_user_entries then crashed with a bare KeyError 'username'. The filter now raises an AnsibleFilterError that names the user and the missing fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:56:12 +01:00
kevinveenbirkenbach 2f2b004b1c Merge branch 'triage/nextcloud0-conductor-design' into feature/store-i18n 2026-10-31 10:05:41 +01:00
kevinveenbirkenbachandClaude Opus 5.5 815714592c fix(web-svc-logout): keep each conductor's sweep on its own network
CI run 41147980556 Nextcloud#0 failed both conductor design specs on a
multi-network node. e0179a0893 handed every conductor the other
network's conductor in LOGOUT_SIBLINGS. With that list set,
universal-logout v1.5.0 hides #iframe-warning on the stand-alone page
("'#iframe-warning' matches nothing visible"), and v1.5.1 fetches the
sibling after its sweep, which the conductor's per-network CSP blocks
("Connecting to 'https://logout.main.infinito.test/' violates ...
connect-src 'self' *.<onion>"). Requirement 037 limits each vhost's CSP
to its own network, so the browser hand-off could never complete in
either direction; its only effects were the two red specs.

Drop the LOGOUT_SIBLINGS wiring and its README bullet, so the compose
template and README match their state before e0179a0893. Each conductor
still logs out of every domain of its own network; ending the sessions
of the other network belongs on the server side, not in the browser.
The test now asserts that no conductor on a single-network, tor-only or
multi-network node is handed a conductor of another network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:05:29 +01:00
kevinveenbirkenbachandClaude Opus 5.5 0590104ce2 fix(svc-opt-swapfile): keep an active swapfile that already meets the requested size
A production redeploy of echoserver hung at the swap-forge task and the
host stopped answering SSH. swap-forge v1.0.1 recreates the swapfile
whenever its size differs from the requested MiB by any amount, starting
with swapoff on the live file. The request is the host's MemTotal, which
drifts by a few MiB across kernels, so a routine deploy paged the whole
swap back in, charged it to the containers' memory cgroups and starved
sshd until the OOM killer took swapoff.

Read the active swap files with swapon before swap-forge runs and skip
it when an active file is within 5 % of the requested size, reporting
the kept size against the request. The sizing formula is unchanged, so
no host is forced into a one-time recreate; a deliberate resize beyond
the tolerance still reaches swap-forge.

The unit test resolves the real vars through Ansible's Templar for MiB
drift both ways, no swap, zram and partitions, a halved size, an exact
pin, a lowercase unit and an escaped file name, and shows that the exact
MiB comparison swap-forge uses misses the drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:03:15 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ae40a573c2 fix(web-app-weblate): give the static tmpfs mountpoint its mode on swarm NFS volumes
CI run 40979252521 Weblate#1 (swarm) never converged: every task after
the first died in collectstatic with "PermissionError: [Errno 13]
Permission denied: '/app/cache/static/weblate_fonts'". The tmpfs at
/app/cache/static sits inside the shared weblate_cache volume; the first
task's runc creates the mountpoint as root 0755, and runc gives a tmpfs
on an existing mountpoint that directory's mode. 99a7d28067 declared
mode 1777, which compose honours, but docker stack deploy keeps only the
tmpfs size (docker/cli 29.9.2 convert/volume.go), so swarm still got a
root 0755 tmpfs.

- utils/storage/nfs.py: nested_tmpfs_mountpoints() derives every tmpfs
  mountpoint whose innermost enclosing volume of the same service is
  NFS-backed, with its declared mode; a nested tmpfs without a mode, or
  one mountpoint declared with two modes, raises.
- plugins/lookup/nfs_tmpfs_mountpoints.py feeds it the rendered compose
  volumes, so compose_volumes stays the source of which volumes live on
  NFS.
- sys-svc-compose nfs_prep.yml creates those mountpoints on the NFS
  server with the declared mode before every stack deploy, which also
  repairs a mountpoint an earlier deploy left at 0755.
- web-app-weblate: the cache volume declares nfs uid 1000, gid 0,
  mode 0770; a pre-created mountpoint makes the volume non-empty, so
  Docker no longer copies the image's /app/cache root in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:05:59 +01:00
kevinveenbirkenbachandClaude Opus 5.5 043d81f78e test(web-app-keycloak): stub the entity_name filter under its current name
main's admin allowlist test renders sys-svc-proxy's location/html.conf.j2,
which calls the entity_name Jinja filter. The test stubbed it as
get_entity_name, the name store-i18n retired when it registered the
filter as entity_name (plugins/filter/get/entity_name.py), so all eleven
cases failed with "No filter named 'entity_name'" after the main merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:54:06 +01:00
kevinveenbirkenbach 3504f17557 Merge remote-tracking branch 'origin/main' into feature/store-i18n
# Conflicts:
#	.github/workflows/call-test-deploy.yml
#	roles/sys-svc-compose/tasks/main.yml
#	roles/web-app-keycloak/TODO.md
#	utils/docker/registry.py
2026-10-30 23:36:38 +01:00
kevinveenbirkenbach 166da8f8d2 Merge branch 'feature/shared-stack-caches' into feature/store-i18n 2026-10-30 23:31:00 +01:00
kevinveenbirkenbach b97f38ca49 Merge pull request #1261 from infinito-nexus/fix/web-app-keycloak/admin-network-allowlist/1121
feat(web-app-keycloak): serve /admin only to declared source networks
2026-10-30 23:15:41 +01:00
kevinveenbirkenbach 8a19b024db Merge pull request #1265 from infinito-nexus/feature/sys-ctl-hlth-runtime/vulnerable-runtimes/1127
feat(sys-ctl-hlth-runtime): alert on EOL or unpatched PHP and missing PHP hardening
2026-10-30 22:45:25 +01:00
kevinveenbirkenbach 80814daf7c Merge branch 'triage/objstore-onion-assets' into integration/store-i18n-merge 2026-10-30 22:23:38 +01:00
kevinveenbirkenbach b75f55da0c Merge branch 'feature/logout-sibling-handoff' into integration/store-i18n-merge 2026-10-30 22:23:35 +01:00
kevinveenbirkenbachandClaude Opus 5.5 637acbaf2a test(web-svc-logout): parse the rendered compose file through load_yaml_str
test_compose_siblings.py called yaml.safe_load on the rendered compose file, which TestNoDirectYamlCalls rejects in favour of the utils.cache.yaml helpers (quality-high run 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:23:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 22614627d5 test(objstore): read web-app-wordpress meta through ROLE_FILE_META_SERVICES
test_s3_public_url.py built roles/web-app-wordpress/meta/services.yml with a pathlib chain, which TestRoleFileSpot rejects in favour of the utils.roles.mapping constant (quality-high run 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:19:46 +01:00
kevinveenbirkenbachandClaude Opus 5.5 74d99201e0 feat(cache): share package and tool caches across stacks and release swarm clusters after rescue
Parallel compose slots, swarm lab clusters and GUI lanes each kept their own pip, npm and pacman downloads, and a failed swarm run left its cluster (about 25 GB of node docker data plus the rescue bundle in the act container) on the root filesystem until someone ran make swarm-down. INFINITO_CACHE_SHARED_HOST_PATH (default <DIR_CACHE>/shared) is now mounted read-write at the same path into every compose slot and every swarm lab node, with PIP_CACHE_DIR and npm_config_cache below it; empty, the renders stay byte-identical to before. On a host whose os-release ID is exactly arch running an arch stack, INFINITO_CACHE_HOST_PACMAN_PATH mounts the host pacman cache read-only and scripts/docker/cache/pacman.sh makes it the first CacheDir with <shared>/pacman/ second, so new downloads land in the shared folder and the host cache stays untouched; on Manjaro and every other host pacman uses the shared folder alone.

make swarm-zombie without keep=true now copies the act job's rescue diagnostics and Playwright artefacts to the host and then releases the lab nodes, the lab network, the NFS export volume and the act outer container, on success and on failure; when the copy fails the nodes still go, the act container stays and the run exits naming make swarm-down. make roundtrip passes keep to every swarm step, make clean-cache removes the shared folder, and the disc-space cleanup skips pacman cache directories it cannot write. apt and dnf keep their per-container caches behind the package-cache proxy, and no docker data root is shared between daemons. Requirement 065.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:04:38 +01:00
kevinveenbirkenbach 2df447821d Merge branch 'triage/objstore-onion-assets' into integration/store-i18n-merge 2026-10-30 21:57:50 +01:00
kevinveenbirkenbach 227c861e34 Merge branch 'feature/logout-sibling-handoff' into integration/store-i18n-merge 2026-10-30 21:57:48 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f287322065 fix(objstore): serve public bucket files same-origin on every vhost of the consumer
WordPress put objstore.public_url, the clearnet api.seaweedfs.s3 host, into S3_UPLOADS_BUCKET_URL, so the onion pass of WordPress#0 (CI run 40979252521) loaded the site logo from clearnet and the leak spec failed; the /objstore rebase only fired on onion-only consumers and never on a multi-network node. lookup('objstore', app, 'relative_url') now returns /_infinito/objstore/<bucket> for a shared consumer with a public bucket and an API domain, and locations.conf.j2 serves that path on every vhost of the consumer, clearnet and onion alike, by proxying over the loopback to the provider's own vhost.

The route passes GET and HEAD only, answers 404 for a path ending in / (no ListObjects), drops the query string (no S3 subresources or presigned queries) and the Authorization and Cookie headers, so only the public bucket's anonymous read applies, and adds nosniff plus a sandbox CSP; private and embedded buckets get no route. Only web-app-wordpress switches to relative_url: mastodon, pixelfed, baserow, bookwyrm, snipe-it, listmonk, mediawiki, shopware and peertube need absolute URLs for federation, mail or server-side rendering, and decidim and funkwhale sign URLs, which the route deliberately refuses. The prefix moves from /objstore to /_infinito/objstore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:52:35 +01:00
kevinveenbirkenbachandClaude Opus 5.5 e0179a0893 feat(logout): hand the browser on to the conductor of the next network
A node serving several networks runs one universal-logout conductor per network, and a page served over HTTPS cannot fetch the HTTP onion hosts, so one conductor cannot clear the other network's sessions. compose.yml.j2 now gives every conductor the url.origin of the other networks' conductors in LOGOUT_SIBLINGS (empty on a single-network node, which disables the hand-off), and the Keycloak logout panel that frames the conductor follows its handoff report to the next conductor once the sweep is done, accepting only an http or https URL.

The pinned image v1.4.2 ignores LOGOUT_SIBLINGS and never reports handoff, so behaviour changes only with the bump to the universal-logout release that carries the reachability probe. test_compose_siblings.py renders single- and multi-network nodes; logout-panel.test.js covers the handoff branch and rejects non-web URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:46:00 +01:00
kevinveenbirkenbachandClaude Opus 5.5 cb18214760 fix(tests): resolve CodeQL unused PROJECT_ROOT in two test packages
CodeQL py/unused-global-variable flagged two test package inits
(code-scanning alerts #1068 and #1072):

- tests/unit/python/roles/web-app-fider/__init__.py exports PROJECT_ROOT
  to test_tenant_bootstrap_gate.py through `from . import PROJECT_ROOT`
  but declared no __all__. Declare it, as every other test package
  that re-exports PROJECT_ROOT does.
- tests/unit/python/roles/sys-front-tls/__init__.py defined a
  PROJECT_ROOT nothing imports: the only test lives in templates/, whose
  own __init__.py provides it. Leave the role-level package init empty,
  like the other role-level test packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:10:21 +01:00
kevinveenbirkenbach e22cffe201 Merge branch 'triage/docs-cli-disc-space' into integration/store-i18n-merge 2026-10-30 21:09:51 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c68b9833bf fix(web-app-erpnext): grant the API bot Script Manager when server scripts are on
Frappe v16 limits the Server Script doctype to the Script Manager role and ServerScript.validate calls frappe.only_for('Script Manager'), so services.erpnext.server_scripts: true still left every integration on erpnext-api-bot with 403 Insufficient Permission for Server Script. The same switch now grants the bot Script Manager when true and revokes it when false; System Manager stays, the save runs only on a change, and an undeclared value fails before the site is touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:07:26 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4f725d6fa9 fix(web-app-docs): replay the guide under the stack host's disc-space limit
The CLI test replays the published Production block in a throwaway target that shares the runner's disk. The replayed deploy knew only the default SIZE_PERCENT_CLEANUP_DISC_SPACE of 85 while the stack host runs with its own inventory value, so sys-ctl-hlth-disc-space failed in the target at 87% (Wazuh#1, CI run 40979252521). disc_space.sh writes the stack host's value into the block's provision --vars and fails on a non-integer value or a block without --vars; the published instructions stay unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:34:27 +01:00
kevinveenbirkenbach 5e39b946c9 Merge branch 'fix/sys-svc-compose/pull-test-cold-start' into integration/store-i18n-merge 2026-10-30 19:33:56 +01:00
kevinveenbirkenbach 9c7b615743 Merge branch 'triage/wordpress1' into integration/store-i18n-merge 2026-10-30 19:33:38 +01:00
kevinveenbirkenbach a785f6d956 Merge branch 'triage/wordpress0-editor-canvas' into integration/store-i18n-merge 2026-10-30 19:33:32 +01:00
kevinveenbirkenbach c5a0e5445e Merge branch 'feature/037-onion-network-aligned-config' into integration/store-i18n-merge 2026-10-30 19:33:27 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b5736bda44 feat(web-app-wordpress): serve each multisite site on its own network sibling
CI run 40979252521 WordPress#1 (multisite, multi network): a request on a network sibling resolved to the clearnet site, because WordPress picks the site by Host before any mu-plugin runs and the network kept only clearnet base URLs per site. Operator decision: a real multisite per network with real child domains (option B).

infinito-network-origins.php is now also installed as wp-content/sunrise.php (SUNRISE=true), so the sibling host resolves to its own site before mu-plugins load; a guard constant stops the second load from mu-plugins. WORDPRESS_SITE_DOMAINS holds the canonical domains in the first domain's network, WORDPRESS_NETWORK_BASE_URLS carries every sibling of each site's base URL (sites ';', siblings ','), and INFINITO_NETWORK_BASE_URLS takes that list. Child sites are inserted by files/php/multisite/sites.php only where no site exists on the domain and path, replacing wp site list plus wp site create. The multisite variant's child domains move from server.domains to domains.canonical.

Covered by 29 Python tests and two PHPUnit suites; PHPUnit and a variant 1 deploy with an onion pass still have to run (store-i18n gate and deploy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:49:33 +01:00
kevinveenbirkenbachandClaude Opus 5.5 7b6800493b test(sys-svc-compose): give the streamed-pull test a silence window above a loaded cold start
test_run_streamed_outlives_a_deadline_while_it_keeps_talking patched RUN_SILENCE_SECONDS to 1.0 while the chatty child prints its first line only after the interpreter has started. run_streamed measures silence from process start, which is right for production (a command that never speaks must time out), so under xdist load on a full disk the cold start alone exceeded the window and the run returned 124 (reported from IAM's make quality).

The window is now 2.0 s against a 0.1 s cadence over 40 lines (4 s, still twice the window, so the test keeps proving it outlives a deadline while talking), and the child runs with -I -S to skip the site import. With a simulated 1.5 s cold start the old parameters return 124 and the new ones 0. pull.py is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:43:39 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f12d328fee fix(sys-front-tls): strip Secure from upstream cookies on every plain-HTTP vhost
On an onion vhost the browser talks plain HTTP, so an upstream that marks its session cookie Secure (the app still believes in its canonical https origin) loses that cookie and the login loops. n8n carried its own proxy_cookie_flags n8n-auth nosecure for this; every other app on the onion pass was exposed to the same failure.

The plain-HTTP branch of nginx_ssl_header.j2 now emits proxy_cookie_flags ~ nosecure, which matches every cookie; TLS vhosts are unchanged. n8n's per-cookie line goes. Taiga and Semaphore keep their tested location-level flags, which replace the server rule for their location, and network_modes.md says so. test_nginx_ssl_header.py asserts the rule on a plain-HTTP vhost and its absence on a TLS one; without the template line the plain-HTTP case fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:10:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 55604ca19c fix(web-app-wordpress): load the editor canvas design tokens from the page's own origin
CI run 40979252521 WordPress#0 (swarm, variant 0, onion pick): the block editor canvas stayed transparent (rgba(0, 0, 0, 0) instead of --design-surface-1). The trace shows the canvas iframe requesting https://cdn.main.infinito.test/_shared/css/default.css and the onion page's CSP (style-src-elem 'self' http://cdn.<onion> ...) blocking it; the clearnet pass of the same spec was green. design.php writes WORDPRESS_DESIGN_CONFIG.tokens into the infinito_design option once for every network sibling, so an absolute CDN host pins the onion editor to clearnet.

The token sheet now uses the cdn lookup's relative_urls, rendering /_infinito/cdn/_shared/css/default.css. WordPress prefixes a relative style source with site_url(), which infinito-network-origins derives from the request host, and every generic vhost serves /_infinito/cdn/ (basic.conf.j2 includes cdn.conf.j2), so the canvas loads from the page's origin on both networks. Same approach as the Joomla TinyMCE fix 175e00218f. test_design_tokens.py renders the tokens through the real cdn lookup and fails on the old URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:04:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 41ea801fd9 feat(network): point logout and peer allowlists at every network a vhost serves
CI run 40979252521 Nextcloud#0: on the onion pass the biber OIDC logout cleared only the clearnet session, because Keycloak holds one front-channel URL per client and the injected logout sent no post-logout target for the onion network; and Collabora's aliasgroup1 listed only the clearnet WOPI host.

On a vhost outside the application's canonical network (and without an external issuer) the injected logout one-liner now adds client_id and post_logout_redirect_uri set to that network's logout conductor, and Keycloak registers every conductor sibling as post-logout redirect URI next to '+'. Collabora's aliasgroup1 lists the Nextcloud WOPI host of every network (one instance, union allowlist). network_modes.md gains a Network-Aligned Values table, and test_network_allowlist_sinks.py fails when a template names an allowlist sink without the sibling union.

Rendering is covered by 14 tests; the full gate and the runtime acceptance of the '##' post-logout list by Keycloak and the comma aliasgroup by Collabora still need a gate on store-i18n and an onion deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:00:24 +01:00
kevinveenbirkenbach d9a65873de Merge commit '455d06552a' into feature/store-i18n 2026-10-30 17:29:51 +01:00
kevinveenbirkenbachandClaude Opus 5.5 455d06552a fix(web-app-baserow): keep the idle database connection alive across the swarm VIP
CI run 40979252521, Baserow#1 (swarm, variant 1): the PASS 2 MCP probe got a 500 and the deploy refused the endpoint. In replica 3, worker 244 served MCP at 11:20:40 and then sat idle for 3824 s; the next request at 12:24:24 failed with "OperationalError: server closed the connection unexpectedly", and every later one, including the 13:09:10 probe, with "InterfaceError: connection already closed". Postgres stayed up throughout. The swarm service VIP (IPVS) drops TCP idle for 900 s.

BASEROW_DATABASE_URL now carries libpq keepalives (keepalives_idle=60, interval 10, count 6, tcp_user_timeout 120000), the same set as the OpenProject swarm fix. test_database_keepalive.py parses the rendered URL with libpq's parse_dsn and fails on the old URL.

Baserow 2.4.1 mounts its MCP Starlette app before Django's handler, so close_old_connections never runs there and a dropped connection stays dead until the worker restarts; that half is upstream. A server-side keepalive on svc-db-postgres that replaces the per-role options is tracked in #1298.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:52:50 +01:00
kevinveenbirkenbachandClaude Opus 5.5 2393e8ddba fix(web-app-keycloak): reject scoped IPv6 admin networks, drop module docstring
Addresses two CodeRabbit findings on PR #1261.

1. Scoped IPv6 (thread 4421596356): ipaddress.ip_network accepts a scope
   ID, so `fe80::1%eth0` resolved to `fe80::1%eth0/128` and the vhost
   would carry `allow fe80::1%eth0/128;`, which nginx refuses to load.
   kc_admin_networks now raises AnsibleFilterError for any entry whose
   network address carries a scope ID. test_admin_allowlist.py covers a
   scoped address and a scoped CIDR; both fail against the previous
   filter.
2. Module docstring (thread 4421596351): removed; it described the
   module instead of documenting parameters or a trip-wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:52:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 3c13d3e0ca fix(web-app-nextcloud): judge the Talk TURN assert on the stack host only
Swarm run 2 of the 061 Nextcloud step failed on every worker with "Assert Talk has the TURN server its calls relay through". The TURN server comes from web-svc-coturn's group membership (meta/services.yml standalone_enabled), which only the stack host carries; workers never run Talk, so the assert judged hosts it has nothing to say about. It came in with 5728502a19 and exists only on this line, not on main.

The assert now runs where IS_STACK_HOST holds: every compose host and the swarm manager, unchanged. test_talk_turn_assert.py evaluates the block and task guards plus the assertion with Jinja; against the unfixed file only the worker case fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:02:31 +01:00
kevinveenbirkenbachandClaude Opus 5.5 11308259b3 test(sys-svc-container): read the role vars through ROLE_FILE_VARS_MAIN
Gate pass 5 flagged test_gpu_detection.py:31 in test_file_spot: the
`ROLE / "vars" / "main.yml"` chain hard-codes a role-file path the
utils.roles.mapping SPOT already names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:43:02 +01:00
kevinveenbirkenbachandClaude Opus 5.5 137afc60cc fix(sys-svc-container): require device and driver for a GPU host, refuse a driverless bare host
Follow-up to ae259a9f58 after an independent review:

- The 136-character task name broke the 80-character name lint
  (tests/lint/repository/test_name_emoji_and_length.py). Names are short
  again; the device/driver/host matrix lives in the role README.
- Both stats now always run and lookup('gpu') requires both registers,
  instead of reading a skipped device stat as "no device".
- A bare host that shows /dev/nvidia0 without nvidia-smi stops the
  deploy with a message naming the fix. It used to die on the missing
  binary anyway; ae259a9f58 would have run its GPU services on the CPU
  silently. A container in that state (a swarm lab node, the dev
  container without its GPU override) still runs on the CPU.
- svc-ai-s1 decided its CUDA build from its own stat of /dev/nvidia0 and
  nvidia-container-runtime while its runtime came from lookup('gpu'), so
  a host with the toolkit but no nvidia-smi would have built a CUDA image
  that gets no GPU. S1_GPU now reads lookup('gpu'), the one detection.
- The libretranslate-engine README and the s1 TODO describe the new
  detection.

test_gpu_detection walks the real tasks over device, driver and
container; against the original 00_core.yml it fails in the
driverless-container, driverless-bare and device-and-driver cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:57:24 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ae259a9f58 fix(sys-svc-container): count a host as GPU host only beside the NVIDIA driver
The local 061 Nextcloud swarm step failed on every lab node in
"Ask the driver which CUDA devices this host carries" with
"[Errno 2] No such file or directory: b'nvidia-smi'". The lab nodes are
privileged containers (compose/swarm/compose.yml), so on a workstation
with an NVIDIA card they see the host's /dev/nvidia0, but they carry no
driver. The device node alone made them GPU hosts, for the GPU include
and for lookup('gpu') alike, which would also have picked a -cuda image
and runtime: nvidia for a GPU service there.

The role now stats SYS_SVC_CONTAINER_GPU_DRIVER (/usr/bin/nvidia-smi)
first and stats the device node only where the driver exists; the
skipped register reads as "no device" in lookup('gpu'), and the include
requires a device stat that ran and found the node. The query runs the
same binary the detection probed. Hosts with driver and device (bare
metal with the driver, the compose dev container through the GPU
override) and hosts without a device (CI) behave as before.

test_gpu_detection walks the real tasks over the four driver/device
cells and fails on the old 00_core.yml in exactly the device-without-
driver cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:44:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 46962b5110 Merge branch 'feature/updater' into feature/store-i18n
Brings in feature/updater 8c14873225: lockdown-probe with the Bluesky
broker key-rotation fix (0a6b6f9130) and the occ-only trusted_proxies
(406998f43a), the ERPNext server_scripts switch, and main with the
hotfixes #1290 (BBB apps-akka READ_COMMITTED), #1291 (FusionDirectory
PHP_MEMORY_LIMIT) and #1293 (Matrix variant 1 force_verification false).

Conflict resolutions:
- web-app-erpnext: create_site.yml takes updater's include of
  restart_frappe.yml; that file's compose/swarm gates use
  IS_COMPOSE_MODE / IS_SWARM_MODE instead of DEPLOYMENT_MODE literals.
- web-app-mailu, web-app-stalwart vars: store-i18n's RUF from
  users.platform_administrator plus the new RUA vars; dmarc.rua_user is
  platform_administrator (on store-i18n `administrator` is the host
  account), so RUA and RUF share the app mailbox. READMEs follow.
- web-app-stalwart 09_dns-records.yml: store-i18n's block form with the
  alias-domain MX task, plus rua=mailto in the DMARC record.
- web-app-pretix: store-i18n's source build stays; the version bump
  lands as source_version v2026.9.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:16:23 +01:00