Build spec and host brief carried in from C:\Claude and WRPS/02-env; the plant model (equipment, tags, alarm bitmask, enums, unit conversions) is derived from WRPS/04-plc/register-map.csv, WRPS/05-scada/modbus/scada-points.csv and WRPS-CTL-003. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
4.7 KiB
Bash
115 lines
4.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Verify the AI stack on lin001. Read-only: it starts nothing and changes nothing.
|
|
#
|
|
# ./scripts/verify.sh
|
|
#
|
|
# "docker ps showing Up" is not proof of anything. What this checks instead:
|
|
# a 302 to the auth portal on every public hostname, clean logs, pg-ai
|
|
# unreachable from outside its own network, agent_ro genuinely read-only, and
|
|
# no host ports published by anything we added.
|
|
#
|
|
# Exit code is the number of failed checks, so it is usable in CI.
|
|
|
|
set -uo pipefail
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; PASS=$((PASS+1)); }
|
|
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=$((FAIL+1)); }
|
|
head_() { printf '\n\033[1m%s\033[0m\n' "$*"; }
|
|
|
|
head_ "Containers"
|
|
for name in pg-ai cube ai-api ai-web langfuse lf-db; do
|
|
if docker ps --format '{{.Names}}' | grep -qx "$name"; then
|
|
state=$(docker inspect -f '{{.State.Status}}' "$name")
|
|
health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name")
|
|
if [ "$state" = "running" ] && [ "$health" != "unhealthy" ]; then
|
|
ok "$name running (health: $health)"
|
|
else
|
|
bad "$name state=$state health=$health"
|
|
fi
|
|
else
|
|
bad "$name is not running"
|
|
fi
|
|
done
|
|
|
|
head_ "No published host ports on anything we added"
|
|
# openplc-runtime publishing 502 is the one deliberate exception on this host,
|
|
# and it is not ours. Everything in the AI stack must publish nothing.
|
|
for name in pg-ai cube ai-api ai-web langfuse lf-db; do
|
|
ports=$(docker port "$name" 2>/dev/null || true)
|
|
if [ -z "$ports" ]; then
|
|
ok "$name publishes no host port"
|
|
else
|
|
bad "$name publishes: $ports"
|
|
fi
|
|
done
|
|
|
|
head_ "pg-ai network isolation"
|
|
if docker inspect pg-ai -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null | grep -qw proxy; then
|
|
bad "pg-ai is attached to the proxy network - it must be ai-internal only"
|
|
else
|
|
ok "pg-ai is not on the proxy network"
|
|
fi
|
|
|
|
head_ "Public endpoints - expect 302 to the auth portal"
|
|
for host in lf.yokogawa.tech cube.yokogawa.tech api.yokogawa.tech ai.yokogawa.tech; do
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -I "https://$host" --max-time 10 || echo "000")
|
|
case "$code" in
|
|
302|303) ok "$host -> $code (auth portal)" ;;
|
|
200) bad "$host -> 200 WITHOUT AUTH - check 'import authelia' in ~/Caddyfile" ;;
|
|
000) bad "$host unreachable - DNS A record missing, or Caddy has no certificate" ;;
|
|
*) bad "$host -> $code" ;;
|
|
esac
|
|
done
|
|
|
|
head_ "agent_ro is read-only"
|
|
if docker exec pg-ai psql -U agent_ro -d plant -tAc 'SELECT count(*) FROM equipment' >/dev/null 2>&1; then
|
|
ok "agent_ro can SELECT"
|
|
else
|
|
bad "agent_ro cannot SELECT"
|
|
fi
|
|
if docker exec pg-ai psql -U agent_ro -d plant -tAc \
|
|
"INSERT INTO equipment (equipment_id) VALUES ('VERIFY-DELETE-ME')" >/dev/null 2>&1; then
|
|
bad "agent_ro CAN INSERT - this is a Phase 1 failure, fix db/003_roles.sql now"
|
|
docker exec pg-ai psql -U postgres -d plant -c \
|
|
"DELETE FROM equipment WHERE equipment_id='VERIFY-DELETE-ME'" >/dev/null 2>&1
|
|
else
|
|
ok "agent_ro INSERT is rejected"
|
|
fi
|
|
|
|
head_ "Reference data"
|
|
missing_eq=$(docker exec pg-ai psql -U postgres -d plant -tAc \
|
|
"SELECT count(*) FROM equipment WHERE coalesce(array_length(aliases,1),0)=0" 2>/dev/null || echo "?")
|
|
missing_tag=$(docker exec pg-ai psql -U postgres -d plant -tAc \
|
|
"SELECT count(*) FROM tags WHERE coalesce(array_length(aliases,1),0)=0" 2>/dev/null || echo "?")
|
|
[ "$missing_eq" = "0" ] && ok "every equipment item has an alias" || bad "$missing_eq equipment items have no alias"
|
|
[ "$missing_tag" = "0" ] && ok "every tag has an alias" || bad "$missing_tag tags have no alias"
|
|
|
|
if docker exec pg-ai psql -U postgres -d plant -tAc \
|
|
"SELECT 1 FROM pg_extension WHERE extname='vector'" 2>/dev/null | grep -q 1; then
|
|
ok "pgvector extension present"
|
|
else
|
|
bad "pgvector extension missing"
|
|
fi
|
|
|
|
head_ "Fixture data"
|
|
if docker exec pg-ai psql -U postgres -d plant -tAc \
|
|
"SELECT 1 FROM information_schema.schemata WHERE schema_name='fixture'" 2>/dev/null | grep -q 1; then
|
|
rows=$(docker exec pg-ai psql -U postgres -d plant -tAc \
|
|
"SELECT count(*) FROM fixture.alarm_history" 2>/dev/null)
|
|
printf ' \033[33m!!\033[0m fixture schema present (%s alarm rows) - answers are TEST DATA, not plant history\n' "$rows"
|
|
fi
|
|
|
|
head_ "Disk"
|
|
df -h / /datadisk | sed 's/^/ /'
|
|
|
|
head_ "Recent errors in the logs"
|
|
for name in pg-ai cube ai-api ai-web; do
|
|
errors=$(docker logs --tail 200 "$name" 2>&1 | grep -icE 'error|fatal|panic' || true)
|
|
[ "${errors:-0}" -eq 0 ] && ok "$name logs clean (last 200 lines)" \
|
|
|| bad "$name has $errors error lines - docker logs --tail 200 $name"
|
|
done
|
|
|
|
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
|
|
exit "$FAIL"
|