Commit Graph
554 Commits
Author SHA1 Message Date
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
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 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
kevinveenbirkenbachandClaude Opus 5.5 d5a7152575 test(web-app-erpnext): read the script through the shared cache in the server_scripts test
test_no_uncached_file_reads flagged four direct reads in test_server_scripts.py. The script comes from the project tree and is now read through utils.cache.files.read_text. The three reads of common_site_config.json stay direct and carry `# nocheck: cache-read tempdir fixture`: the file lives in a temporary directory and the script under test rewrites it within the same test, so a cached read would return stale content. Each read sits on its own line so ruff format keeps the marker beside it. Quality-high pass 16 ran green with this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:43:47 +01:00
kevinveenbirkenbachandClaude Opus 5.5 131e8ea108 fix(web-app-suitecrm): keep the onion sibling's SAML login on its own host
CI run 40979252521 SuiteCRM#0 (swarm) picked the onion sibling and every
signed-in design spec failed on "the administrator owns a SuiteCRM
session" (100 times in the job log). The SAML ACS is
%env(SITE_URL)%/saml/acs and SITE_URL is the single canonical URL, so the
trace shows Keycloak posting the assertion to
https://suite.crm.main.infinito.test/saml/acs; the session landed on
clearnet and the onion host bounced back to /saml/login. Keycloak would
also have refused an onion ACS, because the SAML client listed only the
clearnet one.

- SuiteCRM: an auto_prepend_file (site_url.php, config-mounted) sets
  $_ENV['SITE_URL'] to the network sibling the request arrived on,
  matched only against the sibling URLs rendered at deploy time;
  Kernel::setSiteURLEnv keeps an existing value. The canonical host and
  CLI runs keep the installer's site_url. The mount target is read from
  the volume registry.
- Keycloak: the SAML client's redirectUris come from
  lookup('network_urls', <app>/saml/acs). Additive: clearnet stays first
  and a single-network node renders the same one-entry list.

Executing the rendered site_url.php in PHP sets the onion URL for the
onion host (with and without :80) and leaves SITE_URL unset for the
clearnet host, a forged host and the CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:32:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 8f5937d3a4 fix(web-app-espocrm): keep the onion sibling's OIDC callback on its own host
CI run 40979252521 EspoCRM#0 (swarm) picked the onion sibling and failed
the OIDC addon spec plus every signed-in design spec: EspoCRM builds its
redirect_uri as siteUrl + /oauth-callback.php from the single canonical
URL, so the onion login popup returned to
https://espo.crm.main.infinito.test/oauth-callback.php (12 times in the
job log), oidc.js could not read the code across origins and the onion
host never got a session.

A data/config-override.php, which Config::load reads on every request,
now answers siteUrl with the network sibling the request arrived on,
matched against the sibling URLs rendered at deploy time and nothing
else. The canonical host and CLI runs see no override. The manager ops
write it through container exec with a compare-and-rename that only
reports a change when the content moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:12:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 99a7d28067 fix(web-app-weblate): let the uid-1000 containers write their static tmpfs
CI run 40979252521 Weblate#1 (swarm) never converged in its first pass:
every weblate, worker and beat task died on collectstatic with
"PermissionError: [Errno 13] Permission denied:
'/app/cache/static/weblate_fonts'". The per-container tmpfs from
a0bcd59150 sits on the weblate_cache volume, whose static mountpoint is
root-owned 0755 (nfs-volume-contents.txt: drwxr-xr-x 0:0), and runc gives
a tmpfs on an existing mountpoint that directory's mode, while Weblate
runs as uid 1000.

