test-e2e-cli loads test.env with `set -a && . test.env && set +a`. The
unquoted `ONION_PORTS=25 80 110 ...` is not an assignment there but the
command `80` with a one-word env prefix: bash prints `command not found`,
leaves ONION_PORTS unset, and onion_ports.py reports "no onion-forwarded
ports declared; nothing to probe". The port check passed vacuously in every
tor job with more than one forwarded port (OpenResty, Listmonk, Pixelfed in
run 35661520542) and only ran where a single port was declared.
Pass the value through dotenv_quote, which quotes a space-separated value in
both compose and swarm and keeps the env-file lint satisfied. A unit test
renders the template in both modes and sources it the way test-e2e-cli does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tor attaches a stream to an onion to the rendezvous circuit that matches its
isolation and keeps it there until SocksTimeout; it never moves the stream to a
fresh circuit. The probe connected without SOCKS authentication and gave up
after 60s, so a dead rendezvous circuit would absorb both passes. That is the
most likely reading of run 35661520542 (web-app-mastodon#1, public Tor), where
<onion>:80 timed out twice right after curl had reached the same onion port.
Authenticate every connect with fresh random username/password credentials.
Tor accepts any and, under its default IsolateSOCKSAuth, builds a separate
rendezvous circuit per credential set, so a retry can no longer land on the
circuit that failed the attempt before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in the LMStudio loopback fix (LMS_SERVER_HOST=0.0.0.0 plus the
readiness probe changes), which is what failed the LiteLLM model warm and took
Matrix, Mattermost, Moodle, XWiki and Zammad down with it on the last run.
Also folds CLAUDE.md and GEMINI.md into AGENTS.md, and carries upstream's own
regeneration of the MCP audit report, which matches ours.
No conflicts.
The entry described what the release does but not what it moved or who wrote
it, which every earlier entry states. The version list is derived from the
diff between the two release tags over roles/*/meta/services.yml and
meta/addons/*.yml, deduplicated per role and annotated where a sub-service
differs from the role's own image, so GitLab appears once for its six images
and Matrix once for Synapse and once for Element.
Pins are listed apart from jumps, because a moving tag that now carries a
release is not an upgrade: the tag names the same software it always did. The
Debian codename bases that became numeric slim tags sit in that paragraph for
the same reason, and web-app-flowise is called out as an image switch, since
Flowise itself stays at 3.1.4 under a locally built node base.
Contributors follow the shape of the 13.0.0 and 11.x entries and come from
git shortlog over the tag range. The bot commits are not listed there; what
they produced is the version list itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md and GEMINI.md existed as per-runtime extensions of AGENTS.md, and
only one of them carried anything at all: GEMINI.md stated that it had no
additions, while CLAUDE.md held rules that bind every runtime anyway — the
permission announcement, the make-over-raw preference, the loop, chain and
heredoc bans, the stream-the-log-to-/tmp rule, the CI-evidence rule, the push
ban and the comment rule. A rule that binds every agent does not belong in one
agent's file. AGENTS.md absorbs them and now states that no tool-specific
extension file exists; agents whose runtime auto-loads a different filename
reach it through their own configuration instead.
The filenames were machinery as much as prose. The PR templates, the branch
and pull-request scope tables, the settings and security docs that justified
the loop deny by "the textual CLAUDE.md rule", six requirement documents whose
push instruction linked ../../CLAUDE.md, and a test docstring citing the
shell-loop ban all named them, and now name AGENTS.md.
scripts/meta/resolve/pr/scope.sh deliberately keeps matching CLAUDE.md and
GEMINI.md: this change deletes them, and without those patterns the change
itself would not classify as agent-scope.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An image bump in a provider redeployed every variant of every consumer,
including the variants that switch that provider off and therefore never
deploy the bumped image at all. The diff-derived whitelist now narrows each
affected role to the variants whose own round closure still reaches a seed,
emitted as `role#0,2` selection tokens. That closure is walked on the same
variant-merged services maps the matrix planner builds its rounds from, so the
narrowing follows a parent's variant choice across every generation of the
chain. It covers every auto-derived whitelist: the weekly update PRs,
Dependabot, and any other diff-driven run.
The narrowing never shrinks the matrix on partial information. A seed role
keeps every variant, because its own files changed. A role reached only
through a run_after edge keeps every variant too, because the round closure
does not follow that edge and a role whose hits are empty is therefore
unnarrowed rather than dropped.
A pinned token naming a role the discovery query does not return now warns
instead of aborting the run. The diff-derived whitelist legitimately names
roles the lifecycle envelope filters out, and once it pins variants such a
token would abort every chunk; role names are still checked against roles/
before the run starts, so a typo is caught one layer earlier.
The round's variant merge moves out of the deploy inventory package into
utils/roles/applications/variants.py. The CI selection has to build the same
maps, and importing the deploy package for them pulls in a module that reads
INFINITO_SRC_DIR at import time, which the runner host does not set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The readiness wait called a bare curl, which test_curl_via_filter rejects: a peer that accepts the connection and never answers keeps curl hanging, and the task's own retries never fire. The probe now renders through the curl filter with a 10 second cap per attempt. It passes retries=0, so the task's 60 x 5 s loop stays the only retry layer instead of nesting curl's --retry inside every attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous reason named only multi edit, which made the relaxation look
specific to one tool and newly needed. Neither is true.
LAM emits inline <script> elements through htmlJavaScript from 18 shipped
files, covering two-factor setup, deletion, tree view, mass upload, PDF
editing, several account modules and the config pages. Every one was blocked
before; nothing had driven those paths until a test used multi edit.
Hashing was evaluated and does not work here. A directive carrying any hash
makes browsers ignore unsafe-inline entirely, so all 18 sites would have to be
enumerated, and several build their snippet per request.
The preload ran lms get as soon as the container existed. On a slow runner that reaches the daemon while its delayed init is still running, and the daemon then comes up without .internal/temp. Every later model load fails, because the chat template is written with mkdtemp into that missing folder: Failed to load model "qwen2.5-0.5b-instruct". Error: ENOENT: no such file or directory, mkdtemp '/root/.lmstudio/.internal/temp/lmstudio-chat-template-...'. litellm's warm-up then returned HTTP 400 in Moodle#2 of run 35617847242, while a local deploy on faster hardware stayed green.
Reproduced on fresh llmster containers capped at 0.3 CPU: an lms get issued during startup left .internal/temp absent; the same lms get issued after /lmstudio-greeting answered left it in place. The preload now waits for that endpoint, which only answers once lms server start ran, and daemon-run.sh starts the server only after the daemon's ready file exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role test asked for /v1/models on 127.0.0.1 from inside the container, the one address that still answered while the server was bound to loopback alone. It stayed green and the failure surfaced one role later as a 500 in litellm's warm-up.
Step 3 now resolves the container's first IPv4 through hostname -I and queries that address, which a loopback-only bind refuses. It reads the interfaces directly, so it needs neither the service name nor swarm's route back through the service VIP, and compose and swarm take the same path. When the models never appear, one more request tells an unreachable server apart from an unindexed store.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream re-pushed lmstudio/llmster-preview:cpu on 2026-09-18. The old build's daemon-run.sh wrote networkInterface 0.0.0.0 into .internal/http-server-config.json before starting the server. The new one drops that block, and its lms resolves the address as --bind, then LMS_SERVER_HOST, then 127.0.0.1, reading only the port from that file.
The server therefore listened on loopback alone. The image healthcheck curls localhost and every role task drives lms through container exec, so svc-ai-lmstudio stayed green, while litellm's warm-up got Connect call failed on lmstudio:1234 and failed Mattermost#2, Matrix#2, XWiki#2, Moodle#2 and Zammad#2 in fork run 35545771696.
LMS_SERVER_HOST is the documented knob and restores exactly the old bind. The port stays expose-only on the container network, so nothing new is published.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_the_committed_report_is_freshly_generated compares the committed table
against a fresh render, and baserow had moved to 2.3.4 in its services.yml
while the table still read 2.3.3.
The bump arrived from the image-version updater, which edits services.yml and
nothing else. The report is derived from those files but is committed, so every
version bump for an MCP-serving role leaves it stale until someone runs
`make mcp-audit`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eighteen bullets to ten. Merges the four seal-and-recovery notes into two,
folds the three path traps into one, compresses the rotation block from six
sub-bullets to a paragraph, and drops the note on role-declared account
passwords entirely: the commit it was waiting on has landed, so the
workaround it described is no longer needed.
Nothing operational is lost. The seal key still says a raft backup is
undecryptable without the inventory, and the recovery key still says where it
lives and what it cannot do.
Stubs native_metrics_apps and asserts the intersection, including the case the
refactor exists for: a role that ships a fragment and is deployed but is not
scraped must not reach the rule file.
The lookup filtered on group_names, so each fragment had to re-check whether
its app was actually scraped. Deployed and scraped are different conditions:
metrics can be off on a deployed role, and a rule on a series nothing scrapes
alerts on absent() forever.
Intersecting with native_metrics_apps moves that check to one place, and every
fragment from here on is plain rules with no guard.
Review had these removed across the role once already, and the 11 September
pass cleared every file that was flagged. The seal-key pin and the
recovery-key work then reintroduced the same pattern in nine new places.
Also removes two that predate the review and were never flagged: the
healthcheck note in meta/services.yml and the entrypoint note in
compose.yml.j2. Both are the same narrative-exception shape.
Param docs, file headers and the plain contract notes in the Playwright specs
stay: separate categories, none of them objected to. What the deleted
comments recorded lives in the role README.
The merge from main moved web-app-baserow to 2.3.4 without regenerating the
report the version is rendered into, so test_mcp_audit_report saw a document
that no longer matched a fresh generation. Produced by make mcp-audit.