fix(hermes): run coding workers unattended
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 193

This commit is contained in:
jenkins 2026-08-10 02:15:34 -03:00
parent 17e2ecfcf6
commit 2c51b46b7f
8 changed files with 116 additions and 22 deletions

View File

@ -4,6 +4,13 @@ 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.
RUN apt-get update \
&& apt-get install -y --no-install-recommends bubblewrap \
&& rm -rf /var/lib/apt/lists/*
# Keep a credential-free search provider available for private chat tenants.
# Paid/provider-backed search remains selectable through normal Hermes config.
RUN uv pip install --python /opt/hermes/.venv/bin/python ddgs==9.14.4

View File

@ -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:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
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:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
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:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
@ -243,12 +243,13 @@ spec:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
- name: install-herdr-integrations
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command:
- sh
- -ec
- |
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
herdr integration install codex
herdr integration install claude
herdr integration install hermes
@ -257,7 +258,7 @@ spec:
- {name: HOME, value: /opt/data/home}
- {name: CODEX_HOME, value: /opt/data/home/.codex}
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
- {name: PATH, value: /opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
@ -266,11 +267,12 @@ spec:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 128Mi}
- name: prepare-ttyd-index
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
@ -292,7 +294,7 @@ spec:
limits: {cpu: 250m, memory: 128Mi}
containers:
- name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command: [/opt/hermes/.venv/bin/hermes]
args: [gateway, run, --no-supervise]
@ -307,7 +309,7 @@ spec:
- {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml}
- {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PATH, value: /opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/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"}
@ -407,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:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
@ -442,9 +444,10 @@ spec:
- {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/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: tmp, mountPath: /tmp}
- {name: ttyd-index, mountPath: /ttyd-index, readOnly: true}
startupProbe:
@ -475,7 +478,7 @@ spec:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 500m, memory: 512Mi}
- name: herdr-server
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command:
- sh
@ -557,7 +560,7 @@ spec:
- {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PYTHONPATH, value: /opt/hermes}
- {name: PATH, value: /opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
- {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"}
@ -579,7 +582,7 @@ spec:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: "1", memory: 2Gi}
- name: model-steward
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
image: registry.bstein.dev/bstein/hermes-agent@sha256:22d36d47a65877e0e45c3b004641e038ae10eb522e7463e791d4a00adf42b928
imagePullPolicy: IfNotPresent
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
env:

View File

@ -45,6 +45,9 @@ configMapGenerator:
- name: hermes-coordinator
namespace: hermes
files:
- claude=scripts/claude
- codex=scripts/codex
- configure_agent_clients.py=scripts/configure_agent_clients.py
- gitea_askpass.sh=scripts/gitea_askpass.sh
- herdr_dispatch.py=scripts/herdr_dispatch.py
- herdr_tab_router.py=scripts/herdr_tab_router.py

9
services/hermes/scripts/claude Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
# Run Claude Code unattended inside the externally isolated Hermes agent pod.
set -eu
exec /opt/data/tools/bin/claude \
--dangerously-skip-permissions \
--permission-mode bypassPermissions \
--autocompact auto \
"$@"

8
services/hermes/scripts/codex Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
# Run Codex unattended inside the externally isolated Hermes agent pod.
set -eu
exec /opt/data/tools/bin/codex \
--dangerously-bypass-approvals-and-sandbox \
--dangerously-bypass-hook-trust \
"$@"

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Apply persistent non-interactive settings for managed coding clients."""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
def configure_claude_state(path: Path) -> None:
"""Disable Claude's long-session resume chooser without losing state."""
value: dict[str, Any] = {}
if path.is_file():
try:
loaded = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
loaded = {}
if isinstance(loaded, dict):
value = loaded
value["resumeReturnDismissed"] = True
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
os.chmod(path, 0o600)
def main() -> None:
"""Configure provider clients under the persistent Hermes home."""
home = Path(os.environ.get("CLAUDE_CONFIG_DIR", "/opt/data/home/.claude"))
configure_claude_state(home / ".claude.json")
if __name__ == "__main__":
main()

View File

@ -164,9 +164,6 @@ def launch_worker(
plan["model"],
"-c",
f'model_reasoning_effort="{plan["effort"]}"',
"--approve-for-me",
"--sandbox",
"workspace-write",
]
)
else:
@ -176,8 +173,6 @@ def launch_worker(
plan["model"],
"--effort",
plan["effort"],
"--permission-mode",
"auto",
]
)
started = _run(command, env)

View File

@ -27,6 +27,7 @@ dispatch = _load("herdr_dispatch")
tab_router = _load("herdr_tab_router")
auth_patch = _load("patch_hermes_auth")
ttyd_patch = _load("patch_ttyd_index")
client_config = _load("configure_agent_clients")
def test_herdr_plan_chooses_task_shape_and_caps_effort():
@ -113,10 +114,16 @@ def test_claude_worker_waits_for_prompt_readiness(tmp_path: Path, monkeypatch):
assert workspace_create[-3:] == ["--label", "project-claude-review", "--no-focus"]
start = calls[1]
assert start[1:4] == ["agent", "start", "project-claude-review"]
assert start[-2:] == ["--permission-mode", "auto"]
assert start[-4:] == [
"--model",
"claude-haiku-4-5-20251001",
"--effort",
"low",
]
assert "--permission-mode" not in start
def test_codex_worker_uses_automatic_review_without_prompts(tmp_path: Path, monkeypatch):
def test_codex_worker_leaves_inner_policy_to_unattended_wrapper(tmp_path: Path, monkeypatch):
herdr = tmp_path / "herdr"
herdr.touch()
project = tmp_path / "cassandra"
@ -141,11 +148,38 @@ def test_codex_worker_uses_automatic_review_without_prompts(tmp_path: Path, monk
assert calls[0][-3:] == ["--label", "cassandra-codex-api-fix", "--no-focus"]
start = calls[1]
assert start[1:4] == ["agent", "start", "cassandra-codex-api-fix"]
assert "--approve-for-me" in start
assert start[-2:] == ["--sandbox", "workspace-write"]
assert "--dangerously-bypass-approvals-and-sandbox" not in start
assert "--dangerously-bypass-hook-trust" not in start
assert "--approve-for-me" not in start
assert "--sandbox" not in start
assert not any("on-request" in value for value in start)
def test_provider_wrappers_force_unattended_modes():
codex = (HERMES / "scripts/codex").read_text(encoding="utf-8")
claude = (HERMES / "scripts/claude").read_text(encoding="utf-8")
assert "/opt/data/tools/bin/codex" in codex
assert "--dangerously-bypass-approvals-and-sandbox" in codex
assert "--dangerously-bypass-hook-trust" in codex
assert "/opt/data/tools/bin/claude" in claude
assert "--dangerously-skip-permissions" in claude
assert "--permission-mode bypassPermissions" in claude
assert "--autocompact auto" in claude
def test_claude_resume_prompt_is_persistently_suppressed(tmp_path: Path):
state = tmp_path / ".claude.json"
state.write_text('{"promptQueueUseCount": 4}\n', encoding="utf-8")
client_config.configure_claude_state(state)
value = yaml.safe_load(state.read_text(encoding="utf-8"))
assert value["promptQueueUseCount"] == 4
assert value["resumeReturnDismissed"] is True
assert state.stat().st_mode & 0o777 == 0o600
def test_agent_config_uses_bounded_noninteractive_approvals():
configmap = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())
config = yaml.safe_load(configmap["data"]["config.yaml"])