The three static tmpfs mounts now declare mode 1777, rendered as
tmpfs.mode 1023; test_static_root_per_container.py asserts it per
service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:08:51 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5faa21c458 fix(web-app-matrix): start Synapse on the rendered config before MDAD creates users
CI run 40979252521 Matrix#3 (ansible flavor) failed the MDAD bootstrap
in the pass after the credential rotation at "Ensure Synapse user
registered - baibot". store-i18n runs the playbook with
setup-all,ensure-matrix-users-created,start, so the user creator runs
before start. After the reconfigure restart systemd had already booted
matrix-synapse on the previous pass's homeserver.yaml, which failed
against the rotated database password ("password authentication failed
for user matrix" in the nested journal) and crash-looped. The user
creator's `state: started` returned ok for the still-activating unit,
skipped its pause, and the registration found no container.

bootstrap.sh now stops an installed matrix-synapse.service right before
the playbook, so the user creator starts it on the configuration the
playbook has just rendered and waits for it. A first install has no unit
yet and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:06:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 7947e80f9d feat(web-app-erpnext): switch Server Scripts through services.erpnext.server_scripts
applications.web-app-erpnext.services.erpnext.server_scripts (bool, default false) now sets server_script_enabled on every deploy, so an inventory can turn ERPNext Server Scripts on and off again. The security check app needs them, and the validator aborted on its inventory with "Missing default for web-app-erpnext: services.erpnext.server_scripts" while core declared no such key.

Frappe v16 reads the key only from common_site_config.json, through a cached lookup (frappe/utils/safe_exec.py:86-88), and tests it with bool(). files/python/apply/server_scripts.py therefore writes the global file, never the site config, and stores the integer 1 or 0: a plain `bench set-config -g server_script_enabled 0` stores the string "0", which bool() reads as enabled. A value already stored as a string is normalised. The script reports changed only when the value differs, and only then are the Frappe processes restarted, so they drop the cached config.

The restart, the backend re-resolve and the gunicorn wait move from create_site.yml into 02_bench_bootstrap/restart_frappe.yml, unchanged apart from the task names, so the new site and the switch share one restart path for compose and swarm. Six unit tests cover enabling, disabling a string value, the string "0", an aligned value left untouched, the capitalised bool Ansible renders and a rejected value; storing strings instead fails three of them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:05:02 +01:00
kevinveenbirkenbachandClaude Opus 5.5 68c8e8620f fix(web-app-homeassistant): check the realigned owner against the recreated hub
CI run 40979252521 homeassistant#0 (swarm) failed in the pass after the
credential rotation: the owner realign rewrote the auth store and flushed
the recreate handler, which in swarm runs `docker service update
--force` and starts a new task container. The following owner check
still exec'd into the container id resolved before the flush and hit
"container eb0523a644de... is not running" 30 times, while the rescue
dump lists eb0523a644de exited and bbec666459ae up and healthy.

The resolve_host_cid include moves into tasks/utils/resolve.yml; 00_core
uses it as before and owner/realign.yml runs it again right after the
flush, so the check and the MCP tasks after it talk to the new
container. Compose keeps its stable container name, so nothing changes
there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:03:47 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4607f57ebe Merge remote-tracking branch 'origin/main' into feature/updater-lockdown-probe
Brings 20 commits from main: the DMARC aggregate report address (rua) for Mailu and Stalwart with its hardened checks, the Stalwart production facts and version pin update, the Pretix and Shopware image updates, and ruff 0.17.3. The merge was clean; the auto-merged YAML files carry no duplicate keys and the merged Python files are ruff-format clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:03:22 +01:00
kevinveenbirkenbachandClaude Opus 5.5 a4932f6a40 fix(web-app-taiga): serve the onion sibling's login and logo on its own host
CI run 40979252521 Taiga#0 picked the onion sibling and failed five specs
(test-login-oidc, test-themed-routes, network-leak, design:242/263).

- Login: TAIGA_COOKIE_SECURE renders True once clearnet has TLS, so
  taiga-back marks sessionid and csrftoken Secure. Chromium drops them on
  the plaintext onion, the /oidc/callback/ arrives without a session and
  mozilla-django-oidc falls into login_failure() (gateway log: the 302 to
  /login?type=oidc&error=None six times, no sessionid on the callback in
  the trace). The plaintext sibling vhost now strips Secure from both
  cookies via proxy_cookie_flags, wired as webserver_location_extra_
  configuration like Semaphore; TAIGA_COOKIE_SECURE is pinned to the
  canonical domain so the per-vhost tls lookup cannot flip it.
- Logo: the design sheet embedded absolute clearnet CDN URLs, rendered
  once and served to both siblings. It now uses design.relative_urls,
  served same-origin under /_infinito/cdn/; the spec resolves the logo
  URLs against page.url().

A local compose deploy that picked the onion passed all five specs (19
passed); the onion vhost carries both proxy_cookie_flags lines, the
clearnet vhost none. test_onion_sibling.py renders the real vars through
Ansible's Templar; reverting TAIGA_COOKIE_SECURE fails two of its cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:30:30 +01:00
kevinveenbirkenbach 37cade8df6 Merge pull request #1255 from infinito-nexus/fix/mail/dmarc-rua/1125
feat(mail): publish a DMARC aggregate report address (rua)
2026-10-30 08:19:06 +01:00
kevinveenbirkenbachandClaude Opus 5.5 3f6f762397 perf(web-app-nextcloud): skip the no-op enable after a fresh install and set plugin config in one occ boot
Fresh compose deploy (compose-deploy-req061-4.log) spent 764.9 s in the plugin phase. Two parts of it did no work:

Enable stage after a fresh install: occ app:install runs enableApp and prints "<app> enabled" (core/Command/App/Install.php, stable34), so the app:enable that 03_enable.yml ran next only answered "<app> already enabled". The include, the exec, the classify, the skip notice and the queue cost 90.9 s for the 26 plugins installed fresh. nextcloud_install_status now reports enabled when occ exited 0 and printed an "<app> enabled" line; 02_install.yml then queues the plugin for the configure stage directly and runs 03_enable.yml only for an install that left the plugin off ("already installed" from a pinned archive or the recognize background job, or a future --keep-disabled). Queue order is unchanged.

Configure stage: 49 pending config:app:set calls ran as 49 occ processes in one sh -s and took 62.8 s, about 1.28 s of occ boot each. files/php/utils/occ_batch.php boots the console the way Nextcloud's console.php does (lib/base.php, OC\Console\Application::loadCommands, setAutoExit(false)) and runs Application::run once per call, so every call goes through the same command object, app loading, ConsoleEvent and exit code as occ, with one boot. The filter nextcloud_occ_config_set_batch builds the argv per entry; a unit test proves it equals shlex.split of the former shell line for typed, quoted, multi-line, empty and non-ASCII values. The calls travel as base64 JSON assigned in front of the script on stdin, so no value reaches a host or container argv; the first failing call stops the run and names appid/configkey on stderr, the task still reports changed on "set to".

Expected saving per fresh deploy: about 74 s (enable chain, net of the skipped include and the new set_fact, 0.64 s per plugin) plus about 60 s (configure exec down to one boot), about 134 s of 764.9 s. A converged mode=update deploy installs nothing and writes about 3 values, so it saves about 2.5 s; only a live deploy can confirm both numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 01:21:42 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4b1dba6b78 test(sys-svc-container): render the userland-proxy test with store-i18n's daemon variables
The test from feature/updater renders the real daemon.json.j2, which on
store-i18n also reads IS_SWARM_MODE, the NVIDIA runtime shim and the
daemon MTU. Its context lacked them, so both cases died with
"'IS_SWARM_MODE' is undefined". The context now carries them and derives
the mode booleans from DEPLOYMENT_MODE like test_sandbox_runtime does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:16:47 +01:00
kevinveenbirkenbach 6b026b40c9 Merge branch 'feature/updater' into feature/store-i18n 2026-10-30 00:48:49 +01:00
kevinveenbirkenbachandClaude Opus 5.5 44ff41c875 style: rewrap three test lines past ruff's line length
make autoformat output for three lines the feature/updater merge brought
in over 88 characters; matches feature/updater-lockdown-probe 88d138ba1c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:48:41 +01:00
kevinveenbirkenbachandClaude Opus 5.5 88d138ba1c style: rewrap three test lines past ruff's line length
make autoformat rewraps the firewall-backend entry in test_userland_proxy.py, the _direct_media call in test_talk_direct_media.py and the _git listing context in test_files.py, which ran past 88 characters. The text is ruff's own output, the same store-i18n commits, so the next merge between the lines resolves cleanly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 00:46:44 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5e8890d208 Merge branch 'feature/updater' into feature/store-i18n
Brings in feature/updater accb22da75, which carries main 5995766f58.

Conflict resolutions:
- web-app-fider: main's tenant bootstrap (01_tenant.yml) runs first on
  the stack host, OIDC setup moves to 02_setup_oidc.yml, design to
  03_design.yml; flat includes so main's gate test reads them.
- sys-front-tls test_nginx_ssl_header: the tls fake maps the domain term
  store-i18n passes back to its app.
- group_vars 04_webserver.yml, sys-svc-proxy CSP header: store-i18n's
  webserver_resource lookup plus #1256's WEBSERVER_SECURITY_HEADERS and
  the security_headers.lua include after the CSP block.
- web-app-nextcloud: updater's decoupled Talk stack (Janus, signaling,
  NATS, recording builds) replaces aio-talk; store-i18n's per-network
  Talk backends move from the wrapper script into signaling.conf.j2 urls
  (NEXTCLOUD_HPB_SIGNALING_BACKEND_URLS from network_urls, canonical
  first); IS_COMPOSE_MODE/IS_SWARM_MODE booleans kept; the integration
  OpenProject digest stays delegated to STACK_HOST.
