#!/usr/bin/env bash set -euo pipefail BASE_URL="${BRIDGE_SERVER_URL:-https://xworkmate-bridge.svc.plus}" AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-}" REQUEST_ORIGIN="${OPENCLAW_SMOKE_ORIGIN:-https://xworkmate.svc.plus}" RPC_TIMEOUT_SECONDS="${OPENCLAW_SMOKE_RPC_TIMEOUT_SECONDS:-180}" POLL_TIMEOUT_SECONDS="${OPENCLAW_SMOKE_POLL_TIMEOUT_SECONDS:-120}" POLL_INTERVAL_SECONDS="${OPENCLAW_SMOKE_POLL_INTERVAL_SECONDS:-2}" if [[ -z "${AUTH_TOKEN}" ]]; then echo "BRIDGE_AUTH_TOKEN is required" >&2 exit 1 fi normalize_url() { local raw="$1" printf '%s\n' "${raw%/}" } resolved_base_url="$(normalize_url "${BASE_URL}")" rpc_url="${resolved_base_url}/acp/rpc" stream_file="$(mktemp)" trap 'rm -f "$stream_file"' EXIT session_id="validate-openclaw-$(date +%s)" request_body="$(cat < POST ${rpc_url} (session=${session_id})" curl --http1.1 --fail --silent --show-error --no-buffer --max-time "${RPC_TIMEOUT_SECONDS}" \ -H "Authorization: Bearer ${AUTH_TOKEN}" \ -H "Origin: ${REQUEST_ORIGIN}" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ --data "${request_body}" \ "${rpc_url}" > "${stream_file}" OPENCLAW_AUTH_TOKEN="${AUTH_TOKEN}" \ OPENCLAW_STREAM_FILE="${stream_file}" \ OPENCLAW_POLL_URL="${rpc_url}" \ OPENCLAW_POLL_TIMEOUT_SECONDS="${POLL_TIMEOUT_SECONDS}" \ OPENCLAW_POLL_INTERVAL_SECONDS="${POLL_INTERVAL_SECONDS}" \ python3 - <<'PY' import json import os import sys import time import urllib.request stream_path = os.environ["OPENCLAW_STREAM_FILE"] poll_url = os.environ["OPENCLAW_POLL_URL"] auth_token = os.environ["OPENCLAW_AUTH_TOKEN"] poll_timeout = int(os.environ["OPENCLAW_POLL_TIMEOUT_SECONDS"]) poll_interval = float(os.environ["OPENCLAW_POLL_INTERVAL_SECONDS"]) payloads = [] for block in open(stream_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)) def terminal_result(payload): if not isinstance(payload, dict): return {} nested = payload.get("result") status = str(payload.get("status", "")).lower() if isinstance(nested, dict) and nested and status in { "completed", "failed", "cancelled", "canceled", }: return nested return payload def output_text_from(payload): if not isinstance(payload, dict): return "" candidates = [] stack = [payload] seen = set() while stack: item = stack.pop(0) if not isinstance(item, dict): continue identity = id(item) if identity in seen: continue seen.add(identity) for key in ("output", "message", "summary", "resultSummary", "assistantText", "text"): value = item.get(key) if isinstance(value, str): candidates.append(value) for key in ("result", "payload", "data", "task", "progress", "artifacts"): value = item.get(key) if isinstance(value, dict): stack.append(value) return " ".join(part for part in candidates if part) def require_nonempty(payload, key): value = payload.get(key) if isinstance(value, str) and value.strip(): return raise SystemExit(f"OpenClaw smoke result missing {key}: {json.dumps(payload, ensure_ascii=False, sort_keys=True)[:1000]}") def first_nonempty(payload, *keys): if not isinstance(payload, dict): return "" for key in keys: value = payload.get(key) if isinstance(value, str) and value.strip(): return value.strip() return "" def openclaw_session_key_for_app_thread(app_thread_key): app_thread_key = str(app_thread_key or "").strip() if not app_thread_key: app_thread_key = "main" return "agent:main:" + app_thread_key def task_handle_from_payload(payload): if not isinstance(payload, dict): return {} candidates = [] for key in ("result", "payload", "params"): if isinstance(payload.get(key), dict): candidates.append(payload[key]) candidates.append(payload) for candidate in candidates: if not isinstance(candidate, dict): continue session_id = first_nonempty(candidate, "sessionId") thread_id = first_nonempty(candidate, "threadId") turn_id = first_nonempty(candidate, "turnId") run_id = first_nonempty(candidate, "runId") app_thread_key = first_nonempty(candidate, "appThreadKey") openclaw_session_key = first_nonempty(candidate, "openclawSessionKey") if (app_thread_key and openclaw_session_key and run_id) or (session_id and thread_id and (turn_id or run_id)): return { "sessionId": session_id, "threadId": thread_id, "turnId": turn_id, "runId": run_id or turn_id, "appThreadKey": app_thread_key or session_id or thread_id, "openclawSessionKey": openclaw_session_key or openclaw_session_key_for_app_thread(app_thread_key or session_id or thread_id), } return {} def find_task_handle(payloads, final): for payload in reversed(payloads): handle = task_handle_from_payload(payload) if handle: return handle return task_handle_from_payload(final) def is_valid_no_displayable_contract(payload): if not isinstance(payload, dict): return False if payload.get("code") != "OPENCLAW_NO_DISPLAYABLE_OUTPUT": return False if payload.get("resolvedGatewayProviderId") != "openclaw": return False for key in ("sessionId", "threadId", "runId", "artifactScope"): require_nonempty(payload, key) if not first_nonempty(payload, "openclawSessionKey", "sessionKey"): artifact_scope = first_nonempty(payload, "artifactScope") if not artifact_scope.startswith("tasks/"): raise SystemExit(f"OpenClaw smoke result missing session scope: {json.dumps(payload, ensure_ascii=False, sort_keys=True)[:1000]}") return True def is_valid_no_native_task_record_contract(payload): if not isinstance(payload, dict): return False if payload.get("code") != "no_native_task_record": return False if payload.get("ok") is not False: return False message = str(payload.get("message") or "") if "No native OpenClaw task record found" not in message: raise SystemExit(f"OpenClaw smoke native task lookup returned unexpected message: {json.dumps(payload, ensure_ascii=False, sort_keys=True)[:1000]}") return True 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") if isinstance(final.get("error"), dict) and final["error"]: error = final["error"] message = error.get("message") or json.dumps(error, ensure_ascii=False, sort_keys=True) preview = json.dumps(final, ensure_ascii=False, sort_keys=True)[:1500] raise SystemExit(f"OpenClaw smoke RPC error: {message}\nenvelope preview: {preview}") result = terminal_result(final.get("result") or final.get("payload") or {}) handle = result if result.get("status") != "running": handle = find_task_handle(payloads, final) if handle.get("status") == "running" or (not result and handle): run_id = first_nonempty(handle, "runId", "turnId") app_thread_key = first_nonempty(handle, "appThreadKey", "sessionId", "threadId") openclaw_session_key = first_nonempty(handle, "openclawSessionKey") or openclaw_session_key_for_app_thread(app_thread_key) if not app_thread_key: raise SystemExit(f"OpenClaw smoke running handle missing appThreadKey: {json.dumps(handle, ensure_ascii=False, sort_keys=True)[:1000]}") if not openclaw_session_key: raise SystemExit(f"OpenClaw smoke running handle missing openclawSessionKey: {json.dumps(handle, ensure_ascii=False, sort_keys=True)[:1000]}") if not run_id: raise SystemExit(f"OpenClaw smoke running handle missing runId: {json.dumps(handle, ensure_ascii=False, sort_keys=True)[:1000]}") deadline = time.time() + poll_timeout while time.time() < deadline: req_body = json.dumps({ "jsonrpc": "2.0", "id": "poll-task", "method": "xworkmate.tasks.get", "params": { "appThreadKey": app_thread_key, "openclawSessionKey": openclaw_session_key, "runId": run_id, }, }).encode("utf-8") req = urllib.request.Request( poll_url, data=req_body, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {auth_token}", }, ) try: with urllib.request.urlopen(req) as resp: resp_data = json.loads(resp.read().decode("utf-8")) poll_result = resp_data.get("result") or {} status = poll_result.get("status") if is_valid_no_native_task_record_contract(poll_result): result = poll_result final["result"] = poll_result break if status in ("completed", "failed", "cancelled"): terminal = terminal_result(poll_result) result = terminal if terminal else poll_result final["result"] = poll_result break except Exception as exc: print(f"poll error: {exc}", file=sys.stderr) time.sleep(poll_interval) else: raise SystemExit("timeout waiting for OpenClaw smoke task to complete") 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}") final_text = json.dumps(final, ensure_ascii=False) for marker in ( "Requested agent harness", "provider is not one of", "Agent failed before reply", "ACP_HTTP_", ): if marker in final_text: raise SystemExit(f"OpenClaw smoke returned runtime error text: {marker}") output_text = output_text_from(result) if "pong" not in output_text.lower(): if is_valid_no_displayable_contract(result): print("OpenClaw smoke OK: session contract completed without displayable output") sys.exit(0) if is_valid_no_native_task_record_contract(result): print("OpenClaw smoke OK: session started; no native task record available for polling") sys.exit(0) result_preview = json.dumps(result, ensure_ascii=False, sort_keys=True)[:1000] payload_preview = json.dumps(payloads[:6], ensure_ascii=False, sort_keys=True)[:1500] raise SystemExit(f"OpenClaw smoke did not return pong: {output_text[:500]}\nresult preview: {result_preview}\nSSE preview: {payload_preview}") print("OpenClaw smoke OK: pong received from session contract") PY