288 lines
9.1 KiB
Bash
Executable File
288 lines
9.1 KiB
Bash
Executable File
#!/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 <<EOF
|
|
[openclaw-smoke] ERROR: ${operation} requires board/operator auth.
|
|
|
|
Provide one of:
|
|
PAPERCLIP_AUTH_HEADER=\"Bearer <board-token>\"
|
|
PAPERCLIP_COOKIE=\"<board-session-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 Onboarding" <<<"$RESPONSE_BODY"; then
|
|
fail "onboarding.txt response missing expected header"
|
|
fi
|
|
|
|
log "submitting OpenClaw agent join request"
|
|
JOIN_PAYLOAD="$(jq -nc \
|
|
--arg name "$OPENCLAW_AGENT_NAME" \
|
|
--arg url "$OPENCLAW_WEBHOOK_URL" \
|
|
--arg auth "$OPENCLAW_WEBHOOK_AUTH" \
|
|
'{
|
|
requestType: "agent",
|
|
agentName: $name,
|
|
adapterType: "openclaw",
|
|
capabilities: "Automated OpenClaw smoke harness",
|
|
agentDefaultsPayload: (
|
|
{ url: $url, method: "POST", timeoutSec: 30 }
|
|
+ (if ($auth | length) > 0 then { webhookAuthHeader: $auth } else {} end)
|
|
)
|
|
}')"
|
|
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
|