At -vvv Ansible prints each module argument and result, so a password in
a command line, a request body, a header or a fact lands in the deploy
log. Nothing enforced masking outside the MCP task files, and no check
looked at parents at all.
A new lint, test_credential_tasks_are_masked, finds tasks whose printed
parts (module arguments, environment, loop) reference an identifier ending
in password, secret, token, api_key or private_key, or
secrets.credentials. A task passes when it or an enclosing block sets
no_log, when every file that pulls it in does so through include_tasks
with apply.no_log or import_tasks with no_log (transitively), when the
secret only reaches a parameter the module masks itself, or when the
module masks its values. no_log on an include_tasks task itself does not
count, because it does not reach the included tasks. The rule can be
suppressed per task with `# nocheck: credential-masking`, now listed in
the suppression catalog, and the lint tests its parent and suppression
logic on synthetic cases.
It found 39 unmasked tasks in 19 roles, all real: join tokens, client
secrets, API tokens in Cloudflare headers, database passwords in
command lines, the ChatGPT bot login body and more. Each now sets
no_log from MASK_CREDENTIALS_IN_LOGS. The sys-dns-hetzner-rdns alias
hetzner_no_log, which only restated that switch, is gone.
Not covered yet: template and copy tasks whose rendered file carries a
secret print it as a diff under --diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLI contract test rejected WordPress in job 102705652296 of run
34421944328:
REJECTED server serves ['mcp-adapter-discover-abilities', 'mcp-adapter-execute-ability', 'mcp-adapter-get-ability-info'] but the contract declares ['infinito-get-post', 'infinito-list-categories', 'infinito-search-posts']
There are two layers. The mcp-adapter v0.5.0 default server lists only its
three meta-tools and reaches public abilities through discover and execute; it
takes its tool list from the mcp_adapter_default_server_config filter
(includes/Servers/DefaultServerFactory.php). And the three infinito abilities
were never registered at all: the mu-plugin hooked abilities_api_init and
passed no category, while WordPress 7.1.0 core fires only
wp_abilities_api_init, refuses wp_register_ability() outside it and requires a
category (src/wp-includes/abilities-api.php:290-297,
abilities-api/class-wp-ability.php:295-297).
The mu-plugin now registers an `infinito` category on
wp_abilities_api_categories_init, registers the abilities on
wp_abilities_api_init with that category, and sets the default server's tools
to exactly the three abilities, with empty resources and prompts, at
PHP_INT_MAX. With the meta-tools gone, execute-ability no longer reaches every
public ability any other plugin registers.
The abilities return objects ({posts}, {categories}, {post}), since MCP defines
structuredContent as a JSON object. get-post now serves published posts only,
as search-posts already did; before, it returned any published type, including
customize_changeset and wp_block entries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The container_address lookup does not return a container name in swarm. It
returns a shell fragment, "$(/usr/bin/resolve-container-id <stack> <service>)",
which is only a name once an interpreter evaluates the substitution. The shell
module provides one. The command module does not -- it execs the binary directly,
so the fragment arrives at container exec as literal text and the call dies with
"No such container: $(...)".
Compose hides the entire class: there the same lookup yields a bare name and
command: works. The failure exists only in swarm, and no swarm run had touched
these paths -- both were added by feature commits (b23653adf8, 0421ebeb16) that
wrote the same argv shape independently, which is the argument for a rule rather
than six edits.
Six call sites moved to the shell module: the moodle egress task, the three
moodle AI-gateway tasks, and the two wordpress AI-Engine tasks. The wordpress
pair keeps its stdin, so it uses the dict form with cmd:. Moving off argv exposes
the surrounding values to word splitting, so they now go through quote -- the
address itself must NOT, because single quotes would suppress exactly the
substitution this depends on.
test_argv_exec_needs_shell.py closes the gap that let this through.
container-exec-resolver checks that the resolver is used; it has nothing to say
about whether anything can evaluate the result. The new rule checks that other
half. It reuses the sibling's argv parser, which recognises only container/docker
in first position, so an argv list that does start an interpreter
(argv: [bash, -lc, ...], as joomla does) is correctly not matched.
Run against the tree the rule reported exactly the five sites found by hand plus
bigbluebutton, and passed over joomla and the discourse task, whose address is a
config lookup rather than the resolver. Bigbluebutton is pinned to compose via
compose_mode_force and can only ever see a bare name, so it takes a nocheck.
Five container-exec-resolver markers disappear with the argv form: under shell
the resolver is the ordinary path, not an exception.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The relocation in dfe7a7fdd moved provision_mcp.php,
provision_mcp_identity.php and the ai_engine pair into files/php/ but left
their callers pointing at the old locations, so moodle's and snipe-it's MCP
provisioning and wordpress's AI Engine configuration resolved a path that
no longer exists.
The reference sweep that accompanied the move searched for python, shell,
ruby and sql folders and never looked for php, so these four lookups were
missed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.
The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.
Resolutions that were not unions:
Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.
plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.
compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.
tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.
Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.
01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.
Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.
roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.
config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.
READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.
Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_prefix_groups_belong_in_subfolders flagged three groups added by this change
set: a shared delimiter-bounded prefix is a folder in disguise, so the group moves
into a folder named after the prefix and the prefix leaves the file names.
sql/select/ai-{bot-user,service}.sql.j2 -> sql/select/ai/{bot-user,service}.sql.j2
files/provision_{ai,mcp}.py -> files/provision/{ai,mcp}.py
files/ai_engine_{configure,endpoint}.php -> files/ai_engine/{configure,endpoint}.php
All six are recorded as renames, so history follows them. Every reference moves
with them: four templated query paths in mattermost's ai_gateway.yml, the two
lookup('file', ...) calls in n8n, the two in wordpress, and SCRIPT_PATH plus the
module docstring in the n8n unit test. lookup('file', ...) resolves relative to
the role's files/ directory, so the new argument is provision/mcp.py rather than
a repo-relative path.
Worth recording why this survived every gate: make quality was green on each run
this session, including immediately before the commits that introduced these
files. The lint enumerates through git ls-files, and the files were untracked, so
it could not see them. Committing them is what made them visible. A green gate
taken before new files are tracked says nothing about the gate after.
Verified: repository lints go from 88 passed with 1 failure to 89 passed, the
unit suite passes at 4448, and a repo-wide grep for every old path returns
nothing.
Not verified: no deploy has run. The lookup('file', ...) arguments and the
templated SQL paths resolve at play time, so a wrong path would surface as a task
failure rather than a test failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WordPress has no AI surface in core, so the requirement applies to whichever
connector the role pins rather than leaving the choice to a site administrator.
The pinned connector is ai-engine, whose `custom` engine type takes an
OpenAI-compatible base URL and an API key per environment.
AI Engine keeps every setting in a single mwai_options array and its REST route
replaces that array wholesale, so the deploy merges instead of assigning:
ai_engine_configure.php filters ai_envs by id, re-appends the managed
environment, and leaves every other environment and API key untouched. A
straight write would delete configuration this deploy never owned.
Both scripts are fed to `wp eval-file -` over stdin with their input passed as
an environment variable. The first attempt used `wp eval` with the code inline,
which cannot survive the quoting: a YAML folded scalar hands Jinja a literal
backslash sequence and PHP fails to parse. That was reproduced in the container,
along with the detail that `wp eval-file -` requires the opening <?php tag,
before the approach was changed. Passing the payload as base64 in an env var
means no shell quoting touches either the code or the virtual key.
ai_engine_endpoint.php reads back the endpoint AI Engine actually stored for the
managed environment, so the assertion compares the plugin's own state rather
than the payload the deploy just sent.
Not verified: the configure script has never run against a live AI Engine
install, so the merge is reasoned from the option shape rather than observed,
and addons/ai-engine.spec.js has executed zero times because the addon bridge
gate resolved svc-ai-litellm against the Playwright role list and skipped it.
ai-engine is third-party, so a change to its option schema would leave this
writing a shape the plugin no longer reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine roles reached their MCP surface as the deployment administrator or with
an account password kept beside the account in credentials. A Moodle site
admin bypasses every capability check, a Nextcloud app password is not
scopable, a Gitea token reads every private repository, and a GitLab token on
root reads every private project. Each role now declares its own principal in
meta/users.yml and provisions it, so the exposed surface is what the service
role permits. jellyfin, zammad and snipe-it kept their account but took its
password from a credential named token; they read the registry too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four readiness probes waited on lookup('domain', <app>) port 80 -- a
publication identity used as a machine address. With svc-net-tor deployed
that name is onion-only, because exclusive: true replaces the clearnet
domain rather than adding to it (utils/cache/domains.py).
Nextcloud 5 then burned its full 600s on zammad.helpdesk.<onion>:80. The task
carries no delegate_to but inherits apply: delegate_to: NEXTCLOUD_HOST_NODE
from 02_manager_ops.yml:46-50, which propagates through nested includes, so it
ran on a worker. svc-net-tor is placement: manager, so that worker had no
.onion split-DNS, no TransPort redirect and no :80 listener. wait_for swallows
socket.gaierror in a bare except and retries, so an unresolvable name is
indistinguishable from a slow service -- it just burns the timeout.
Point the probes at DOCKER_REACH_HOST and the service's own
ports.local.http instead. No name resolution, so the onion replacement cannot
reach it; node-local, so the inherited delegation stops mattering; and it hits
the service's own listener rather than the shared front proxy, which is what
the probe was supposed to measure in the first place.
Verified on the failing run's artifacts that this is node-independent in
swarm: zammad_railsserver.1 ran only on mgr-01, yet dockerd listens on *:8077
on all three nodes -- the ingress mesh. Only svc-prx-openresty and svc-net-tor
publish with mode: host; every other role, these four included, goes through
ingress.
Same swap for openproject, suitecrm and wordpress->discourse, which had the
identical construct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WordPress serves MCP through two addons rather than a sidecar, because the
application already carries the extension point.
The mu-plugin is the entire tool surface. The MCP Adapter publishes an ability
only when it carries meta.mcp.public, so whatever is not registered in that file
cannot be reached over MCP no matter what else the installation has. Every
ability reads already-published content and carries its own permission
callback, which is not decoration: the adapter authorizes the transport, the
callback authorizes the operation, and dropping it would let any authenticated
caller reach the ability.
The second addon fetches the release asset rather than the tag's source
archive. The source archive unpacks as mcp-adapter-<version>, which no longer
answers to the slug the generic activation step uses, and it ships no vendor
directory.
Three Playwright specs cover what needs no credential: both addons active, the
abilities registered, and a guest served nothing. The mcp role joins the role
catalogue instead of replacing it.
The structure lint requires files sharing a name prefix to live in a
directory of that name. tasks/utils/install_plugin.yml and
tasks/utils/install_mu_plugin.yml violated that from the moment they
landed in 4d9bf6da3; move both into tasks/utils/install/ and repoint the
three includes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deploy died in the drill's second pass on `wp plugin install activitypub
--activate`: the first pass had installed it, the DR restore put the tree back,
and wp-cli refuses to unpack onto an occupied directory. `--force` would have
made that green by overwriting whatever the restore produced - on a branch about
NFS-backed volumes, the worst possible place to paper over a restore. The probe
keeps it red instead: `wp plugin is-installed` answers from `get_plugins()`
alone (verified in the pinned wp-cli phar: `Fetchers\Plugin::get()` iterates
`get_plugins()` and nothing else), so a tree WordPress cannot parse still
reaches the install and still fails on folder_exists.
Activation moves into its own step. Chained to `install --activate`, wp-cli sets
chained_command and skips the result check, so an activation failure returns 0
today.
The same install path now serves both call sites; the addon hook carried its own
copy, which is why one defect needed fixing twice.
mu-plugins follow the declaration the way discourse already does. The loop reads
`mechanism: mu_plugin` from meta/addons/ instead of spelling two filenames, which
is how `infinito-http-ca-trust` shipped undeclared while `infinito-oidc-rbac-mapper`
carried a `required: true` that nothing evaluated. Both install paths honour that
field now - a required addon aborts the play, an optional one warns and is
skipped - which is the first place in the repo to read it.
Three guards, because the loop replaced two guaranteed literals: an assert that
every required addon is claimed by some install path, a lint pairing each
vendored addon with its shipped file in both directions, and the spec the addon
contract demands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The container-exec-resolver lint only saw the inline shell form, so the
`command: argv: [container, exec, <name>]` list form (two separate lines)
slipped through, which is how checkmk's bare name reached swarm. Extend the
scanner to parse argv sequences via yaml.compose_all with node line numbers,
and annotate the nine legitimate bare-name execs (mariadb recovery container,
matomo compose-only debug, probe-gated nextcloud partner provisioning, pixelfed
runtime-resolved CID, wordpress launcher-managed discourse) with
# nocheck: container-exec-resolver.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the now-unused TAIGA_INSTANCE_DIRECTORY (its only consumer left
with the orphaned compose-inits render), mark the two discourse
launcher execs with the container-exec-resolver nocheck (the launcher
container is named literally in both modes), and purge narration
comments from the dotenv quote tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects broke the wordpress jobs in both modes:
The wp-discourse shell tasks used folded scalars whose continuation
lines were indented deeper than the first, so YAML preserved the
newlines and the shell ran three separate commands instead of one
(--path=... reached the host shell as its own command). Align the
continuations so the fold collapses to a single line.
The discourse exec address came from container_address, which expands
to a resolve subshell under swarm; the argv-form command task passed it
verbatim to docker as a container name. Discourse is launcher-based and
its container is literally named 'discourse' in both modes, so use the
services.discourse.name config lookup instead.
The playwright env rendered keycloak credential lookups uncondition-
ally, crashing the sso-disabled variant; gate them on
lookup('service','sso').required like the sibling flags, and drop
narration comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
container cp needed the role checkout on the node running the docker
CLI, which does not exist on swarm workers; the mu-plugins now stream
through container exec -i tee with the file read controller-side. The
daggerhart WP-CLI eval line was indented deeper than its folded scalar
block, so YAML kept the newline and bash -lc received no script
argument; realigning the indent folds it back into one command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a 'timeout' lookup plugin that returns round(base * TIMEOUT_FACTOR)
(group_vars default 1, so value-identical to the old literals). One knob
in group_vars stretches every wait in the stack for a slow uplink:
TIMEOUT_FACTOR: 2 doubles them all.
- plugins/lookup/timeout.py + unit tests (identity, factor scaling,
string coercion, error paths).
- group_vars/all/17_resource.yml: TIMEOUT_FACTOR: 1.
- Convert all 34 literal 'timeout:' task keywords / module args across 25
task+handler files to "{{ lookup('timeout', <base>) | int }}".
- tests/lint/repository/yaml/test_timeout_uses_lookup.py enforces the
lookup in every task/handler timeout (opt-out: # nocheck: timeout-lookup).
- swarm.yml pre-deploy build handler: add the missing timeout (same
network-build hang vector as compose.yml's Build compose).
- Drop 13 pre-existing narration comments in the touched files (comment
lint enforces whole-file on any edited file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.
- emit emoji names at the source: cli/build/include generator now writes
"🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate the wordpress per-addon task files from tasks/plugins/ to tasks/addons/ (pure renames) and repoint tasks/main.yml, giving every role a uniform tasks/addons/ layout. Remove roles/web-app-xwiki/meta/addons/matomo.yml: matomo is provided as a service via services.matomo, not an Extension-Manager extension, and the org.xwiki.contrib:matomo extension does not exist in the nexus catalog (install plan failed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>