diff --git a/roles/svc-ai-litellm/files/ensure_keys.py b/roles/svc-ai-litellm/files/python/ensure_keys.py similarity index 100% rename from roles/svc-ai-litellm/files/ensure_keys.py rename to roles/svc-ai-litellm/files/python/ensure_keys.py diff --git a/roles/svc-ai-litellm/tasks/01_keys.yml b/roles/svc-ai-litellm/tasks/01_keys.yml index 2757b0979a..1a30d6a46b 100644 --- a/roles/svc-ai-litellm/tasks/01_keys.yml +++ b/roles/svc-ai-litellm/tasks/01_keys.yml @@ -8,7 +8,7 @@ - name: "🔑 Ensure a virtual key exists per consumer" ansible.builtin.shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node - container exec -i -e LITELLM_KEYS_PAYLOAD={{ LITELLM_CONSUMER_KEYS | to_json | quote }} -e LITELLM_MK={{ LITELLM_MASTER_KEY | quote }} -e LITELLM_PORT={{ LITELLM_PORT | quote }} {{ LITELLM_LOCAL_CID | quote }} python3 -c {{ lookup('file', lookup('path_absolute', 'roles/svc-ai-litellm/files/ensure_keys.py')) | quote }} + container exec -i -e LITELLM_KEYS_PAYLOAD={{ LITELLM_CONSUMER_KEYS | to_json | quote }} -e LITELLM_MK={{ LITELLM_MASTER_KEY | quote }} -e LITELLM_PORT={{ LITELLM_PORT | quote }} {{ LITELLM_LOCAL_CID | quote }} python3 -c {{ lookup('file', lookup('path_absolute', 'roles/svc-ai-litellm/files/python/ensure_keys.py')) | quote }} register: litellm_keys_result retries: 10 delay: 6 diff --git a/roles/svc-ai-mcp-adapter/README.md b/roles/svc-ai-mcp-adapter/README.md index 17634d3f32..6fd39702b4 100644 --- a/roles/svc-ai-mcp-adapter/README.md +++ b/roles/svc-ai-mcp-adapter/README.md @@ -56,7 +56,7 @@ The deployment renders one `ADAPTER_CONTRACT` JSON document per instance: } ``` -`files/policy.py` validates it at startup. A contract missing a limit, carrying an empty allowlist, naming a wildcard path or lacking a pinned schema hash is refused before the process serves anything. +`files/python/policy.py` validates it at startup. A contract missing a limit, carrying an empty allowlist, naming a wildcard path or lacking a pinned schema hash is refused before the process serves anything. ## Endpoints diff --git a/roles/svc-ai-mcp-adapter/files/passthrough.py b/roles/svc-ai-mcp-adapter/files/python/passthrough.py similarity index 100% rename from roles/svc-ai-mcp-adapter/files/passthrough.py rename to roles/svc-ai-mcp-adapter/files/python/passthrough.py diff --git a/roles/svc-ai-mcp-adapter/files/policy.py b/roles/svc-ai-mcp-adapter/files/python/policy.py similarity index 100% rename from roles/svc-ai-mcp-adapter/files/policy.py rename to roles/svc-ai-mcp-adapter/files/python/policy.py diff --git a/roles/svc-ai-mcp-adapter/files/probe/contract.py b/roles/svc-ai-mcp-adapter/files/python/probe/contract.py similarity index 100% rename from roles/svc-ai-mcp-adapter/files/probe/contract.py rename to roles/svc-ai-mcp-adapter/files/python/probe/contract.py diff --git a/roles/svc-ai-mcp-adapter/files/probe/isolation.py b/roles/svc-ai-mcp-adapter/files/python/probe/isolation.py similarity index 92% rename from roles/svc-ai-mcp-adapter/files/probe/isolation.py rename to roles/svc-ai-mcp-adapter/files/python/probe/isolation.py index dd5b9582f8..bfbeca3c29 100644 --- a/roles/svc-ai-mcp-adapter/files/probe/isolation.py +++ b/roles/svc-ai-mcp-adapter/files/python/probe/isolation.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - the measurement is an SIOCGIFADDR ioctl over the +# sidecar's own interfaces, issued at import; anything a unit test could substitute for +# those interfaces would be the answer it is meant to check """Prove the adapter sidecar is attached to exactly the networks it may reach. Runs inside the sidecar itself. A container joined to N docker networks carries diff --git a/roles/svc-ai-mcp-adapter/files/server.py b/roles/svc-ai-mcp-adapter/files/python/server.py similarity index 100% rename from roles/svc-ai-mcp-adapter/files/server.py rename to roles/svc-ai-mcp-adapter/files/python/server.py diff --git a/roles/svc-ai-mcp-adapter/tasks/instance.yml b/roles/svc-ai-mcp-adapter/tasks/instance.yml index 1153b514ce..297d7fbf64 100644 --- a/roles/svc-ai-mcp-adapter/tasks/instance.yml +++ b/roles/svc-ai-mcp-adapter/tasks/instance.yml @@ -46,15 +46,15 @@ dest: "{{ [mcp_adapter_instance_dir, 'Dockerfile'] | path_join }}" mode: "0644" notify: [] - - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/policy.py') }}" + - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/python/policy.py') }}" dest: "{{ [mcp_adapter_instance_dir, 'policy.py'] | path_join }}" mode: "0644" notify: [] - - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/passthrough.py') }}" + - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/python/passthrough.py') }}" dest: "{{ [mcp_adapter_instance_dir, 'passthrough.py'] | path_join }}" mode: "0644" notify: [] - - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/server.py') }}" + - src: "{{ lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/python/server.py') }}" dest: "{{ [mcp_adapter_instance_dir, 'server.py'] | path_join }}" mode: "0644" notify: [] diff --git a/roles/svc-ai-mcp-adapter/tasks/isolation.yml b/roles/svc-ai-mcp-adapter/tasks/isolation.yml index 953beaad33..a2d7bba1b1 100644 --- a/roles/svc-ai-mcp-adapter/tasks/isolation.yml +++ b/roles/svc-ai-mcp-adapter/tasks/isolation.yml @@ -74,7 +74,7 @@ - "{{ MCP_SIDECAR_LOCAL_CID }}" # nocheck: container-exec-resolver - python3 - "-" - stdin: "{{ lookup('file', lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/probe/isolation.py')) }}" + stdin: "{{ lookup('file', lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/python/probe/isolation.py')) }}" register: mcp_isolation_probe changed_when: false failed_when: false diff --git a/roles/svc-ai-mcp-adapter/tasks/probe.yml b/roles/svc-ai-mcp-adapter/tasks/probe.yml index 4b0cdc514f..13a2fd458b 100644 --- a/roles/svc-ai-mcp-adapter/tasks/probe.yml +++ b/roles/svc-ai-mcp-adapter/tasks/probe.yml @@ -108,7 +108,7 @@ - "{{ MCP_PROBE_IMAGE }}" - python3 - "-" - stdin: "{{ lookup('file', lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/probe/contract.py')) }}" + stdin: "{{ lookup('file', lookup('path_absolute', 'roles/svc-ai-mcp-adapter/files/python/probe/contract.py')) }}" register: mcp_contract_probe retries: 30 delay: 10 diff --git a/roles/sys-svc-compose/files/swarm/stack_ready.sh b/roles/sys-svc-compose/files/swarm/stack_ready.sh deleted file mode 100755 index 7506607912..0000000000 --- a/roles/sys-svc-compose/files/swarm/stack_ready.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${STACK:?STACK env var is required}" - -is_completed_oneshot() { - local ps - ps=$(timeout 15 docker service ps --no-trunc \ - --format '{{.Name}}|{{.DesiredState}}|{{.CurrentState}}' "$1" 2>/dev/null) || return 1 - [ -n "$ps" ] || return 1 - awk -F'|' ' - !seen[$1]++ { - if ($2 ~ /Running/ || $2 ~ /Ready/) pending = 1 - else if ($2 ~ /Shutdown/ && $3 ~ /Complete/) done_ok = 1 - else bad = 1 - } - END { exit (done_ok && !pending && !bad) ? 0 : 1 } - ' <<<"$ps" -} - -report_tasks() { - local ps rows - if ! ps=$(timeout 15 docker service ps --no-trunc \ - --format '{{.Name}} desired={{.DesiredState}} current={{.CurrentState}} error={{.Error}}' "$1" 2>&1); then - printf ' %s: docker service ps failed: %s\n' "$1" "$ps" >&2 - return 0 - fi - rows="$(printf '%s\n' "$ps" | awk '!/error=$/' | head -10)" - [ -n "${rows}" ] || return 0 - printf '%s\n' "${rows}" | sed 's/^/ /' >&2 -} - -if ! services=$(timeout 15 docker stack services --format '{{.Name}} {{.Replicas}}' "$STACK"); then - echo "not converged: docker stack services failed or timed out for ${STACK}" >&2 - exit 1 -fi - -mapfile -t service_names < <(printf '%s\n' "$services" | awk 'NF {print $1}') -latched="" -if [ ${#service_names[@]} -gt 0 ]; then - update_states=$(timeout 15 docker service inspect \ - --format '{{.Spec.Name}} {{if .UpdateStatus}}{{.UpdateStatus.State}}{{else}}none{{end}}' \ - "${service_names[@]}" 2>/dev/null || true) - while read -r name state; do - [ -n "$name" ] || continue - case "$state" in - paused | rollback_paused | rollback_started | rollback_completed) - latched="${latched} ${name}(${state})" - ;; - esac - done <<<"$update_states" -fi - -if [ -n "$latched" ]; then - echo "update latched:${latched}; it cannot leave this state without another stack deploy" >&2 - printf '%s\n' "$latched" | tr ' ' '\n' | sed 's/(.*//' | while read -r svc; do - [ -n "$svc" ] || continue - report_tasks "$svc" - done - exit 2 -fi - -not_running="" -while read -r name reps; do - [ -n "$name" ] || continue - if awk -v r="$reps" 'BEGIN { split(r, a, "/"); exit (a[1] == a[2]) ? 0 : 1 }'; then - continue - fi - is_completed_oneshot "$name" && continue - not_running="$not_running $name" -done <<<"$services" - -if [ -n "$not_running" ]; then - echo "not converged:$not_running" >&2 - printf '%s\n' "$not_running" | tr ' ' '\n' | while read -r svc; do - [ -n "$svc" ] || continue - report_tasks "$svc" - done - exit 1 -fi diff --git a/roles/web-app-baserow/files/probe_mcp_endpoint.py b/roles/web-app-baserow/files/python/probe_mcp_endpoint.py similarity index 87% rename from roles/web-app-baserow/files/probe_mcp_endpoint.py rename to roles/web-app-baserow/files/python/probe_mcp_endpoint.py index 53a895481f..42eda67bf4 100644 --- a/roles/web-app-baserow/files/probe_mcp_endpoint.py +++ b/roles/web-app-baserow/files/python/probe_mcp_endpoint.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - reads MCP_BASE_URL and the endpoint key from the +# container environment at import and then drives a live SSE handshake; the module +# cannot be loaded outside the running Baserow container import os import sys import urllib.error diff --git a/roles/web-app-baserow/files/provision_mcp.py b/roles/web-app-baserow/files/python/provision_mcp.py similarity index 86% rename from roles/web-app-baserow/files/provision_mcp.py rename to roles/web-app-baserow/files/python/provision_mcp.py index cbcba79aa8..fba5dfc2c4 100644 --- a/roles/web-app-baserow/files/provision_mcp.py +++ b/roles/web-app-baserow/files/python/provision_mcp.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - imports baserow.core and django at module level and +# writes through the ORM inside `baserow-cli`; neither the framework nor a database is +# available outside the Baserow image import os from baserow.core.handler import CoreHandler diff --git a/roles/web-app-checkmk/files/provision_mcp_identity.py b/roles/web-app-checkmk/files/python/provision_mcp_identity.py similarity index 96% rename from roles/web-app-checkmk/files/provision_mcp_identity.py rename to roles/web-app-checkmk/files/python/provision_mcp_identity.py index 47f69ea326..dc41de0a68 100644 --- a/roles/web-app-checkmk/files/provision_mcp_identity.py +++ b/roles/web-app-checkmk/files/python/provision_mcp_identity.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - reads the administrator credential from the container +# environment at import and every helper is one authenticated REST call against the +# live Checkmk site; a mocked transport would assert the mock, not the identity """Give the MCP adapter its own read-only Checkmk identity. Checkmk accepts a caller-chosen automation secret, so the identity is the diff --git a/roles/web-app-flowise/files/mint_mcp_api_key.py b/roles/web-app-flowise/files/python/mint_mcp_api_key.py similarity index 96% rename from roles/web-app-flowise/files/mint_mcp_api_key.py rename to roles/web-app-flowise/files/python/mint_mcp_api_key.py index c5dccb20e3..e8b8e751f9 100644 --- a/roles/web-app-flowise/files/mint_mcp_api_key.py +++ b/roles/web-app-flowise/files/python/mint_mcp_api_key.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - the contract is the register/login/mint order Flowise +# 3.1.3 enforces over a session cookie; every step is a live HTTP call and a stubbed +# opener would only replay the order this file already states """Mint the scoped Flowise API identity the MCP registry reconciler uses. Flowise 3.1.3 guards ``/api/v1/apikey`` with ``apikeys:create``, so a key diff --git a/roles/web-app-flowise/files/reconcile/mcp_servers.py b/roles/web-app-flowise/files/python/reconcile/mcp_servers.py similarity index 100% rename from roles/web-app-flowise/files/reconcile/mcp_servers.py rename to roles/web-app-flowise/files/python/reconcile/mcp_servers.py diff --git a/roles/web-app-gitlab/files/probe_mcp_token.rb b/roles/web-app-gitlab/files/ruby/probe_mcp_token.rb similarity index 87% rename from roles/web-app-gitlab/files/probe_mcp_token.rb rename to roles/web-app-gitlab/files/ruby/probe_mcp_token.rb index 3daf510e3f..0270a8b8fc 100644 --- a/roles/web-app-gitlab/files/probe_mcp_token.rb +++ b/roles/web-app-gitlab/files/ruby/probe_mcp_token.rb @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - opens an MCP session against the running GitLab sidecar +# from inside `gitlab-rails runner`, reading the host, port and path from the +# container's environment; the live handshake is the entire contract require 'json' require 'net/http' require 'uri' diff --git a/roles/web-app-gitlab/files/provision_mcp_token.rb b/roles/web-app-gitlab/files/ruby/provision_mcp_token.rb similarity index 86% rename from roles/web-app-gitlab/files/provision_mcp_token.rb rename to roles/web-app-gitlab/files/ruby/provision_mcp_token.rb index f026cb369c..418f50adee 100644 --- a/roles/web-app-gitlab/files/provision_mcp_token.rb +++ b/roles/web-app-gitlab/files/ruby/provision_mcp_token.rb @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - runs in `gitlab-rails runner` against User, +# Organizations::Organization and PersonalAccessToken; the models come from the GitLab +# image and are not loadable anywhere else name = ENV.fetch('GITLAB_MCP_TOKEN_NAME') username = ENV.fetch('GITLAB_MCP_TOKEN_OWNER') display_name = ENV.fetch('GITLAB_MCP_TOKEN_OWNER_NAME') diff --git a/roles/web-app-homeassistant/files/probe_mcp_token.py b/roles/web-app-homeassistant/files/python/probe_mcp_token.py similarity index 81% rename from roles/web-app-homeassistant/files/probe_mcp_token.py rename to roles/web-app-homeassistant/files/python/probe_mcp_token.py index 4b44bcc2df..6f5f97f1b5 100644 --- a/roles/web-app-homeassistant/files/probe_mcp_token.py +++ b/roles/web-app-homeassistant/files/python/probe_mcp_token.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - the whole module is one authenticated GET against the +# running hub, issued at import from the container's own environment; there is no +# callable unit below it """Report whether the stored Home Assistant token still authenticates. Environment: diff --git a/roles/web-app-homeassistant/files/provision_mcp.py b/roles/web-app-homeassistant/files/python/provision_mcp.py similarity index 97% rename from roles/web-app-homeassistant/files/provision_mcp.py rename to roles/web-app-homeassistant/files/python/provision_mcp.py index d7e78de316..45f2017466 100644 --- a/roles/web-app-homeassistant/files/provision_mcp.py +++ b/roles/web-app-homeassistant/files/python/provision_mcp.py @@ -1,3 +1,6 @@ +# nocheck: mirrored-unit-test - resolves the hub's onboarding or login flow over a live +# WebSocket command channel, reading every credential from the container environment at +# import; nothing here runs without a started Home Assistant import asyncio import contextlib import json diff --git a/roles/web-app-jellyfin/files/provision_mcp_identity.py b/roles/web-app-jellyfin/files/python/provision_mcp_identity.py similarity index 100% rename from roles/web-app-jellyfin/files/provision_mcp_identity.py rename to roles/web-app-jellyfin/files/python/provision_mcp_identity.py diff --git a/roles/web-app-moodle/files/provision_mcp.php b/roles/web-app-moodle/files/php/provision_mcp.php similarity index 93% rename from roles/web-app-moodle/files/provision_mcp.php rename to roles/web-app-moodle/files/php/provision_mcp.php index dbb3e99e58..37c7d34773 100644 --- a/roles/web-app-moodle/files/provision_mcp.php +++ b/roles/web-app-moodle/files/php/provision_mcp.php @@ -1,4 +1,7 @@ make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); diff --git a/roles/web-app-wordpress/files/ai_engine/configure.php b/roles/web-app-wordpress/files/php/ai_engine/configure.php similarity index 81% rename from roles/web-app-wordpress/files/ai_engine/configure.php rename to roles/web-app-wordpress/files/php/ai_engine/configure.php index 3f23d06fd9..d1fec0d2d3 100644 --- a/roles/web-app-wordpress/files/ai_engine/configure.php +++ b/roles/web-app-wordpress/files/php/ai_engine/configure.php @@ -4,6 +4,10 @@ * AI_ENGINE_PAYLOAD: base64 of {"environment": {...}, "default_model": "..."}. */ +// nocheck: mirrored-unit-test - runs through `wp eval-file`, calling WP_CLI and the +// get_option/update_option pair of the booted site; the WordPress runtime it needs is +// only present inside the container + $payload = json_decode(base64_decode(getenv('AI_ENGINE_PAYLOAD')), true); if (!is_array($payload) || empty($payload['environment']['id'])) { WP_CLI::error('AI_ENGINE_PAYLOAD did not decode to an environment'); diff --git a/roles/web-app-wordpress/files/ai_engine/endpoint.php b/roles/web-app-wordpress/files/php/ai_engine/endpoint.php similarity index 72% rename from roles/web-app-wordpress/files/ai_engine/endpoint.php rename to roles/web-app-wordpress/files/php/ai_engine/endpoint.php index 3652e41f6c..24162c4948 100644 --- a/roles/web-app-wordpress/files/ai_engine/endpoint.php +++ b/roles/web-app-wordpress/files/php/ai_engine/endpoint.php @@ -4,6 +4,9 @@ * AI_ENGINE_ENV_ID: id of the environment the deploy manages. */ +// nocheck: mirrored-unit-test - runs through `wp eval-file` and reads mwai_options off +// the booted site; the WordPress runtime it needs is only present inside the container + $wanted = getenv('AI_ENGINE_ENV_ID'); $options = get_option('mwai_options', array()); $envs = (is_array($options) && isset($options['ai_envs']) && is_array($options['ai_envs'])) diff --git a/tests/lint/ansible/services/test_mcp_adapter_endpoint_path.py b/tests/lint/ansible/services/test_mcp_adapter_endpoint_path.py index 8009c08e82..2a625b1fdf 100644 --- a/tests/lint/ansible/services/test_mcp_adapter_endpoint_path.py +++ b/tests/lint/ansible/services/test_mcp_adapter_endpoint_path.py @@ -28,7 +28,7 @@ from utils.roles.mapping import ROLE_FILE_META_MCP from . import PROJECT_ROOT _RULE = "mcp-adapter-endpoint-path" -_ADAPTER_SERVER = PROJECT_ROOT / "roles/svc-ai-mcp-adapter/files/server.py" +_ADAPTER_SERVER = PROJECT_ROOT / "roles/svc-ai-mcp-adapter/files/python/server.py" _ENDPOINT_CONST = re.compile(r'^ENDPOINT\s*=\s*"([^"]+)"', re.MULTILINE)