- users lookup docs, token-store write_many, cache README, meta info
  lint: both sides combined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:40:05 +01:00
kevinveenbirkenbachandClaude Opus 5.5 6da4e49db3 fix(web-app-nextcloud): let the recording server verify self-signed TLS through the injected CA bundle
On a clearnet deploy with self-signed TLS, test-talk-recording.js failed three times: the call recording stayed at state 4 (starting audio) because the recording server could not notify Nextcloud. Its log showed CERTIFICATE_VERIFY_FAILED on POST /ocs/v2.php/apps/spreed/api/v1/recording/backend, although compose-ca sets REQUESTS_CA_BUNDLE to the mounted bundle. nextcloud-talk-recording's BackendNotifier sends through `session.send(prepared, verify=True)`, and Session.send() skips the environment merge that only Session.request() does, so requests falls back to certifi's bundle, which does not know the injected root CA. A probe in the recording container's network namespace verified Nextcloud with the mounted bundle (200) and failed without it.

When the CA is injected, the recording image now points certifi's cacert.pem at the mounted CA bundle (the system CAs plus the injected root), in the same build step as the Firefox certificate policy. Verification stays on. Without an injected CA, as with real certificates, the build arg is empty and the image is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 00:19:56 +01:00
kevinveenbirkenbachandClaude Opus 5.5 0a52dc946e Merge branch 'main' into feature/updater-lockdown-probe
Conflict in roles/web-app-gitea/tasks/01_manager_ops.yml: main (c59e2838d6, c2577560f8) moved the readiness probes inside the Gitea container on its hosting node, because a host port lands on the swarm ingress mesh; this line (381c3cd5bc) had reduced them to one wait for /api/v1/version = 200 and dropped the explicit `gitea migrate`, which raced Gitea's own migration on galaxyserver. The resolution keeps main's in-container probe as that single wait with `curl -fS` (200 required), after the resolve and with this line's 600 s budget scaled by lookup('timeout'), and drops main's app.ini wait, its migrate task and its post-admin readiness wait.

