Commit Graph
14682 Commits
Author SHA1 Message Date
dependabot[bot] f83cb061c7 chore(deps-dev): bump fast-uri
Bumps the npm_and_yarn group with 1 update in the / directory: [fast-uri](https://github.com/fastify/fast-uri).


Updates `fast-uri` from 3.1.5 to 3.1.7
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 05:23:33 +00:00
kevinveenbirkenbachandClaude Fable 5.1 eab8f5fc90 fix(inventory): give pinned user passwords the mark the realm policy demands
The shell-safe default drew from letters and digits only, and every user without declared accounts is an identity account, so kcadm set-password rejected hermes and openclaw with invalidPasswordMinSpecialCharsMessage as soon as keycloak entered the GitLab#1 round without LDAP (4164f37981) and the extras file stopped replacing the users map (03e42bae6d; run 33689701895). The generator now draws from letters, digits and -_. and guarantees upper case, lower case, a digit and one mark, never in the leading position.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 02:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5.1 32ee8992b5 fix(webserver-core): keep the proxy in the deployment's mode whoever loads it
matrix (ansible flavor) and bigbluebutton set compose_mode_force as a role var. Since 9d7fc70900 scoped the preload to the whitelist closure, openresty is first loaded beneath such a consumer, inherits the var through the include chain into its sys-svc-compose handlers, composes the proxy up and skips the stack deploy (swarm Matrix#1, run 33689701895). The reset on the Load OpenResty include hands the proxy DEPLOYMENT_MODE again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 02:14:11 +02:00
kevinveenbirkenbach d29e112db3 Merge pull request #585 from infinito-nexus/update/docker-image-versions-web-app-erpnext-20260902-d443dce
update(images): update Docker image versions (web-app-erpnext)
2026-09-03 01:57:04 +02:00
kevinveenbirkenbach 7c03052466 Merge pull request #586 from infinito-nexus/update/docker-image-versions-web-app-jira-20260902-9f5e9ad
update(images): update Docker image versions (web-app-jira)
2026-09-03 01:56:06 +02:00
kevinveenbirkenbach ac838a624b Merge pull request #587 from infinito-nexus/update/docker-image-versions-web-app-matrix-20260902-4089ec0
update(images): update Docker image versions (web-app-matrix)
2026-09-03 01:55:34 +02:00
kevinveenbirkenbach b9ca736dfb Merge pull request #588 from infinito-nexus/update/docker-image-versions-web-app-nextcloud-20260902-5142dc8
update(images): update Docker image versions (web-app-nextcloud)
2026-09-03 01:55:09 +02:00
kevinveenbirkenbach 675383611f Merge pull request #584 from infinito-nexus/update/docker-image-versions-web-app-confluence-20260902-7f67ed1
update(images): update Docker image versions (web-app-confluence)
2026-09-03 01:54:41 +02:00
infinito-nexus-updater[bot] a9a81816a4 update(images): update Docker image versions (web-app-nextcloud) 2026-09-02 23:53:02 +00:00
infinito-nexus-updater[bot] a787f55b3d update(images): update Docker image versions (web-app-matrix) 2026-09-02 23:52:59 +00:00
infinito-nexus-updater[bot] a911260838 update(images): update Docker image versions (web-app-jira) 2026-09-02 23:52:55 +00:00
infinito-nexus-updater[bot] 142317c3e4 update(images): update Docker image versions (web-app-erpnext) 2026-09-02 23:52:51 +00:00
infinito-nexus-updater[bot] 7d7a9358fa update(images): update Docker image versions (web-app-confluence) 2026-09-02 23:52:47 +00:00
kevinveenbirkenbachandClaude Fable 5.1 d8cf537c3c style(swarm-test): let shfmt align the deadline helper's nocheck comment
0b1565c493 went in with --no-verify and without the autoformat pass, so the
trailing nocheck comment in run_within_deadline() sat sixteen columns right
of the comment column the next line uses. The quality gate's autoformat step
realigns it on every pass; this lands that realignment so the tree stops
dirtying itself after each gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 00:14:51 +02:00
kevinveenbirkenbachandClaude Fable 5.1 1a4ee72249 feat(ci): let a retrigger override chunk_gate, with i8ciallon as the non-stop i8ciall
A retrigger copies every dispatch input from its source run, chunk_gate
included, so a sweep that stopped at its first red chunk came back stopping
there again, and nothing short of a manual dispatch could ask the chain to
deploy every chunk and report all of it at once.

`infinito administration deploy ci trigger --chunk-gate {true,false}` now
overrides the carried value after the source run's configuration is read; it
is the one deliberate departure from "reproduce the source run exactly", and
the module docstring says so. Omitted, the carried value or the workflow
default applies as before.

`i8ciallon` is `i8ciall` with `--chunk-gate false`: force-cancel the run, put
everything without a verdict back on the priority line, and keep deploying
past a failed chunk. The alias lint caps a name at two adjacent consonants,
which is why it is not `i8ciallnonstop`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 00:12:56 +02:00
kevinveenbirkenbachandClaude Fable 5.1 113113e553 perf(applications): render the applications SPOT once per play instead of once per task
Ansible forks a fresh worker process for every task, so the in-process
cache behind lookup('applications') was cold in every task. Each task that
touched the merged applications tree (config, domains, nginx, tls, cdn and
their callers) paid a full render: about 16 s on main and 21 to 23 s on this
branch, where ten more application roles carry MCP and AI Jinja. Measured on
the workspace jobs of run 33574056480 against the main run: 331 such tasks
(93 min, 39 % of the job) versus 391 (141 min, 43 %); the same unit is 9 to
43 % of every compose and swarm deploy job on disk. The parent process
rendered once per play, late, while templating a task name, and only the
tasks after that point ran warm.

The constructor now renders once through the new applications_carrier
action and parks the payload with its cache key in the host fact
_INFINITO_APPLICATIONS_RENDERED. Every later worker inherits the fact at
fork time and get_merged_applications serves it while the key (roles dir,
applications, users, DOMAIN_PRIMARY, SYSTEM_EMAIL_DOMAIN) still matches; a
mismatch falls back to the old render. The action masks its own result: the
payload is the whole tree with every credential, and CI runs with
MASK_CREDENTIALS_IN_LOGS false.

The task sits after the networks, design and service_provider merges, after
the add_host placements and after the token store. An independent review
showed that an earlier position bakes stale values into the play: the
web-app-wordpress title rendered to 'Blog by ' because service_provider did
not exist yet. User records inside the payload are the token store's state
at render time; readers of tokens captured later keep using lookup('users')
or lookup('config', app, 'users.…'), which read the users cache fresh.

Expected savings per job: about 130 min on a fork workspace run, 16 min on
an average compose deploy and 28 min on an average swarm deploy. The
constructor's own lookups before the carrier stay cold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:55:32 +02:00
Alejandro Roman c2edae7b74 fix(stalwart): fix lint issus 2026-09-03 00:18:03 +04:00
kevinveenbirkenbachandClaude Opus 5 03e42bae6d fix(swarm-test): stop the extras file from replacing the inventory's users map
The extras writer carved `applications` out of the deploy-facing twin and said
why in its own docstring: extra-vars replace rather than merge, and would strip
every generated credential. `users` carries the same hazard and stayed in, so
every swarm deploy ran with `-e @swarm-nfs-extras.deploy.yml` replacing a
nine-entry users map with the three from inventories/development/default.yml.

Everything not in those three then fell to its role default. For zammad-api-bot
that default is `{{ 42 | strong_password }}`, and utils/manager/value_generator.py
opens by warning what that means: it is a template, not a value, re-rendered at
every use site, so the task that creates an account and the task that later
authenticates as it read two different secrets. Playwright sent a 42-character
password that no deploy had ever written, and Zammad answered

    Seed POST /api/v1/tickets failed: 401 Invalid BasicAuth credentials

on both swarm jobs of Zammad#1 while both compose jobs passed. Compose never
sees an `-e @file`; its driver emits only scalar pairs.

`users` now leaves the twin the way `applications` already does. The two deltas
the block actually carried, the swarm-test pubkeys for administrator and backup,
travel the merging channel instead: baked into base_overrides, handed to the
provisioner through --vars, and deep-merged into host_vars. ensure_swarm_keypairs
is split out so the keys exist before provisioning rather than at extras time.

Two things this restores beyond the 401. The rotation gate rotates
users.<name>.password, and swarm was overwriting the result with the defaults
straight afterwards, so user rotation was never actually exercised there. And
user-administrator/meta/users.yml ties the administrator account password to
ansible_become_password; the extras had been pinning it to default.yml's sample
literal instead, which sudo on these nodes does need to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:49:30 +02:00
kevinveenbirkenbachandClaude Opus 5 4d422a7a98 fix(swarm): scale the deploy timeout through the lookup like every other wait
87e4313e1 gave the blocking stack deploy a timeout computed straight from
swarm_stack_converge_retries. That bypasses lookup('timeout', <base>), which is
how every wait in this repo scales from one TIMEOUT_FACTOR, and
test_timeout_uses_lookup enforces it. The commit went out having run only
tests/lint/ansible, and the rule lives under tests/lint/repository/yaml.

At the shipped factor of 1 the value is unchanged at 1200s, so this restores the
scaling without moving the number.

It also corrects a claim that commit made. The two budgets do not hang on one
knob: the gate cannot scale, because test_swarm_converge_budget multiplies its
retries by a literal delay to prove the worst case fits
INFINITO_CI_DISTRO_BUDGET_SECONDS, and a templated delay would end that proof.
That rule exists because the two budgets drifted once already and the shorter
one killed a healthy but slow stack. The gate's budget is deliberately static
and provable; the deploy's timeout scales with the environment, and the deploy
is the authoritative wait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:26:55 +02:00
kevinveenbirkenbachandClaude Opus 5 87e4313e11 fix(swarm): let the deploy wait for its own update instead of guessing after it
docker stack deploy defaults to --detach, so it returns before the daemon has
registered the rolling update. In that window a converged stack and one about to
be replaced look identical: UpdateStatus still reads completed from the previous
round, the outgoing task is still desired-state running and Running, and the
replica column still reads 1/1. The gate answers correctly and answers the wrong
question.

Moodle in run 33574056480 shows the whole shape. The stack deploy reported
changed at 02:26:30.797, the convergence gate went green 1.14s later, cfg.php
failed at 02:26:36 with

    Access denied for user 'moodle'@'192.168.201.4'

and the task carrying the new password did not start until 02:27:15, 38 seconds
after the failure. There was one task at the moment of the failure and it was the
old one, so this is not a resolver picking the wrong task of two.

--detach=false hands the waiting to the daemon, which is the only party that
knows an update has been created. The task after it starts once the update is
through, and the window is gone.

This sits next to 4b9e191200 rather than replacing it: that one stops an outgoing
task from satisfying the replica count while an update is in flight. This one
covers the moment before the update exists at all.

The deploy timeout derives from swarm_stack_converge_retries, the same knob the
gate polls on, so the two budgets cannot drift apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:26:08 +02:00
Alejandro Roman 6a4785ea87 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-02 22:22:12 +04:00
Alejandro Roman 17e2b151ab fix(stalwart): fix some spots and issues found 2026-09-02 22:21:31 +04:00
kevinveenbirkenbachandClaude Opus 5 e6a76dbf72 fix(sys-svc-rdbms): realign only an engine this role actually rendered
The generic realign ran for every app whose database resolves to local
postgres and handed set_postgres_superuser_password.sh the app's entity name
as the superuser role. That pairing only holds for an engine sys-svc-rdbms
initdb'd itself, because templates/env/postgres.env.j2 fills POSTGRES_USER
from the same lookup.

BigBlueButton does not go down that path. Its postgres comes from the vendored
upstream compose and is initialised as superuser postgres, so
`psql -U bigbluebutton -d postgres -Atc 'SHOW hba_file;'` fails on every
attempt, the script's nested 30x30 retry burns its whole budget, and the task
ends in [FAIL]. In run 33574056480 that cost 32m59s of a Nextcloud variant-11
round, which reaches BBB as a dependency, and took the deploy with it.

main.yml already gates dedicated.yml on the app rendering its own
templates/compose.yml.j2. The realign now carries the same condition. It has to
live in this file rather than in main.yml, because sys-svc-compose's post_deploy
and sys-stk-backend include the task directly and never see main.yml's facts.

Measured against the current role set: 32 postgres roles can be local in some
round, 31 keep the realign, and BigBlueButton alone loses it - the only one
with a static shared: false and no compose template of its own, and the only
one carrying its own tasks/01_compose/postgres_password_sync.yml. No coverage
is lost, only a retry storm against a role that does not exist.

The stat carries the same compose-mode condition as the block, so a swarm round
runs exactly what it ran before, and it is run_once because the path does not
vary by host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:10:57 +02:00
Alejandro Roman 942fdb5fad fix(stalwart): fix all comments, fix tor approach, SPOT fixes and chagne some values to follow standars + remove comemnts 2026-09-02 22:05:45 +04:00
kevinveenbirkenbachandClaude Opus 5 5dd1f331f8 fix(mastodon): let the tootctl failure speak without leaking the password
no_log on the create task is right: tootctl prints "New password: #{password}"
on stdout when it succeeds. It also suppresses the syslog record, so a failure
left nothing anywhere. The rescue bundle for run 33574056480 carries the
neighbouring unblock task with its whole Ruby script and no trace of this one.

The failure path is a different stream. report_errors goes through
fail_with_message, which raises Thor::Error, and Thor writes that to stderr,
after the create has already given up on generating anything. Quoting stderr
therefore costs no secret.

The task now records without judging, and an assert decides. It reads the
already-existing case from either stream rather than stderr alone, which is
hardening rather than the fix: the source shows Thor puts it on stderr, so the
old condition would have tolerated it and the account being taken was never
what failed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:32:10 +02:00
kevinveenbirkenbachandClaude Opus 5 4986e80dd4 feat(readme): ask the reader for a domain the deployment can resolve
The Production block never set one. inventory provision writes only
ansible_connection for a local host, so DOMAIN_PRIMARY stayed at the
group_vars default of infinito.localhost and the deployment served a zone its
own resolver knows nothing about. CoreDNS templates answers for
INFINITO_DOMAIN_RE and nothing else, so every lookup of the deployment's own
name came back NXDOMAIN.

Mastodon is where that surfaced. tootctl accounts create validates the address
through EmailMxValidator, which resolves MX and then, for the domain itself,
A and AAAA. All six queries returned NXDOMAIN in run 33574056480, the
validator recorded email :unreachable, and the admin account was never created.
Only the guide rows were affected: the matrix rows write
DOMAIN_PRIMARY: "{{ lookup('env', 'INFINITO_DOMAIN') }}" into their host_vars
and resolve fine.

The template now carries DOMAIN=<your-domain> in both the host and the
container form and threads it into --vars, with the container form passing it
through the docker run boundary as well. The two guide runners substitute it
the way they already substitute the SSH key, so the replay deploys under
INFINITO_DOMAIN like every other CI deploy. compose_deploy.sh moves its env
load ahead of the substitutions to have the value; host_deploy.sh had no env
load at all.

A production reader is asked for a domain now instead of silently inheriting
infinito.localhost, which no real deployment wants either. The shipped default
keeps its value: sys-svc-nss documents that *.localhost resolves through the
myhostname NSS module without any zone, and host-side tasks lean on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:30:25 +02:00
kevinveenbirkenbachandClaude Opus 5 bb0fcf727f fix(prometheus): drop a settle wait that never settles and decides nothing
The line waited for networkidle after the logout, swallowed the rejection in a
.catch, and cost 225s on onion because resolveTimeout multiplies its 45s base
by five. Four independent runs land on that cap to the tenth of a second, 225.0
twice under tor and 45.0 twice on clearnet, and pass anyway: networkidle is
never reached on a post-logout oauth2-proxy surface, and a swallowed rejection
cannot change a verdict.

Nothing waits on it either. inAppLogout only returns past an awaited
domcontentloaded and confirmKeycloakLogoutIfPrompted, which awaits another one
after the Keycloak confirm click, so the end-session redirect chain is already
complete when the wait began. The line after it navigates afresh and the
expect.poll on the URL is what actually proves the session is gone.

Worth 450s per tor job in serial wall clock, since the runner pins workers to 1
and fullyParallel to false. The identical line on the Nextcloud side is left
alone: measured there, the whole login-and-logout test runs 1.2 minutes on a
swarm+tor job, so that wait does reach networkidle and removing it would buy
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 17:52:56 +02:00
kevinveenbirkenbachandClaude Opus 5 22425d6f83 fix(nextcloud): keep the ExApp dial off the tor proxy that cannot resolve it
The Context Agent this branch introduces is the first thing Nextcloud dials by
a container name from inside its own server process. proxy.config.php.j2 sets a
global socks5h proxy whenever SSO runs against an onion issuer, and socks5h
hands name resolution to Tor, which has no idea what nextcloud-context-agent
is. Every attempt stalls to cURL's 30s ceiling, the AppAPI retry loop repeats
it, and SYS_TIMEOUT_CONTAINER_EXEC shoots the exec after an hour. The registration
returns 124 and the whole e2e stage behind it never runs.

The exclusion list was written for appstore downloads and lists five public
hosts. It now carries the internal names this same file defines as dial
targets, split so the two kinds stay legible: the role's own proxy and the
Context Agent, both resolved through their mode-aware DNS name so the exemption
holds under compose and swarm alike.

The template is byte-identical to main and the list's gap is as old as the
list, but main declares no context_agent at all, so nothing there ever dialled
an internal name and the gap stayed invisible. This is the branch's own dial
walking into it.

Evidence from run 33574056480: the ExApp was healthy throughout, its log
carrying 142 heartbeat answers, every one of them from 127.0.0.1 and not one
from Nextcloud, while Nextcloud logged eleven cURL error 28 timeouts at 30002ms
spaced 31s apart. The dial dies at the caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 17:46:12 +02:00
kevinveenbirkenbachandClaude Opus 5 0b1565c493 fix(swarm-drill): spend the teardown reserve instead of overrunning it
The EXIT trap handed out four fixed budgets, 1500 + 900 + 900 + 900, against
the 900s reserve the governor sets aside for it. Its first step alone was
allowed to run longer than the whole reserve, so the trap regularly outlived
the deadline and the outer `timeout -k 60` escalated to SIGKILL. Nextcloud#3
and Nextcloud#11 in run 33574056480 died that way, both at 20460s to the
second, with the diagnostics still running in their own process group and the
teardown never reached.

Each step now takes the smaller of its nominal budget and the time actually
left, recomputed before it runs, and leaves behind what every later step needs.
The worst case where each one exhausts its cap fits the reserve exactly: 480 +
120 + 30 + 120 + 30 + 120. Two steps that carried no timeout at all join the
accounting, because one unbounded step defeats the whole arrangement.

The cap only engages near the deadline. A deploy that fails on its own with
hours of budget left sees the nominal budgets unchanged, so the depth is
traded away only in the case where the exit code already names the cause.

What that case gains is a truthful ending. The job now reports the deploy's own
rc rather than 137, the cluster is torn down instead of leaked into the next
distro, and the reports are collected. A step cut short by its cap says so on
stdout and returns 124 to the caller, so a half-written rescue bundle can no
longer read as a complete one.

The drill is not made faster by any of this. A round too slow for
INFINITO_CI_DISTRO_BUDGET_SECONDS still fails, now as a clean 124.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:05:11 +02:00
kevinveenbirkenbachandClaude Opus 5 f068efc728 fix(domains): carry the rename into the escaped form the search missed
INFINITO_DOMAIN_RE is INFINITO_DOMAIN with the dot escaped for regex use, and
the comment directly above it says to keep the two in sync. The rename moved
INFINITO_DOMAIN to infinito.test and left the escaped twin on infinito.example,
because the backslash hid it from a literal search for the old domain.

Its only consumer is compose/coredns/Corefile.tmpl, which matches on it four
times. CoreDNS was therefore rewriting for infinito.example while every deploy
answered under infinito.test, so none of the four rules could match a name the
stack actually resolves.

The two test files are ruff format reflow: the shorter domain lets the calls
fit on one line again. autoformat wrote them; they are here because the rename
shipped without it, not because anything about them changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:53:20 +02:00
kevinveenbirkenbachandClaude Opus 5 4164f37981 fix(mcp): give every MCP round an identity provider to sign in with
An MCP client spec signs in through the client's own OIDC button, which
redirects to Keycloak. Every provider put its MCP pairing in the lean variant,
and that variant pinned sso off - so the round that exists to prove the pairing
was the one round without an identity provider. Run 33574056480 shows what that
costs: openwebui's MCP specs reached _shared.js:131 and died on "openwebui OIDC
sign-in button must be visible on the auth page".

The rule now demands two things of the variant that carries the consumers: the
provider pins sso on, and each admitted consumer, in the variant that round
actually pulls in, does not pin it off. The second half matters because a round
deploys every dependency at its own index and falls back to variant 0 only when
the dependency has fewer, so the client shipping alongside is that one and not
the client's variant 0. A role that declares no sso, or pins it to a literal
false because upstream speaks no OIDC, is exempt from a flag it cannot have.

Twenty roles moved to satisfy it. Ten already had a second sso: false pin and
only needed the flip. Six could host the pairing in variant 0, which already
has sso on, and dropped back to two variants. Six kept a third lean variant to
carry the false polarity variants-coverage wants. moodle and mattermost took
swaps instead: moodle's variant 1 is the mandated LDAP-only branch, and
mattermost was already at the three-variant cap with sso on in its variant 2.
nextcloud also turns context_agent on in that round, the ExApp that serves the
endpoint - with it off the round proved nothing it was built to prove.

The rule moves to tests/lint/ansible/services/ because it reads meta YAML and
executes nothing, which is where integration.md says such a check belongs, and
its suppression key joins the catalog it was never listed in.

nextcloud_exapp_reregister becomes nextcloud_exapp_renew: mcp-secret-masking
matches `register:` unanchored, so the substring in the old name read as a task
that prints a secret. register.yml already masks the two tasks that carry one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:51:24 +02:00
kevinveenbirkenbach c5c0977c2a Merge pull request #582 from infinito-nexus/update/docker-image-versions-web-app-matrix-20260901-3e325f5
update(images): update Docker image versions (web-app-matrix)
2026-09-02 13:36:28 +02:00
kevinveenbirkenbach 766cac2049 Merge pull request #583 from infinito-nexus/update/docker-image-versions-web-app-seaweedfs-20260901-80d0fd0
update(images): update Docker image versions (web-app-seaweedfs)
2026-09-02 13:36:04 +02:00
kevinveenbirkenbachandClaude Opus 5 4e7fe45914 fix(nextcloud): re-register the ExApp whose secret the endpoint rejects
The Context Agent registration ran once and never again, so a rotation of the
app secret left Nextcloud holding the old one. AppAPI then refused both ways -
"Invalid App secret:" in the ExApp and "Invalid signature for ExApp:
context_agent" in nextcloud.log - and the deploy either died at the
registration or later at the endpoint answering 401 with a bearer nothing
would accept.

The role already escalates on that exact signal for the app password: probe
the endpoint, mint a fresh password when it refuses, probe again, and refuse
the deploy if it still will not answer 200. The ExApp joins that ladder as the
next rung. When the endpoint still rejects after a fresh password, the
registration itself is what is stale, so it is dropped and made again, and the
endpoint is asked once more before the assert has the final word.

Registering is deliberately driven by the probe rather than by a recorded
fingerprint of the declared secret. A fingerprint has to be written by a
registration, so every instance already registered would have read no
fingerprint, judged itself stale, and paid a re-registration on the first
deploy after this shipped - and that carries --wait-finish, the ExApp init
that costs a tor round an hour it does not have. Keyed on the probe, a healthy
deployment does nothing at all, and the cost falls only where the endpoint is
already broken.

The caller says which of the two it wants; neither call site leaves it to a
default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:18:20 +02:00
kevinveenbirkenbachandClaude Opus 5 af605b049f refactor(domains): give local testing a .test domain, leave .example to docs
RFC 6761 reserves .test for testing and .example for documentation, and the
project's own domain was doing both jobs under the documentation name. It is
what default.env hands every local deploy and what the test fixtures assert
against, so it is a testing name.

Rename it wherever it stands for the running machine: the environment
defaults, the development inventory, the workspace and trust scripts, the test
suites, and the four documents that describe real behaviour - the DNS mapping
to loopback, the browser CA trust, WSL2 resolution. Those documents would have
started lying the moment default.env changed.

The sandbox allowlist moves with them, since egress is matched per pattern and
nothing resolves the old name any more, and the sandbox document that mirrors
that list follows it.

Two places keep .example on purpose: the link-style sample in the
documentation guide, which is documentation about documentation, and every
generic fixture name elsewhere in the suite - a.example, foo.example and their
kin. Those are a larger sweep of their own and are not touched here.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:52:11 +02:00
kevinveenbirkenbachandClaude Opus 5 a0dd564955 fix(openwebui): declare the sign-in every MCP spec actually needs
Each of these specs declared the service it exercises and then, as its first
action, signed in through the dashboard's OIDC button. In a round that ships
no identity provider that button does not exist, so the spec spent its whole
timeout waiting for it and reported the missing login rather than anything
about MCP. The junit already said as much, carrying the SSO_SERVICE_ENABLED
flag as the skip reason on the neighbouring tests.

Nine specs in the directory sign in that way; two already declared sso. The
other seven now do too, through the same gate they already imported.

Signing in as the deployed identity instead was not open to us: that helper
authenticates as the administrator whatever username it is handed, which
contradicts the non-member spec asserting the very endpoint the administrator
is asserted to reach. Nor can a native member stand in, because the account is
created by the OIDC login itself, so a round without one has no members to
grant anything to.

This buys the honest label at a price worth naming: a round that ships
seaweedfs without sso now reports green with no seaweedfs coverage, where it
used to report red for the wrong reason. Covering that needs a sign-in the
suite does not have yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:47:35 +02:00
kevinveenbirkenbachandClaude Opus 5 4b9e191200 fix(swarm): do not call a stack converged while it is still updating
The gate read UpdateStatus but latched only on the states an update cannot
leave by itself, so an in-flight "updating" passed. It then accepted the stack
replica column, whose current count is satisfied by old-spec tasks that are
still running. Between the two, a rolling update reported ready before it had
begun.

Moodle is where that surfaced. A rotation between rounds changes the database
password, the role alters the MariaDB user and renders the new one into the
service environment, and the post-deploy tasks then run inside a container
from the previous spec:

    Access denied for user 'moodle'@'192.168.201.4'

with MariaDB healthy throughout and that address being the manager node's
overlay load-balancer sandbox.

Name "updating" and block on it, and collect any state the case does not know
into its own bucket that reports itself before being treated as in flight, so
a state Docker adds later is diagnosable rather than a silent repository-wide
freeze. Count replicas from the tasks whose desired state is running, which a
rolling update sets to shutdown on the outgoing spec.

The cheap replica comparison stays the fast path and the per-service query
runs only when it passes, which is the only case the old count got wrong. This
gate runs on every swarm deploy, and a converging stack must not pay a
subprocess per service per poll. The service inspect gets one retry before it
fails the gate, so a timeout does not end a deploy on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:46:10 +02:00
kevinveenbirkenbachandClaude Opus 5 907851a3ce fix(matrix): hand the realign SQL to stdin in the form the guard reads
Moving the statement into files/sql left it on stdin as a quoted scalar, and
the password-quoting guard strips stdin sub-blocks only when they are block
scalars. A quoted one falls through and is scanned as if it were shell, where
any expression naming a password must carry the quote filter - and the file is
called realign_user_password.sql.

    realign_synapse_password.yml:24: In shell tasks, password expressions must
    include '| quote': {{ lookup('file', 'sql/realign_user_password.sql') }}

Quoting it would paste literal quotes into the SQL, and renaming the file
would only hide the token from the pattern. A folded scalar is the shape the
guard already understands, and it holds whatever the file is called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:44:36 +02:00
kevinveenbirkenbachandClaude Opus 5 f7ad2ff0fa fix(flowise): read a refused registration as the deny list working
The deploy points a throwaway registry entry at loopback and at the metadata
address and fails unless Flowise refuses to reach it, because
HTTP_SECURITY_CHECK=false replaces the built-in deny list rather than merging
into it, and a typo there would disable every protection while the registry
kept working.

The check assumed the entry always registers and that only authorize can
refuse. Flowise consults the list at registration too, so the create came back

    400 {"statusCode":400,"success":false,
         "message":"Server URL is not allowed by policy"}

and the probe reported its own strongest proof as a failure, taking the deploy
with it after forty minutes.

Accept that answer, and only that answer: a 400 naming the policy is the list
refusing the address. Every other non-2xx still aborts, and a list that is not
live still returns the entry, so the authorize call below remains the judge of
that case. The check cannot pass on a dead deny list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:12:07 +02:00
kevinveenbirkenbachandClaude Opus 5 932f4df328 fix(matrix): address synapse by its service, not by its exec target
The ChatGPT bridge got its homeserver URL from container_address, which
answers "what do I docker exec into". Under swarm that is a shell subshell,
and it was being written into a compose value, so docker stack config refused
the whole stack before anything deployed:

    invalid interpolation format for
    services.matrix-chatgpt-bot.environment.MATRIX_HOMESERVER_URL

container_service answers the other question, "what is this service
addressable as", and answers it per mode: the declared container name under
compose, <stack>_<key> under swarm. Compose therefore keeps exactly the name
it already used; only the branch that was broken changes.

Escaping the $ would have satisfied the parser and shipped a literal $(...)
into the bridge's environment, and the declared container name is emitted
through compose_only, so under swarm it has no DNS record at all. Neither is
a fix.

Name the resolved value once. The same lookup was already spelled out a
second time for the swarm restart, so both now read MATRIX_SYNAPSE_SERVICE.

Rename MATRIX_MDAD_SERVICE to MATRIX_MDAD_SERVICE_KEY while the vocabulary is
in hand: every one of its uses passes it as a service key, one of them as the
key of a rendered compose service, and _SERVICE now means the resolved form.
The role's four synapse identifiers finally say what they are - the declared
name, the addressable service, the exec target, and the key.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:59:06 +02:00
kevinveenbirkenbachandClaude Opus 5 57920344bb fix(matrix): send the password realign as a script psql will interpolate
The realign statement was passed through psql -c with :'hash' and :'name'
placeholders. psql expands those only while reading a script; under -c the
string goes to the server untouched, so postgres answered

    ERROR:  syntax error at or near ":"
    LINE 1: UPDATE users SET password_hash = :'hash' WHERE name = :'name...

on every attempt, in both matrix rows of the run. The statement now arrives on
stdin through -f -, where the lexer does substitute, so the values stay
parameters instead of being pasted into SQL.

Move the statement to files/sql and read it with lookup('file', ...), the way
listmonk already provisions its MCP identity. That also makes the script form
structural: the statement now lives where only psql's script lexer reaches it,
so folding it back into -c is no longer a one-character edit.

The exec target was mode-blind as well. lookup('database', ..., 'container')
resolves the same value in every mode, while 'address' is the one that becomes
the resolver subshell under swarm; the shell evaluates it, so the quote filter
goes with it.

This alone does not turn the role green: the swarm rows die earlier, when
docker stack config rejects a compose value carrying the same kind of resolver
subshell. That is a separate defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:49:48 +02:00
kevinveenbirkenbachandClaude Opus 5 236881af3b fix(gitlab): create the default organization a headless install never gets
GitLab 19 places a new user's personal namespace in an organization, so the
MCP token owner cannot be created without one. The script looked the default
organization up and aborted when it was missing, on the assumption that an
installed instance always has it.

A CNG install does not. On a fully migrated v19.3.1, whose migrations log ends
"Performing custom instance setup" and "Registering OAuth applications", the
table is empty:

    ORGS=0
    DEFAULT=nil

Not a row under another id: no organizations at all. Upstream still resolves
the default as find_by(id: DEFAULT_ORGANIZATION_ID), so the lookup is right
and simply finds nothing, and nothing on a headless deploy creates the row.
The first account provisioned here therefore has to.

Create it from GitLab's own constants rather than literals, and report a
failed create through the model's errors, keeping the abort markers this
script already speaks in. Instances that have an organization are untouched:
the branch runs only when the user is absent and the lookup came back nil.

Verified against a live instance before and after: the create yields
"CREATED id=1 path=default" and the lookup then resolves, Users::CreateService
builds the personal namespace, and the deploy's mint task reports changed
where it previously aborted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:33:49 +02:00
kevinveenbirkenbachandClaude Opus 5 f19d224dd0 fix(ci-trigger): never resume at a row the priority line takes
A retrigger by role hands the priority line a bare role name so the rotation
can pick fresh axes, and the matrix then drops that role from the regular
query entirely. The resume offset was computed against a ranking discovered
without the priority line, so it could name one of exactly those rows, and
the run it was handed to had already guaranteed the row would not be there.

Chunk 0 of sweep 77 died on it before deploying anything:

    offset 'web-app-nextcloud#11' names no row of the regular line

and with it every other chunk of the run. The priority line of that run
promoted 27 roles whole, nextcloud among them; its regular line held no
nextcloud row at all.

Give resume_offset the priority pins and let it skip what they claim, by the
same two rules the matrix applies: a variant-less pin takes its whole role, a
pinned one takes those variants. The walk continues past a claimed row rather
than stopping, so a promoted role in the middle of the green window does not
truncate the resume point, and a window that is claimed end to end yields no
offset and starts at the head.

The ranking itself still discovers without the priority line, so an
unresolvable token there still cannot abort the helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 02:07:41 +02:00
kevinveenbirkenbachandClaude Opus 5 0b13fb73d0 style: apply make autoformat
Two leftovers of my own. The sweep that removed the reverse client entries
appended a newline where it cut a block, which at end of file leaves a blank
line after the last key in four services.yml. And the two cache-read nocheck
comments in the bond editor were written past the line limit, so the
formatter breaks their read_text calls across three lines and carries the
comment to the closing paren.

The wrap does not cost the suppression: test_no_uncached_file_reads matches
the statement, not the line the directive sits on, and passes on this form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 01:24:47 +02:00
kevinveenbirkenbachandClaude Opus 5 92f97a53ad docs(readme): regenerate after the reverse client edges came out
The dependency diagrams are drawn from the service edges, so dropping the
client-side homeassistant and baserow declarations changes both ends of each
relation. The four client roles lose the provider node, its service node and
the edge between them; homeassistant loses its whole Dependents subgraph,
because nothing pulls it in from that side any more.

The provider-side edges that carry the MCP pairing are untouched, which is
the point: the relation is now drawn once, from the provider, instead of
from both ends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 01:23:49 +02:00
kevinveenbirkenbachandClaude Opus 5 65a6ca0b50 fix(tests): attribute a config lookup only to a role that owns the id
A lookup('config', application_id, ...) is validated against the defaults of
the role the call is attributed to, and when the app argument is a variable
that role is guessed from the file's own roles/<role>/ location. The guess
holds for a role configuring itself and breaks for one rendering a template
on another role's behalf.

test-e2e-cli is the second kind. Its mcp/test.env.j2 is the single source
twenty provider roles render through lookup('template', ...), and the role
ships no vars/main.yml at all, so application_id there is always the
provider under test. The wildcard check read it as test-e2e-cli's own and
demanded services.*.name and services.*.ports.internal.* from a role that
owns no services.

Skip the fallback when the file's role declares no application_id, which is
how the sibling role-local check already guards the same guess. A literal
app argument stays authoritative either way. Folding the None case into
that condition also drops a branch the guard had made unreachable.

Measured: 114 wildcard paths still checked, exactly the two test-e2e-cli
entries dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 01:22:19 +02:00
infinito-nexus-updater[bot] e9c34d6d78 update(images): update Docker image versions (web-app-seaweedfs) 2026-09-01 23:17:13 +00:00