diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index 5c596df15..1df292e14 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -5,10 +5,10 @@ FROM nousresearch/hermes-agent@sha256:9c841866021c54c4596849f6135717e8a4d52ba510 USER root # Codex uses the distribution bubblewrap package when a bounded sandbox is -# requested. Full-access workers rely on the pod as their isolation boundary, -# but keep bwrap installed for diagnostics and any future restricted profile. +# requested. tmux keeps the stock Hermes TUI alive across browser reconnects; +# Herdr remains a background worker transport instead of the visible shell. RUN apt-get update \ - && apt-get install -y --no-install-recommends bubblewrap \ + && apt-get install -y --no-install-recommends bubblewrap tmux \ && rm -rf /var/lib/apt/lists/* # Keep a credential-free search provider available for private chat tenants. diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index d540c4e15..dbe05aa3b 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -21,10 +21,10 @@ spec: app: hermes-agent annotations: ai.bstein.dev/role: project-coordinator - ai.bstein.dev/execution: Hermes delegation with durable Codex and Claude Code CLI workers + ai.bstein.dev/execution: Stock Hermes TUI with durable Codex and Claude Code CLI workers ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available - ai.bstein.dev/config-rev: "20260810-per-subtask-routing" + ai.bstein.dev/config-rev: "20260810-stock-tui-per-subtask-routing" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: hermes-agent vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens @@ -142,7 +142,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 100m, memory: 64Mi} - name: install-agent-tools - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - sh @@ -194,7 +194,7 @@ spec: requests: {cpu: 100m, memory: 256Mi} limits: {cpu: "1", memory: 1Gi} - name: patch-auth - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -217,7 +217,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: bootstrap-coordinator - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -243,7 +243,7 @@ spec: requests: {cpu: 50m, memory: 128Mi} limits: {cpu: 500m, memory: 512Mi} - name: install-herdr-integrations - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - sh @@ -272,7 +272,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 250m, memory: 128Mi} - name: prepare-ttyd-index - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -294,7 +294,7 @@ spec: limits: {cpu: 250m, memory: 128Mi} containers: - name: hermes - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: [/opt/hermes/.venv/bin/hermes] args: [gateway, run, --no-supervise] @@ -409,7 +409,7 @@ spec: requests: {cpu: 50m, memory: 128Mi} limits: {cpu: 750m, memory: 1Gi} - name: herdr-tui - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: @@ -426,28 +426,48 @@ spec: --terminal-type xterm-256color \ --index /ttyd-index/index.html \ --client-option disableReconnect=true \ - --client-option "titleFixed=Hermes Agent - HERDR" \ + --client-option "titleFixed=Hermes Agent" \ --client-option fontSize=15 \ - /bin/sh -c ' - while true; do - /opt/data/tools/bin/herdr - exit_code=$? - printf "\r\nHerdr disconnected (exit %s); reconnecting in 2 seconds...\r\n" "${exit_code}" - sleep 2 - done - ' + /usr/bin/tmux new-session -A \ + -s hermes-coordinator \ + -c /opt/data/workspace \ + /bin/sh -lc ' + while true; do + /opt/hermes/.venv/bin/hermes \ + --continue \ + --tui \ + --yolo \ + --accept-hooks \ + --checkpoints + exit_code=$? + printf "\r\nHermes exited (exit %s); resuming in 2 seconds...\r\n" "${exit_code}" + sleep 2 + done + ' ports: - {name: herdr-tui, containerPort: 7681, protocol: TCP} env: - {name: HOME, value: /opt/data/home} + - {name: HERMES_HOME, value: /opt/data} + - {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json} - {name: CODEX_HOME, value: /opt/data/home/.codex} - {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude} - {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml} - {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock} - - {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} + - {name: KUBECONFIG, value: /opt/data/home/.kube/config} + - {name: PYTHONPATH, value: /opt/hermes} + - {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} + - {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright} + - {name: AGENT_BROWSER_EXECUTABLE_PATH, value: /opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell} + - {name: AGENT_BROWSER_ARGS, value: "--no-sandbox,--disable-dev-shm-usage"} volumeMounts: - {name: home, mountPath: /opt/data} + - {name: provider-auth, mountPath: /shared-auth} - {name: coordinator, mountPath: /opt/coordinator, readOnly: true} + - {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py} + - {name: coordinator, mountPath: /opt/data/home/.local/bin/herdr-dispatch, subPath: herdr_dispatch.py, readOnly: true} + - {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true} + - {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true} - {name: tmp, mountPath: /tmp} - {name: ttyd-index, mountPath: /ttyd-index, readOnly: true} startupProbe: @@ -478,7 +498,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 500m, memory: 512Mi} - name: herdr-server - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: - sh @@ -495,60 +515,6 @@ spec: if herdr status server >/dev/null 2>&1; then break; fi sleep 1 done - pane_file=/opt/data/herdr/coordinator-pane-id - pane="$(cat "${pane_file}" 2>/dev/null || true)" - pane_live=no - if [ -n "${pane}" ]; then - for attempt in $(seq 1 20); do - if herdr pane process-info --pane "${pane}" >/dev/null 2>&1; then - pane_live=yes - break - fi - sleep 1 - done - fi - if [ "${pane_live}" != "yes" ]; then - if [ -n "${pane}" ]; then - workspace="${pane%%:*}" - herdr workspace close "${workspace}" >/dev/null 2>&1 || true - fi - created="$(herdr workspace create \ - --cwd /opt/data/workspace \ - --label coordinator \ - --env HERMES_HOME=/opt/data \ - --env HERMES_AUTH_FILE=/shared-auth/auth.json \ - --env HOME=/opt/data/home \ - --env KUBECONFIG=/opt/data/home/.kube/config \ - --env PYTHONPATH=/opt/hermes \ - --env PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright \ - --env AGENT_BROWSER_EXECUTABLE_PATH=/opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell \ - --env AGENT_BROWSER_ARGS=--no-sandbox,--disable-dev-shm-usage \ - --focus)" - pane="$(printf '%s' "${created}" | /opt/hermes/.venv/bin/python -c \ - 'import json,sys; print(json.load(sys.stdin)["result"]["root_pane"]["pane_id"])')" - printf '%s\n' "${pane}" > "${pane_file}" - fi - active_workspace="${pane%%:*}" - coordinator_workspaces="$(herdr workspace list | /opt/hermes/.venv/bin/python -c \ - 'import json,sys; active=sys.argv[1]; payload=json.load(sys.stdin); print(" ".join(item["workspace_id"] for item in payload["result"]["workspaces"] if item.get("label") == "coordinator" and item["workspace_id"] != active))' \ - "${active_workspace}")" - for stale_workspace in ${coordinator_workspaces}; do - herdr workspace close "${stale_workspace}" >/dev/null 2>&1 || true - done - process_info="$(herdr pane process-info --pane "${pane}" 2>/dev/null || printf '%s\n' '{"result":{"process_info":{"foreground_processes":[]}}}')" - coordinator_running="$(printf '%s' "${process_info}" | /opt/hermes/.venv/bin/python -c \ - 'import json,sys; processes=json.load(sys.stdin)["result"]["process_info"].get("foreground_processes", []); print("yes" if any("hermes" in " ".join(str(p.get(k) or "") for k in ("name", "argv0", "cmdline")) for p in processes) else "no")')" - if [ "${coordinator_running}" != "yes" ]; then - herdr agent start coordinator \ - --kind hermes \ - --pane "${pane}" \ - --timeout 60000 - fi - /opt/hermes/.venv/bin/python /opt/coordinator/herdr_tab_router.py \ - --workspace-label coordinator \ - --interval 1 & - router_pid=$! - trap 'kill "${router_pid}" "${server_pid}" 2>/dev/null || true' TERM INT wait "${server_pid}" env: - {name: HERMES_HOME, value: /opt/data} @@ -582,7 +548,7 @@ spec: requests: {cpu: 50m, memory: 128Mi} limits: {cpu: "1", memory: 2Gi} - name: model-steward - image: registry.bstein.dev/bstein/hermes-agent@sha256:3f454c13b6050e590598a7e067ef6f2c9b0e7f395219b1d294e59c0020d7b50c + image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5 imagePullPolicy: IfNotPresent command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"] env: diff --git a/testing/tests/test_hermes_herdr.py b/testing/tests/test_hermes_herdr.py index 1b0edcbc1..7c39e0bf9 100644 --- a/testing/tests/test_hermes_herdr.py +++ b/testing/tests/test_hermes_herdr.py @@ -580,22 +580,19 @@ def test_agent_installs_hermes_integration_before_startup(): assert "--no-sandbox" in env["AGENT_BROWSER_ARGS"] server_command = containers["herdr-server"]["command"][-1] - assert 'herdr pane process-info --pane "${pane}"' in server_command - assert 'herdr workspace close "${workspace}"' in server_command - assert 'item.get("label") == "coordinator"' in server_command - assert 'item["workspace_id"] != active' in server_command - assert 'herdr workspace close "${stale_workspace}"' in server_command - assert "herdr agent start coordinator" in server_command - assert "/opt/coordinator/herdr_tab_router.py" in server_command - assert "--workspace-label coordinator" in server_command - assert "--kind hermes" in server_command - assert "--timeout 60000" in server_command - assert "--env AGENT_BROWSER_EXECUTABLE_PATH=" in server_command + assert "herdr server" in server_command + assert "herdr agent start coordinator" not in server_command + assert "/opt/coordinator/herdr_tab_router.py" not in server_command tui_command = containers["herdr-tui"]["args"][0] - assert "while true" in tui_command - assert "Herdr disconnected" in tui_command - assert "/opt/data/tools/bin/herdr" in tui_command + assert "/usr/bin/tmux new-session -A" in tui_command + assert "-s hermes-coordinator" in tui_command + assert "/opt/hermes/.venv/bin/hermes" in tui_command + assert "--continue" in tui_command + assert "--tui" in tui_command + assert "--yolo" in tui_command + assert "--accept-hooks" in tui_command + assert "--checkpoints" in tui_command def test_agent_mounts_auto_router_into_both_hermes_runtimes():