Files
owui-tools/tests/vm_agent_asserts.sh
2026-09-15 15:24:03 -04:00

140 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# =====================================================================
# What the sub-agent container can and cannot reach.
#
# ./tests/vm_agent_asserts.sh
#
# WHY THIS IS A SEPARATE BOUNDARY FROM THE GUEST'S
#
# The VM's isolation is about code somebody else wrote. This one is about
# OUR code, and it exists because of two specific things on the other
# side of it:
#
# Ollama has no authentication at all, and serves DELETE /api/delete,
# POST /api/pull and POST /api/create. Anything that can reach
# 172.17.0.1:11434 can delete the model the whole assistant runs on.
#
# The Open WebUI key has to be an ADMIN key, because anyone may invoke
# vm_agent and the chat belongs to whoever is talking. An admin key in
# the agent container would be an agent that can read every
# conversation on this instance.
#
# So the agent reaches exactly one address, and the thing at that address
# exposes exactly three URLs. This checks both halves.
# =====================================================================
set -uo pipefail
AGENT=owuitools-vmagent
GW=172.22.2.10:8000
fails=0
ok() { printf ' ok %s\n' "$*"; }
bad() { printf ' FAIL %s\n' "$*"; fails=$((fails+1)); }
docker inspect "$AGENT" >/dev/null 2>&1 || { echo "$AGENT is not running" >&2; exit 1; }
echo "== 1. the agent reaches vmgw, and nothing else"
probe=$(docker exec -i "$AGENT" python3 - <<'PY'
import socket
def p(h, port):
try:
socket.create_connection((h, port), 3).close(); return "REACHED"
except Exception: return "blocked"
for h, port, name in [("172.22.2.10", 8000, "vmgw"), ("172.17.0.1", 11434, "ollama-direct"),
("172.19.0.2", 8080, "openwebui-direct"), ("192.168.1.10", 7200, "gitea-lan"),
("192.168.1.10", 22, "host-sshd"), ("192.168.1.1", 80, "lan-gateway"),
("1.1.1.1", 443, "internet")]:
print(name, p(h, port))
PY
)
while read -r name verdict; do
[ -z "$name" ] && continue
if [ "$name" = "vmgw" ]; then
[ "$verdict" = "REACHED" ] && ok "vmgw is reachable (positive control)" \
|| bad "vmgw is NOT reachable - this suite proves nothing"
else
[ "$verdict" = "blocked" ] && ok "$name is blocked" || bad "$name was REACHABLE"
fi
done <<< "$probe"
echo
echo "== 2. vmgw exposes two URLs and refuses everything else"
paths=$(docker exec -i "$AGENT" python3 - <<PY
import json, urllib.request, urllib.error
BASE = "http://$GW"
def code(path, method="GET"):
try:
r = urllib.request.Request(BASE + path, method=method,
data=b"{}" if method == "POST" else None)
if method == "POST": r.add_header("Content-Type", "application/json")
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(r, timeout=8) as resp:
return resp.status
except urllib.error.HTTPError as e:
return e.code
except Exception:
return 0
print("health", code("/health"))
print("ollama-chat-path-exists", 0 if code("/ollama/api/ps") != 200 else 200)
for p, m in [("/ollama/api/delete", "DELETE"), ("/ollama/api/pull", "POST"),
("/ollama/api/create", "POST"), ("/ollama/api/tags", "GET"),
("/ollama/../api/delete", "DELETE"),
# These two were a real location once, with an Open WebUI
# ADMIN key injected here as a header. That design is gone -
# live progress is collected by a filter inside Open WebUI -
# and these assert that it stays gone.
("/ui/00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000000/event", "POST"),
("/api/v1/chats/a/messages/b/event", "POST")]:
print("refuse:" + p, code(p, m))
PY
)
while read -r name value; do
[ -z "$name" ] && continue
case "$name" in
health) [ "$value" = "200" ] && ok "/health answers" || bad "/health returned $value" ;;
ollama-chat-path-exists)
[ "$value" = "200" ] && ok "the permitted Ollama path works (positive control)" \
|| bad "the permitted Ollama path returned $value" ;;
refuse:*)
p=${name#refuse:}
[ "$value" = "404" ] && ok "refused $p" || bad "$p returned $value, expected 404" ;;
esac
done <<< "$paths"
echo
echo "== 3. no Open WebUI credential exists anywhere in this stack"
# Not just absent from the agent: absent from the gateway too, which is
# where it used to live. The whole point of moving live progress into a
# filter was that no such key has to exist.
gw_env=$(docker exec owuitools-vmgw env 2>/dev/null || true)
if printf '%s' "$gw_env" | grep -q '^OWUITOOLS_OWUI_API_KEY=.'; then
bad "owuitools-vmgw still holds an Open WebUI API key"
else
ok "owuitools-vmgw holds no Open WebUI API key"
fi
# A proxy_pass, not a mention: the config's own header explains what used
# to be here, and matching that comment reported the opposite of the truth.
if docker exec owuitools-vmgw grep -rqE '^[^#]*proxy_pass.*chats' /etc/nginx/conf.d/ 2>/dev/null; then
bad "owuitools-vmgw still proxies the Open WebUI event endpoint"
else
ok "owuitools-vmgw does not proxy the Open WebUI event endpoint"
fi
env_dump=$(docker exec "$AGENT" env 2>/dev/null || true)
for secret in OWUITOOLS_OWUI_API_KEY GITEA_TOKEN OWUITOOLS_AUTH_TOKEN; do
if printf '%s' "$env_dump" | grep -q "^${secret}=."; then
bad "$secret is in the agent's environment"
else
ok "$secret is not in the agent's environment"
fi
done
# vmd's config holds the Gitea token and is 0640 root:owuivmd.
if docker exec "$AGENT" test -r /etc/owuivm/vmd.env 2>/dev/null; then
bad "the agent can read vmd's credential file"
else
ok "the agent cannot read vmd's credential file"
fi
echo
if [ "$fails" -gt 0 ]; then echo "FAILED $fails"; exit 1; fi
echo "the agent container is confined as designed"