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>
265 lines
11 KiB
YAML
265 lines
11 KiB
YAML
---
|
||
- 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 }}"
|