main's new tests/unit/python/roles/sys-front-tls/templates/test_nginx_ssl_header.py renders nginx_ssl_header.j2 without `domain`, which ssl_credentials.j2 needs on this line since 47160d755e; its render() now passes the vhost's domain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 00:08:59 +01:00
kevinveenbirkenbach 7404aa4e6a Merge remote-tracking branch 'origin/main' into fix/web-app-fider/tenant-without-sso 2026-10-29 23:55:04 +01:00
kevinveenbirkenbachandClaude Opus 5.5 73b21eef98 fix(web-app-fider): bootstrap the tenant without SSO
00_core.yml included 01_setup_oidc.yml only when services.sso.enabled
was true, but that file carried the whole tenant bootstrap: wait for
HTTP, count tenants, POST /_api/tenants, create the admin user and
activate the tenant. Only its last task configured the Keycloak
provider. With SSO off no tenant was ever created.

Fider's RequireTenant middleware then answers every tenant route,
/api/v1/tags included, with 307 to /signup, and /signup is open to any
visitor, who can claim the fresh installation and become its
administrator. The MCP read fider_list_tags got that 307 in the swarm
SSO-off matrix row.

The tenant bootstrap moves unchanged into tasks/01_tenant.yml, which
00_core.yml includes on the stack host regardless of SSO. The OIDC file
becomes tasks/02_setup_oidc.yml and keeps only the provider upsert,
still gated on services.sso.enabled. The README states which bootstrap
steps run without SSO.

A unit test evaluates the include and task guards for every SSO,
stack-host and deploy-mode state: a fresh stack host posts exactly one
tenant with and without SSO, an existing tenant is never posted again,
the Keycloak provider is written only with SSO, and a non-stack host
runs none of it. The compose matrix row with SSO off stayed green
because nothing there read a tenant route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:55:01 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5728502a19 feat(web-app-nextcloud): run the Talk backend from pinned upstream components
nextcloud/aio-talk bundled signaling, Janus, NATS and an onboard TURN server behind a start script that rewrote their configs, and AIO publishes only build stamps, so the updaters could not raise it and the role had to guard Janus against config drift. The backend now runs as talk (Janus built from a meetecho/janus-gateway tag, files/Dockerfiles/Janus), talk_signaling (upstream strukturag/nextcloud-spreed-signaling image), talk_nats (official nats image) and talk_recording (built from a nextcloud-talk-recording release tag, files/Dockerfiles/Recording), each semver-pinned, single-replica, healthchecked and resource-limited. The Dockerfiles take every value as a build arg from compose, and Janus and the recording server get their config path through `command:`.

The role renders janus.jcfg, the signaling server.conf and the recording server.conf; they match what AIO stamp 20260409_094910 generated apart from the service addresses and the session keys, which are now derived from the signaling secret. Secrets reach the services only through those files. Janus may join the host network in compose mode, which keeps direct media available there, and an assert refuses host mode in swarm. talk and talk_recording keep their keys, so one deploy replaces the AIO containers in place.

The onboard TURN server is gone with its ports, vars and credential, so Talk uses web-svc-coturn only, and the deploy now stops when Talk is enabled without it instead of handing out no STUN or TURN server. The recording image installs the injected root CA as a Firefox policy when the CA is mounted, so recordings work against self-signed TLS, and the recording scratch volume stays off NFS. The Janus drift guard and the janus_config_drift filter are removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 23:42:04 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4a14a49da9 fix(web-app-nextcloud): decide Talk direct media in the role and keep it opt-in
services.talk.direct_media was an expression in meta/services.yml that read ansible_facts['all_ipv4_addresses'] and nested config lookups. lookup('config') renders it where the applications are merged without host facts; the render fails there, the lookup hands back the raw expression and `| bool` coerces it to False with a deprecation warning. Direct media therefore never switched on: both the first requirement-061 deploy and CI run 40877408319 logged the coercion, and a render in the development container returned the raw string without facts and true with them.

meta/services.yml keeps only the inventory switch, now `direct_media: false`, so every Talk stream goes through web-svc-coturn unless an operator opts a host in. That is the behaviour the bug produced everywhere, so no host changes on deploy, and it keeps Janus off the public network by default. vars/main.yml decides in task context from the switch, Talk, standalone TURN, the bridged network, compose mode and NEXTCLOUD_HPB_HOST_ADDRESSES, which reads the facts the way web-svc-coturn does. The flag tests render that chain against the merged applications: off by default, on for an opted-in compose host with coturn and an owned address, off for host networking, a foreign or loopback address, swarm, onboard TURN and missing facts. Administration.md documents the opt-in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 23:37:29 +01:00
kevinveenbirkenbachandClaude Opus 5.5 16975a1e14 perf(sys-svc-container): publish ports through the kernel instead of a docker-proxy per port
With the userland proxy on, Docker starts one docker-proxy process for every published port. A published range pays that per port: Janus's direct-media range showed 74 proxies for 64 UDP ports plus the other published ports in the development stack, at about 1.7 MB unique memory (PSS) each, which projects to roughly 1.7 GB for the default 1000-port range and the same again for OpenTalk's.

