Commit Graph
4 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 f5413db03c fix(listmonk): keep the password lint off a filename
test_quote_in_shell matches any token containing 'password' anywhere in a shell task and requires it to carry '| quote'. In the realign task that token was the filename in the stdin lookup, not a credential: the password itself already goes through '| quote' on the line above. The check was right to look and wrong about what it found, because the name invited it.

Renaming removes the cause. A nocheck would have blinded the check at that spot for good, including for a genuinely unquoted password added there later, and 'credential' describes the file just as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 01:49:17 +02:00
kevinveenbirkenbachandClaude Opus 5 686ae66726 fix(listmonk): realign the administrator password Listmonk persisted at install
Listmonk seeds the administrator from LISTMONK_ADMIN_PASSWORD only while the users table is still empty, and its CLI exposes install, upgrade and config flags but nothing for users. The swarm matrix rotates credentials between the sync and the async pass, so the async pass typed a password the app had never stored; in run 33923190220 listmonk#1 answered 'Invalid login or password'.

The task above already reaches into the same table for email and password_login, so this joins it rather than inventing a second path. Ansible's password_hash cannot produce the bcrypt: passlib 1.7.4 against bcrypt 4.x raises on any input in this toolchain. pgcrypto does it in the database instead, and it is a trusted extension since PostgreSQL 13, so the owner creates it without superuser rights; the control file in postgis/postgis:17-3.5 confirms trusted = true.

Idempotence comes from re-hashing with the stored salt: crypt(:'pw', password) reproduces the stored hash when the password is unchanged, the row falls out of the WHERE, and psql reports UPDATE 0.

Verified end to end against listmonk v6.2.0 on that same postgres image: the app seeds users.password as 60-character bcrypt, the first run reports UPDATE 1 and the second UPDATE 0, and the stored hash then verifies against the rotated password and no longer against the install one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 00:07:49 +02:00
kevinveenbirkenbachandClaude Opus 5 2071c347ae fix(listmonk): notice a rotated MCP token, not only a created one
Listmonk#0 of run 33414875521 failed the adapter probe in PASS 2:

    REJECTED read call listmonk_get_stats answered 502:
    {"error":{"code":-32000,"message":"upstream_error","data":{"upstream_status":403}}}

403, not a transport error: the adapter reached Listmonk and was turned away.

changed_when matched the psql tag `INSERT 0 1`, which by construction appears only
on the very first deploy -- mcp_identity.sql:14 guards the INSERT with
WHERE NOT EXISTS. On a rotation the row is already there, the statement takes the
UPDATE branch, the new sha256 lands in users.password, and changed_when evaluates
false. The restart block that 8e6fc81eff built then skips, and Listmonk keeps
answering from the API-user cache it loaded when compose recreated it two seconds
earlier -- still holding the previous hash.

The two halves have to move together. The users UPDATE carried no equality guard,
unlike the roles UPDATE two statements above it, so `UPDATE 1` fired on every run
and meant nothing. Guarding it makes the tag informative; only then may
changed_when read it. Guard alone leaves the rotation invisible exactly as today;
widened changed_when alone would restart Listmonk on every converged run.

Measured, not reasoned. sha256 recomputed locally and matched against the log:

    'ZOPJ...XUNP' -> 6a8b5b5d...  = PASS 1 token_hash (listmonk0.log:149940)
    'aTiG...91Iz' -> a90e2a4d...  = PASS 2 token_hash (:343692)

and the window is closed on both ends: compose up ended 21:08:57.93, the new hash
landed 21:08:59.70, the probe ran 21:09:02.15, nothing restarted in between. The
gate flip is verbatim in the log -- PASS 1 `INSERT 0 1 ... changed: true` then the
restart task; PASS 2 `INSERT 0 0 / UPDATE 1 ... changed: false` then
`skipping: false_condition: listmonk_mcp_identity.changed`.

Retrying could not have helped: probe.yml:115 short-circuits its 30 attempts on a
REJECTED verdict, and rightly so -- the database row was correct, the running
process was not.

Matching stdout_lines as list membership rather than a substring of stdout is
deliberate: psql prints each command tag on its own line, and 'UPDATE 1' as a
substring is also satisfied by `UPDATE 10`.

Listmonk was the only role in the tree inferring change from a psql tag --
grep -rn "INSERT 0 1" roles/ returns this one hit, while checkmk, n8n, baserow,
pretix, flowise and jellyfin all emit an explicit marker.

Not covered: the restart path is exercised against a first creation, never against
a rotation. A two-pass deploy closes that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 23:55:57 +02:00
kevinveenbirkenbach 1c03f638be feat(mcp): serve MCP from a locked-down sidecar for ten more applications
Ten applications with no MCP server of their own get one without a line of code
inside them: qdrant, checkmk, fider, jellyfin, listmonk, pretix, prometheus,
snipe-it, zammad and libretranslate.

The sidecar is confined rather than trusted. It runs read-only, drops every
capability, forbids privilege escalation, publishes no port and joins only its
own provider's network, carrying neither that provider's alias nor the consumer
networks the provider itself joined. Its tool surface is a JSON contract, not
code: which tools exist, which upstream path each maps to and what shape the
arguments take. Adding a tool is a line of JSON, not a new image.

The image is built locally from the audited adapter source with the base image
as a build argument, so all ten share one reviewed build instead of ten upstream
dependencies. Each role provisions its own read-only identity in the language
its application speaks, and every adapter include is guarded by the role's MCP
flag, so switching it off leaves no container behind.
2026-08-06 20:44:16 +02:00