2026-03-07 08:59:29 -06:00
#!/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 } "
2026-03-07 09:22:40 -06:00
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 } "
2026-03-07 17:05:36 -06:00
PAIRING_AUTO_APPROVE = " ${ PAIRING_AUTO_APPROVE :- 1 } "
2026-03-07 09:22:40 -06:00
PAYLOAD_TEMPLATE_MESSAGE_APPEND = " ${ PAYLOAD_TEMPLATE_MESSAGE_APPEND :- } "
2026-03-07 08:59:29 -06:00
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 "
}
2026-03-07 09:22:40 -06:00
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
}
2026-03-07 08:59:29 -06:00
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 " \
2026-03-07 09:22:40 -06:00
--argjson timeoutSec " $OPENCLAW_ADAPTER_TIMEOUT_SEC " \
--argjson waitTimeoutMs " $OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS " \
2026-03-07 08:59:29 -06:00
' {
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" ,
2026-03-07 09:22:40 -06:00
timeoutSec: $timeoutSec ,
waitTimeoutMs: $waitTimeoutMs ,
2026-03-07 08:59:29 -06:00
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 "
2026-03-07 09:22:40 -06:00
local claimed_raw_file = " ${ workspace_dir } /paperclip-claimed-api-key.raw.json "
2026-03-07 08:59:29 -06:00
mkdir -p " $workspace_dir " " $skill_dir "
2026-03-07 09:22:40 -06:00
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
2026-03-07 08:59:29 -06:00
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
2026-03-07 09:22:40 -06:00
log " wrote claimed key artifacts to ${ claimed_file } and ${ claimed_raw_file } "
2026-03-07 08:59:29 -06:00
}
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"
2026-03-07 09:22:40 -06:00
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
2026-03-07 08:59:29 -06:00
local patch_payload
2026-03-07 09:22:40 -06:00
patch_payload = " $( jq -c --arg message " $base_message " '
2026-03-07 08:59:29 -06:00
{ adapterConfig: ( ( .adapterConfig // { } ) + {
payloadTemplate: ( ( ( .adapterConfig // { } ) .payloadTemplate // { } ) + {
2026-03-07 09:22:40 -06:00
message: $message
2026-03-07 08:59:29 -06:00
} )
} ) }
' <<< " $RESPONSE_BODY " ) "
api_request "PATCH" " /agents/ ${ AGENT_ID } " " $patch_payload "
assert_status "200"
}
2026-03-07 16:01:19 -06:00
validate_joined_gateway_agent( ) {
local expected_gateway_token = " $1 "
api_request "GET" " /agents/ ${ AGENT_ID } "
assert_status "200"
2026-03-07 17:05:36 -06:00
local adapter_type gateway_url configured_token disable_device_auth device_key_len
2026-03-07 16:01:19 -06:00
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 " ) "
2026-03-07 17:05:36 -06:00
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 " ) "
2026-03-07 16:01:19 -06:00
[ [ " $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
2026-03-07 17:05:36 -06:00
[ [ " $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
2026-03-07 16:01:19 -06:00
log " validated joined gateway agent config (token sha256 prefix ${ configured_hash } ) "
}
2026-03-07 17:05:36 -06:00
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
}
2026-03-07 08:59:29 -06:00
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
2026-03-07 09:22:40 -06:00
if [ [ " $status " != "succeeded" ] ] ; then
capture_run_diagnostics " $run_id " "run-nonsuccess"
capture_openclaw_container_logs
fi
2026-03-07 08:59:29 -06:00
echo " $status "
return 0
fi
now = " $( date +%s) "
if ( ( now - started >= timeout_sec ) ) ; then
2026-03-07 09:22:40 -06:00
capture_run_diagnostics " $run_id " "run-timeout"
capture_openclaw_container_logs
2026-03-07 08:59:29 -06:00
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 } "
2026-03-07 09:22:40 -06:00
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
2026-03-07 08:59:29 -06:00
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 } "
2026-03-07 09:22:40 -06:00
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
2026-03-07 08:59:29 -06:00
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) "
2026-03-07 16:16:53 -06:00
local original_issue_reference = "the original case issue you are currently reading"
2026-03-07 08:59:29 -06:00
local description
2026-03-07 16:16:53 -06:00
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. "
2026-03-07 08:59:29 -06:00
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 } "
2026-03-07 09:22:40 -06:00
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
2026-03-07 08:59:29 -06:00
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"
2026-03-07 09:22:40 -06:00
mkdir -p " $OPENCLAW_DIAG_DIR "
log " diagnostics dir: ${ OPENCLAW_DIAG_DIR } "
2026-03-07 08:59:29 -06:00
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 } "
2026-03-07 16:01:19 -06:00
validate_joined_gateway_agent " $gateway_token "
2026-03-07 08:59:29 -06:00
2026-03-07 17:05:36 -06:00
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
2026-03-07 08:59:29 -06:00
connect_status = " $( wait_for_run_terminal " $RUN_ID " " $RUN_TIMEOUT_SEC " ) "
2026-03-07 17:05:36 -06:00
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"
2026-03-07 08:59:29 -06:00
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 " $@ "