diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index 0ce875c8c..41a9e580d 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -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 diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index 888049ae1..a60d3518e 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -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: diff --git a/services/hermes/kustomization.yaml b/services/hermes/kustomization.yaml index 8bd1448ef..69f744ced 100644 --- a/services/hermes/kustomization.yaml +++ b/services/hermes/kustomization.yaml @@ -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 diff --git a/services/hermes/scripts/claude b/services/hermes/scripts/claude new file mode 100755 index 000000000..04f0bc25e --- /dev/null +++ b/services/hermes/scripts/claude @@ -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 \ + "$@" diff --git a/services/hermes/scripts/codex b/services/hermes/scripts/codex new file mode 100755 index 000000000..04dd307c7 --- /dev/null +++ b/services/hermes/scripts/codex @@ -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 \ + "$@" diff --git a/services/hermes/scripts/configure_agent_clients.py b/services/hermes/scripts/configure_agent_clients.py new file mode 100755 index 000000000..eb5f61a60 --- /dev/null +++ b/services/hermes/scripts/configure_agent_clients.py @@ -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() diff --git a/services/hermes/scripts/herdr_dispatch.py b/services/hermes/scripts/herdr_dispatch.py index e8e9730ca..e3d346292 100644 --- a/services/hermes/scripts/herdr_dispatch.py +++ b/services/hermes/scripts/herdr_dispatch.py @@ -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) diff --git a/testing/tests/test_hermes_herdr.py b/testing/tests/test_hermes_herdr.py index cae53961e..24132a3ca 100644 --- a/testing/tests/test_hermes_herdr.py +++ b/testing/tests/test_hermes_herdr.py @@ -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"])