fix(hermes): provision Cassandra diagnostics
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 179

This commit is contained in:
jenkins 2026-08-09 14:38:23 -03:00
parent be2aa52d51
commit c3ad73bd20
8 changed files with 163 additions and 4 deletions

View File

@ -0,0 +1,52 @@
# services/cassandra/hermes-agent-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: hermes-agent-readonly
namespace: cassandra
rules:
- apiGroups: [""]
resources:
- configmaps
- endpoints
- events
- persistentvolumeclaims
- pods
- pods/log
- replicationcontrollers
- services
verbs: ["get", "list", "watch"]
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources:
- daemonsets
- deployments
- replicasets
- statefulsets
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources:
- cronjobs
- jobs
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources:
- ingresses
- networkpolicies
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: hermes-agent-readonly
namespace: cassandra
subjects:
- kind: ServiceAccount
name: hermes-agent
namespace: hermes
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: hermes-agent-readonly

View File

@ -11,6 +11,7 @@ resources:
- limitrange.yaml - limitrange.yaml
- configmap.yaml - configmap.yaml
- rbac.yaml - rbac.yaml
- hermes-agent-rbac.yaml
- artifacts-pvc.yaml - artifacts-pvc.yaml
- postgres-service.yaml - postgres-service.yaml
- postgres-statefulset.yaml - postgres-statefulset.yaml

View File

@ -215,9 +215,11 @@ data:
objective's difficulty warrants it; otherwise synthesize at the original objective's difficulty warrants it; otherwise synthesize at the original
effort. effort.
This pod has no Kubernetes RBAC. Do not use it for automated test intake or This pod has Cassandra-scoped, read-only Kubernetes access for workloads,
cluster mutation. Triage belongs at triage.hermes.bstein.dev and changes to logs, events, networking, and rollout diagnosis. It cannot read Secrets,
Atlas are delivered through the titan-iac Git/Flux workflow. exec into pods, or mutate the cluster. Triage belongs at
triage.hermes.bstein.dev and all Atlas changes are delivered through the
titan-iac Git/Flux workflow.
START-HERE.md: | START-HERE.md: |
# Agent Hermes # Agent Hermes

View File

