#!/usr/bin/env bash # Seam: hermes-webui (the Hermex server) deployment integrity. # The webui is the phone-facing API for the Hermes agent. It must stay # loopback-bound so the SSH tunnel is its only ingress, keep its password in # the host .env, share the gateway data volume, and deploy as an isolated # compose service that cannot drag the gateway or inference along with it. load helpers COMPOSE="$REPO_ROOT/dankclaw/compose.yaml" ENV_EXAMPLE="$REPO_ROOT/dankclaw/.env.example" PLANNER="$REPO_ROOT/scripts/deploy-plan.py" DEPLOY="$REPO_ROOT/scripts/deploy.sh" REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh" webui_service_yaml() { python3 - "$COMPOSE" <<'PYEOF' import sys, yaml with open(sys.argv[1]) as fh: data = yaml.safe_load(fh) svc = (data.get("services") or {}).get("hermes-webui") assert svc, "dankclaw/compose.yaml: no hermes-webui service" print(yaml.safe_dump(svc)) PYEOF } @test "hermes-webui runs the webui bootstrap in foreground mode" { local y y=$(webui_service_yaml) grep -q 'nousresearch/hermes-agent' <<< "$y" grep -q 'bootstrap.py' <<< "$y" grep -q -- '--foreground' <<< "$y" } @test "hermes-webui is loopback-bound and password-gated" { local y y=$(webui_service_yaml) # Host-side publish stays on loopback: the SSH tunnel is the only ingress. grep -q '127.0.0.1:8787' <<< "$y" grep -q 'HERMES_WEBUI_PASSWORD' <<< "$y" # Container-internal bind must be 0.0.0.0 or Podman's port forwarder # (which dials the bridge IP) resets every connection. grep -q 'HERMES_WEBUI_HOST: 0.0.0.0' <<< "$y" } @test "hermes-webui shares the gateway data volume and mounts the webui checkout" { local y y=$(webui_service_yaml) grep -q 'HERMES_HOME: /data' <<< "$y" grep -q '/home/dank/hermes-data:/data' <<< "$y" grep -q '/home/dank/hermes-webui:/webui' <<< "$y" } @test "hermes-webui has a healthcheck that probes /health" { local y y=$(webui_service_yaml) grep -q 'healthcheck' <<< "$y" grep -q '/health' <<< "$y" } @test "dankclaw .env.example defines the webui password placeholder" { env_vars_defined "$ENV_EXAMPLE" | grep -qx 'HERMES_WEBUI_PASSWORD' } # Revision-aware planner: a change to only the hermes-webui service block must # plan hermes-webui and nothing else (the gateway is unchanged). webui_fixture_repo() { local dir="$1" git init -q "$dir" git -C "$dir" config user.email t@t && git -C "$dir" config user.name t mkdir -p "$dir/dankclaw" cat > "$dir/dankclaw/compose.yaml" <<'EOF' services: hermes: image: docker.io/nousresearch/hermes-agent:latest hermes-webui: image: docker.io/nousresearch/hermes-agent:latest ports: - "127.0.0.1:8787:8787" EOF git -C "$dir" add -A && git -C "$dir" commit -qm base # Touch only the webui service block (its port line is unique in the file). sed -i 's|127.0.0.1:8787:8787|127.0.0.1:8788:8787|' "$dir/dankclaw/compose.yaml" git -C "$dir" commit -qam head } @test "planner narrows a hermes-webui compose edit to hermes-webui only" { local dir base head dir=$(mktemp -d) webui_fixture_repo "$dir" base=$(git -C "$dir" rev-parse HEAD~1) head=$(git -C "$dir" rev-parse HEAD) run python3 "$PLANNER" --target dankclaw --repo "$dir" \ --base "$base" --head "$head" --paths dankclaw/compose.yaml [ "$status" -eq 0 ] [ "$output" = "hermes-webui" ] } @test "remote deployer accepts and dispatches hermes-webui" { grep -q 'dankclaw:hermes-webui' "$REMOTE_DEPLOY" awk '/^deploy_compose_service\(\)/,/^}/' "$REMOTE_DEPLOY" | grep -q 'hermes-webui' } @test "top-level deployer accepts hermes-webui as a Dankclaw service" { grep -q 'dankclaw:hermes-webui' "$DEPLOY" }