daemon.json now sets "userland-proxy": false, so iptables/nftables forwards every published port, which the CIS Docker Benchmark (2.16) also asks for. Traffic from outside already took that path, so client addresses are unchanged. Host-local traffic to ports published on 127.0.0.1 now runs through route_localnet and the loopback DNAT Docker installs. From Docker 28.0 on, Docker keeps hosts on the same L2 segment off those loopback ports; every install path of this role ships 29.x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 23:07:39 +01:00
kevinveenbirkenbach 48b571a094 Merge remote-tracking branch 'origin/main' into review/dmarc-rua-1125 2026-10-29 22:24:56 +01:00
kevinveenbirkenbachandClaude Opus 5.5 3f9733068a fix(web-app-stalwart): register the WebUI OAuth callbacks for every network sibling
The WebUI OAuth client Stalwart serves itself registered `<origin>/<prefix>/oauth/callback` only for the canonical WebUI URL. A browser that opens the WebUI on the onion sibling sends `redirect_uri=http://mail.<onion>/account/oauth/callback`, which Stalwart rejects with "The redirect_uri parameter is missing or does not match the client registration". CI run 40877408319 failed test-login-native.js:36 that way in Meta#0, Nextcloud#0, Shopware#0, Bluesky#0 and Moodle#0, every row whose Playwright pass picked the onion sibling.

The client now takes its origins from `lookup('network_urls', STALWART_WEBUI_BASE_URL)` and registers the callback and post-logout URIs for every origin and URL prefix. A host without a sibling still yields the single canonical origin, so clearnet-only deployments register the same client as before. The Align task updates an already registered client once.

The README states the per-network origins and that Stalwart asks for consent after the password form, as the local WebUI v2.0.2 run showed.

Verified with the unit test (20 passed; registering only the first origin fails the new two-sibling test) and a local update deploy whose Playwright pass picked the onion sibling: test-login-native.js:35 passed, recap failed=0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:51:57 +01:00
kevinveenbirkenbach 989eb7c0ea Merge pull request #1256 from infinito-nexus/fix/sys-svc-proxy/per-app-security-headers/1123
feat(proxy): per-app nosniff, Referrer-Policy, Permissions-Policy and HSTS directives
2026-10-29 18:14:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 abee135c53 fix(web-app-nextcloud): replace a pinned app directory instead of overlaying its archive
The pinned onlyoffice archive was unpacked with tar over custom_apps/onlyoffice on every deploy, so files that a newer release no longer ships stayed behind. On cloud.veen.world that left the 52 files of onlyoffice 9.15.1 (NC32 line) inside the 10.2.1 tree, and the integrity check reported them as EXTRA_FILE; the 52 equal exactly the 9.15.1 files absent from 10.2.1. replace_pinned_app.sh now unpacks the release beside the app, keeps the app when diff -rq finds it identical, and otherwise swaps the whole directory, so the next deploy heals the drifted tree and later deploys report ok. A broken archive fails before the live app is touched.

The real install task, run through ansible-playbook against a reproduced 9.15.1 plus 10.2.1 tree, changed it once to a tree identical to a clean 10.2.1 and stayed unchanged on the rerun; eleven unit cases cover identical, drifted, missing and broken inputs under sh and dash.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 17:42:59 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b0c622ab44 fix(web-app-nextcloud): give each PHP server its own NC_serverid
The admin overview reported that no server identifier is configured. Nextcloud 34 accepts 1 to 511, treats values below 1 as unset and asks for one NC_serverid per server when config.php is shared, which it is between nextcloud, cron, the task processing worker and the swarm replicas. A single serverid in config.php would let them mint identical Snowflake IDs in the same millisecond. The nextcloud service now carries NC_serverid 1 in compose and its task slot in swarm, so the check passes and replicas never share an id; the other services keep the hostname fallback.

A unit test renders the service environment in both modes and fails when the compose id leaves the accepted range.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 17:24:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b4329d6044 feat(web-app-nextcloud): run a dedicated Task Processing worker
Nextcloud 34 flags "Task Processing worker status: never run" because only occ taskprocessing:worker records core/ai.taskprocessing_worker_last_iteration, and the role ran background jobs through the cron container alone; since 32.0.7 the worker command is taskprocessing:worker, so background-job:worker would never clear the check. Add a taskprocessing service that reuses the cron image, mounts, healthcheck, restart policy and networks and runs occ taskprocessing:worker -t 60 in an in-container restart loop that backs off while Nextcloud is not yet installed, so swarm neither replaces the task every minute nor marks the stack fatal on a fresh install. It scales with the swarm topology, since task claims use FOR UPDATE SKIP LOCKED.

