playbooks/roles/vhosts/xworkmate_bridge/tasks/validate.yml

383 lines
15 KiB
YAML

---
- name: Validate Caddy config for xworkmate-bridge ingress
ansible.builtin.command: caddy validate --config "{{ xworkmate_bridge_caddyfile_path }}"
changed_when: false
- name: Read deployed xworkmate-bridge Caddy fragment
ansible.builtin.command:
cmd: cat "{{ xworkmate_bridge_service_caddy_fragment_path }}"
changed_when: false
register: xworkmate_bridge_fragment
no_log: true
- name: Read deployed xworkmate-bridge systemd unit
ansible.builtin.command:
cmd: cat "{{ xworkmate_bridge_systemd_unit_path }}"
changed_when: false
register: xworkmate_bridge_systemd_unit_text
no_log: true
- name: Assert Caddy fragment only exposes app-facing bridge routes
ansible.builtin.assert:
that:
- "'handle /acp*' in xworkmate_bridge_fragment.stdout"
- "'handle /api*' in xworkmate_bridge_fragment.stdout"
- "'handle /artifacts/*' in xworkmate_bridge_fragment.stdout"
- "'reverse_proxy {{ xworkmate_bridge_listen_host }}:{{ xworkmate_bridge_listen_port }}' in xworkmate_bridge_fragment.stdout"
- "'flush_interval -1' in xworkmate_bridge_fragment.stdout"
- "'read_timeout 30m' in xworkmate_bridge_fragment.stdout"
- "'write_timeout 30m' in xworkmate_bridge_fragment.stdout"
- "'keepalive 5m' in xworkmate_bridge_fragment.stdout"
- "'/gateway/openclaw' not in xworkmate_bridge_fragment.stdout"
- "'/acp-server' not in xworkmate_bridge_fragment.stdout"
- "'127.0.0.1:18789' not in xworkmate_bridge_fragment.stdout"
- "'127.0.0.1:38992' not in xworkmate_bridge_fragment.stdout"
- "'127.0.0.1:8791' not in xworkmate_bridge_fragment.stdout"
- "'127.0.0.1:3920' not in xworkmate_bridge_fragment.stdout"
no_log: true
- name: Assert Caddy and systemd use the same bridge token set
ansible.builtin.assert:
that:
- >-
'Bearer ' ~ (xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token))
in xworkmate_bridge_fragment.stdout
- >-
'Environment="BRIDGE_AUTH_TOKEN=' ~ (xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token)) ~ '"'
in xworkmate_bridge_systemd_unit_text.stdout
- >-
((xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token) | trim | length) == 0)
or
(
'Bearer ' ~ (xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token))
in xworkmate_bridge_fragment.stdout
)
- >-
((xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token) | trim | length) == 0)
or
(
'Environment="BRIDGE_REVIEW_AUTH_TOKEN=' ~ (xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token)) ~ '"'
in xworkmate_bridge_systemd_unit_text.stdout
)
fail_msg: "xworkmate-bridge Caddy and systemd token configuration are not aligned"
no_log: true
- name: Check xworkmate-bridge systemd service status
ansible.builtin.systemd:
name: "{{ xworkmate_bridge_service_name }}"
register: xworkmate_bridge_service_status
until: xworkmate_bridge_service_status.status.ActiveState | default('') == "active"
retries: 12
delay: 5
- name: Check required ACP and gateway service status
ansible.builtin.systemd:
name: "{{ item }}"
loop: "{{ xworkmate_bridge_required_services }}"
register: xworkmate_bridge_dependency_status
until: xworkmate_bridge_dependency_status.status.ActiveState | default('') == "active"
retries: 12
delay: 5
- name: Capture listening TCP sockets for xworkmate-bridge stack
ansible.builtin.command:
cmd: ss -ltn
register: xworkmate_bridge_listening_sockets
changed_when: false
- name: Assert xworkmate-bridge stack canonical ports are listening
ansible.builtin.assert:
that:
- >-
(xworkmate_bridge_listening_sockets.stdout is search(item.host ~ ':' ~ item.port))
or
(item.host == '127.0.0.1' and xworkmate_bridge_listening_sockets.stdout is search('\\*:' ~ item.port))
or
(item.host == '127.0.0.1' and xworkmate_bridge_listening_sockets.stdout is search('0.0.0.0:' ~ item.port))
fail_msg: "{{ item.name }} listener {{ item.host }}:{{ item.port }} is not active"
loop: "{{ xworkmate_bridge_required_listeners }}"
loop_control:
label: "{{ item.name }} {{ item.host }}:{{ item.port }}"
- name: Check xworkmate-bridge public domain ping
ansible.builtin.uri:
url: "https://{{ xworkmate_bridge_service_domain }}/api/ping"
headers:
Authorization: "Bearer {{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
Origin: "{{ xworkmate_bridge_validation_origin }}"
return_content: true
register: xworkmate_bridge_service_ping
until:
- xworkmate_bridge_service_ping.status == 200
- xworkmate_bridge_service_ping.json is defined
- xworkmate_bridge_service_ping.json.status | default('') == "ok"
retries: 3
delay: 5
changed_when: false
no_log: true
- name: Assert xworkmate-bridge ping reports native binary metadata
ansible.builtin.assert:
that:
- xworkmate_bridge_service_ping.json.version | default('') | trim | length > 0
- xworkmate_bridge_service_ping.json.image | default('') | trim | length == 0
- xworkmate_bridge_service_ping.json.tag | default('') | trim | length == 0
fail_msg: >-
xworkmate-bridge /api/ping must report native binary metadata and must
not expose stale Docker image/tag metadata.
- name: Check xworkmate-bridge capabilities contract
ansible.builtin.uri:
url: "https://{{ xworkmate_bridge_service_domain }}/acp/rpc"
method: POST
headers:
Authorization: "Bearer {{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
Origin: "{{ xworkmate_bridge_validation_origin }}"
Content-Type: application/json
body_format: json
body:
jsonrpc: "2.0"
id: "validate-capabilities"
method: acp.capabilities
params: {}
return_content: true
register: xworkmate_bridge_capabilities
changed_when: false
no_log: true
- name: Check xworkmate-bridge public domain ping with review token
ansible.builtin.uri:
url: "https://{{ xworkmate_bridge_service_domain }}/api/ping"
headers:
Authorization: "Bearer {{ xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token) }}"
Origin: "{{ xworkmate_bridge_validation_origin }}"
return_content: true
register: xworkmate_bridge_review_service_ping
until:
- xworkmate_bridge_review_service_ping.status == 200
- xworkmate_bridge_review_service_ping.json is defined
- xworkmate_bridge_review_service_ping.json.status | default('') == "ok"
retries: 3
delay: 5
changed_when: false
no_log: true
when:
- xworkmate_bridge_effective_review_auth_token | default(xworkmate_bridge_review_auth_token) | trim | length > 0
- name: Assert xworkmate-bridge capabilities expose app contract providers
ansible.builtin.assert:
that:
- xworkmate_bridge_capabilities.status == 200
- "'agent' in xworkmate_bridge_capabilities.json.result.availableExecutionTargets"
- "'gateway' in xworkmate_bridge_capabilities.json.result.availableExecutionTargets"
- xworkmate_bridge_capabilities.json.result.providerCatalog | selectattr('providerId', 'equalto', 'codex') | list | length == 1
- xworkmate_bridge_capabilities.json.result.providerCatalog | selectattr('providerId', 'equalto', 'opencode') | list | length == 1
- xworkmate_bridge_capabilities.json.result.providerCatalog | selectattr('providerId', 'equalto', 'gemini') | list | length == 1
- xworkmate_bridge_capabilities.json.result.providerCatalog | selectattr('providerId', 'equalto', 'hermes') | list | length == 1
- xworkmate_bridge_capabilities.json.result.gatewayProviders | selectattr('providerId', 'equalto', 'openclaw') | list | length == 1
- name: Check xworkmate-bridge routing resolve contract
ansible.builtin.uri:
url: "https://{{ xworkmate_bridge_service_domain }}/acp/rpc"
method: POST
headers:
Authorization: "Bearer {{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
Origin: "{{ xworkmate_bridge_validation_origin }}"
Content-Type: application/json
body_format: json
body:
jsonrpc: "2.0"
id: "validate-routing"
method: xworkmate.routing.resolve
params:
taskPrompt: "search current service status"
workingDirectory: "/tmp"
routing:
routingMode: explicit
explicitExecutionTarget: gateway
preferredGatewayProviderId: openclaw
return_content: true
register: xworkmate_bridge_routing
changed_when: false
no_log: true
- name: Assert xworkmate-bridge routing resolves openclaw gateway
ansible.builtin.assert:
that:
- xworkmate_bridge_routing.status == 200
- xworkmate_bridge_routing.json.result.resolvedExecutionTarget == "gateway"
- xworkmate_bridge_routing.json.result.resolvedGatewayProviderId == "openclaw"
- name: Check lightweight openclaw session contract
ansible.builtin.shell: |
set -euo pipefail
stream_file="$(mktemp)"
trap 'rm -f "$stream_file"' EXIT
curl --http1.1 --fail --silent --show-error --no-buffer --max-time 180 \
-H "Authorization: Bearer ${XWORKMATE_BRIDGE_AUTH_TOKEN}" \
-H "Origin: {{ xworkmate_bridge_validation_origin }}" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
--data @- \
"https://{{ xworkmate_bridge_service_domain }}/acp/rpc" > "$stream_file" <<'JSON'
{
"jsonrpc": "2.0",
"id": "validate-openclaw",
"method": "session.start",
"params": {
"sessionId": "validate-openclaw",
"threadId": "validate-openclaw",
"taskPrompt": "Reply exactly pong.",
"workingDirectory": "/tmp",
"routing": {
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw"
}
}
}
JSON
python3 - "$stream_file" <<'PY'
import json
import sys
payloads = []
for block in open(sys.argv[1], encoding="utf-8").read().split("\n\n"):
data_lines = [
line[len("data: "):]
for line in block.splitlines()
if line.startswith("data: ")
]
if not data_lines:
continue
payload = "\n".join(data_lines).strip()
if payload == "[DONE]":
payloads.append({"done": True})
continue
payloads.append(json.loads(payload))
final = next(
(item for item in payloads if isinstance(item, dict) and item.get("id") == "validate-openclaw"),
None,
)
if final is None:
raise SystemExit("missing final OpenClaw result envelope")
if not payloads or payloads[-1].get("done") is not True:
raise SystemExit("missing SSE done marker")
error_text = json.dumps(final.get("error", {}), ensure_ascii=False)
for code in (
"GATEWAY_PROVIDER_REQUIRED",
"OPENCLAW_GATEWAY_METHOD_NOT_ALLOWED",
"OPENCLAW_GATEWAY_CONFLICT",
"OPENCLAW_TASK_ENDPOINT_REQUIRED",
):
if code in error_text:
raise SystemExit(f"legacy OpenClaw routing error remained: {code}")
PY
args:
executable: /bin/bash
environment:
XWORKMATE_BRIDGE_AUTH_TOKEN: "{{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
changed_when: false
check_mode: false
- name: Check xworkmate-bridge OpenClaw SSE long-task stream contract
ansible.builtin.shell: |
set -euo pipefail
stream_file="$(mktemp)"
trap 'rm -f "$stream_file"' EXIT
stream_started_at="$(date +%s)"
curl --http1.1 --fail --silent --show-error --no-buffer --max-time 120 \
-H "Authorization: Bearer ${XWORKMATE_BRIDGE_AUTH_TOKEN}" \
-H "Origin: {{ xworkmate_bridge_validation_origin }}" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
--data @- \
"https://{{ xworkmate_bridge_service_domain }}/acp/rpc" > "$stream_file" <<'JSON'
{
"jsonrpc": "2.0",
"id": "validate-openclaw-sse",
"method": "session.start",
"params": {
"sessionId": "validate-openclaw-sse",
"threadId": "validate-openclaw-sse",
"taskPrompt": "Wait at least 25 seconds, then reply with exactly pong.",
"workingDirectory": "/tmp",
"routing": {
"routingMode": "explicit",
"explicitExecutionTarget": "gateway",
"preferredGatewayProviderId": "openclaw"
}
}
}
JSON
stream_finished_at="$(date +%s)"
stream_elapsed_seconds="$((stream_finished_at - stream_started_at))"
python3 - "$stream_file" "$stream_elapsed_seconds" <<'PY'
import json
import sys
path = sys.argv[1]
elapsed_seconds = int(sys.argv[2])
payloads = []
for block in open(path, encoding="utf-8").read().split("\n\n"):
data_lines = [
line[len("data: "):]
for line in block.splitlines()
if line.startswith("data: ")
]
if not data_lines:
continue
payload = "\n".join(data_lines).strip()
if payload == "[DONE]":
payloads.append({"done": True})
continue
payloads.append(json.loads(payload))
methods = [item.get("method") for item in payloads if isinstance(item, dict)]
ids = [item.get("id") for item in payloads if isinstance(item, dict)]
if "xworkmate.bridge.accepted" not in methods:
raise SystemExit("missing accepted SSE event")
if elapsed_seconds >= 15 and "xworkmate.bridge.keepalive" not in methods:
raise SystemExit("missing keepalive SSE event")
if "validate-openclaw-sse" not in ids:
raise SystemExit("missing final SSE result envelope")
if not payloads or payloads[-1].get("done") is not True:
raise SystemExit("missing SSE done marker")
PY
args:
executable: /bin/bash
environment:
XWORKMATE_BRIDGE_AUTH_TOKEN: "{{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
changed_when: false
check_mode: false
no_log: true
- name: Check deprecated ACP fragments are absent
ansible.builtin.stat:
path: "{{ item }}"
changed_when: false
loop: "{{ xworkmate_bridge_obsolete_caddy_fragment_paths }}"
register: xworkmate_bridge_obsolete_fragments
- name: Assert deprecated ACP fragments were removed
ansible.builtin.assert:
that:
- not item.stat.exists
fail_msg: "Deprecated ACP fragment still exists: {{ item.item }}"
loop: "{{ xworkmate_bridge_obsolete_fragments.results }}"
loop_control:
label: "{{ item.item }}"
- name: Summarize xworkmate-bridge systemd ingress state
ansible.builtin.debug:
msg:
- "Bridge service public base URL: {{ xworkmate_bridge_service_public_base_url }}"
- "Bridge service status: {{ xworkmate_bridge_service_status.status.ActiveState | default('N/A') }}"
- "App-facing RPC: {{ xworkmate_bridge_service_public_base_url }}/acp/rpc"
- "Codex upstream: {{ xworkmate_bridge_codex_rpc_url }}"
- "OpenCode upstream: {{ xworkmate_bridge_opencode_rpc_url }}"
- "Gemini upstream: {{ xworkmate_bridge_gemini_rpc_url }}"
- "Hermes upstream: {{ xworkmate_bridge_hermes_rpc_url }}"
- "OpenClaw upstream: {{ xworkmate_bridge_openclaw_url }}"