373 lines
15 KiB
YAML
373 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: 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 }}"
|