From 207235420ce5174dd44936008cebf3c933d35950 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 31 Aug 2026 16:25:28 -0700 Subject: [PATCH] hermes: CDP browser daemon + MCP client wiring for the agent Adds a digest-pinned headless-shell Chrome DevTools Protocol daemon as a boot-persistent quadlet unit (chrome-devtools-protocol.container). The entrypoint override drops the image's socat front so the CDP endpoint binds loopback-only on 127.0.0.1:9222 in the host network namespace; only the two host-network consumers (Signal gateway, webui server) can drive it. hermes-webui flips Network=dankclaw_default -> Network=host so its agent process shares the daemon's namespace (0.0.0.0:8787 bind is LAN-facing as before; no exposure change). deploy_hermes installs the new unit and starts it if inactive without restarting in-flight browser sessions. --- .../chrome-devtools-protocol.container | 38 +++++++++++++++++++ .../hermes/quadlet/hermes-webui.container | 20 ++++++---- scripts/deploy-remote.sh | 10 ++++- serverAppConfigs-docs/Dankclaw/Hermes.md | 16 +++++++- 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 dankclaw/hermes/quadlet/chrome-devtools-protocol.container diff --git a/dankclaw/hermes/quadlet/chrome-devtools-protocol.container b/dankclaw/hermes/quadlet/chrome-devtools-protocol.container new file mode 100644 index 0000000..2204bf7 --- /dev/null +++ b/dankclaw/hermes/quadlet/chrome-devtools-protocol.container @@ -0,0 +1,38 @@ +# Quadlet unit: Chrome DevTools Protocol daemon for the hermes agent (rootless). +# +# Install path on dankclaw: ~/.config/containers/systemd/chrome-devtools-protocol.container +# Unit name: chrome-devtools-protocol.service (container name: chrome-devtools-protocol) +# +# Headless Chrome exposing raw CDP over http://127.0.0.1:9222. The hermes +# agent's chrome-devtools MCP client (mcp_servers.chrome-devtools in +# /data/config.yaml) spawns npx chrome-devtools-mcp with --browserUrl pointing +# at this endpoint and drives the browser over it. Both consumers — the Signal +# gateway and the webui server — run in the host network namespace, so a +# loopback-only bind is reachable by exactly the processes that may drive the +# browser and by nobody else on the LAN or WAN. +# +# Entrypoint override drops the image's socat front (which would bind +# 0.0.0.0:9222 in the host namespace and hand browser control to the whole +# LAN). --no-sandbox is required because rootless podman grants Chrome no user +# namespace for its sandbox; the content processes therefore run as the uid +# that already owns this container, capping the blast radius of a hostile page. +# Image pinned by digest: upgrades are a deliberate unit edit + redeploy, not a +# tag drift at pull time. + +[Unit] +Description=Chrome DevTools Protocol daemon (headless-shell) for hermes + +[Container] +ContainerName=chrome-devtools-protocol +Network=host +Image=docker.io/chromedp/headless-shell@sha256:2d349b544a1ea6b5b5fd7c0fe99215ff662339c57407ee2e8c0a11af93516b04 +Entrypoint=/headless-shell/headless-shell +Exec=--no-sandbox --disable-gpu --use-gl=angle --use-angle=swiftshader --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 +ShmSize=1073741824 + +[Service] +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/dankclaw/hermes/quadlet/hermes-webui.container b/dankclaw/hermes/quadlet/hermes-webui.container index 096b213..fe96860 100644 --- a/dankclaw/hermes/quadlet/hermes-webui.container +++ b/dankclaw/hermes/quadlet/hermes-webui.container @@ -1,4 +1,4 @@ -# Quadlet unit: Hermex webui (rootless, boot-persistent). +# Quadlet unit: Hermex webui (Hermex) container (rootless, boot-persistent). # # Install path on dankclaw: ~/.config/containers/systemd/hermes-webui.container # Unit name: hermes-webui.service (container name: hermes-webui) @@ -7,10 +7,16 @@ # 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. Published on the LAN -# interface only (192.168.0.143:8787): password-gated (HERMES_WEBUI_PASSWORD -# in the host .env, git-ignored), so LAN clients connect directly and the -# SSH tunnel (phone -> Dankclaw:22) remains the sole off-network ingress. +# (memory, skills, sessions) as the gateway container. +# +# Host networking on purpose: the agent's chrome-devtools MCP client spawns +# npx chrome-devtools-mcp with --browserUrl=http://127.0.0.1:9222, and the CDP +# 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. [Unit] Description=Hermes webui (Hermex) container @@ -19,9 +25,8 @@ Description=Hermes webui (Hermex) container ContainerName=hermes-webui Image=localhost/hermes-agent:latest Pull=never -Network=dankclaw_default +Network=host EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env -Environment=HERMES_HOME=/data Environment=HERMES_WEBUI_HOST=0.0.0.0 Environment=HERMES_WEBUI_PORT=8787 Environment=HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD} @@ -30,7 +35,6 @@ Environment=HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD} Environment=HERMES_WEBUI_SKIP_ONBOARDING=1 Volume=/home/dank/hermes-data:/data Volume=/home/dank/hermes-webui:/webui -PublishPort=192.168.0.143:8787:8787 Exec=python3 /webui/bootstrap.py --foreground # JSON exec form on purpose: this quadlet generator joins a bare HealthCmd # into ONE argv element with \x20 escapes and then shell-executes it — the diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index cb3f132..b0b08c0 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -576,6 +576,15 @@ deploy_hermes() { # cannot be enabled by this host's v1 quadlet generator). local unit mkdir -p "$quadlet_unit_dir" + # The CDP browser daemon is a companion service to the trio: install its + # unit and start it if inactive, but never restart it on deploy — a + # restart would kill in-flight browser sessions. Consumers (gateway, + # webui) reconnect with backoff. + cp "dankclaw/hermes/quadlet/chrome-devtools-protocol.container" "$quadlet_unit_dir/" + systemctl --user daemon-reload + if ! systemctl --user is-active --quiet chrome-devtools-protocol.service; then + systemctl --user start chrome-devtools-protocol.service + fi # The units run the image under a localhost/-aliased tag: this host's # quadlet path can fail to resolve registry-prefixed refs from local # storage at create time. Tag first, then (re)create via deploy. @@ -583,7 +592,6 @@ deploy_hermes() { for unit in hermes hermes-dashboard hermes-webui; do cp "dankclaw/hermes/quadlet/$unit.container" "$quadlet_unit_dir/" done - systemctl --user daemon-reload if ! systemctl --user is-active --quiet hermes.service; then podman stop hermes hermes-dashboard hermes-webui 2>/dev/null || true podman rm hermes hermes-dashboard hermes-webui 2>/dev/null || true diff --git a/serverAppConfigs-docs/Dankclaw/Hermes.md b/serverAppConfigs-docs/Dankclaw/Hermes.md index aeb3c47..a95e417 100644 --- a/serverAppConfigs-docs/Dankclaw/Hermes.md +++ b/serverAppConfigs-docs/Dankclaw/Hermes.md @@ -43,10 +43,24 @@ both containers. The tracked, sanitized configuration baseline is - Model choice drives quality; the local `frank-default-large` (Dankclaw, 7770) is the intended backend. - Do not expose the gateway without auth — put it behind [[Caddy]] with basic auth. +## Browser automation (Chrome DevTools Protocol daemon) + +The agent's browser tool is an MCP client (`mcp_tool.py` in the image) that +spawns `npx -y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222` +per session; the browser itself lives in a dedicated daemon container, the +digest-pinned headless-shell unit `chrome-devtools-protocol.container` (host +network, loopback-only bind on 127.0.0.1:9222). Both consumers — the Signal +gateway and the webui server — run in the host network namespace, so they can +dial the daemon on loopback; nothing on the LAN can reach the CDP endpoint. +The wiring lives in the live `/data/config.yaml` under `mcp_servers.chrome-devtools` +(command `npx`, args `-y chrome-devtools-mcp@latest --browserUrl=http://127.0.0.1:9222`). +The npx package cache lives in the container layer, so a recreation (not just a +restart) pays one first-call npm download; subsequent calls are cached. + ## See Also - [[Hermes-Dashboard]] — the LAN browser management UI - [[Hermes-WebUI]] — the Hermex server (phone-facing API, SSH-tunneled) - [[Dankclaw Stack]] - [[OpenWebUI]] — the chat UI that pairs with Hermes -- Knowledge base: [[SearXNG]] (Hermes as the search agent) \ No newline at end of file +- Knowledge base: [[SearXNG]] (Hermes as the search agent)