mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The contract check ran as an Ansible task in the middle of every provider deploy, and the two client checks ran the same way. A test that is part of the deploy cannot be run without deploying, cannot be re-run on its own, and reports its verdict as a deploy failure. Twenty-seven task files lose 491 lines to this.
It is now what it always was. Each provider ships files/test/test.sh, templates/test.env.j2 and meta/tests.yml, and test-e2e-cli stages one shared harness beside it: run.sh drives contract.sh in a throwaway container on the provider's own network, then isolation.sh for adapter-fronted providers. The read probe each provider offers moved from the calling task into meta/mcp.yml next to the allowlist it must satisfy, and the schema now rejects a read probe naming a tool outside that allowlist.
The bearer the test presents is the one a client resolves through mcp.credential, via the new mcp_client_bearer lookup over resolve_credential, which moved to utils.roles.applications.mcp so it is the same function mcp_servers renders every client connection with. The probe presented whatever the calling role passed, which for the fifteen adapter-fronted providers is a different secret than any client holds.
Sidecar isolation is read from the engine rather than measured inside the container, so it no longer needs a delegation to the node holding the task: attachment is compared by network name, which drops the gwbridge special case, and a swarm worker says it cannot measure instead of skipping silently.
Retrying is bounded by wall clock against MCP_DEADLINE_SECONDS, which scales through the same lookup('timeout') as the harness budget it has to stay under. A flag that is neither true nor false aborts rather than reading as off.
mcp-provider-probe becomes mcp-provider-cli-test: an enabled provider must ship the test rather than include the task. mcp-probe-bearer becomes mcp-adapter-bearer, tying ADAPTER_BEARER to the declared credential key now that no probe include carries it. A new integration test renders the shared env template for every provider through Ansible's own templar, so a later change to it breaks visibly instead of poisoning twenty deploys quietly.
Hermes and OpenClaw keep their client-side proof as CLI tests of their own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
56 lines
2.0 KiB
YAML
56 lines
2.0 KiB
YAML
---
|
|
- name: "🔌 Provision the managed MCP trigger workflow"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "-"
|
|
stdin: "{{ lookup('file', 'python/provision/mcp.py') }}"
|
|
environment:
|
|
N8N_BASE: "{{ N8N_INTERNAL_API_URL }}"
|
|
N8N_OWNER_EMAIL: "{{ N8N_OWNER_EMAIL }}"
|
|
N8N_OWNER_PASSWORD: "{{ N8N_OWNER_PASSWORD }}"
|
|
N8N_API_KEY_NAME: "{{ N8N_MCP_API_KEY_NAME }}"
|
|
N8N_MCP_PATH: "{{ N8N_MCP_WEBHOOK_PATH }}"
|
|
N8N_MCP_TOKEN: "{{ N8N_MCP_TOKEN }}"
|
|
N8N_MCP_WORKFLOW: "{{ N8N_MCP_WORKFLOW_NAME }}"
|
|
N8N_MCP_CREDENTIAL: "{{ N8N_MCP_CREDENTIAL_NAME }}"
|
|
register: n8n_mcp_provision
|
|
retries: 30
|
|
delay: 10
|
|
until: n8n_mcp_provision.rc == 0
|
|
changed_when: n8n_mcp_provision.stdout.startswith('CHANGED')
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ STACK_HOST }}"
|
|
run_once: true
|
|
|
|
- name: "💾 Persist the MCP bearer so clients can read it"
|
|
ansible.builtin.include_role:
|
|
name: sys-token-store
|
|
tasks_from: write.yml
|
|
vars:
|
|
sys_token_store_user_key: "{{ lookup('config', application_id, 'mcp.credential.owner') }}"
|
|
sys_token_store_app: "{{ application_id }}"
|
|
sys_token_store_token: "{{ N8N_MCP_TOKEN }}"
|
|
|
|
- name: "🔌 Reconcile the managed agent over every discovered MCP provider"
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- "-"
|
|
stdin: "{{ lookup('file', 'python/provision/client.py') }}"
|
|
environment:
|
|
N8N_AI_CREDENTIAL: "{{ N8N_LITELLM_CREDENTIAL_NAME }}"
|
|
N8N_API_KEY_NAME: "{{ N8N_MCP_API_KEY_NAME }}"
|
|
N8N_BASE: "{{ N8N_INTERNAL_API_URL }}"
|
|
N8N_CHAT_MODEL: "{{ LITELLM_CHAT_MODEL }}"
|
|
N8N_CLIENT_WORKFLOW: "{{ N8N_MCP_CLIENT_WORKFLOW_NAME }}"
|
|
N8N_MCP_PROVIDERS: "{{ N8N_MCP_PROVIDERS | to_json }}"
|
|
N8N_OWNER_EMAIL: "{{ N8N_OWNER_EMAIL }}"
|
|
N8N_OWNER_PASSWORD: "{{ N8N_OWNER_PASSWORD }}"
|
|
register: n8n_client_provision
|
|
changed_when: n8n_client_provision.stdout.startswith('CHANGED')
|
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
|
delegate_to: "{{ STACK_HOST }}"
|
|
run_once: true
|
|
when: N8N_LITELLM_ENABLED | bool
|