#!/usr/bin/env bash set -euo pipefail if ! command -v curl >/dev/null 2>&1; then echo "curl is required" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "jq is required" >&2 exit 1 fi PAPERCLIP_API_URL="${PAPERCLIP_API_URL:-http://localhost:3100}" API_BASE="${PAPERCLIP_API_URL%/}/api" COMPANY_ID="${COMPANY_ID:-${PAPERCLIP_COMPANY_ID:-}}" OPENCLAW_AGENT_NAME="${OPENCLAW_AGENT_NAME:-OpenClaw Smoke Agent}" OPENCLAW_WEBHOOK_URL="${OPENCLAW_WEBHOOK_URL:-}" OPENCLAW_WEBHOOK_AUTH="${OPENCLAW_WEBHOOK_AUTH:-Bearer openclaw-smoke-secret}" USE_DOCKER_RECEIVER="${USE_DOCKER_RECEIVER:-1}" SMOKE_IMAGE="${SMOKE_IMAGE:-paperclip-openclaw-smoke:local}" SMOKE_CONTAINER_NAME="${SMOKE_CONTAINER_NAME:-paperclip-openclaw-smoke}" SMOKE_PORT="${SMOKE_PORT:-19091}" SMOKE_TIMEOUT_SEC="${SMOKE_TIMEOUT_SEC:-45}" AUTH_HEADERS=() if [[ -n "${PAPERCLIP_AUTH_HEADER:-}" ]]; then AUTH_HEADERS+=(-H "Authorization: ${PAPERCLIP_AUTH_HEADER}") fi if [[ -n "${PAPERCLIP_COOKIE:-}" ]]; then AUTH_HEADERS+=(-H "Cookie: ${PAPERCLIP_COOKIE}") fi STARTED_CONTAINER=0 RESPONSE_CODE="" RESPONSE_BODY="" log() { echo "[openclaw-smoke] $*" } fail() { echo "[openclaw-smoke] ERROR: $*" >&2 exit 1 } fail_board_auth_required() { local operation="$1" echo "$RESPONSE_BODY" >&2 cat >&2 <\" PAPERCLIP_COOKIE=\"\" Current auth context appears insufficient (HTTP ${RESPONSE_CODE}). EOF exit 1 } cleanup() { if [[ "$STARTED_CONTAINER" == "1" ]]; then docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true fi } trap cleanup EXIT api_request() { local method="$1" local path="$2" local data="${3-}" local tmp tmp="$(mktemp)" local url if [[ "$path" == http://* || "$path" == https://* ]]; then url="$path" elif [[ "$path" == /api/* ]]; then url="${PAPERCLIP_API_URL%/}${path}" else url="${API_BASE}${path}" fi if [[ -n "$data" ]]; then RESPONSE_CODE="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "${AUTH_HEADERS[@]}" -H "Content-Type: application/json" "$url" --data "$data")" else RESPONSE_CODE="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "${AUTH_HEADERS[@]}" "$url")" fi RESPONSE_BODY="$(cat "$tmp")" rm -f "$tmp" } assert_status() { local expected="$1" if [[ "$RESPONSE_CODE" != "$expected" ]]; then echo "$RESPONSE_BODY" >&2 fail "expected HTTP $expected, got HTTP $RESPONSE_CODE" fi } assert_json_has_string() { local jq_expr="$1" local value value="$(jq -r "$jq_expr // empty" <<<"$RESPONSE_BODY")" if [[ -z "$value" ]]; then echo "$RESPONSE_BODY" >&2 fail "expected JSON string at: $jq_expr" fi echo "$value" } if [[ "$USE_DOCKER_RECEIVER" == "1" && -z "$OPENCLAW_WEBHOOK_URL" ]]; then if ! command -v docker >/dev/null 2>&1; then fail "docker is required when USE_DOCKER_RECEIVER=1" fi log "building dockerized OpenClaw webhook receiver image" docker build -t "$SMOKE_IMAGE" -f docker/openclaw-smoke/Dockerfile docker/openclaw-smoke >/dev/null docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true log "starting dockerized OpenClaw webhook receiver" docker run -d \ --name "$SMOKE_CONTAINER_NAME" \ -p "${SMOKE_PORT}:8787" \ -e "OPENCLAW_SMOKE_AUTH=${OPENCLAW_WEBHOOK_AUTH}" \ "$SMOKE_IMAGE" >/dev/null STARTED_CONTAINER=1 OPENCLAW_WEBHOOK_URL="http://127.0.0.1:${SMOKE_PORT}/webhook" for _ in $(seq 1 30); do code="$(curl -sS -o /dev/null -w "%{http_code}" "http://127.0.0.1:${SMOKE_PORT}/health" || true)" if [[ "$code" == "200" ]]; then break fi sleep 1 done code="$(curl -sS -o /dev/null -w "%{http_code}" "http://127.0.0.1:${SMOKE_PORT}/health" || true)" if [[ "$code" != "200" ]]; then fail "webhook receiver failed health check on port ${SMOKE_PORT}" fi fi if [[ -z "$OPENCLAW_WEBHOOK_URL" ]]; then fail "OPENCLAW_WEBHOOK_URL must be set when USE_DOCKER_RECEIVER=0" fi log "checking Paperclip health" api_request "GET" "/health" assert_status "200" DEPLOYMENT_MODE="$(jq -r '.deploymentMode // "unknown"' <<<"$RESPONSE_BODY")" DEPLOYMENT_EXPOSURE="$(jq -r '.deploymentExposure // "unknown"' <<<"$RESPONSE_BODY")" log "deployment mode=${DEPLOYMENT_MODE} exposure=${DEPLOYMENT_EXPOSURE}" if [[ -z "$COMPANY_ID" ]]; then log "resolving company id" api_request "GET" "/companies" assert_status "200" COMPANY_ID="$(jq -r '.[0].id // empty' <<<"$RESPONSE_BODY")" if [[ -z "$COMPANY_ID" ]]; then fail "no companies found; create one before running smoke test" fi fi log "creating agent-only invite for company ${COMPANY_ID}" INVITE_PAYLOAD="$(jq -nc '{allowedJoinTypes:"agent"}')" api_request "POST" "/companies/${COMPANY_ID}/invites" "$INVITE_PAYLOAD" if [[ "$RESPONSE_CODE" == "401" || "$RESPONSE_CODE" == "403" ]]; then fail_board_auth_required "Invite creation" fi assert_status "201" INVITE_TOKEN="$(assert_json_has_string '.token')" INVITE_ID="$(assert_json_has_string '.id')" log "created invite ${INVITE_ID}" log "verifying onboarding JSON and text endpoints" api_request "GET" "/invites/${INVITE_TOKEN}/onboarding" assert_status "200" ONBOARDING_TEXT_PATH="$(jq -r '.invite.onboardingTextPath // empty' <<<"$RESPONSE_BODY")" if [[ -z "$ONBOARDING_TEXT_PATH" ]]; then fail "onboarding manifest missing invite.onboardingTextPath" fi api_request "GET" "/invites/${INVITE_TOKEN}/onboarding.txt" assert_status "200" if ! grep -q "Paperclip OpenClaw Gateway Onboarding" <<<"$RESPONSE_BODY"; then fail "onboarding.txt response missing expected header" fi OPENCLAW_GATEWAY_URL="${OPENCLAW_GATEWAY_URL:-ws://127.0.0.1:18789}" OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-${OPENCLAW_WEBHOOK_AUTH#Bearer }}" if [[ -z "$OPENCLAW_GATEWAY_TOKEN" ]]; then fail "OPENCLAW_GATEWAY_TOKEN (or OPENCLAW_WEBHOOK_AUTH) is required for gateway join" fi log "submitting OpenClaw gateway agent join request" JOIN_PAYLOAD="$(jq -nc \ --arg name "$OPENCLAW_AGENT_NAME" \ --arg url "$OPENCLAW_GATEWAY_URL" \ --arg token "$OPENCLAW_GATEWAY_TOKEN" \ '{ requestType: "agent", agentName: $name, adapterType: "openclaw_gateway", capabilities: "Automated OpenClaw gateway smoke harness", agentDefaultsPayload: { url: $url, headers: { "x-openclaw-token": $token }, sessionKeyStrategy: "issue", waitTimeoutMs: 120000 } }')" api_request "POST" "/invites/${INVITE_TOKEN}/accept" "$JOIN_PAYLOAD" assert_status "202" JOIN_REQUEST_ID="$(assert_json_has_string '.id')" CLAIM_SECRET="$(assert_json_has_string '.claimSecret')" CLAIM_API_PATH="$(assert_json_has_string '.claimApiKeyPath')" DIAGNOSTICS_JSON="$(jq -c '.diagnostics // []' <<<"$RESPONSE_BODY")" if [[ "$DIAGNOSTICS_JSON" != "[]" ]]; then log "join diagnostics: ${DIAGNOSTICS_JSON}" fi log "approving join request ${JOIN_REQUEST_ID}" api_request "POST" "/companies/${COMPANY_ID}/join-requests/${JOIN_REQUEST_ID}/approve" "{}" if [[ "$RESPONSE_CODE" == "401" || "$RESPONSE_CODE" == "403" ]]; then fail_board_auth_required "Join approval" fi assert_status "200" CREATED_AGENT_ID="$(assert_json_has_string '.createdAgentId')" log "verifying invalid claim secret is rejected" api_request "POST" "/join-requests/${JOIN_REQUEST_ID}/claim-api-key" '{"claimSecret":"invalid-smoke-secret-value"}' if [[ "$RESPONSE_CODE" == "201" ]]; then fail "invalid claim secret unexpectedly succeeded" fi log "claiming API key with one-time claim secret" CLAIM_PAYLOAD="$(jq -nc --arg secret "$CLAIM_SECRET" '{claimSecret:$secret}')" api_request "POST" "$CLAIM_API_PATH" "$CLAIM_PAYLOAD" assert_status "201" AGENT_API_KEY="$(assert_json_has_string '.token')" KEY_ID="$(assert_json_has_string '.keyId')" log "verifying replay claim is rejected" api_request "POST" "$CLAIM_API_PATH" "$CLAIM_PAYLOAD" if [[ "$RESPONSE_CODE" == "201" ]]; then fail "claim secret replay unexpectedly succeeded" fi if [[ "$USE_DOCKER_RECEIVER" == "1" && "$STARTED_CONTAINER" == "1" ]]; then curl -sS -X POST "http://127.0.0.1:${SMOKE_PORT}/reset" >/dev/null fi log "triggering wakeup for newly created OpenClaw agent" WAKE_PAYLOAD='{"source":"on_demand","triggerDetail":"manual","reason":"openclaw_smoke"}' api_request "POST" "/agents/${CREATED_AGENT_ID}/wakeup" "$WAKE_PAYLOAD" if [[ "$RESPONSE_CODE" == "401" || "$RESPONSE_CODE" == "403" ]]; then fail_board_auth_required "Agent wakeup" fi assert_status "202" RUN_ID="$(jq -r '.id // empty' <<<"$RESPONSE_BODY")" if [[ -z "$RUN_ID" ]]; then log "wakeup response: ${RESPONSE_BODY}" fi log "waiting for webhook callback" FOUND_EVENT="0" LAST_EVENTS='{"count":0,"events":[]}' for _ in $(seq 1 "$SMOKE_TIMEOUT_SEC"); do if [[ "$USE_DOCKER_RECEIVER" == "1" && "$STARTED_CONTAINER" == "1" ]]; then LAST_EVENTS="$(curl -sS "http://127.0.0.1:${SMOKE_PORT}/events")" else break fi MATCH_COUNT="$(jq -r --arg agentId "$CREATED_AGENT_ID" '[.events[] | select(((.body.paperclip.agentId // "") == $agentId))] | length' <<<"$LAST_EVENTS")" if [[ "$MATCH_COUNT" -gt 0 ]]; then FOUND_EVENT="1" break fi sleep 1 done if [[ "$USE_DOCKER_RECEIVER" == "1" && "$STARTED_CONTAINER" == "1" && "$FOUND_EVENT" != "1" ]]; then echo "$LAST_EVENTS" | jq '.' >&2 fail "did not observe OpenClaw webhook callback within ${SMOKE_TIMEOUT_SEC}s" fi log "success" log "companyId=${COMPANY_ID}" log "inviteId=${INVITE_ID}" log "joinRequestId=${JOIN_REQUEST_ID}" log "agentId=${CREATED_AGENT_ID}" log "keyId=${KEY_ID}" if [[ -n "$RUN_ID" ]]; then log "runId=${RUN_ID}" fi if [[ -n "$AGENT_API_KEY" ]]; then log "agentApiKeyPrefix=${AGENT_API_KEY:0:12}..." fi