playbooks/roles/vhosts/xworkmate_bridge/tasks/validate.yml
Haitao Pan cd9a783de7 fix(xworkmate_bridge): align Caddy SSE timeouts with bridge 60min max wait
Caddy /acp* used read/write_timeout 30m while the bridge max gateway wait is
60min, so long tasks had their SSE killed at the edge (ACP_HTTP_CONNECTION_CLOSED)
while OpenClaw kept running. /api*, /artifacts/* and / also lacked flush_interval
and long timeouts, making polling/streaming fragile.

- T1: introduce xworkmate_bridge_acp_stream_timeout (70m = 60min cap + grace),
  acp_dial_timeout, acp_upstream_keepalive; drive /acp* read/write_timeout from it.
- T2: apply flush_interval -1 + the same long timeouts to /api*, /artifacts/*, /.
- Update validate.yml assertions to reference the vars instead of hardcoded 30m.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:49:01 +08:00

265 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
- name: Validate Caddy config for xworkmate-bridge ingress
ansible.builtin.command: caddy validate --config "{{ xworkmate_bridge_caddyfile_path }}"
changed_when: false
when:
- xworkmate_bridge_public_access | bool
- caddy_enabled | bool
- 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
when:
- xworkmate_bridge_public_access | bool
- caddy_enabled | bool
- 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
when:
- xworkmate_bridge_public_access | bool
- ansible_os_family != 'Darwin'
- 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"
# 流式超时与 bridge openClawAgentWaitMaxTimeout(60min) 对齐,由 xworkmate_bridge_acp_stream_timeout 驱动T1/T2
- "'read_timeout ' ~ xworkmate_bridge_acp_stream_timeout in xworkmate_bridge_fragment.stdout"
- "'write_timeout ' ~ xworkmate_bridge_acp_stream_timeout in xworkmate_bridge_fragment.stdout"
- "'keepalive ' ~ xworkmate_bridge_acp_upstream_keepalive 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
when:
- xworkmate_bridge_public_access | bool
- caddy_enabled | bool
- 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
when:
- xworkmate_bridge_public_access | bool
- ansible_os_family != 'Darwin'
- caddy_enabled | bool
- 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
when: ansible_os_family != 'Darwin'
- name: Capture listening TCP sockets for xworkmate-bridge stack
# ss is Linux-only; on macOS fall back to lsof, whose output likewise contains
# host:port (e.g. 127.0.0.1:8787, *:8787) so the assertion below still matches.
ansible.builtin.shell: |
if command -v ss >/dev/null 2>&1; then
ss -ltn
else
lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null || true
fi
args:
executable: /bin/bash
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: "{{ xworkmate_bridge_validation_base_url }}/api/ping"
headers:
Authorization: "Bearer {{ xworkmate_bridge_effective_auth_token | default(xworkmate_bridge_auth_token) }}"
Origin: "{{ xworkmate_bridge_validation_origin }}"
return_content: true
validate_certs: "{{ xworkmate_bridge_validation_validate_certs | bool }}"
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: "{{ xworkmate_bridge_validation_base_url }}/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
validate_certs: "{{ xworkmate_bridge_validation_validate_certs | bool }}"
register: xworkmate_bridge_capabilities
changed_when: false
no_log: true
- name: Check xworkmate-bridge public domain ping with review token
ansible.builtin.uri:
url: "{{ xworkmate_bridge_validation_base_url }}/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
validate_certs: "{{ xworkmate_bridge_validation_validate_certs | bool }}"
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: "{{ xworkmate_bridge_validation_base_url }}/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
validate_certs: "{{ xworkmate_bridge_validation_validate_certs | bool }}"
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 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
when: caddy_enabled | bool
- 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 | default([]) }}"
loop_control:
label: "{{ item.item }}"
when: caddy_enabled | bool
- 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 }}"