Rendered compose and swarm stacks only gain the taskprocessing service and validate against the compose schema; the new unit tests fail when the worker mount is dropped or background-job:worker is used instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 17:14:30 +01:00
kevinveenbirkenbachandClaude Opus 5.5 96ebefb9db perf(spot): re-render only the users SPOT after a token store write
A token store write re-rendered the whole applications SPOT: merged_applications_cache_key carried the token store digest, so the applications_carrier park in sys-token-store/tasks/write_many.yml missed its key and rendered all 269 applications again (stalwart compose roundtrip 18.3 s and 10.2 s per pass, swarm pass 1 12.2 s and 12.6 s).

Rendered from the real roles tree under an empty, a realistic and an exhaustive token store (298 users x 269 applications), the applications tree differs only under <app>.users.<key>.tokens (28783 leaves in 46 applications, 0 elsewhere); the 69 password leaves that also differ change between two renders of the same store. No code reads a token through the applications tree: every reader goes through lookup('users'), lookup('config', <app>, 'users.<key>...') (served from get_merged_users), lookup('email') or the sys_token_store_users fact, and the users carrier keys on the store.

The token digest therefore moves from merged_applications_cache_key into merged_users_cache_key, whose value stays the same. A store write keeps the applications carrier and the write_many park re-renders the users alone; the task is renamed to match. applications.md now says that user records inside the payload can predate every store write and that application config must not derive a value from a token.

Real ansible-playbook harness on 2.20.6 with the real roles, group_vars and write_many.yml: the re-park after a first mint and after a rotation drops from 4.0 s to 0.55 s on one host (applications renders 1 -> 0), and every token probe (users, config users path, email password, stalwart_users) equals the old code in compose and swarm mode. Dropping the digest from the users key as well leaves those probes stale, which the harness and two unit tests catch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 15:56:08 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5689812fbe perf(sys-svc-compose): build with --pull in the build handlers so they hit pull.py's cache
A Dockerfile change notifies compose-up and compose-build in one flush. The compose pull handler (pull.py) builds every buildable service with `compose build --pull`, and the Build compose handler right after it built again with plain `compose build`. BuildKit keys a --pull build and a plain build differently, so the second build missed the first build's cache and ran every RUN layer again; in the stalwart compose roundtrip that was 233 s for tor, 129 s for openldap and 62 s for simpleicons, about 420 s per fresh compose deploy. It also overwrote pull.py's image, built on the freshly resolved base, with one from the plain-build cache: in pass 2 the --pull build of openldap ran its RUN layer again (34.5 s) while the plain build right after it was fully CACHED.

Both build handlers now run `compose build --pull` and fall back to a plain `compose build` when that fails, as pull.py does, so the second build of a flush is a cache hit and a lone-service rebuild keys like the flush builds. Without a reachable registry the fallback builds from the local base exactly as before.

A scratch compose project in the stack's daemon showed the cache behaviour: --pull after --pull CACHED, plain after --pull ran the RUN layer again. The new handler test fails on the old plain `compose build`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 15:49:50 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ff288a3dd4 Merge branch 'feature/updater' into feature/store-i18n
Brings feature/updater 3966abdb16, including updater-lockdown-probe: the deferred probe host guard (aa88429c25), the per-host post-load queue (70ed8a2e98), the NFS probe noauto (0ece4fcbc4), the E2E prepull gate (ef20262813), the worktree swarm lab subnet fixes, the Tor port window and restart skip, the Prometheus Docker poller, the Stalwart JMAP client cache and the FusionDirectory PHP hardening mount.

Conflict resolutions:
- plugins/lookup/email.py: keep store-i18n's NETWORK_MODE forwarding into the provider view and add updater's provider carrier.
- roles/svc-net-tor/templates/test.env.j2: updater's ONION_PORT_TIMEOUT=130, store-i18n's ONION_SUFFIX.
- roles/svc-swarm-node/README.md: updater's ingress guard wording, without the generated Quick Setup and Credits sections.
- roles/test-e2e-cli/tasks/main.yml: updater's cli_role_id loop variable with store-i18n's async cli.once condition.
- roles/web-app-prometheus/templates/nginx/locations.conf.j2: store-i18n's entity_name filter with updater's container_service lookup.
- tasks/stages/01_constructor.yml: updater's order (svc-swarm-node after the preload context opens) under store-i18n's task name, whose loop also covers svc-cache.

