atlas-iac/services/hermes/agent-deployment.yaml

360 lines
15 KiB
YAML

# services/hermes/agent-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hermes-agent
namespace: hermes
labels:
app: hermes-agent
spec:
replicas: 1
revisionHistoryLimit: 2
progressDeadlineSeconds: 2700
strategy:
type: Recreate
selector:
matchLabels:
app: hermes-agent
template:
metadata:
labels:
app: hermes-agent
annotations:
ai.bstein.dev/role: project-coordinator
ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code
ai.bstein.dev/model-policy: difficulty-aware low through xhigh, cross-provider fallback
ai.bstein.dev/placement: titan-20 preferred, Jetson preferred, arm64 fallback
ai.bstein.dev/config-rev: "20260808-herdr-coordinator"
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
vault.hashicorp.com/agent-inject-template-anthropic-token: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{ .Data.data.anthropic_oauth_token }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-gitea-token: kv/data/atlas/hermes/agent-tokens
vault.hashicorp.com/agent-inject-template-gitea-token: |
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
{{- with index .Data.data "gitea_token" -}}
{{ . }}
{{- end -}}
{{- end }}
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-requests-cpu: 25m
vault.hashicorp.com/agent-requests-mem: 32Mi
vault.hashicorp.com/agent-limits-cpu: 100m
vault.hashicorp.com/agent-limits-mem: 128Mi
spec:
serviceAccountName: hermes-agent
automountServiceAccountToken: true
securityContext:
fsGroup: 10000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: [arm64]
- key: node-role.kubernetes.io/worker
operator: In
values: ["true"]
- key: kubernetes.io/hostname
operator: NotIn
values: [titan-13, titan-15, titan-17, titan-18, titan-19]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [titan-20]
- weight: 90
preference:
matchExpressions:
- key: jetson
operator: In
values: ["true"]
- weight: 50
preference:
matchExpressions:
- key: hardware
operator: In
values: [rpi5]
initContainers:
- name: init-config
image: busybox:1.37
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
env_file=/opt/data/.env
mkdir -p \
/opt/data/home/.claude \
/opt/data/home/.codex \
/opt/data/home/.config/herdr \
/opt/data/herdr \
/opt/data/logs \
/opt/data/tools/bin \
/opt/data/workspace/coordinator \
/opt/data/workspace/projects \
/opt/data/workspace/skills
cp /config/config.yaml /opt/data/config.yaml
cp /config/SOUL.md /opt/data/SOUL.md
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
cp /config/START-HERE.md /opt/data/workspace/START-HERE.md
touch "${env_file}"
upsert_env() {
key="$1"
value="$2"
{ grep -v "^${key}=" "${env_file}" || true; printf '%s=%s\n' "${key}" "${value}"; } > "${env_file}.tmp"
mv "${env_file}.tmp" "${env_file}"
}
if ! grep -q '^API_SERVER_KEY=' "${env_file}"; then
api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
upsert_env API_SERVER_KEY "${api_key}"
fi
if [ -s /vault/secrets/anthropic-token ]; then
token="$(tr -d '\r\n' < /vault/secrets/anthropic-token)"
[ -z "${token}" ] || upsert_env CLAUDE_CODE_OAUTH_TOKEN "${token}"
fi
if [ -s /vault/secrets/gitea-token ]; then
token="$(tr -d '\r\n' < /vault/secrets/gitea-token)"
case "${token}" in ""|"<no value>"|"<nil>") ;; *) upsert_env GITEA_TOKEN "${token}" ;; esac
fi
upsert_env GITEA_USERNAME bstein
upsert_env GIT_ASKPASS /opt/coordinator/gitea_askpass.sh
upsert_env GIT_TERMINAL_PROMPT 0
chmod 0600 "${env_file}"
chown -R 10000:10000 /opt/data
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: home
mountPath: /opt/data
- name: config
mountPath: /config
readOnly: true
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi}
- name: install-agent-tools
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
tools=/opt/data/tools
mkdir -p "${tools}/bin"
herdr_version="$("${tools}/bin/herdr" --version 2>/dev/null || true)"
case "${herdr_version}" in *0.8.0*) herdr_ready=1 ;; *) herdr_ready=0 ;; esac
if [ "${herdr_ready}" != "1" ]; then
curl -fsSL -o "${tools}/bin/herdr.tmp" https://github.com/herdrdev/herdr/releases/download/v0.8.0/herdr-linux-aarch64
printf '%s %s\n' f647ac66468d9efbc642fe534fb284468f0aea60641606fc008dfc0d82a3ca87 "${tools}/bin/herdr.tmp" | sha256sum -c -
chmod 0755 "${tools}/bin/herdr.tmp"
mv "${tools}/bin/herdr.tmp" "${tools}/bin/herdr"
fi
if [ ! -f "${tools}/.cli-versions-0.147.0-2.1.226" ]; then
npm install --global --omit=dev --no-audit --no-fund --prefix "${tools}" \
@openai/codex@0.147.0 \
@anthropic-ai/claude-code@2.1.226
touch "${tools}/.cli-versions-0.147.0-2.1.226"
fi
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: home
mountPath: /opt/data
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: "1", memory: 1Gi}
- name: patch-auth
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/patch_hermes_auth.py
- /opt/hermes/hermes_cli/auth.py
- /patched/auth.py
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: coordinator
mountPath: /opt/coordinator
readOnly: true
- name: auth-patch
mountPath: /patched
resources:
requests: {cpu: 25m, memory: 64Mi}
limits: {cpu: 100m, memory: 128Mi}
- name: bootstrap-coordinator
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
command:
- /opt/hermes/.venv/bin/python
- /opt/coordinator/hermes_coordinator.py
- --once
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
- {name: HOME, value: /opt/data/home}
- {name: PYTHONPATH, value: /opt/hermes}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
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}
resources:
requests: {cpu: 50m, memory: 128Mi}
limits: {cpu: 500m, memory: 512Mi}
containers:
- name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
args: [gateway, run]
ports:
- {name: dashboard, containerPort: 9119, protocol: TCP}
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
- {name: HOME, value: /opt/data/home}
- {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/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}
- {name: HERMES_DASHBOARD, value: "1"}
- {name: HERMES_DASHBOARD_HOST, value: 0.0.0.0}
- {name: HERMES_DASHBOARD_PORT, value: "9119"}
- {name: HERMES_DASHBOARD_PUBLIC_URL, value: https://agent.hermes.bstein.dev}
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}
readinessProbe:
httpGet: {path: /api/status, port: dashboard}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet: {path: /api/status, port: dashboard}
initialDelaySeconds: 90
periodSeconds: 30
timeoutSeconds: 10
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
requests: {cpu: 500m, memory: 1Gi}
limits: {cpu: "2", memory: 4Gi}
- name: herdr-server
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
set -a
. /opt/data/.env
set +a
herdr server &
server_pid=$!
trap 'kill "${server_pid}" 2>/dev/null || true' TERM INT
for attempt in $(seq 1 60); do
if herdr status server >/dev/null 2>&1; then break; fi
sleep 1
done
herdr integration install codex || true
herdr integration install claude || true
wait "${server_pid}"
env:
- {name: HOME, value: /opt/data/home}
- {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/data/tools/bin:/usr/local/bin:/usr/bin:/bin}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- {name: home, mountPath: /opt/data}
resources:
requests: {cpu: 100m, memory: 256Mi}
limits: {cpu: "1", memory: 2Gi}
- name: model-steward
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
env:
- {name: HERMES_HOME, value: /opt/data}
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
- {name: HOME, value: /opt/data/home}
- {name: PYTHONPATH, value: /opt/hermes}
securityContext:
allowPrivilegeEscalation: false
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
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}
resources:
requests: {cpu: 25m, memory: 128Mi}
limits: {cpu: 250m, memory: 512Mi}
volumes:
- name: home
persistentVolumeClaim:
claimName: hermes-agent-home
- name: provider-auth
persistentVolumeClaim:
claimName: hermes-provider-auth
- name: config
configMap:
name: hermes-agent-config
- name: coordinator
configMap:
name: hermes-coordinator
defaultMode: 0555
- name: auth-patch
emptyDir: {}