Files
wx-win-agent/docs/validation/raw/collect-live-operations.sh

168 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
: "${CONTROL_PLANE_BIN:?set CONTROL_PLANE_BIN to the committed control-plane binary}"
: "${TRAY_BINARY:?set TRAY_BINARY to the committed self-contained Tray executable}"
: "${WINDOWS_HOST:?set WINDOWS_HOST, e.g. rogee@10.1.1.101}"
: "${WINDOWS_DIR:?set WINDOWS_DIR, e.g. C:/Users/Rogee/wx-agent01}"
: "${ACCOUNT_ID:?set the verified test account id}"
: "${NODE_TOKEN:?set the test node token}"
: "${WEB_PASSWORD:?set the test Web password}"
root=$(mktemp -d)
live="$root/live"
mkdir -p "$live/backups"
server_pid=""
trace_file=${TRACE_FILE:-}
if [[ -n "$trace_file" ]]; then : > "$trace_file"; fi
cleanup() {
set +e
if [[ -n "$server_pid" ]]; then kill "$server_pid" 2>/dev/null || true; fi
ps_script=$(cat <<PS
\$dir = "$WINDOWS_DIR"
Get-Process -Name WxAgent.Tray -ErrorAction SilentlyContinue | Stop-Process -Force
Get-ScheduledTask -TaskName "WxAgent-P4-Audit-*" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:\$false
\$cfg = Get-Content -Raw "\$dir/service.json" | ConvertFrom-Json
\$cfg | Add-Member -MemberType NoteProperty -Name enableDataSync -Value \$false -Force
\$cfg | ConvertTo-Json -Depth 30 | Set-Content -Encoding UTF8 "\$dir/service.json"
Remove-Item "\$dir/data/remote-data-sync-state.json","\$dir/data/remote-data-queue.json" -Force -ErrorAction SilentlyContinue
PS
)
run_ps "$ps_script" >/dev/null 2>&1 || true
rm -rf "$root"
}
trap cleanup EXIT
run_ps() {
local script=$1 encoded
encoded=$(printf '%s' "$script" | iconv -t UTF-16LE | base64 -w0)
if [[ -n "$trace_file" ]]; then
printf '\n--- POWERSHELL COMMAND ---\n%s\n--- RAW OUTPUT ---\n' "$script" >> "$trace_file"
ssh -o BatchMode=yes "$WINDOWS_HOST" "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded" 2>&1 | tee -a "$trace_file"
else
ssh -o BatchMode=yes "$WINDOWS_HOST" "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded"
fi
}
json_from_ps() {
run_ps "$1" 2>/dev/null | tr -d '\r' | awk '/^\{.*\}$/ {line=$0} END {if (line != "") print line}'
}
start_tray() {
local task="WxAgent-P4-Audit-Tray"
local script
script=$(cat <<PS
\$dir = "$WINDOWS_DIR"
\$task = "$task"
Unregister-ScheduledTask -TaskName \$task -Confirm:\$false -ErrorAction SilentlyContinue | Out-Null
\$action = New-ScheduledTaskAction -Execute "\$dir/WxAgent.Tray.exe" -WorkingDirectory \$dir
\$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1)
\$principal = New-ScheduledTaskPrincipal -UserId "DESKTOP-EGI7QCK\\Rogee" -LogonType Interactive -RunLevel Highest
Register-ScheduledTask -TaskName \$task -Action \$action -Trigger \$trigger -Principal \$principal -Force | Out-Null
Start-ScheduledTask -TaskName \$task
Start-Sleep -Seconds 35
\$p = Get-Process -Name WxAgent.Tray | Select-Object -First 1
[ordered]@{pid=\$p.Id;session=\$p.SessionId}|ConvertTo-Json -Compress
PS
)
json_from_ps "$script"
}
queue_json() {
json_from_ps "\$q=\"$WINDOWS_DIR/data/remote-data-queue.json\"; \$pending=0; \$bytes=0; if(Test-Path \$q){\$bytes=(Get-Item \$q).Length; try{\$pending=@((Get-Content -Raw \$q|ConvertFrom-Json).items).Count}catch{}}; \$queueSha=if(Test-Path \$q){(Get-FileHash \$q -Algorithm SHA256).Hash}else{\"\"}; \$state=\"$WINDOWS_DIR/data/remote-data-sync-state.json\"; \$stateSha=if(Test-Path \$state){(Get-FileHash \$state -Algorithm SHA256).Hash}else{\"\"}; [ordered]@{queue_bytes=\$bytes;queue_pending=\$pending;queue_sha256=\$queueSha;state_sha256=\$stateSha}|ConvertTo-Json -Compress"
}
start_server() {
WXAGENT_CONTROL_PLANE_ADDR=0.0.0.0:8090 \
WXAGENT_CONTROL_PLANE_DATA="$live/control-plane-data.json" \
WXAGENT_CONTROL_PLANE_BACKUP_DIR="$live/backups" \
WXAGENT_CONTROL_PLANE_BACKUP_INTERVAL=10s \
WXAGENT_CONTROL_PLANE_BACKUP_COUNT=3 \
WXAGENT_NODE_ID=local-node \
WXAGENT_NODE_TOKEN="$NODE_TOKEN" \
WXAGENT_WEB_USER=admin \
WXAGENT_WEB_PASSWORD="$WEB_PASSWORD" \
"$CONTROL_PLANE_BIN" >"$live/server.log" 2>&1 &
server_pid=$!
if [[ -n "$trace_file" ]]; then printf 'LOCAL_START_SERVER pid=%s sha256=%s\n' "$server_pid" "$(sha256sum "$CONTROL_PLANE_BIN" | awk '{print $1}')" >> "$trace_file"; fi
}
stop_server() { if [[ -n "$server_pid" ]]; then if [[ -n "$trace_file" ]]; then printf 'LOCAL_STOP_SERVER pid=%s\n' "$server_pid" >> "$trace_file"; fi; kill "$server_pid" 2>/dev/null || true; fi; server_pid=""; }
sync_json() { curl -fsS -H "Authorization: Bearer $NODE_TOKEN" "http://127.0.0.1:8090/v1/nodes/local-node/data/accounts/$ACCOUNT_ID/sync-status"; }
sync_summary() { sync_json | jq -c '{state,confirmed_sequence,last_success_at}'; }
shard_json() {
python3 - "$live/control-plane-data.json.accounts" <<'PY'
import json, pathlib, sqlite3, sys
base=pathlib.Path(sys.argv[1])
p=next(base.glob('accounts/*/data.sqlite'))
c=sqlite3.connect(p)
rows=c.execute('select chat_id,title,source,directory_state from conversations').fetchall()
print(json.dumps({'messages':c.execute('select count(*) from messages').fetchone()[0],'conversations':len(rows),'batches':c.execute('select count(*) from ingest_batches').fetchone()[0],'coverage':c.execute('select coverage_state from sync_state').fetchone()[0],'integrity':c.execute('pragma integrity_check').fetchone()[0],'sources':sorted({r[2] for r in rows}),'title_equals_chat_id':sum(r[0]==r[1] for r in rows)}))
c.close()
PY
}
run_ps "\$dir=\"$WINDOWS_DIR\"; Get-Process -Name WxAgent.Tray -ErrorAction SilentlyContinue | Stop-Process -Force; Get-ScheduledTask -TaskName \"WxAgent-P4-Audit-*\" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:\$false; Remove-Item \"\$dir/data/remote-data-sync-state.json\",\"\$dir/data/remote-data-queue.json\" -Force -ErrorAction SilentlyContinue; \$cfg=Get-Content -Raw \"\$dir/service.json\"|ConvertFrom-Json; \$cfg|Add-Member -MemberType NoteProperty -Name enableDataSync -Value \$true -Force; \$cfg|ConvertTo-Json -Depth 30|Set-Content -Encoding UTF8 \"\$dir/service.json\"" >/dev/null
scp -q "$TRAY_BINARY" "$WINDOWS_HOST:$WINDOWS_DIR/WxAgent.Tray.exe"
remote_hash=$(run_ps "(Get-FileHash \"$WINDOWS_DIR/WxAgent.Tray.exe\" -Algorithm SHA256).Hash" 2>/dev/null | tr -d '\r' | awk '/^[0-9A-Fa-f]{64}$/ {line=$0} END {print line}')
echo "{\"event\":\"deployment\",\"source_commit\":\"$(git rev-parse HEAD)\",\"tray_sha256\":\"$remote_hash\"}"
# Collect while the control plane is deliberately absent.
offline_process=$(start_tray)
sleep 5
queue=$(queue_json)
echo "{\"event\":\"offline-queue\",\"process\":$offline_process,\"queue\":$queue}"
start_server
for _ in $(seq 1 30); do
sleep 3
queue=$(queue_json)
status=$(jq -r '.state // .status // .node_status // "missing"' <(sync_json 2>/dev/null || echo '{}'))
[[ "$(jq -r .queue_pending <<<"$queue")" == 0 && "$status" == "complete" ]] && break
done
echo "{\"event\":\"replay\",\"queue\":$queue,\"sync\":$(sync_summary),\"shard\":$(shard_json)}"
before=$(sync_summary)
stop_ps='Get-Process -Name WxAgent.Tray -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Seconds 15'
run_ps "$stop_ps" >/dev/null 2>&1
restart_process=$(start_tray)
after=$(sync_summary)
echo "{\"event\":\"agent-restart\",\"process\":$restart_process,\"before\":$before,\"after\":$after}"
run_ps "$stop_ps" >/dev/null 2>&1
# Create a real, valid local pending batch while the control plane is unavailable.
# The agent must later discard this queued body after authorization is revoked.
stop_server
run_ps "Remove-Item \"$WINDOWS_DIR/data/remote-data-sync-state.json\",\"$WINDOWS_DIR/data/remote-data-queue.json\" -Force -ErrorAction SilentlyContinue" >/dev/null 2>&1
pending_process=$(start_tray)
pending_queue=$(queue_json)
run_ps "$stop_ps" >/dev/null 2>&1
start_server
for _ in $(seq 1 20); do
sleep 1
curl -fsS http://127.0.0.1:8090/healthz >/dev/null 2>&1 && break
done
web_token=$(curl -fsS -X POST http://127.0.0.1:8090/v1/auth/login -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$WEB_PASSWORD\"}" | jq -r .access_token)
revoke=$(curl -sS -o "$root/revoke" -w '%{http_code}' -X POST -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/revoke")
conv=$(curl -sS -o "$root/conv" -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/conversations")
msgs=$(curl -sS -o "$root/msg" -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/messages?chat_id=filehelper&limit=10")
sync=$(curl -sS -o "$root/sync" -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/sync-status")
reject_payload=$(jq -nc --arg account "$ACCOUNT_ID" '{node_id:"local-node",account_id:$account,batch_id:"revoked-batch-harness",source_generation:$account,stream_key:"messages",sequence:5,cursor_start:"4",cursor_end:"5",payload_hash:"revoked-batch-hash",coverage_state:"complete",conversations:[],messages:[{message_id:"revoked-message-harness",chat_id:"filehelper",chat_type:"Private",source_message_id:"revoked-source-harness",direction:"incoming",message_type:"text",text:"fixture-revoked",source_time:"2026-09-22T03:00:00Z",observed_at:"2026-09-22T03:00:00Z",source_version:"harness",payload_hash:"revoked-message-hash"}]}')
batch_rejected=$(curl -sS -o "$root/rejected" -w '%{http_code}' -X POST -H "Authorization: Bearer $NODE_TOKEN" -H 'Content-Type: application/json' --data-binary "$reject_payload" http://127.0.0.1:8090/v1/data/batches)
# Reconnect after revocation. The connection itself is the authorization boundary,
# so registration restores the verified account without a separate confirmation.
revoked_process=$(start_tray)
revoked_queue=$(queue_json)
sleep 90
revoked_queue_after=$(queue_json)
web_token=$(curl -fsS -X POST http://127.0.0.1:8090/v1/auth/login -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$WEB_PASSWORD\"}" | jq -r .access_token)
reconnect_conv=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/conversations")
reconnect_msgs=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/messages?chat_id=filehelper&limit=10")
reconnect_sync=$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $web_token" "http://127.0.0.1:8090/v1/data/accounts/$ACCOUNT_ID/sync-status")
reconnect_queue=$(queue_json)
echo "{\"event\":\"authorization-revoke\",\"pending_process\":$pending_process,\"pending_queue\":$pending_queue,\"revoke\":$revoke,\"conversations\":$conv,\"messages\":$msgs,\"sync_status\":$sync,\"batch_rejected\":$batch_rejected,\"revoked_process\":$revoked_process,\"revoked_queue\":$revoked_queue,\"revoked_queue_after\":$revoked_queue_after,\"reconnect_conversations\":$reconnect_conv,\"reconnect_messages\":$reconnect_msgs,\"reconnect_sync_status\":$reconnect_sync,\"reconnect_queue\":$reconnect_queue}"
echo "{\"event\":\"authorization-restore\",\"process\":$revoked_process,\"conversations\":$reconnect_conv,\"messages\":$reconnect_msgs,\"queue\":$reconnect_queue}"
before_shard=$(shard_json)
old_server_pid=$server_pid
stop_server
start_server
for _ in $(seq 1 20); do sleep 2; state=$(sync_summary 2>/dev/null | jq -r .state || true); [[ "$state" == "complete" ]] && break; done
after_shard=$(shard_json)
echo "{\"event\":\"control-plane-restart\",\"old_pid\":$old_server_pid,\"new_pid\":$server_pid,\"before\":$before_shard,\"after\":$after_shard}"