@ -24,7 +24,7 @@ spec:
ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback 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/placement: rpi5 preferred; Jetson deferred until state storage is available
ai.bstein.dev/config-rev: "20260809-browser-runtime-complete-resume" ai.bstein.dev/config-rev: "20260809-cassandra-readonly"
vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@ -88,6 +88,7 @@ spec:
/opt/data/home/.claude \ /opt/data/home/.claude \
/opt/data/home/.codex \ /opt/data/home/.codex \
/opt/data/home/.config/herdr \ /opt/data/home/.config/herdr \
/opt/data/home/.kube \
/opt/data/herdr \ /opt/data/herdr \
/opt/data/logs \ /opt/data/logs \
/opt/data/tools/bin \ /opt/data/tools/bin \
@ -172,6 +173,14 @@ spec:
@anthropic-ai/claude-code@2.1.226 @anthropic-ai/claude-code@2.1.226
touch "${tools}/.cli-versions-0.147.0-2.1.226" touch "${tools}/.cli-versions-0.147.0-2.1.226"
fi fi
kubectl_version="$("${tools}/bin/kubectl" version --client --output=json 2>/dev/null || true)"
case "${kubectl_version}" in *\"gitVersion\":\"v1.33.3\"*) kubectl_ready=1 ;; *) kubectl_ready=0 ;; esac
if [ "${kubectl_ready}" != "1" ]; then
curl -fsSL -o "${tools}/bin/kubectl.tmp" https://dl.k8s.io/release/v1.33.3/bin/linux/arm64/kubectl
printf '%s %s\n' 3d514dbae5dc8c09f773df0ef0f5d449dfad05b3aca5c96b13565f886df345fd "${tools}/bin/kubectl.tmp" | sha256sum -c -
chmod 0755 "${tools}/bin/kubectl.tmp"
mv "${tools}/bin/kubectl.tmp" "${tools}/bin/kubectl"
fi
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
runAsUser: 10000 runAsUser: 10000
@ -276,6 +285,7 @@ spec:
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude} - {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
- {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml} - {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml}
- {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock} - {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/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: 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_EXECUTABLE_PATH, value: /opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell}
@ -292,6 +302,7 @@ spec:
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true} - {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py} - {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: 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: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
startupProbe: startupProbe:
tcpSocket: {port: api} tcpSocket: {port: api}
@ -480,6 +491,7 @@ spec:
--env HERMES_HOME=/opt/data \ --env HERMES_HOME=/opt/data \
--env HERMES_AUTH_FILE=/shared-auth/auth.json \ --env HERMES_AUTH_FILE=/shared-auth/auth.json \
--env HOME=/opt/data/home \ --env HOME=/opt/data/home \
--env KUBECONFIG=/opt/data/home/.kube/config \
--env PYTHONPATH=/opt/hermes \ --env PYTHONPATH=/opt/hermes \
--env PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright \ --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_EXECUTABLE_PATH=/opt/hermes/.playwright/chromium_headless_shell-1228/chrome-linux/headless_shell \
@ -519,6 +531,7 @@ spec:
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude} - {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
- {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml} - {name: HERDR_CONFIG_PATH, value: /opt/data/home/.config/herdr/config.toml}
- {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock} - {name: HERDR_SOCKET_PATH, value: /opt/data/herdr/herdr.sock}
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PYTHONPATH, value: /opt/hermes} - {name: PYTHONPATH, value: /opt/hermes}
- {name: PATH, value: /opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} - {name: PATH, value: /opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
- {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright} - {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
@ -536,6 +549,7 @@ spec:
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true} - {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py} - {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: 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: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
resources: resources:
requests: {cpu: 50m, memory: 128Mi} requests: {cpu: 50m, memory: 128Mi}
@ -577,6 +591,10 @@ spec:
configMap: configMap:
name: hermes-coordinator name: hermes-coordinator
defaultMode: 0555 defaultMode: 0555
- name: kubeconfig
configMap:
name: hermes-agent-kubeconfig
defaultMode: 0444
- name: auth-patch - name: auth-patch
emptyDir: {} emptyDir: {}
- name: auto-router-plugin - name: auto-router-plugin

View File

@ -0,0 +1,19 @@
# services/hermes/agent-kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: atlas
cluster:
server: https://kubernetes.default.svc
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
users:
- name: hermes-agent
user:
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
contexts:
- name: cassandra-readonly
context:
cluster: atlas
user: hermes-agent
namespace: cassandra
current-context: cassandra-readonly

View File

@ -52,6 +52,12 @@ configMapGenerator:
- patch_hermes_auth.py=scripts/patch_hermes_auth.py - patch_hermes_auth.py=scripts/patch_hermes_auth.py
options: options:
disableNameSuffixHash: true disableNameSuffixHash: true
- name: hermes-agent-kubeconfig
namespace: hermes
files:
- config=agent-kubeconfig.yaml
options:
disableNameSuffixHash: true
- name: hermes-auto-router-plugin - name: hermes-auto-router-plugin
namespace: hermes namespace: hermes
files: files:

View File

@ -108,6 +108,20 @@ spec:
app: hermes-model-gate app: hermes-model-gate
ports: ports:
- {protocol: TCP, port: 8080} - {protocol: TCP, port: 8080}
- to:
- ipBlock:
cidr: 10.43.0.1/32
ports:
- {protocol: TCP, port: 443}
- to:
- ipBlock:
cidr: 192.168.22.11/32
- ipBlock:
cidr: 192.168.22.12/32
- ipBlock:
cidr: 192.168.22.13/32
ports:
- {protocol: TCP, port: 6443}
- to: - to:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:

View File

@ -343,6 +343,53 @@ def test_agent_mounts_auto_router_into_both_hermes_runtimes():
assert volume["configMap"]["name"] == "hermes-auto-router-plugin" assert volume["configMap"]["name"] == "hermes-auto-router-plugin"
def test_agent_has_cassandra_readonly_kubernetes_context():
deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
pod = deployment["spec"]["template"]["spec"]
containers = {container["name"]: container for container in pod["containers"]}
installer = next(
container
for container in pod["initContainers"]
if container["name"] == "install-agent-tools"
)
install_command = installer["command"][-1]
assert "v1.33.3/bin/linux/arm64/kubectl" in install_command
assert "3d514dbae5dc8c09f773df0ef0f5d449" in install_command
for name in ("hermes", "herdr-server"):
env = {item["name"]: item["value"] for item in containers[name]["env"]}
assert env["KUBECONFIG"] == "/opt/data/home/.kube/config"
mounts = {
mount["mountPath"]: mount for mount in containers[name]["volumeMounts"]
}
assert mounts["/opt/data/home/.kube/config"]["name"] == "kubeconfig"
assert mounts["/opt/data/home/.kube/config"]["readOnly"] is True
config = yaml.safe_load((HERMES / "agent-kubeconfig.yaml").read_text())
assert config["current-context"] == "cassandra-readonly"
assert config["contexts"][0]["context"]["namespace"] == "cassandra"
assert config["users"][0]["user"] == {
"tokenFile": "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
rbac_path = HERMES.parent / "cassandra" / "hermes-agent-rbac.yaml"
documents = [document for document in yaml.safe_load_all(rbac_path.read_text()) if document]
role = next(document for document in documents if document["kind"] == "Role")
binding = next(document for document in documents if document["kind"] == "RoleBinding")
assert role["metadata"]["namespace"] == "cassandra"
assert binding["metadata"]["namespace"] == "cassandra"
assert binding["subjects"] == [
{"kind": "ServiceAccount", "name": "hermes-agent", "namespace": "hermes"}
]
assert "secrets" not in {
resource for rule in role["rules"] for resource in rule["resources"]
}
assert {
verb for rule in role["rules"] for verb in rule["verbs"]
} == {"get", "list", "watch"}
def test_agent_reconnect_renders_complete_conversation_history(): def test_agent_reconnect_renders_complete_conversation_history():
documents = [ documents = [
document document