merge: wave 2 executor (01-02) into main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-25 23:46:16 -04:00
1 parent a5b8fcf9d3
commit aa1a78d7d9
1081 files changed
+445341

No files matched your search

@@ -0,0 +1,329 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
echo "[openclaw-docker-ui] $*"
}
fail() {
echo "[openclaw-docker-ui] ERROR: $*" >&2
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd"
}
reset_openclaw_state_dir() {
local state_dir="$1"
local resolved_state_dir resolved_home
[[ -n "$state_dir" ]] || fail "OPENCLAW_CONFIG_DIR must not be empty when resetting state"
mkdir -p "$state_dir"
resolved_state_dir="$(cd "$state_dir" && pwd -P)"
resolved_home="$(cd "$HOME" && pwd -P)"
case "$resolved_state_dir" in
"/"|"$resolved_home")
fail "refusing to reset unsafe OPENCLAW_CONFIG_DIR: $resolved_state_dir"
;;
esac
log "resetting OpenClaw state under $resolved_state_dir"
rm -rf \
"$resolved_state_dir/agents" \
"$resolved_state_dir/canvas" \
"$resolved_state_dir/cron" \
"$resolved_state_dir/credentials" \
"$resolved_state_dir/devices" \
"$resolved_state_dir/identity" \
"$resolved_state_dir/logs" \
"$resolved_state_dir/memory" \
"$resolved_state_dir/skills" \
"$resolved_state_dir/workspace"
rm -f \
"$resolved_state_dir/openclaw.json" \
"$resolved_state_dir/openclaw.json.bak" \
"$resolved_state_dir/update-check.json"
}
require_cmd docker
require_cmd git
require_cmd curl
require_cmd openssl
require_cmd grep
OPENCLAW_REPO_URL="${OPENCLAW_REPO_URL:-https://github.com/openclaw/openclaw.git}"
OPENCLAW_DOCKER_DIR="${OPENCLAW_DOCKER_DIR:-/tmp/openclaw-docker}"
OPENCLAW_REPO_REF="${OPENCLAW_REPO_REF:-v2026.3.2}"
OPENCLAW_IMAGE="${OPENCLAW_IMAGE:-openclaw:local}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR:-${TMPDIR:-/tmp}}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR%/}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR:-/tmp}"
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$OPENCLAW_TMP_DIR/openclaw-paperclip-smoke}"
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$OPENCLAW_CONFIG_DIR/workspace}"
OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-$(openssl rand -hex 32)}"
OPENCLAW_BUILD="${OPENCLAW_BUILD:-1}"
OPENCLAW_WAIT_SECONDS="${OPENCLAW_WAIT_SECONDS:-45}"
OPENCLAW_OPEN_BROWSER="${OPENCLAW_OPEN_BROWSER:-0}"
OPENCLAW_SECRETS_FILE="${OPENCLAW_SECRETS_FILE:-$HOME/.secrets}"
# Keep default one-command UX: local smoke run should not require manual pairing.
OPENCLAW_DISABLE_DEVICE_AUTH="${OPENCLAW_DISABLE_DEVICE_AUTH:-1}"
OPENCLAW_MODEL_PRIMARY="${OPENCLAW_MODEL_PRIMARY:-openai/gpt-5.2}"
OPENCLAW_MODEL_FALLBACK="${OPENCLAW_MODEL_FALLBACK:-openai/gpt-5.2-chat-latest}"
OPENCLAW_RESET_STATE="${OPENCLAW_RESET_STATE:-1}"
PAPERCLIP_HOST_PORT="${PAPERCLIP_HOST_PORT:-3100}"
PAPERCLIP_HOST_FROM_CONTAINER="${PAPERCLIP_HOST_FROM_CONTAINER:-host.docker.internal}"
case "$OPENCLAW_DISABLE_DEVICE_AUTH" in
1|true|TRUE|True|yes|YES|Yes)
OPENCLAW_DISABLE_DEVICE_AUTH_JSON="true"
;;
0|false|FALSE|False|no|NO|No)
OPENCLAW_DISABLE_DEVICE_AUTH_JSON="false"
;;
*)
fail "OPENCLAW_DISABLE_DEVICE_AUTH must be one of: 1,0,true,false,yes,no"
;;
esac
if [[ -z "${OPENAI_API_KEY:-}" && -f "$OPENCLAW_SECRETS_FILE" ]]; then
set +u
# shellcheck source=/dev/null
source "$OPENCLAW_SECRETS_FILE"
set -u
fi
[[ -n "${OPENAI_API_KEY:-}" ]] || fail "OPENAI_API_KEY is required (set env var or include it in $OPENCLAW_SECRETS_FILE)"
log "preparing OpenClaw repo at $OPENCLAW_DOCKER_DIR"
if [[ -d "$OPENCLAW_DOCKER_DIR/.git" ]]; then
git -C "$OPENCLAW_DOCKER_DIR" fetch --quiet --tags origin || true
else
rm -rf "$OPENCLAW_DOCKER_DIR"
git clone "$OPENCLAW_REPO_URL" "$OPENCLAW_DOCKER_DIR"
git -C "$OPENCLAW_DOCKER_DIR" fetch --quiet --tags origin || true
fi
resolved_openclaw_ref=""
if git -C "$OPENCLAW_DOCKER_DIR" rev-parse --verify --quiet "origin/$OPENCLAW_REPO_REF" >/dev/null; then
resolved_openclaw_ref="origin/$OPENCLAW_REPO_REF"
elif git -C "$OPENCLAW_DOCKER_DIR" rev-parse --verify --quiet "$OPENCLAW_REPO_REF" >/dev/null; then
resolved_openclaw_ref="$OPENCLAW_REPO_REF"
fi
[[ -n "$resolved_openclaw_ref" ]] || fail "unable to resolve OPENCLAW_REPO_REF=$OPENCLAW_REPO_REF in $OPENCLAW_DOCKER_DIR"
git -C "$OPENCLAW_DOCKER_DIR" checkout --quiet "$resolved_openclaw_ref"
log "using OpenClaw ref $resolved_openclaw_ref ($(git -C "$OPENCLAW_DOCKER_DIR" rev-parse --short HEAD))"
if [[ "$OPENCLAW_BUILD" == "1" ]]; then
log "building Docker image $OPENCLAW_IMAGE"
docker build -t "$OPENCLAW_IMAGE" -f "$OPENCLAW_DOCKER_DIR/Dockerfile" "$OPENCLAW_DOCKER_DIR"
fi
log "writing OpenClaw config under $OPENCLAW_CONFIG_DIR"
if [[ "$OPENCLAW_RESET_STATE" == "1" ]]; then
# Ensure deterministic smoke behavior across reruns by restarting with a clean state dir.
OPENCLAW_CONFIG_DIR="$OPENCLAW_CONFIG_DIR" \
OPENCLAW_WORKSPACE_DIR="$OPENCLAW_WORKSPACE_DIR" \
OPENCLAW_GATEWAY_PORT="$OPENCLAW_GATEWAY_PORT" \
OPENCLAW_BRIDGE_PORT="$OPENCLAW_BRIDGE_PORT" \
OPENCLAW_GATEWAY_BIND="$OPENCLAW_GATEWAY_BIND" \
OPENCLAW_GATEWAY_TOKEN="$OPENCLAW_GATEWAY_TOKEN" \
OPENCLAW_IMAGE="$OPENCLAW_IMAGE" \
OPENAI_API_KEY="$OPENAI_API_KEY" \
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" down --remove-orphans >/dev/null 2>&1 || true
reset_openclaw_state_dir "$OPENCLAW_CONFIG_DIR"
fi
mkdir -p "$OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_CONFIG_DIR/identity" "$OPENCLAW_CONFIG_DIR/credentials"
chmod 700 "$OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR/credentials"
cat > "$OPENCLAW_CONFIG_DIR/openclaw.json" <<EOF
{
"gateway": {
"mode": "local",
"port": ${OPENCLAW_GATEWAY_PORT},
"bind": "${OPENCLAW_GATEWAY_BIND}",
"auth": {
"mode": "token",
"token": "${OPENCLAW_GATEWAY_TOKEN}"
},
"controlUi": {
"enabled": true,
"dangerouslyDisableDeviceAuth": ${OPENCLAW_DISABLE_DEVICE_AUTH_JSON},
"allowedOrigins": [
"http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}",
"http://localhost:${OPENCLAW_GATEWAY_PORT}"
]
}
},
"env": {
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
},
"agents": {
"defaults": {
"model": {
"primary": "${OPENCLAW_MODEL_PRIMARY}",
"fallbacks": [
"${OPENCLAW_MODEL_FALLBACK}"
]
},
"workspace": "/home/node/.openclaw/workspace"
}
}
}
EOF
chmod 600 "$OPENCLAW_CONFIG_DIR/openclaw.json"
cat > "$OPENCLAW_DOCKER_DIR/.env" <<EOF
OPENCLAW_CONFIG_DIR=$OPENCLAW_CONFIG_DIR
OPENCLAW_WORKSPACE_DIR=$OPENCLAW_WORKSPACE_DIR
OPENCLAW_GATEWAY_PORT=$OPENCLAW_GATEWAY_PORT
OPENCLAW_BRIDGE_PORT=$OPENCLAW_BRIDGE_PORT
OPENCLAW_GATEWAY_BIND=$OPENCLAW_GATEWAY_BIND
OPENCLAW_GATEWAY_TOKEN=$OPENCLAW_GATEWAY_TOKEN
OPENCLAW_IMAGE=$OPENCLAW_IMAGE
OPENAI_API_KEY=$OPENAI_API_KEY
EOF
COMPOSE_OVERRIDE="${OPENCLAW_DOCKER_DIR}/.paperclip-openclaw.override.yml"
cat > "$COMPOSE_OVERRIDE" <<EOF
services:
openclaw-gateway:
tmpfs:
- /tmp:exec,size=512M
extra_hosts:
- "host.docker.internal:host-gateway"
openclaw-cli:
tmpfs:
- /tmp:exec,size=512M
EOF
compose() {
docker compose \
-f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" \
-f "$COMPOSE_OVERRIDE" \
"$@"
}
detect_paperclip_base_url() {
local bridge_gateway candidate health_url
bridge_gateway="$(docker network inspect openclaw-docker_default --format '{{(index .IPAM.Config 0).Gateway}}' 2>/dev/null || true)"
for candidate in "$PAPERCLIP_HOST_FROM_CONTAINER" "$bridge_gateway"; do
[[ -n "$candidate" ]] || continue
health_url="http://${candidate}:${PAPERCLIP_HOST_PORT}/api/health"
if compose exec -T openclaw-gateway node -e "fetch('${health_url}').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1; then
echo "http://${candidate}:${PAPERCLIP_HOST_PORT}"
return 0
fi
done
return 1
}
log "starting OpenClaw gateway container"
compose up -d openclaw-gateway
log "waiting for gateway health on http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/"
READY="0"
for _ in $(seq 1 "$OPENCLAW_WAIT_SECONDS"); do
code="$(curl -sS -o /dev/null -w "%{http_code}" "http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/" || true)"
if [[ "$code" == "200" ]]; then
READY="1"
break
fi
sleep 1
done
if [[ "$READY" != "1" ]]; then
compose logs --tail=100 openclaw-gateway || true
fail "gateway did not become healthy in ${OPENCLAW_WAIT_SECONDS}s"
fi
paperclip_base_url="$(detect_paperclip_base_url || true)"
dashboard_output="$(compose run --rm openclaw-cli dashboard --no-open)"
dashboard_url="$(grep -Eo 'https?://[^[:space:]]+#token=[^[:space:]]+' <<<"$dashboard_output" | head -n1 || true)"
if [[ -z "$dashboard_url" ]]; then
dashboard_url="http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/#token=${OPENCLAW_GATEWAY_TOKEN}"
fi
cat <<EOF
OpenClaw gateway is running.
Dashboard URL:
$dashboard_url
EOF
if [[ "$OPENCLAW_DISABLE_DEVICE_AUTH_JSON" == "true" ]]; then
cat <<EOF
Pairing:
Device pairing is disabled by default for this local smoke run.
No extra env vars are required for the default path.
(Security tradeoff: enable pairing with OPENCLAW_DISABLE_DEVICE_AUTH=0.)
Model:
${OPENCLAW_MODEL_PRIMARY} (fallback: ${OPENCLAW_MODEL_FALLBACK})
State:
OPENCLAW_RESET_STATE=$OPENCLAW_RESET_STATE
Paperclip URL for OpenClaw container:
EOF
if [[ -n "$paperclip_base_url" ]]; then
cat <<EOF
$paperclip_base_url
(Use this base URL for invite/onboarding links from inside OpenClaw Docker.)
EOF
else
cat <<EOF
Auto-detect failed. Try: http://host.docker.internal:${PAPERCLIP_HOST_PORT}
(Do not use http://127.0.0.1:${PAPERCLIP_HOST_PORT} inside the container.)
If Paperclip rejects the host, run on host machine:
pnpm paperclipai allowed-hostname host.docker.internal
Then restart Paperclip and re-run this script.
EOF
fi
cat <<EOF
Useful commands:
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" logs -f openclaw-gateway
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" down
EOF
else
cat <<EOF
Pairing:
Device pairing is enabled.
If UI shows "pairing required", run:
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" run --rm openclaw-cli devices list
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" run --rm openclaw-cli devices approve --latest
Model:
${OPENCLAW_MODEL_PRIMARY} (fallback: ${OPENCLAW_MODEL_FALLBACK})
State:
OPENCLAW_RESET_STATE=$OPENCLAW_RESET_STATE
Paperclip URL for OpenClaw container:
EOF
if [[ -n "$paperclip_base_url" ]]; then
cat <<EOF
$paperclip_base_url
(Use this base URL for invite/onboarding links from inside OpenClaw Docker.)
EOF
else
cat <<EOF
Auto-detect failed. Try: http://host.docker.internal:${PAPERCLIP_HOST_PORT}
(Do not use http://127.0.0.1:${PAPERCLIP_HOST_PORT} inside the container.)
If Paperclip rejects the host, run on host machine:
pnpm paperclipai allowed-hostname host.docker.internal
Then restart Paperclip and re-run this script.
EOF
fi
cat <<EOF
Useful commands:
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" logs -f openclaw-gateway
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" -f "$COMPOSE_OVERRIDE" down
EOF
fi
if [[ "$OPENCLAW_OPEN_BROWSER" == "1" ]] && command -v open >/dev/null 2>&1; then
log "opening dashboard in browser"
open "$dashboard_url"
fi
@@ -0,0 +1,954 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
echo "[openclaw-gateway-e2e] $*"
}
warn() {
echo "[openclaw-gateway-e2e] WARN: $*" >&2
}
fail() {
echo "[openclaw-gateway-e2e] ERROR: $*" >&2
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd"
}
require_cmd curl
require_cmd jq
require_cmd docker
require_cmd node
require_cmd shasum
PAPERCLIP_API_URL="${PAPERCLIP_API_URL:-http://127.0.0.1:3100}"
API_BASE="${PAPERCLIP_API_URL%/}/api"
COMPANY_SELECTOR="${COMPANY_SELECTOR:-CLA}"
OPENCLAW_AGENT_NAME="${OPENCLAW_AGENT_NAME:-OpenClaw Gateway Smoke Agent}"
OPENCLAW_GATEWAY_URL="${OPENCLAW_GATEWAY_URL:-ws://127.0.0.1:18789}"
OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR:-${TMPDIR:-/tmp}}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR%/}"
OPENCLAW_TMP_DIR="${OPENCLAW_TMP_DIR:-/tmp}"
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-${OPENCLAW_TMP_DIR}/openclaw-paperclip-smoke}"
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-${OPENCLAW_CONFIG_DIR}/workspace}"
OPENCLAW_CONTAINER_NAME="${OPENCLAW_CONTAINER_NAME:-openclaw-docker-openclaw-gateway-1}"
OPENCLAW_IMAGE="${OPENCLAW_IMAGE:-openclaw:local}"
OPENCLAW_DOCKER_DIR="${OPENCLAW_DOCKER_DIR:-/tmp/openclaw-docker}"
OPENCLAW_RESET_DOCKER="${OPENCLAW_RESET_DOCKER:-1}"
OPENCLAW_BUILD="${OPENCLAW_BUILD:-1}"
OPENCLAW_WAIT_SECONDS="${OPENCLAW_WAIT_SECONDS:-60}"
OPENCLAW_RESET_STATE="${OPENCLAW_RESET_STATE:-1}"
PAPERCLIP_API_URL_FOR_OPENCLAW="${PAPERCLIP_API_URL_FOR_OPENCLAW:-http://host.docker.internal:3100}"
CASE_TIMEOUT_SEC="${CASE_TIMEOUT_SEC:-420}"
RUN_TIMEOUT_SEC="${RUN_TIMEOUT_SEC:-300}"
STRICT_CASES="${STRICT_CASES:-1}"
AUTO_INSTALL_SKILL="${AUTO_INSTALL_SKILL:-1}"
OPENCLAW_DIAG_DIR="${OPENCLAW_DIAG_DIR:-/tmp/openclaw-gateway-e2e-diag-$(date +%Y%m%d-%H%M%S)}"
OPENCLAW_ADAPTER_TIMEOUT_SEC="${OPENCLAW_ADAPTER_TIMEOUT_SEC:-120}"
OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS="${OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS:-120000}"
PAIRING_AUTO_APPROVE="${PAIRING_AUTO_APPROVE:-1}"
PAYLOAD_TEMPLATE_MESSAGE_APPEND="${PAYLOAD_TEMPLATE_MESSAGE_APPEND:-}"
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}" )
PAPERCLIP_BROWSER_ORIGIN="${PAPERCLIP_BROWSER_ORIGIN:-${PAPERCLIP_API_URL%/}}"
AUTH_HEADERS+=( -H "Origin: ${PAPERCLIP_BROWSER_ORIGIN}" -H "Referer: ${PAPERCLIP_BROWSER_ORIGIN}/" )
fi
RESPONSE_CODE=""
RESPONSE_BODY=""
COMPANY_ID=""
AGENT_ID=""
AGENT_API_KEY=""
JOIN_REQUEST_ID=""
INVITE_ID=""
RUN_ID=""
CASE_A_ISSUE_ID=""
CASE_B_ISSUE_ID=""
CASE_C_ISSUE_ID=""
CASE_C_CREATED_ISSUE_ID=""
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
if (( ${#AUTH_HEADERS[@]} > 0 )); 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" -H "Content-Type: application/json" "$url" --data "$data")"
fi
else
if (( ${#AUTH_HEADERS[@]} > 0 )); then
RESPONSE_CODE="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "${AUTH_HEADERS[@]}" "$url")"
else
RESPONSE_CODE="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "$url")"
fi
fi
RESPONSE_BODY="$(cat "$tmp")"
rm -f "$tmp"
}
capture_run_diagnostics() {
local run_id="$1"
local label="${2:-run}"
[[ -n "$run_id" ]] || return 0
mkdir -p "$OPENCLAW_DIAG_DIR"
api_request "GET" "/heartbeat-runs/${run_id}/events?limit=1000"
if [[ "$RESPONSE_CODE" == "200" ]]; then
printf "%s\n" "$RESPONSE_BODY" > "${OPENCLAW_DIAG_DIR}/${label}-${run_id}-events.json"
else
warn "could not fetch events for run ${run_id} (HTTP ${RESPONSE_CODE})"
fi
api_request "GET" "/heartbeat-runs/${run_id}/log?limitBytes=524288"
if [[ "$RESPONSE_CODE" == "200" ]]; then
printf "%s\n" "$RESPONSE_BODY" > "${OPENCLAW_DIAG_DIR}/${label}-${run_id}-log.json"
jq -r '.content // ""' <<<"$RESPONSE_BODY" > "${OPENCLAW_DIAG_DIR}/${label}-${run_id}-log.txt" 2>/dev/null || true
else
warn "could not fetch log for run ${run_id} (HTTP ${RESPONSE_CODE})"
fi
}
capture_issue_diagnostics() {
local issue_id="$1"
local label="${2:-issue}"
[[ -n "$issue_id" ]] || return 0
mkdir -p "$OPENCLAW_DIAG_DIR"
api_request "GET" "/issues/${issue_id}"
if [[ "$RESPONSE_CODE" == "200" ]]; then
printf "%s\n" "$RESPONSE_BODY" > "${OPENCLAW_DIAG_DIR}/${label}-${issue_id}.json"
fi
api_request "GET" "/issues/${issue_id}/comments"
if [[ "$RESPONSE_CODE" == "200" ]]; then
printf "%s\n" "$RESPONSE_BODY" > "${OPENCLAW_DIAG_DIR}/${label}-${issue_id}-comments.json"
fi
}
capture_openclaw_container_logs() {
mkdir -p "$OPENCLAW_DIAG_DIR"
local container
container="$(detect_openclaw_container || true)"
if [[ -z "$container" ]]; then
warn "could not detect OpenClaw container for diagnostics"
return 0
fi
docker logs --tail=1200 "$container" > "${OPENCLAW_DIAG_DIR}/openclaw-container.log" 2>&1 || true
}
assert_status() {
local expected="$1"
if [[ "$RESPONSE_CODE" != "$expected" ]]; then
echo "$RESPONSE_BODY" >&2
fail "expected HTTP ${expected}, got ${RESPONSE_CODE}"
fi
}
require_board_auth() {
if [[ ${#AUTH_HEADERS[@]} -eq 0 ]]; then
fail "board auth required. Set PAPERCLIP_COOKIE or PAPERCLIP_AUTH_HEADER."
fi
api_request "GET" "/companies"
if [[ "$RESPONSE_CODE" != "200" ]]; then
echo "$RESPONSE_BODY" >&2
fail "board auth invalid for /api/companies (HTTP ${RESPONSE_CODE})"
fi
}
maybe_cleanup_openclaw_docker() {
if [[ "$OPENCLAW_RESET_DOCKER" != "1" ]]; then
log "OPENCLAW_RESET_DOCKER=${OPENCLAW_RESET_DOCKER}; skipping docker cleanup"
return
fi
log "cleaning OpenClaw docker state"
if [[ -d "$OPENCLAW_DOCKER_DIR" ]]; then
docker compose -f "$OPENCLAW_DOCKER_DIR/docker-compose.yml" down --remove-orphans >/dev/null 2>&1 || true
fi
if docker ps -a --format '{{.Names}}' | grep -qx "$OPENCLAW_CONTAINER_NAME"; then
docker rm -f "$OPENCLAW_CONTAINER_NAME" >/dev/null 2>&1 || true
fi
docker image rm "$OPENCLAW_IMAGE" >/dev/null 2>&1 || true
}
start_openclaw_docker() {
log "starting clean OpenClaw docker"
OPENCLAW_CONFIG_DIR="$OPENCLAW_CONFIG_DIR" OPENCLAW_WORKSPACE_DIR="$OPENCLAW_WORKSPACE_DIR" \
OPENCLAW_RESET_STATE="$OPENCLAW_RESET_STATE" OPENCLAW_BUILD="$OPENCLAW_BUILD" OPENCLAW_WAIT_SECONDS="$OPENCLAW_WAIT_SECONDS" \
./scripts/smoke/openclaw-docker-ui.sh
}
wait_http_ready() {
local url="$1"
local timeout_sec="$2"
local started_at now code
started_at="$(date +%s)"
while true; do
code="$(curl -sS -o /dev/null -w "%{http_code}" "$url" || true)"
if [[ "$code" == "200" ]]; then
return 0
fi
now="$(date +%s)"
if (( now - started_at >= timeout_sec )); then
return 1
fi
sleep 1
done
}
detect_openclaw_container() {
if docker ps --format '{{.Names}}' | grep -qx "$OPENCLAW_CONTAINER_NAME"; then
echo "$OPENCLAW_CONTAINER_NAME"
return 0
fi
local detected
detected="$(docker ps --format '{{.Names}}' | grep 'openclaw-gateway' | head -n1 || true)"
if [[ -n "$detected" ]]; then
echo "$detected"
return 0
fi
return 1
}
detect_gateway_token() {
if [[ -n "$OPENCLAW_GATEWAY_TOKEN" ]]; then
echo "$OPENCLAW_GATEWAY_TOKEN"
return 0
fi
local config_path
config_path="${OPENCLAW_CONFIG_DIR%/}/openclaw.json"
if [[ -f "$config_path" ]]; then
local token
token="$(jq -r '.gateway.auth.token // empty' "$config_path")"
if [[ -n "$token" ]]; then
echo "$token"
return 0
fi
fi
local container
container="$(detect_openclaw_container || true)"
if [[ -n "$container" ]]; then
local token_from_container
token_from_container="$(docker exec "$container" sh -lc "node -e 'const fs=require(\"fs\");const c=JSON.parse(fs.readFileSync(\"/home/node/.openclaw/openclaw.json\",\"utf8\"));process.stdout.write(c.gateway?.auth?.token||\"\");'" 2>/dev/null || true)"
if [[ -n "$token_from_container" ]]; then
echo "$token_from_container"
return 0
fi
fi
return 1
}
hash_prefix() {
local value="$1"
printf "%s" "$value" | shasum -a 256 | awk '{print $1}' | cut -c1-12
}
probe_gateway_ws() {
local url="$1"
local token="$2"
node - "$url" "$token" <<'NODE'
const WebSocket = require("ws");
const url = process.argv[2];
const token = process.argv[3];
const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` } });
const timeout = setTimeout(() => {
console.error("gateway probe timed out");
process.exit(2);
}, 8000);
ws.on("message", (raw) => {
try {
const message = JSON.parse(String(raw));
if (message?.type === "event" && message?.event === "connect.challenge") {
clearTimeout(timeout);
ws.close();
process.exit(0);
}
} catch {
// ignore
}
});
ws.on("error", (err) => {
clearTimeout(timeout);
console.error(err?.message || String(err));
process.exit(1);
});
NODE
}
resolve_company_id() {
api_request "GET" "/companies"
assert_status "200"
local selector
selector="$(printf "%s" "$COMPANY_SELECTOR" | tr '[:lower:]' '[:upper:]')"
COMPANY_ID="$(jq -r --arg sel "$selector" '
map(select(
((.id // "") | ascii_upcase) == $sel or
((.name // "") | ascii_upcase) == $sel or
((.issuePrefix // "") | ascii_upcase) == $sel
))
| .[0].id // empty
' <<<"$RESPONSE_BODY")"
if [[ -z "$COMPANY_ID" ]]; then
local available
available="$(jq -r '.[] | "- id=\(.id) issuePrefix=\(.issuePrefix // "") name=\(.name // "")"' <<<"$RESPONSE_BODY")"
echo "$available" >&2
fail "could not find company for selector '${COMPANY_SELECTOR}'"
fi
log "resolved company ${COMPANY_ID} from selector ${COMPANY_SELECTOR}"
}
cleanup_openclaw_agents() {
api_request "GET" "/companies/${COMPANY_ID}/agents"
assert_status "200"
local ids
ids="$(jq -r '.[] | select((.adapterType == "openclaw" or .adapterType == "openclaw_gateway")) | .id' <<<"$RESPONSE_BODY")"
if [[ -z "$ids" ]]; then
log "no prior OpenClaw agents to cleanup"
return
fi
while IFS= read -r id; do
[[ -n "$id" ]] || continue
log "terminating prior OpenClaw agent ${id}"
api_request "POST" "/agents/${id}/terminate" "{}"
if [[ "$RESPONSE_CODE" != "200" && "$RESPONSE_CODE" != "404" ]]; then
warn "terminate ${id} returned HTTP ${RESPONSE_CODE}"
fi
api_request "DELETE" "/agents/${id}"
if [[ "$RESPONSE_CODE" != "200" && "$RESPONSE_CODE" != "404" ]]; then
warn "delete ${id} returned HTTP ${RESPONSE_CODE}"
fi
done <<<"$ids"
}
cleanup_pending_join_requests() {
api_request "GET" "/companies/${COMPANY_ID}/join-requests?status=pending_approval"
if [[ "$RESPONSE_CODE" != "200" ]]; then
warn "join-request cleanup skipped (HTTP ${RESPONSE_CODE})"
return
fi
local ids
ids="$(jq -r '.[] | select((.adapterType == "openclaw" or .adapterType == "openclaw_gateway")) | .id' <<<"$RESPONSE_BODY")"
if [[ -z "$ids" ]]; then
return
fi
while IFS= read -r request_id; do
[[ -n "$request_id" ]] || continue
log "rejecting stale pending join request ${request_id}"
api_request "POST" "/companies/${COMPANY_ID}/join-requests/${request_id}/reject" "{}"
if [[ "$RESPONSE_CODE" != "200" && "$RESPONSE_CODE" != "404" && "$RESPONSE_CODE" != "409" ]]; then
warn "reject ${request_id} returned HTTP ${RESPONSE_CODE}"
fi
done <<<"$ids"
}
create_and_approve_gateway_join() {
local gateway_token="$1"
local invite_payload
invite_payload="$(jq -nc '{allowedJoinTypes:"agent"}')"
api_request "POST" "/companies/${COMPANY_ID}/invites" "$invite_payload"
assert_status "201"
local invite_token
invite_token="$(jq -r '.token // empty' <<<"$RESPONSE_BODY")"
INVITE_ID="$(jq -r '.id // empty' <<<"$RESPONSE_BODY")"
[[ -n "$invite_token" && -n "$INVITE_ID" ]] || fail "invite creation missing token/id"
local join_payload
join_payload="$(jq -nc \
--arg name "$OPENCLAW_AGENT_NAME" \
--arg url "$OPENCLAW_GATEWAY_URL" \
--arg token "$gateway_token" \
--arg paperclipApiUrl "$PAPERCLIP_API_URL_FOR_OPENCLAW" \
--argjson timeoutSec "$OPENCLAW_ADAPTER_TIMEOUT_SEC" \
--argjson waitTimeoutMs "$OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS" \
'{
requestType: "agent",
agentName: $name,
adapterType: "openclaw_gateway",
capabilities: "OpenClaw gateway smoke harness",
agentDefaultsPayload: {
url: $url,
headers: { "x-openclaw-token": $token },
role: "operator",
scopes: ["operator.admin"],
sessionKeyStrategy: "fixed",
sessionKey: "paperclip",
timeoutSec: $timeoutSec,
waitTimeoutMs: $waitTimeoutMs,
paperclipApiUrl: $paperclipApiUrl
}
}')"
api_request "POST" "/invites/${invite_token}/accept" "$join_payload"
assert_status "202"
JOIN_REQUEST_ID="$(jq -r '.id // empty' <<<"$RESPONSE_BODY")"
local claim_secret
claim_secret="$(jq -r '.claimSecret // empty' <<<"$RESPONSE_BODY")"
local claim_path
claim_path="$(jq -r '.claimApiKeyPath // empty' <<<"$RESPONSE_BODY")"
[[ -n "$JOIN_REQUEST_ID" && -n "$claim_secret" && -n "$claim_path" ]] || fail "join accept missing claim metadata"
log "approving join request ${JOIN_REQUEST_ID}"
api_request "POST" "/companies/${COMPANY_ID}/join-requests/${JOIN_REQUEST_ID}/approve" "{}"
assert_status "200"
AGENT_ID="$(jq -r '.createdAgentId // empty' <<<"$RESPONSE_BODY")"
[[ -n "$AGENT_ID" ]] || fail "join approval missing createdAgentId"
log "claiming one-time agent API key"
local claim_payload
claim_payload="$(jq -nc --arg secret "$claim_secret" '{claimSecret:$secret}')"
api_request "POST" "$claim_path" "$claim_payload"
assert_status "201"
AGENT_API_KEY="$(jq -r '.token // empty' <<<"$RESPONSE_BODY")"
[[ -n "$AGENT_API_KEY" ]] || fail "claim response missing token"
persist_claimed_key_artifacts "$RESPONSE_BODY"
inject_agent_api_key_payload_template
}
persist_claimed_key_artifacts() {
local claim_json="$1"
local workspace_dir="${OPENCLAW_CONFIG_DIR%/}/workspace"
local skill_dir="${OPENCLAW_CONFIG_DIR%/}/skills/paperclip"
local claimed_file="${workspace_dir}/paperclip-claimed-api-key.json"
local claimed_raw_file="${workspace_dir}/paperclip-claimed-api-key.raw.json"
mkdir -p "$workspace_dir" "$skill_dir"
local token
token="$(jq -r '.token // .apiKey // empty' <<<"$claim_json")"
[[ -n "$token" ]] || fail "claim response missing token/apiKey"
printf "%s\n" "$claim_json" > "$claimed_raw_file"
chmod 600 "$claimed_raw_file"
jq -nc --arg token "$token" '{ token: $token, apiKey: $token }' > "$claimed_file"
# Keep this readable for OpenClaw runtime users across sandbox/container contexts.
chmod 644 "$claimed_file"
local container
container="$(detect_openclaw_container || true)"
if [[ -n "$container" ]]; then
docker exec "$container" sh -lc "mkdir -p /home/node/.openclaw/workspace" >/dev/null 2>&1 || true
docker cp "$claimed_file" "${container}:/home/node/.openclaw/workspace/paperclip-claimed-api-key.json" >/dev/null 2>&1 || true
docker exec "$container" sh -lc "chmod 644 /home/node/.openclaw/workspace/paperclip-claimed-api-key.json" >/dev/null 2>&1 || true
fi
if [[ "$AUTO_INSTALL_SKILL" == "1" ]]; then
api_request "GET" "/skills/paperclip"
assert_status "200"
{
echo "PAPERCLIP_API_URL=${PAPERCLIP_API_URL_FOR_OPENCLAW}"
echo
printf "%s\n" "$RESPONSE_BODY"
} > "${skill_dir}/SKILL.md"
chmod 600 "${skill_dir}/SKILL.md"
fi
log "wrote claimed key artifacts to ${claimed_file} and ${claimed_raw_file}"
}
inject_agent_api_key_payload_template() {
[[ -n "$AGENT_ID" ]] || fail "inject_agent_api_key_payload_template requires AGENT_ID"
[[ -n "$AGENT_API_KEY" ]] || fail "inject_agent_api_key_payload_template requires AGENT_API_KEY"
api_request "GET" "/agents/${AGENT_ID}"
assert_status "200"
local base_message
base_message="Set PAPERCLIP_API_KEY=${AGENT_API_KEY} in your run context before running Paperclip heartbeat steps."
if [[ -n "$PAYLOAD_TEMPLATE_MESSAGE_APPEND" ]]; then
base_message="${base_message}\n\n${PAYLOAD_TEMPLATE_MESSAGE_APPEND}"
fi
local patch_payload
patch_payload="$(jq -c --arg message "$base_message" '
{adapterConfig: ((.adapterConfig // {}) + {
payloadTemplate: (((.adapterConfig // {}).payloadTemplate // {}) + {
message: $message
})
})}
' <<<"$RESPONSE_BODY")"
api_request "PATCH" "/agents/${AGENT_ID}" "$patch_payload"
assert_status "200"
}
validate_joined_gateway_agent() {
local expected_gateway_token="$1"
api_request "GET" "/agents/${AGENT_ID}"
assert_status "200"
local adapter_type gateway_url configured_token disable_device_auth device_key_len
adapter_type="$(jq -r '.adapterType // empty' <<<"$RESPONSE_BODY")"
gateway_url="$(jq -r '.adapterConfig.url // empty' <<<"$RESPONSE_BODY")"
configured_token="$(jq -r '.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // empty' <<<"$RESPONSE_BODY")"
disable_device_auth="$(jq -r 'if .adapterConfig.disableDeviceAuth == true then "true" else "false" end' <<<"$RESPONSE_BODY")"
device_key_len="$(jq -r '(.adapterConfig.devicePrivateKeyPem // "" | length)' <<<"$RESPONSE_BODY")"
[[ "$adapter_type" == "openclaw_gateway" ]] || fail "joined agent adapterType is '${adapter_type}', expected 'openclaw_gateway'"
[[ "$gateway_url" =~ ^wss?:// ]] || fail "joined agent gateway url is invalid: '${gateway_url}'"
[[ -n "$configured_token" ]] || fail "joined agent missing adapterConfig.headers.x-openclaw-token"
if (( ${#configured_token} < 16 )); then
fail "joined agent gateway token looks too short (${#configured_token} chars)"
fi
local expected_hash configured_hash
expected_hash="$(hash_prefix "$expected_gateway_token")"
configured_hash="$(hash_prefix "$configured_token")"
if [[ "$expected_hash" != "$configured_hash" ]]; then
fail "joined agent gateway token hash mismatch (expected ${expected_hash}, got ${configured_hash})"
fi
[[ "$disable_device_auth" == "false" ]] || fail "joined agent has disableDeviceAuth=true; smoke requires device auth enabled with persistent key"
if (( device_key_len < 32 )); then
fail "joined agent missing persistent devicePrivateKeyPem (length=${device_key_len})"
fi
log "validated joined gateway agent config (token sha256 prefix ${configured_hash})"
}
run_log_contains_pairing_required() {
local run_id="$1"
api_request "GET" "/heartbeat-runs/${run_id}/log?limitBytes=262144"
if [[ "$RESPONSE_CODE" != "200" ]]; then
return 1
fi
local content
content="$(jq -r '.content // ""' <<<"$RESPONSE_BODY")"
grep -qi "pairing required" <<<"$content"
}
approve_latest_pairing_request() {
local gateway_token="$1"
local container
container="$(detect_openclaw_container || true)"
[[ -n "$container" ]] || return 1
log "approving latest gateway pairing request in ${container}"
local output
if output="$(docker exec \
-e OPENCLAW_GATEWAY_URL="$OPENCLAW_GATEWAY_URL" \
-e OPENCLAW_GATEWAY_TOKEN="$gateway_token" \
"$container" \
sh -lc 'openclaw devices approve --latest --json --url "$OPENCLAW_GATEWAY_URL" --token "$OPENCLAW_GATEWAY_TOKEN"' 2>&1)"; then
log "pairing approval response: $(printf "%s" "$output" | tr '\n' ' ' | cut -c1-400)"
return 0
fi
warn "pairing auto-approve failed: $(printf "%s" "$output" | tr '\n' ' ' | cut -c1-400)"
return 1
}
trigger_wakeup() {
local reason="$1"
local issue_id="${2:-}"
local payload
if [[ -n "$issue_id" ]]; then
payload="$(jq -nc --arg issueId "$issue_id" --arg reason "$reason" '{source:"on_demand",triggerDetail:"manual",reason:$reason,payload:{issueId:$issueId,taskId:$issueId}}')"
else
payload="$(jq -nc --arg reason "$reason" '{source:"on_demand",triggerDetail:"manual",reason:$reason}')"
fi
api_request "POST" "/agents/${AGENT_ID}/wakeup" "$payload"
if [[ "$RESPONSE_CODE" != "202" ]]; then
echo "$RESPONSE_BODY" >&2
fail "wakeup failed (HTTP ${RESPONSE_CODE})"
fi
RUN_ID="$(jq -r '.id // empty' <<<"$RESPONSE_BODY")"
if [[ -z "$RUN_ID" ]]; then
warn "wakeup response did not include run id; body: ${RESPONSE_BODY}"
fi
}
get_run_status() {
local run_id="$1"
api_request "GET" "/companies/${COMPANY_ID}/heartbeat-runs?agentId=${AGENT_ID}&limit=200"
if [[ "$RESPONSE_CODE" != "200" ]]; then
echo ""
return 0
fi
jq -r --arg runId "$run_id" '.[] | select(.id == $runId) | .status' <<<"$RESPONSE_BODY" | head -n1
}
wait_for_run_terminal() {
local run_id="$1"
local timeout_sec="$2"
local started now status
[[ -n "$run_id" ]] || fail "wait_for_run_terminal requires run id"
started="$(date +%s)"
while true; do
status="$(get_run_status "$run_id")"
if [[ "$status" == "succeeded" || "$status" == "failed" || "$status" == "timed_out" || "$status" == "cancelled" ]]; then
if [[ "$status" != "succeeded" ]]; then
capture_run_diagnostics "$run_id" "run-nonsuccess"
capture_openclaw_container_logs
fi
echo "$status"
return 0
fi
now="$(date +%s)"
if (( now - started >= timeout_sec )); then
capture_run_diagnostics "$run_id" "run-timeout"
capture_openclaw_container_logs
echo "timeout"
return 0
fi
sleep 2
done
}
get_issue_status() {
local issue_id="$1"
api_request "GET" "/issues/${issue_id}"
if [[ "$RESPONSE_CODE" != "200" ]]; then
echo ""
return 0
fi
jq -r '.status // empty' <<<"$RESPONSE_BODY"
}
wait_for_issue_terminal() {
local issue_id="$1"
local timeout_sec="$2"
local started now status
started="$(date +%s)"
while true; do
status="$(get_issue_status "$issue_id")"
if [[ "$status" == "done" || "$status" == "blocked" || "$status" == "cancelled" ]]; then
echo "$status"
return 0
fi
now="$(date +%s)"
if (( now - started >= timeout_sec )); then
echo "timeout"
return 0
fi
sleep 3
done
}
issue_comments_contain() {
local issue_id="$1"
local marker="$2"
api_request "GET" "/issues/${issue_id}/comments"
if [[ "$RESPONSE_CODE" != "200" ]]; then
echo "false"
return 0
fi
jq -r --arg marker "$marker" '[.[] | (.body // "") | contains($marker)] | any' <<<"$RESPONSE_BODY"
}
create_issue_for_case() {
local title="$1"
local description="$2"
local priority="${3:-high}"
local payload
payload="$(jq -nc \
--arg title "$title" \
--arg description "$description" \
--arg assignee "$AGENT_ID" \
--arg priority "$priority" \
'{title:$title,description:$description,status:"todo",priority:$priority,assigneeAgentId:$assignee}')"
api_request "POST" "/companies/${COMPANY_ID}/issues" "$payload"
assert_status "201"
local issue_id issue_identifier
issue_id="$(jq -r '.id // empty' <<<"$RESPONSE_BODY")"
issue_identifier="$(jq -r '.identifier // empty' <<<"$RESPONSE_BODY")"
[[ -n "$issue_id" ]] || fail "issue create missing id"
echo "${issue_id}|${issue_identifier}"
}
patch_agent_session_strategy_run() {
api_request "GET" "/agents/${AGENT_ID}"
assert_status "200"
local patch_payload
patch_payload="$(jq -c '{adapterConfig: ((.adapterConfig // {}) + {sessionKeyStrategy:"run"})}' <<<"$RESPONSE_BODY")"
api_request "PATCH" "/agents/${AGENT_ID}" "$patch_payload"
assert_status "200"
}
find_issue_by_query() {
local query="$1"
local encoded_query
encoded_query="$(jq -rn --arg q "$query" '$q|@uri')"
api_request "GET" "/companies/${COMPANY_ID}/issues?q=${encoded_query}"
if [[ "$RESPONSE_CODE" != "200" ]]; then
echo ""
return 0
fi
jq -r '.[] | .id' <<<"$RESPONSE_BODY" | head -n1
}
run_case_a() {
local marker="OPENCLAW_CASE_A_OK_$(date +%s)"
local description
description="Case A validation.\n\n1) Read this issue.\n2) Post a comment containing exactly: ${marker}\n3) Mark this issue done."
local created
created="$(create_issue_for_case "[OpenClaw Gateway Smoke] Case A" "$description")"
CASE_A_ISSUE_ID="${created%%|*}"
local case_identifier="${created##*|}"
log "case A issue ${CASE_A_ISSUE_ID} (${case_identifier})"
trigger_wakeup "openclaw_gateway_smoke_case_a" "$CASE_A_ISSUE_ID"
local run_status issue_status marker_found
if [[ -n "$RUN_ID" ]]; then
run_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")"
log "case A run ${RUN_ID} status=${run_status}"
else
run_status="unknown"
fi
issue_status="$(wait_for_issue_terminal "$CASE_A_ISSUE_ID" "$CASE_TIMEOUT_SEC")"
marker_found="$(issue_comments_contain "$CASE_A_ISSUE_ID" "$marker")"
log "case A issue_status=${issue_status} marker_found=${marker_found}"
if [[ "$issue_status" != "done" || "$marker_found" != "true" ]]; then
capture_issue_diagnostics "$CASE_A_ISSUE_ID" "case-a"
if [[ -n "$RUN_ID" ]]; then
capture_run_diagnostics "$RUN_ID" "case-a"
fi
capture_openclaw_container_logs
fi
if [[ "$STRICT_CASES" == "1" ]]; then
[[ "$run_status" == "succeeded" ]] || fail "case A run did not succeed"
[[ "$issue_status" == "done" ]] || fail "case A issue did not reach done"
[[ "$marker_found" == "true" ]] || fail "case A marker not found in comments"
fi
}
run_case_b() {
local marker="OPENCLAW_CASE_B_OK_$(date +%s)"
local message_text="${marker}"
local description
description="Case B validation.\n\nUse the message tool to send this exact text to the user's main chat session in webchat:\n${message_text}\n\nAfter sending, post a Paperclip issue comment containing exactly: ${marker}\nThen mark this issue done."
local created
created="$(create_issue_for_case "[OpenClaw Gateway Smoke] Case B" "$description")"
CASE_B_ISSUE_ID="${created%%|*}"
local case_identifier="${created##*|}"
log "case B issue ${CASE_B_ISSUE_ID} (${case_identifier})"
trigger_wakeup "openclaw_gateway_smoke_case_b" "$CASE_B_ISSUE_ID"
local run_status issue_status marker_found
if [[ -n "$RUN_ID" ]]; then
run_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")"
log "case B run ${RUN_ID} status=${run_status}"
else
run_status="unknown"
fi
issue_status="$(wait_for_issue_terminal "$CASE_B_ISSUE_ID" "$CASE_TIMEOUT_SEC")"
marker_found="$(issue_comments_contain "$CASE_B_ISSUE_ID" "$marker")"
log "case B issue_status=${issue_status} marker_found=${marker_found}"
if [[ "$issue_status" != "done" || "$marker_found" != "true" ]]; then
capture_issue_diagnostics "$CASE_B_ISSUE_ID" "case-b"
if [[ -n "$RUN_ID" ]]; then
capture_run_diagnostics "$RUN_ID" "case-b"
fi
capture_openclaw_container_logs
fi
warn "case B requires manual UX confirmation in OpenClaw main webchat: message '${message_text}' appears in main chat"
if [[ "$STRICT_CASES" == "1" ]]; then
[[ "$run_status" == "succeeded" ]] || fail "case B run did not succeed"
[[ "$issue_status" == "done" ]] || fail "case B issue did not reach done"
[[ "$marker_found" == "true" ]] || fail "case B marker not found in comments"
fi
}
run_case_c() {
patch_agent_session_strategy_run
local marker="OPENCLAW_CASE_C_CREATED_$(date +%s)"
local ack_marker="OPENCLAW_CASE_C_ACK_$(date +%s)"
local original_issue_reference="the original case issue you are currently reading"
local description
description="Case C validation.\n\nTreat this run as a fresh/new session.\nCreate a NEW Paperclip issue in this same company with title exactly:\n${marker}\nUse description: 'created by case C smoke'.\n\nThen post a comment on ${original_issue_reference} containing exactly: ${ack_marker}\nDo NOT post the ACK comment on the newly created issue.\nThen mark the original case issue done."
local created
created="$(create_issue_for_case "[OpenClaw Gateway Smoke] Case C" "$description")"
CASE_C_ISSUE_ID="${created%%|*}"
local case_identifier="${created##*|}"
log "case C issue ${CASE_C_ISSUE_ID} (${case_identifier})"
trigger_wakeup "openclaw_gateway_smoke_case_c" "$CASE_C_ISSUE_ID"
local run_status issue_status marker_found created_issue
if [[ -n "$RUN_ID" ]]; then
run_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")"
log "case C run ${RUN_ID} status=${run_status}"
else
run_status="unknown"
fi
issue_status="$(wait_for_issue_terminal "$CASE_C_ISSUE_ID" "$CASE_TIMEOUT_SEC")"
marker_found="$(issue_comments_contain "$CASE_C_ISSUE_ID" "$ack_marker")"
created_issue="$(find_issue_by_query "$marker")"
if [[ "$created_issue" == "$CASE_C_ISSUE_ID" ]]; then
created_issue=""
fi
CASE_C_CREATED_ISSUE_ID="$created_issue"
log "case C issue_status=${issue_status} marker_found=${marker_found} created_issue_id=${CASE_C_CREATED_ISSUE_ID:-none}"
if [[ "$issue_status" != "done" || "$marker_found" != "true" || -z "$CASE_C_CREATED_ISSUE_ID" ]]; then
capture_issue_diagnostics "$CASE_C_ISSUE_ID" "case-c"
if [[ -n "$CASE_C_CREATED_ISSUE_ID" ]]; then
capture_issue_diagnostics "$CASE_C_CREATED_ISSUE_ID" "case-c-created"
fi
if [[ -n "$RUN_ID" ]]; then
capture_run_diagnostics "$RUN_ID" "case-c"
fi
capture_openclaw_container_logs
fi
if [[ "$STRICT_CASES" == "1" ]]; then
[[ "$run_status" == "succeeded" ]] || fail "case C run did not succeed"
[[ "$issue_status" == "done" ]] || fail "case C issue did not reach done"
[[ "$marker_found" == "true" ]] || fail "case C ack marker not found in comments"
[[ -n "$CASE_C_CREATED_ISSUE_ID" ]] || fail "case C did not create the expected new issue"
fi
}
main() {
log "starting OpenClaw gateway E2E smoke"
mkdir -p "$OPENCLAW_DIAG_DIR"
log "diagnostics dir: ${OPENCLAW_DIAG_DIR}"
wait_http_ready "${PAPERCLIP_API_URL%/}/api/health" 15 || fail "Paperclip API health endpoint not reachable"
api_request "GET" "/health"
assert_status "200"
log "paperclip health deploymentMode=$(jq -r '.deploymentMode // "unknown"' <<<"$RESPONSE_BODY") exposure=$(jq -r '.deploymentExposure // "unknown"' <<<"$RESPONSE_BODY")"
require_board_auth
resolve_company_id
cleanup_openclaw_agents
cleanup_pending_join_requests
maybe_cleanup_openclaw_docker
start_openclaw_docker
wait_http_ready "http://127.0.0.1:18789/" "$OPENCLAW_WAIT_SECONDS" || fail "OpenClaw HTTP health not reachable"
local gateway_token
gateway_token="$(detect_gateway_token || true)"
[[ -n "$gateway_token" ]] || fail "could not resolve OpenClaw gateway token"
log "resolved gateway token (sha256 prefix $(hash_prefix "$gateway_token"))"
log "probing gateway websocket challenge at ${OPENCLAW_GATEWAY_URL}"
probe_gateway_ws "$OPENCLAW_GATEWAY_URL" "$gateway_token"
create_and_approve_gateway_join "$gateway_token"
log "joined/approved agent ${AGENT_ID} invite=${INVITE_ID} joinRequest=${JOIN_REQUEST_ID}"
validate_joined_gateway_agent "$gateway_token"
local connect_status="unknown"
local connect_attempt
for connect_attempt in 1 2; do
trigger_wakeup "openclaw_gateway_smoke_connectivity_attempt_${connect_attempt}"
if [[ -z "$RUN_ID" ]]; then
connect_status="unknown"
break
fi
connect_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")"
if [[ "$connect_status" == "succeeded" ]]; then
log "connectivity wake run ${RUN_ID} succeeded (attempt=${connect_attempt})"
break
fi
if [[ "$PAIRING_AUTO_APPROVE" == "1" && "$connect_attempt" -eq 1 ]] && run_log_contains_pairing_required "$RUN_ID"; then
log "connectivity run hit pairing gate; attempting one-time pairing approval"
approve_latest_pairing_request "$gateway_token" || fail "pairing approval failed after pairing-required run ${RUN_ID}"
sleep 2
continue
fi
fail "connectivity wake run failed: ${connect_status} (attempt=${connect_attempt}, runId=${RUN_ID})"
done
[[ "$connect_status" == "succeeded" ]] || fail "connectivity wake run did not succeed after retries"
run_case_a
run_case_b
run_case_c
log "success"
log "companyId=${COMPANY_ID}"
log "agentId=${AGENT_ID}"
log "inviteId=${INVITE_ID}"
log "joinRequestId=${JOIN_REQUEST_ID}"
log "caseA_issueId=${CASE_A_ISSUE_ID}"
log "caseB_issueId=${CASE_B_ISSUE_ID}"
log "caseC_issueId=${CASE_C_ISSUE_ID}"
log "caseC_createdIssueId=${CASE_C_CREATED_ISSUE_ID:-none}"
log "agentApiKeyPrefix=${AGENT_API_KEY:0:12}..."
}
main "$@"
@@ -0,0 +1,295 @@
#!/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 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
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
echo "[openclaw-sse-standalone] $*"
}
fail() {
echo "[openclaw-sse-standalone] ERROR: $*" >&2
exit 1
}
require_cmd() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd"
}
require_cmd curl
require_cmd jq
require_cmd grep
OPENCLAW_URL="${OPENCLAW_URL:-}"
OPENCLAW_METHOD="${OPENCLAW_METHOD:-POST}"
OPENCLAW_AUTH_HEADER="${OPENCLAW_AUTH_HEADER:-}"
OPENCLAW_TIMEOUT_SEC="${OPENCLAW_TIMEOUT_SEC:-180}"
OPENCLAW_MODEL="${OPENCLAW_MODEL:-openclaw}"
OPENCLAW_USER="${OPENCLAW_USER:-paperclip-smoke}"
PAPERCLIP_RUN_ID="${PAPERCLIP_RUN_ID:-smoke-run-$(date +%s)}"
PAPERCLIP_AGENT_ID="${PAPERCLIP_AGENT_ID:-openclaw-smoke-agent}"
PAPERCLIP_COMPANY_ID="${PAPERCLIP_COMPANY_ID:-openclaw-smoke-company}"
PAPERCLIP_API_URL="${PAPERCLIP_API_URL:-http://localhost:3100}"
PAPERCLIP_TASK_ID="${PAPERCLIP_TASK_ID:-openclaw-smoke-task}"
PAPERCLIP_WAKE_REASON="${PAPERCLIP_WAKE_REASON:-openclaw_smoke_test}"
PAPERCLIP_WAKE_COMMENT_ID="${PAPERCLIP_WAKE_COMMENT_ID:-}"
PAPERCLIP_APPROVAL_ID="${PAPERCLIP_APPROVAL_ID:-}"
PAPERCLIP_APPROVAL_STATUS="${PAPERCLIP_APPROVAL_STATUS:-}"
PAPERCLIP_LINKED_ISSUE_IDS="${PAPERCLIP_LINKED_ISSUE_IDS:-}"
OPENCLAW_TEXT_PREFIX="${OPENCLAW_TEXT_PREFIX:-Standalone OpenClaw SSE smoke test.}"
[[ -n "$OPENCLAW_URL" ]] || fail "OPENCLAW_URL is required"
read -r -d '' TEXT_BODY <<EOF || true
${OPENCLAW_TEXT_PREFIX}
PAPERCLIP_RUN_ID=${PAPERCLIP_RUN_ID}
PAPERCLIP_AGENT_ID=${PAPERCLIP_AGENT_ID}
PAPERCLIP_COMPANY_ID=${PAPERCLIP_COMPANY_ID}
PAPERCLIP_API_URL=${PAPERCLIP_API_URL}
PAPERCLIP_TASK_ID=${PAPERCLIP_TASK_ID}
PAPERCLIP_WAKE_REASON=${PAPERCLIP_WAKE_REASON}
PAPERCLIP_WAKE_COMMENT_ID=${PAPERCLIP_WAKE_COMMENT_ID}
PAPERCLIP_APPROVAL_ID=${PAPERCLIP_APPROVAL_ID}
PAPERCLIP_APPROVAL_STATUS=${PAPERCLIP_APPROVAL_STATUS}
PAPERCLIP_LINKED_ISSUE_IDS=${PAPERCLIP_LINKED_ISSUE_IDS}
Run your Paperclip heartbeat procedure now.
EOF
PAYLOAD="$(jq -nc \
--arg text "$TEXT_BODY" \
--arg model "$OPENCLAW_MODEL" \
--arg user "$OPENCLAW_USER" \
--arg runId "$PAPERCLIP_RUN_ID" \
--arg agentId "$PAPERCLIP_AGENT_ID" \
--arg companyId "$PAPERCLIP_COMPANY_ID" \
--arg apiUrl "$PAPERCLIP_API_URL" \
--arg taskId "$PAPERCLIP_TASK_ID" \
--arg wakeReason "$PAPERCLIP_WAKE_REASON" \
--arg wakeCommentId "$PAPERCLIP_WAKE_COMMENT_ID" \
--arg approvalId "$PAPERCLIP_APPROVAL_ID" \
--arg approvalStatus "$PAPERCLIP_APPROVAL_STATUS" \
--arg linkedIssueIds "$PAPERCLIP_LINKED_ISSUE_IDS" \
'{
model: $model,
user: $user,
input: $text,
stream: true,
metadata: {
PAPERCLIP_RUN_ID: $runId,
PAPERCLIP_AGENT_ID: $agentId,
PAPERCLIP_COMPANY_ID: $companyId,
PAPERCLIP_API_URL: $apiUrl,
PAPERCLIP_TASK_ID: $taskId,
PAPERCLIP_WAKE_REASON: $wakeReason,
PAPERCLIP_WAKE_COMMENT_ID: $wakeCommentId,
PAPERCLIP_APPROVAL_ID: $approvalId,
PAPERCLIP_APPROVAL_STATUS: $approvalStatus,
PAPERCLIP_LINKED_ISSUE_IDS: $linkedIssueIds,
paperclip_session_key: ("paperclip:run:" + $runId)
}
}')"
headers_file="$(mktemp)"
body_file="$(mktemp)"
cleanup() {
rm -f "$headers_file" "$body_file"
}
trap cleanup EXIT
args=(
-sS
-N
--max-time "$OPENCLAW_TIMEOUT_SEC"
-X "$OPENCLAW_METHOD"
-H "content-type: application/json"
-H "accept: text/event-stream"
-H "x-openclaw-session-key: paperclip:run:${PAPERCLIP_RUN_ID}"
-D "$headers_file"
-o "$body_file"
--data "$PAYLOAD"
"$OPENCLAW_URL"
)
if [[ -n "$OPENCLAW_AUTH_HEADER" ]]; then
args=(-H "Authorization: $OPENCLAW_AUTH_HEADER" "${args[@]}")
fi
log "posting SSE wake payload to ${OPENCLAW_URL}"
http_code="$(curl "${args[@]}" -w "%{http_code}")"
log "http status: ${http_code}"
if [[ ! "$http_code" =~ ^2 ]]; then
tail -n 80 "$body_file" >&2 || true
fail "non-success HTTP status: ${http_code}"
fi
if ! grep -Eqi '^content-type:.*text/event-stream' "$headers_file"; then
tail -n 40 "$body_file" >&2 || true
fail "response content-type was not text/event-stream"
fi
if grep -Eqi 'event:\s*(error|failed|cancel)|"status":"(failed|cancelled|error)"|"type":"[^"]*(failed|cancelled|error)"' "$body_file"; then
tail -n 120 "$body_file" >&2 || true
fail "stream reported a failure event"
fi
if ! grep -Eqi 'event:\s*(done|completed|response\.completed)|\[DONE\]|"status":"(completed|succeeded|done)"|"type":"response\.completed"' "$body_file"; then
tail -n 120 "$body_file" >&2 || true
fail "stream ended without a terminal completion marker"
fi
event_count="$(grep -Ec '^event:' "$body_file" || true)"
log "stream completed successfully (events=${event_count})"
echo
tail -n 40 "$body_file"