The convergence check compared the stored service element to the declared one with
jsonb equality, and the deploy failed on "The Agents plugin still stores a service or
bot that differs from the deployed gateway contract".
The sequence is write via SQL, cycle the plugin, read back. Cycling is exactly when
the plugin rewrites that structure, and it owns it. Any field it adds of its own makes
byte-identical equality impossible, permanently, while the contract itself holds.
Containment says what the task means. Its own name asks whether the stored config
still matches the gateway contract, not whether it is the same object: the deployed
keys must be present with the deployed values, and what the plugin keeps besides is
its business.
Only that one condition changes. The bot check, the legacy-bot check, the FROM clause
and the per-active-row semantics stay as they were -- a first attempt restructured the
whole query into CTEs, which would have aggregated across active rows instead of
evaluating each, and reported converged as soon as any one of them matched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four SQL bodies behind the Agents integration were Jinja templates read
through lookup('template'), which meant the raw query: parameter and a
"# nocheck: database-query-raw" on all five call sites in
tasks/utils/ai_gateway.yml.
They now use the mechanism the database_query module already offers: static
.sql files under files/sql/, reached by query_file:, with the values supplied
as named_args and %(name)s placeholders. All five suppressions are gone.
Same move as 4e9d4b53db made for odoo, for the reason that commit gives: an
interpolation straight into SQL carries an injection surface that a bound
parameter does not. Postgres dollar-quoting was doing that job here; the
module's own escaping does it now, doubling single quotes per the _ENGINES
postgres entry.
The | to_json filters did not disappear, they moved to the call site.
MATTERMOST_LITELLM_SERVICE and MATTERMOST_LITELLM_BOT are dicts, and the
module raises TypeError on those, so pre-serialising is what it asks for.
set_ai/ became update/ai/ to match the select/ and update/ verbs the role's
existing files/sql already uses.
The api key inside MATTERMOST_LITELLM_SERVICE is now a named_args value, which
the module adds to no_log_values; as part of the query string it was not.
Verified statically only: the database-query-raw lint passes, the task file
still parses to the same 11 tasks in the same order, every placeholder has a
matching named_args entry, and no reference to the old paths survives. Nothing
ran against a database, and the adversarial review of this change had not
finished when it was committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merged branch renumbered the Nextcloud tasks to 01/02, leaving the
MCP tasks stranded at 09 with a gap the numbering lint rejects; rename
them to 03 and follow every reference.
The Mattermost select statements only formed a prefix group once the
incoming branch's own select landed beside them, so they move into
sql/select/, and the two user-scoped ones again into sql/select/user/.
Pin the object kind on the kata isolation inspect: this repository names
a role's overlay network after the same entity as its main container, so
an untyped inspect can resolve the network and report a missing State key
instead of the missing container. Suppress the address-lookup rule there
because both callers gate the assertion on compose, where the container
name is the address.
Give the sandbox-runtime test the data-root variable the incoming
daemon.json template reads. Both sides were green alone: their template
gained the variable, this test renders it under StrictUndefined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mattermost keeps the plugin's MCP server switch in the database rather
than in a config file, so the role reads the plugin state, flips the
server on, and reads back the personal access token through SQL, with
reading and writing statements kept in separate directories.
Take over the token the server issued instead of generating one: the
value only exists once Mattermost has minted it, so it is persisted
through sys-token-store rather than declared as a vault credential.
Calls execute as the account the token belongs to, so the surface is
bounded by that account's permissions rather than by an administrator
role.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.
Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace 13 raw `container exec mysql/psql` invocations across
web-app-pixelfed, web-app-fider, and web-app-mattermost with the
canonical `database_query` module + `query_file:` + `named_args:`
pattern. The module already speaks both shared (central `svc-db-*`)
and non-shared (per-role `<entity>-database`) modes via the
`database` lookup, so the same task shape works regardless of DB
topology — the per-role hardcoded container-exec argv was the only
blocker.
- Extract every inline SQL body into per-role `files/sql/` trees.
Pixelfed splits further into `files/sql/oidc-mapping/` because the
OIDC-mapping cluster is a distinct concern from the bootstrap /
administrator queries. Each SQL file is single-purpose and named by
verb-noun (count-users, select-admin-id, upsert, etc.) to match the
precedent set by web-app-odoo.
- Fider's existing `files/{create_admin_user,configure_oidc_provider}.sql`
move to `files/sql/` and switch from psql-specific `:'var'`
variable substitution (which forced `stdin:` piping with `-v`
flags) to engine-neutral `%(name)s` placeholders the module
escapes per backend.
- Pixelfed loses `PIXELFED_MARIADB_ARGS` + 3 derived DB vars
(`*_CONTAINER`, `*_USERNAME`, `*_PASSWORD`) from `vars/main.yml` —
they were the manual argv bundle for the old container-exec
pattern and become orphan once `lookup('database', application_id)`
takes over.
- Every refactored task carries `mask_values:
MASK_CREDENTIALS_IN_LOGS` (module-level credential redaction);
tasks that previously used `no_log: MASK_CREDENTIALS_IN_LOGS`
keep both directives so the task-level masking stays alongside
the module-level one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>