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
- configmap.yaml
- rbac.yaml
- hermes-agent-rbac.yaml
- artifacts-pvc.yaml
- postgres-service.yaml
- postgres-statefulset.yaml

View File

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

View File

@ -24,7 +24,7 @@ spec:
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/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/role: hermes-agent
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/.codex \
/opt/data/home/.config/herdr \
/opt/data/home/.kube \
/opt/data/herdr \
/opt/data/logs \
/opt/data/tools/bin \
@ -172,6 +173,14 @@ spec:
@anthropic-ai/claude-code@2.1.226
touch "${tools}/.cli-versions-0.147.0-2.1.226"
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:
allowPrivilegeEscalation: false
runAsUser: 10000
@ -276,6 +285,7 @@ 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: 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: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
- {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: 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}
startupProbe:
tcpSocket: {port: api}
@ -480,6 +491,7 @@ spec:
--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 \
@ -519,6 +531,7 @@ 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: KUBECONFIG, value: /opt/data/home/.kube/config}
- {name: PYTHONPATH, value: /opt/hermes}
- {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}
@ -536,6 +549,7 @@ spec:
- {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}
resources:
requests: {cpu: 50m, memory: 128Mi}
@ -577,6 +591,10 @@ spec:
configMap:
name: hermes-coordinator
defaultMode: 0555
- name: kubeconfig
configMap:
name: hermes-agent-kubeconfig
defaultMode: 0444
- name: auth-patch
emptyDir: {}
- 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
options:
disableNameSuffixHash: true
- name: hermes-agent-kubeconfig
namespace: hermes
files:
- config=agent-kubeconfig.yaml
options:
disableNameSuffixHash: true
- name: hermes-auto-router-plugin
namespace: hermes
files:

View File

@ -108,6 +108,20 @@ spec:
app: hermes-model-gate
ports:
- {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:
- namespaceSelector:
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"
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():
documents = [
document