hermes: the webui was LAN-facing, not loopback — and Dankclaw's compose is gone
Two things, found while checking whether the hermes trio still needed migrating. 1. **The phone-facing webui was exposed to the LAN.** Its unit runs with Network=host, so there is no Podman port forwarder and the app's own bind is the exposure. The compose service it replaced published `127.0.0.1:8787:8787` (loopback); the unit bound `HERMES_WEBUI_HOST=0.0.0.0`, and the LAN address answered 200 — password-gated, but on every interface, which is not the documented "SSH tunnel is the sole ingress" posture. It now binds 127.0.0.1, which serves the tunnel just as well (sshd dials localhost:8787 from the host). The unit comment claiming "LAN-facing as before" was wrong about "before", and is corrected. 2. **Dankclaw's compose.yaml is retired.** Auditing ownership showed every running container on that host reports a PODMAN_SYSTEMD_UNIT label — the hermes trio's units have owned their containers since 2026-09-12 — so the three service blocks left in compose.yaml were dead weight that could only create a duplicate container fighting a unit for a name. deploy_hermes was already fully unit-based. The file is staged in .AGENTTRASH/; the planner plans nothing for it, an unknown service on that target now errors instead of silently no-oping through `podman-compose`, and DANKCLAW_COMPOSE_SERVICES is empty. The hermes-webui seam now reads the unit — it read compose, which is exactly how the bind drifted while the test kept passing against a file nobody ran.
This commit is contained in:
@@ -1,147 +0,0 @@
|
||||
# Dankclaw stack — AI, search, development & observability services.
|
||||
# Deploy: cd /home/dank/serverAppConfigs && podman-compose -f dankclaw/compose.yaml up -d
|
||||
# See ../docs/Dankclaw/ and ../docs/Infrastructure/ for service documentation.
|
||||
|
||||
services:
|
||||
# --- SearXNG stack (gluetun + searxng + valkey) --------------------------
|
||||
# These three services are now boot-persistent Quadlet units, not compose
|
||||
# services. They live in searxng/quadlet/ and install to
|
||||
# ~/.config/containers/systemd/ on the host. searxng reuses gluetun's netns
|
||||
# (Network=gluetun.container) so its traffic exits the WireGuard tunnel.
|
||||
# Deploy with: scripts/deploy-searxng-quadlet.sh dankclaw
|
||||
# See ../docs/Dankclaw/SearXNG.md, Gluetun.md, Valkey.md.
|
||||
|
||||
# --- AI image generation (ROCm GPU backend, gfx1151) ---------------------
|
||||
# ComfyUI is a boot-persistent Quadlet unit since 2026-09-16:
|
||||
# comfyui/quadlet/comfyui.container, installed by deploy-remote.sh. It left
|
||||
# compose after the container it had started went stale — created before the
|
||||
# GPU declarations existed, then restarted by `restart: unless-stopped`
|
||||
# without /dev/kfd or /dev/dri until it had failed 9,146 times on
|
||||
# "No HIP GPUs are available". Built from Containerfile.comfyui (AMD's
|
||||
# gfx1151 base), models and data under /home/dank/comfyui-data.
|
||||
# See ../docs/Dankclaw/ComfyUI.md.
|
||||
|
||||
# --- Chat UI (open-webui) ------------------------------------------------
|
||||
# Now a boot-persistent Quadlet unit (quadlet, slirp4netns, port 3005), not
|
||||
# a compose service. See open-webui/quadlet/open-webui.container and
|
||||
# ../docs/Dankclaw/OpenWebUI.md.
|
||||
|
||||
hermes:
|
||||
image: docker.io/nousresearch/hermes-agent:latest
|
||||
container_name: hermes
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
# The image's default s6 entrypoint runs the interactive TUI, which exits
|
||||
# without a TTY and crash-loops the container. Override it to run the
|
||||
# headless gateway (the documented Docker mode). The gateway drops to the
|
||||
# unprivileged hermes user (uid 10000), so the data dir must be writable by it.
|
||||
entrypoint: ["bash", "-c", "hermes gateway run --force -v"]
|
||||
environment:
|
||||
HERMES_DATA_DIR: /data
|
||||
# Point hermes's config/.env at the mounted data dir (persistent).
|
||||
HERMES_HOME: /data
|
||||
# Signal adapter -> the signal-cli daemon, a HOST process listening on
|
||||
# :8080. The service runs with network_mode: host below, so it dials
|
||||
# loopback directly; from a bridge network only DNAT'd published ports
|
||||
# accept connections to the host LAN IP, and un-forwarded ports refuse
|
||||
# instantly (verified live 2026-08-30).
|
||||
SIGNAL_HTTP_URL: "http://127.0.0.1:8080"
|
||||
SIGNAL_ACCOUNT: "${SIGNAL_ACCOUNT}"
|
||||
SIGNAL_ALLOWED_USERS: "${SIGNAL_ALLOWED_USERS}"
|
||||
ports:
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
- /home/dank/hermes-data:/data
|
||||
# No healthcheck: 'hermes gateway run' starts the messaging gateway (Signal
|
||||
# SSE), not the :8090 web dashboard, so there is no HTTP endpoint to probe.
|
||||
# The container's running state + the daemon's SSE connection are the signal.
|
||||
|
||||
# --- Hermes web dashboard (management UI: config, API keys, sessions) -----
|
||||
# A separate process from the Signal gateway (the gateway entrypoint does not
|
||||
# start the dashboard). Bound to the LAN with basic auth: the username + a
|
||||
# scrypt password_hash live in the git-ignored /home/dank/hermes-data config.
|
||||
# Port 9119 is published so a browser on the LAN can reach it.
|
||||
hermes-dashboard:
|
||||
image: docker.io/nousresearch/hermes-agent:latest
|
||||
container_name: hermes-dashboard
|
||||
restart: unless-stopped
|
||||
entrypoint: ["bash", "-c", "hermes dashboard --host 0.0.0.0 --port 9119 --no-open --skip-build"]
|
||||
environment:
|
||||
HERMES_DATA_DIR: /data
|
||||
HERMES_HOME: /data
|
||||
ports:
|
||||
- "9119:9119"
|
||||
volumes:
|
||||
- /home/dank/hermes-data:/data
|
||||
|
||||
# --- Hermes web UI (Hermex server; phone-facing API for the agent) --------
|
||||
# Runs the open-source hermes-webui (github.com/nesquena/hermes-webui) in
|
||||
# foreground mode: bootstrap.py creates .venv inside the mounted checkout,
|
||||
# installs WebUI + agent deps, then execs server.py as a single foreground
|
||||
# process. It reads HERMES_HOME=/data, so it drives the SAME agent state
|
||||
# (memory, skills, sessions) as the gateway container.
|
||||
# Loopback-only publish: the SSH tunnel (phone -> Dankclaw:22) is the sole
|
||||
# ingress. The password gates Hermex logins and lives in the host .env.
|
||||
hermes-webui:
|
||||
image: docker.io/nousresearch/hermes-agent:latest
|
||||
container_name: hermes-webui
|
||||
restart: unless-stopped
|
||||
entrypoint: ["bash", "-c", "python3 /webui/bootstrap.py --foreground"]
|
||||
environment:
|
||||
HERMES_HOME: /data
|
||||
# Container-internal bind: 0.0.0.0 so Podman's port forwarder (which
|
||||
# dials the container's bridge IP) can reach it. The *host-side*
|
||||
# publish below stays on 127.0.0.1, so loopback-only ingress holds.
|
||||
HERMES_WEBUI_HOST: 0.0.0.0
|
||||
HERMES_WEBUI_PORT: "8787"
|
||||
HERMES_WEBUI_PASSWORD: "${HERMES_WEBUI_PASSWORD}"
|
||||
# Headless host: never show the first-run wizard (a profile already
|
||||
# exists in /data from the gateway).
|
||||
HERMES_WEBUI_SKIP_ONBOARDING: "1"
|
||||
ports:
|
||||
- "127.0.0.1:8787:8787"
|
||||
volumes:
|
||||
- /home/dank/hermes-data:/data
|
||||
# rw on purpose: bootstrap.py creates /webui/.venv in the repo root.
|
||||
- /home/dank/hermes-webui:/webui
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
# First boot pip-installs WebUI + agent deps into a fresh venv.
|
||||
start_period: 600s
|
||||
|
||||
# --- Git hosting (Gitea) -------------------------------------------------
|
||||
# Now boot-persistent Quadlet units (private bridge network, DNS enabled),
|
||||
# not compose services. See gitea/quadlet/ and ../docs/Dankclaw/Gitea.md.
|
||||
|
||||
# --- Observability -------------------------------------------------------
|
||||
# Now boot-persistent Quadlet units, not compose services: prometheus,
|
||||
# grafana, loki, alloy and node-exporter install from
|
||||
# dankclaw/observability/quadlet/ and are dispatched by deploy_observability
|
||||
# in scripts/deploy-remote.sh. They left compose on 2026-09-11, after a host
|
||||
# reboot proved that compose-only containers do not come back on their own —
|
||||
# the same gap that had already cost the fleet Caddy and Pi-hole.
|
||||
#
|
||||
# The three HTTP UIs publish on 127.0.0.1 only. Off-host access is through
|
||||
# the guarded franksdank.cloud vhosts (Caddy reaches these containers by
|
||||
# name over this network) and the host's own homepage status timer probes
|
||||
# them over loopback. See
|
||||
# serverAppConfigs-docs/Infrastructure/Observability.md.
|
||||
|
||||
# --- Authentik (SSO / OIDC provider) -------------------------------------
|
||||
# Now boot-persistent Quadlet units, not compose services: authentik-
|
||||
# postgres -> authentik-server -> authentik-worker start in dependency order
|
||||
# via ~/.config/containers/systemd/*.container on the host (repo source:
|
||||
# dankclaw/authentik/quadlet/; deploy-remote.sh deploy_authentik installs
|
||||
# and starts them). Redis was removed in 2025.10 (sessions/caching moved to
|
||||
# PostgreSQL). The Caddyfile's forward_auth vhosts proxy to
|
||||
# authentik-server:9000 over the dankclaw_default network (A2A DNS).
|
||||
# See ../docs/Infrastructure/Authentik.md.
|
||||
|
||||
# Caddy (the homelab's single reverse proxy) left compose on 2026-08-26:
|
||||
# it is boot-persistent via its Quadlet unit (dankclaw/caddy/quadlet/
|
||||
# caddy.container), installed by deploy_caddy in scripts/deploy-remote.sh.
|
||||
# The rendered Caddyfile, certs, and config still live under
|
||||
# /home/dank/Containers/caddy (rootless-writable).
|
||||
@@ -14,9 +14,13 @@
|
||||
# daemon binds loopback-only in the host network namespace (see
|
||||
# chrome-devtools-protocol.container). Only processes sharing that namespace
|
||||
# can drive the browser; a bridge-network client could not reach it at all.
|
||||
# The server therefore listens on 0.0.0.0:8787 of the host netns (LAN-facing as
|
||||
# before); access control is the password gate, and the SSH tunnel
|
||||
# (phone -> Dankclaw:22) remains the sole off-network ingress path.
|
||||
# The server binds LOOPBACK (HERMES_WEBUI_HOST=127.0.0.1). Host networking
|
||||
# means there is no Podman port forwarder, so the bind IS the exposure: the
|
||||
# compose service this replaced published 127.0.0.1:8787:8787, and the first
|
||||
# version of this unit bound 0.0.0.0 instead, which put the phone-facing API on
|
||||
# the whole LAN with only the password in front of it (measured 2026-09-16:
|
||||
# 192.168.0.143:8787 answered 200). The SSH tunnel (phone -> Dankclaw:22) dials
|
||||
# localhost:8787 from the host, so loopback serves it exactly as well.
|
||||
|
||||
[Unit]
|
||||
Description=Hermes webui (Hermex) container
|
||||
@@ -27,7 +31,7 @@ Image=localhost/hermes-agent:latest
|
||||
Pull=never
|
||||
Network=host
|
||||
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
||||
Environment=HERMES_WEBUI_HOST=0.0.0.0
|
||||
Environment=HERMES_WEBUI_HOST=127.0.0.1
|
||||
Environment=HERMES_WEBUI_PORT=8787
|
||||
Environment=HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD}
|
||||
Environment=HERMES_HOME=/data
|
||||
|
||||
+16
-19
@@ -125,21 +125,12 @@ FRANKBOOK_SERVICES = {
|
||||
"readarr",
|
||||
}
|
||||
|
||||
DANKCLAW_COMPOSE_SERVICES = {
|
||||
"comfyui",
|
||||
"hermes",
|
||||
"hermes-dashboard",
|
||||
"hermes-webui",
|
||||
"prometheus",
|
||||
"grafana",
|
||||
"loki",
|
||||
"alloy",
|
||||
"node-exporter",
|
||||
"authentik-postgres",
|
||||
"authentik-server",
|
||||
"authentik-worker",
|
||||
"caddy",
|
||||
}
|
||||
DANKCLAW_COMPOSE_SERVICES = set()
|
||||
# Dankserver's compose file is still the source of truth for its media stack;
|
||||
# Dankclaw's is gone (deleted 2026-09-16). Every service on Dankclaw is a
|
||||
# Quadlet unit now — the last compose services were the hermes trio, whose units
|
||||
# have owned their containers since 2026-09-12 and whose compose blocks were
|
||||
# dead weight that could only create a duplicate container.
|
||||
|
||||
DANKSERVER_COMPOSE_SERVICES = {
|
||||
"mariadb",
|
||||
@@ -198,8 +189,14 @@ def _yaml_services(contents: str | None) -> dict[str, Any]:
|
||||
|
||||
|
||||
def compose_changes(repo: str, target: str, base: str, head: str) -> set[str]:
|
||||
"""Return only compose services whose YAML definitions or dependencies changed."""
|
||||
"""Return only compose services whose YAML definitions or dependencies changed.
|
||||
|
||||
A target with no compose file (Dankclaw since 2026-09-16) plans nothing:
|
||||
its services are Quadlet units, selected by their own paths.
|
||||
"""
|
||||
path = f"{target}/compose.yaml"
|
||||
if not _git_show(repo, head, path) and not _git_show(repo, base, path):
|
||||
return set()
|
||||
old_doc = _yaml_document(_git_show(repo, base, path))
|
||||
new_doc = _yaml_document(_git_show(repo, head, path))
|
||||
old = old_doc.get("services", {}) if isinstance(old_doc.get("services", {}), dict) else {}
|
||||
@@ -264,9 +261,9 @@ def services_for_path(path: str, target: str) -> set[str]:
|
||||
|
||||
if parts[0] == "dankclaw":
|
||||
if len(parts) >= 2 and parts[1] == "compose.yaml":
|
||||
# Revision-aware callers replace this broad fallback with the
|
||||
# service-level YAML comparison in compose_changes().
|
||||
_add(services, *DANKCLAW_COMPOSE_SERVICES)
|
||||
# Dankclaw has no compose file since 2026-09-16: every service is a
|
||||
# Quadlet unit, each selected by its own path. A change (or a
|
||||
# deletion) of this path therefore restarts nothing.
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "Caddyfile":
|
||||
_add(services, "caddy")
|
||||
|
||||
@@ -382,10 +382,15 @@ deploy_compose_service() {
|
||||
echo "WARN: $service is recorded in dankserver/compose.yaml; production container management is host-specific and no restart was attempted."
|
||||
return 0
|
||||
fi
|
||||
if [[ "$TARGET" == dankclaw ]]; then
|
||||
# Dankclaw has no compose file since 2026-09-16: every service is a
|
||||
# Quadlet unit with its own handler above. Reaching this means the
|
||||
# dispatcher is missing a case, and the old fallback (`podman-compose
|
||||
# up -d`) would now be a silent no-op or a duplicate container.
|
||||
echo "no compose services on dankclaw; $service has no quadlet handler" >&2
|
||||
return 2
|
||||
fi
|
||||
case "$service" in
|
||||
hermes|hermes-dashboard|hermes-webui)
|
||||
podman-compose -f dankclaw/compose.yaml up -d --no-deps "$service"
|
||||
;;
|
||||
*)
|
||||
echo "unknown compose service: $service" >&2
|
||||
return 2
|
||||
|
||||
+51
-55
@@ -2,59 +2,69 @@
|
||||
# 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.
|
||||
# the host .env, and share the gateway data volume.
|
||||
#
|
||||
# It reads the QUADLET UNIT, which is the live source of truth. It used to read
|
||||
# dankclaw/compose.yaml, and that is exactly how the bind drifted: the compose
|
||||
# service published 127.0.0.1:8787:8787 (loopback), the unit that replaced it
|
||||
# bound 0.0.0.0 with only the password in front, and the test kept passing
|
||||
# because it was checking the file nobody ran any more.
|
||||
|
||||
load helpers
|
||||
|
||||
COMPOSE="$REPO_ROOT/dankclaw/compose.yaml"
|
||||
UNIT="$REPO_ROOT/dankclaw/hermes/quadlet/hermes-webui.container"
|
||||
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
|
||||
# Normalize the unit's key=value lines into the same shape the assertions
|
||||
# below always used, so the contract reads the same as it did for compose.
|
||||
[ -f "$UNIT" ] || { echo "missing $UNIT" >&2; return 1; }
|
||||
grep -E '^(Image|Exec|Environment|Volume|HealthCmd|PublishPort)=' "$UNIT"
|
||||
}
|
||||
|
||||
@test "hermes-webui runs the webui bootstrap in foreground mode" {
|
||||
local y
|
||||
y=$(webui_service_yaml)
|
||||
grep -q 'nousresearch/hermes-agent' <<< "$y"
|
||||
# The unit runs the localhost-aliased tag (this host's quadlet path cannot
|
||||
# resolve registry-prefixed refs from local storage); the deployer tags it
|
||||
# from the upstream name, which is what keeps the alias fresh.
|
||||
grep -q 'Image=localhost/hermes-agent:latest' <<< "$y"
|
||||
grep -q 'podman tag docker.io/nousresearch/hermes-agent:latest localhost/hermes-agent:latest' \
|
||||
"$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
grep -q 'bootstrap.py' <<< "$y"
|
||||
grep -q -- '--foreground' <<< "$y"
|
||||
}
|
||||
|
||||
@test "hermes-webui is loopback-bound and password-gated" {
|
||||
@test "hermes-webui binds loopback, not the LAN" {
|
||||
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"
|
||||
grep -q 'HERMES_WEBUI_PASSWORD' <<< "$y" \
|
||||
|| { echo "password gate missing"; return 1; }
|
||||
# With Network=host there is no port forwarder: the app's own bind IS the
|
||||
# exposure. The compose service this replaced published 127.0.0.1:8787:8787,
|
||||
# and an early version of the unit bound 0.0.0.0 instead — which answered
|
||||
# 200 on 192.168.0.143:8787 for anyone on the LAN (measured 2026-09-16).
|
||||
grep -q 'Environment=HERMES_WEBUI_HOST=127.0.0.1' <<< "$y" \
|
||||
|| { echo "webui must bind loopback (HERMES_WEBUI_HOST=127.0.0.1)"; return 1; }
|
||||
! grep -q 'HERMES_WEBUI_HOST=0.0.0.0' <<< "$y" \
|
||||
|| { echo "webui must not bind 0.0.0.0 on a host network"; return 1; }
|
||||
}
|
||||
|
||||
@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"
|
||||
grep -q 'Environment=HERMES_HOME=/data' <<< "$y"
|
||||
grep -q 'Volume=/home/dank/Containers/hermes/data:/data' <<< "$y"
|
||||
grep -q 'Volume=/home/dank/Containers/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 'HealthCmd=' <<< "$y"
|
||||
grep -q '/health' <<< "$y"
|
||||
}
|
||||
|
||||
@@ -62,43 +72,29 @@ PYEOF
|
||||
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
|
||||
@test "planner maps a hermes-webui unit edit to the hermes service" {
|
||||
# Any file under dankclaw/hermes/** selects the `hermes` service, because
|
||||
# the trio (gateway, dashboard, webui) deploys as one unit-set — see
|
||||
# deploy_hermes, which installs all three units and restarts them together.
|
||||
# The three names are aliases of that handler, not separate deployments.
|
||||
# (This test used to assert that a *compose* edit narrowed to hermes-webui;
|
||||
# Dankclaw has had no compose file since 2026-09-16.)
|
||||
run python3 "$PLANNER" --target dankclaw \
|
||||
--paths dankclaw/hermes/quadlet/hermes-webui.container
|
||||
[ "$status" -eq 0 ]
|
||||
[ "$output" = "hermes-webui" ]
|
||||
[ "$output" = "hermes" ]
|
||||
# And an unrelated service must not be dragged in.
|
||||
! grep -qE 'caddy|prometheus|frank-large' <<< "$output"
|
||||
}
|
||||
|
||||
@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'
|
||||
# Dispatched to the unit handler, never to compose: deploy_hermes installs
|
||||
# the quadlets and retires compose-era containers.
|
||||
grep -q 'hermes|hermes-dashboard|hermes-webui) deploy_hermes' "$REMOTE_DEPLOY"
|
||||
# deploy_hermes installs the trio with one loop over the unit names.
|
||||
grep -q 'for unit in hermes hermes-dashboard hermes-webui' "$REMOTE_DEPLOY"
|
||||
grep -q 'dankclaw/hermes/quadlet/$unit.container' "$REMOTE_DEPLOY"
|
||||
}
|
||||
|
||||
@test "top-level deployer accepts hermes-webui as a Dankclaw service" {
|
||||
|
||||
Reference in New Issue
Block a user