Integration fixes:
- roles/sys-stk-front-proxy: git applied updater's IS_STACK_HOST guard to the "Freeze front proxy context" task, which would have left workers without front_proxy_* facts. main.yml stays as on store-i18n; the guard moves into the per-sibling vhost.yml split, where the probe facts and the queue sit behind IS_STACK_HOST and front_proxy_domain_conf_dst_resolved stays unguarded for the Deploy arguments. deferred_probe.yml takes updater's host guard verbatim.
- roles/svc-net-tor/tasks/00_core.yml: the restart probe tests IS_SWARM_MODE instead of a literal DEPLOYMENT_MODE comparison.
- roles/web-app-stalwart/tasks/jmap_call.yml: the cached client splits into stalwart_jmap_url and stalwart_jmap_user_password, so the exec argv references no fact that carries a credential.
- roles/sys-service-loader/tasks/list_or_shoot.yml: the two direct-shoot tasks share their preload condition in one block.
- tests: the Prometheus wiring test imports entity_name and pins the host resources its renders need; the email carrier tests use an onion-only provider, which on store-i18n is what drops TLS (a dual-stack provider keeps it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:54:41 +01:00
kevinveenbirkenbach f57ecc3433 Merge remote-tracking branch 'origin/main' into feature/updater-lockdown-probe 2026-10-29 12:06:27 +01:00
kevinveenbirkenbachandClaude Opus 5.5 21e05b9d42 feat(env): switch the GPU-only tests with INFINITO_TEST_GPU
The two tests that only answer on a GPU host, the logout catalogue translation check and the complete-catalogs integration test, were gated on accelerated() alone: a contributor with an NVIDIA runtime had no way to keep them out of a local gate while the GPU translation run is paused, so every gate on such a host stayed red on them.

INFINITO_TEST_GPU is a new env key. Its handler derives the default from INFINITO_GPU_COUNT: true where the compose services reserve a GPU, false for 0 or none. custom.env is applied after every handler, so `INFINITO_TEST_GPU=false` there switches the tests off on a GPU host; an exported value wins as well. tests/utils/gpu.py reads the flag for both tests, fails on any value other than true or false, and falls back to the same GPU detection only where no .env is loaded, such as a direct pytest run.

The handler joins gpu_count in utils/env/handlers/infinito/gpu/ (count.py, only_tests.py), because two handlers sharing the gpu prefix are a folder by the prefix-grouping lint. The new module avoids a test* name so unittest discovery never collects it. customize.md lists the key among the common overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:50:26 +01:00
kevinveenbirkenbachandClaude Opus 5.5 a3e41a59de perf(web-app-stalwart): resolve the JMAP URL and admin login once per run
Every JMAP call templated STALWART_ADMIN_USER and STALWART_ADMIN_PASSWORD (lookup('users')) and STALWART_JMAP_URL (config lookup) again. Ansible forks a fresh worker per task, so get_merged_users re-rendered the whole user catalogue in every call: about 0.4 to 0.6 s of controller time per call, 37 to 39 calls per deploy pass.

jmap_call.yml now sets the host fact stalwart_jmap_client (url, user_password) on the first call of a run, masked like the call itself, and every call reads it. STALWART_CONTAINER_ADDRESS stays in the command, so swarm keeps resolving the running container per call; 11_oidc_restart.yml replaces that container mid-run. The include interface is unchanged: stalwart_jmap_body in, stalwart_jmap out.

A scratch harness with a fake container measured the shell task at 1.01 to 1.10 s before and 0.46 to 0.49 s after, with argv and stdin sha256 identical in compose, swarm, vaulted-password and two-host runs. The admin password only rotates between runs, so a per-run fact stays current.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 10:54:19 +01:00
kevinveenbirkenbachandClaude Opus 5.5 699e5d8d83 fix(web-app-stalwart): write the 1.x store guard in store-i18n's conventions
updater's Stalwart v1 port (store_guard.yml, migrate.env.j2) arrived in
the merge (c9e7f950d9) with three constructs store-i18n's lints reject:

- `DEPLOYMENT_MODE == 'swarm'` in store_guard.yml and migrate.env.j2
  (test_deployment_mode_booleans): now IS_SWARM_MODE;
- `get_entity_name` in the store-engine fail_msg, a filter store-i18n
  renamed to entity_name (a05071fe78), so TestAllUsedFiltersAreDefined
  failed: now entity_name;
- two pairs of sibling tasks repeating the same `when`
  (test_no_repeated_when_runs): the shared-database and dedicated-volume
  lookups now sit in one block gated on `not (stalwart_store_marked |
  bool)`, the migrate include and the pre-1.0 assert in one block gated
  on `stalwart_store_layout is not skipped`; each task keeps its own
  remaining condition. Ansible evaluates a block's `when` per task, so
  the assert still sees the layout migrate.yml registers again.

updater's test_store_guard.py walked only the top-level tasks and failed
with KeyError: 'register' on the block wrappers. It now walks
block/rescue/always (utils.roles.invocations.NESTED_KEYS), hands a
block's `when` down to its children and checks the block condition
before the task's own, as Ansible does; the assertions are unchanged.
With either block's `when` removed from a copy of store_guard.yml, two
respectively four of its tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 03:28:37 +01:00
kevinveenbirkenbachandClaude Opus 5.5 2656a0f119 test(sys-token-store): read write_many's tasks through their block gate
updater's TestWriteMany (86e7c844f3) looked up the persist, mirror and
hand-over tasks at the top level of write_many.yml and evaluated only
each task's own `when`. The merge (c9e7f950d9) moved those three tasks
into store-i18n's change-gated block ("💾 Write the token store when the
merge changed it", `when: sys_token_store_users_merged !=
sys_token_store_users`) and switched the mirror to IS_SWARM_MODE, so the
lookups raised StopIteration.

The test now walks into block/rescue/always, hands a block's `when` down
to its children and ANDs it with a task's own condition (a task without
any condition counts as true, as in Ansible), renders with
IS_SWARM_MODE, and counts the re-render inside blocks too. The
assertions are unchanged; with the block's `when` removed from a copy of
write_many.yml, "an unchanged store skips the write and the re-render"
fails again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 03:06:14 +01:00
kevinveenbirkenbachandClaude Opus 5.5 8fdfa6ef51 feat(web-app-semaphore): sign in on the onion sibling as well as on clearnet
Semaphore v2.19.17 builds its post-login redirect from its one WebHost
(url.JoinPath(WebHost, return)), marks the session cookie Secure when
WebHost is https, and registered a single OIDC provider whose
redirect_url is clearnet. A login started on the onion sibling therefore
lost the oauthstate cookie at the clearnet callback and the session on
the plain-http onion origin: semaphore#0 of run 40625679270 failed five
login and design specs in PASS 2, where Playwright picked the .onion
sibling ("http: named cookie not present" 24 times in the container
log, while LDAP accepted the rotated credentials six times).

Semaphore keys OIDC providers by id from the URL path and gives each its
own redirect_url, so when the role has an onion sibling and SSO is on:

- a second provider keycloak_onion, same issuer, client and claims, with
  the onion sibling's plaintext redirect_url; Keycloak's redirect_uris
  lookup already registers http://<onion>/*;
- the onion vhost rewrites the absolute WebHost redirects to relative
  ones (proxy_redirect) and drops Secure from the semaphore cookie
  (proxy_cookie_flags), next to the existing base-href rewrite;
- location = /api/auth/login filters the provider list per vhost with
  sub_filter, so clearnet offers keycloak and the onion vhost
  keycloak_onion, both labelled as before; each dropped provider has a
  pattern for either list position because Semaphore does not fix the
  order.

Without an onion sibling the provider JSON and both proxy hooks render
exactly as before. test_onion_login renders the vars through Ansible's
templar for both cases; 16 of its 18 tests fail against the previous
files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 02:29:59 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c9e7f950d9 Merge branch 'feature/updater' into feature/store-i18n
68 conflicts, resolved per hunk; resolutions agreed with the Updater
session were applied as agreed:

- svc-db-postgres (superuser password script and core) and
  sys-svc-rdbms realign_dedicated_db: updater's argv/stdin redesign with
  expand_argument_vars: false, nested into store-i18n's block structure.
- svc-db-redis 01_init: store-i18n's ACL password on stdin.
- sys-svc-proxy forwarded.conf.j2: store-i18n's handover-aware file,
  without the forwarded_base include (Host and X-Forwarded-Proto would
  be sent twice); forwarded_base serves the inline consumers, e.g.
  web-app-seaweedfs nginx.conf.j2.
- basic.conf.j2 and web-app-openproject proxy.conf.j2: issuer redirect,
  then handover, else updater's location/sso_acl.conf.j2; the
  server-level webserver_extra_configuration hook stays.
- web-app-keycloak: updater's admin-REST settings step and renumbered
  update steps; update/01_client.yml removed; store-i18n's design step
  and LDAP block kept; a73435cc9d arrived through rename detection.
- web-app-nextcloud OpenProject provisioning: updater's rewrite, all
  credentials on the Ruby runner's stdin.
- web-app-semaphore: store-i18n's canonical web root (a1d7835863).
- Role READMEs: no generated Cosmos/Architecture/Quick Setup/Credits
  sections; desk-* paths resolved to the dsk-* renames.

Integration fixes for what merged cleanly but did not fit together:

- web-app-matrix ansible vars: drop updater's second
  matrix_client_element_configuration_extension_json, which silently
  overrode store-i18n's design-aware value.
- url.origin instead of a stripped url.base in 19 places store-i18n
  added since 44aaa68924, same lookup targets (the double-slash lint).
- Package batch (18ea75049e, cb4d340de9): installs the deploy-start
  batch covers are removed (caffeine, cursor, blender, npm in dsk-gnt-*,
  with shared references), the rest carry batch: false and a nocheck
  with the verified reason (PyPI fallback, before the batch, vendor
  repository, configured flavor only, addon gate); dev-npm references
  the root npm id.
- sys-token-store write_many.yml: store-i18n's change-gated block and
  IS_SWARM_MODE.
- web-app-openproject: IS_COMPOSE_MODE in 02_migrate/run.yml; the
  seeded-admin lock (ccbf8f168d) ported into 04_admin/provision.yml and
  covered by the admin-state digest.
- Bucket policy (3b4ac40df5 and follow-ups) moved to web-svc-seaweedfs,
  where store-i18n's object store lives; the S3 admin secret travels on
  stdin instead of a task-level environment.
- act workflow.sh: no Exception-labelled comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 02:28:16 +01:00
kevinveenbirkenbach 386d9a89cc Merge remote-tracking branch 'origin/main' into review/dmarc-rua-1125 2026-10-29 01:49:51 +01:00
kevinveenbirkenbach 666ed3c1ac Merge remote-tracking branch 'origin/main' into fix/sys-svc-proxy/per-app-security-headers/1123 2026-10-29 01:49:34 +01:00
kevinveenbirkenbach a8187b70a2 Merge remote-tracking branch 'origin/main' into fix/svc-net-tor/onion-port-rendezvous/1214 2026-10-29 01:49:31 +01:00