atlas-iac/services/hermes/deployment.yaml
jenkins 9965b34534 feat(hermes): take the Anthropic credential from Vault
The Claude subscription OAuth token was created as a manual kubectl Secret in
the interest of demo time, with migration to Vault agreed as follow-up. The
value now lives at kv/atlas/hermes/agent-tokens and is injected as a file.

The hermes role gains that path and binds the hermes-triage service account
the deployment actually runs as; it previously bound only hermes-vault. The
init container prefers the Vault file and falls back to the Secret, so this
can be rolled back by removing the annotations alone, and the Secret should be
deleted once Vault has been serving it for a while.

Vault was reachable all along without the operator credential: Ariadne already
holds a vault-admin Kubernetes auth role, which is how the value was written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:14:52 -03:00

356 lines
13 KiB
YAML

# services/hermes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hermes
namespace: hermes
labels:
app: hermes
spec:
replicas: 1
revisionHistoryLimit: 2
progressDeadlineSeconds: 2700
strategy:
type: Recreate
selector:
matchLabels:
app: hermes
template:
metadata:
labels:
app: hermes
annotations:
ai.bstein.dev/frontend-fix: scope PTY attachment by selected conversation
ai.bstein.dev/model: openai-codex/gpt-5.6-terra with local gpt-oss:20b fallback
ai.bstein.dev/role: testing-triage
ai.bstein.dev/placement: arm64 gateway lane (rpi5 preferred)
ai.bstein.dev/config-rev: "20260804-root-operator-docs"
# The Anthropic credential comes from Vault rather than a manually
# created Secret. The role is declared in
# services/vault/scripts/vault_k8s_auth_configure.sh and bound to
# this pod's service account.
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes
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-pre-populate-only: "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-triage
automountServiceAccountToken: true
securityContext:
fsGroup: 10000
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: atlas.bstein.dev/spillover
operator: DoesNotExist
- weight: 90
preference:
matchExpressions:
- key: hardware
operator: In
values:
- rpi5
- weight: 50
preference:
matchExpressions:
- key: hardware
operator: In
values:
- rpi4
initContainers:
- name: init-config
image: busybox:1.37
imagePullPolicy: IfNotPresent
env:
# When the Flux/Vault-managed shared key Secret exists, it becomes
# the API_SERVER_KEY in the persistent .env (which overrides pod
# env at runtime). Optional: absent Secret keeps the old behavior
# of generating a random key on first boot.
- name: API_SERVER_KEY_SEED
valueFrom:
secretKeyRef:
name: hermes-api-server-key
key: api-key
optional: true
# Fallback only; Vault is preferred when its file is present.
- name: CLAUDE_CODE_OAUTH_TOKEN_SEED
valueFrom:
secretKeyRef:
name: hermes-anthropic-token
key: token
optional: true
command:
- sh
- -c
- |
set -eu
mkdir -p /opt/data/workspace/triage-proof /opt/data/home/.local/bin /opt/data/logs
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
cp /config/HERMES-CAPABILITIES.md /opt/data/workspace/HERMES-CAPABILITIES.md
cp /guide/OPERATOR-RUNBOOK.md /opt/data/workspace/HERMES-OPERATOR-RUNBOOK.md
cp /config/ATLAS-TRIAGE-PROOFS.md /opt/data/workspace/triage-proof/ATLAS-TRIAGE-PROOFS.md
cp /config/START-HERE.md /opt/data/START-HERE.md
cp /config/HERMES-CAPABILITIES.md /opt/data/HERMES-CAPABILITIES.md
cp /guide/OPERATOR-RUNBOOK.md /opt/data/HERMES-OPERATOR-RUNBOOK.md
cp /config/ATLAS-TRIAGE-PROOFS.md /opt/data/ATLAS-TRIAGE-PROOFS.md
touch /opt/data/.env
if [ -n "${API_SERVER_KEY_SEED:-}" ]; then
grep -v '^API_SERVER_KEY=' /opt/data/.env > /opt/data/.env.tmp || true
printf 'API_SERVER_KEY=%s\n' "${API_SERVER_KEY_SEED}" >> /opt/data/.env.tmp
mv /opt/data/.env.tmp /opt/data/.env
fi
if ! grep -q '^API_SERVER_KEY=' /opt/data/.env; then
api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
printf '\nAPI_SERVER_KEY=%s\n' "${api_key}" >> /opt/data/.env
fi
# Anthropic credential: Vault first, then the manual Secret. The
# Secret is retained only as a rollback path while the migration
# settles; delete it once Vault has been serving for a while.
anthropic=""
if [ -r /vault/secrets/anthropic-token ]; then
anthropic="$(cat /vault/secrets/anthropic-token)"
elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN_SEED:-}" ]; then
anthropic="${CLAUDE_CODE_OAUTH_TOKEN_SEED}"
fi
if [ -n "${anthropic}" ]; then
grep -v '^CLAUDE_CODE_OAUTH_TOKEN=' /opt/data/.env > /opt/data/.env.tmp || true
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${anthropic}" >> /opt/data/.env.tmp
mv /opt/data/.env.tmp /opt/data/.env
fi
chmod 0600 /opt/data/.env
chown -R 10000:10000 /opt/data
securityContext:
runAsUser: 0
runAsGroup: 0
volumeMounts:
- name: home
mountPath: /opt/data
- name: config
mountPath: /config
- name: operator-guide
mountPath: /guide
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
- name: install-kubectl
image: bitnami/kubectl@sha256:554ab88b1858e8424c55de37ad417b16f2a0e65d1607aa0f3fe3ce9b9f10b131
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
set -e
cp "$(command -v kubectl)" /tools/kubectl
chmod 0755 /tools/kubectl
chown 10000:10000 /tools/kubectl
securityContext:
runAsUser: 0
runAsGroup: 0
volumeMounts:
- name: tools
mountPath: /tools
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
containers:
- name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
imagePullPolicy: IfNotPresent
args:
- gateway
- run
ports:
- name: api
containerPort: 8642
protocol: TCP
- name: dashboard
containerPort: 9119
protocol: TCP
env:
- name: HERMES_HOME
value: /opt/data
- name: HOME
value: /opt/data/home
- name: PATH
value: /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.bstein.dev
- name: HERMES_DASHBOARD_OIDC_ISSUER
value: https://sso.bstein.dev/realms/atlas
- name: HERMES_DASHBOARD_OIDC_CLIENT_ID
value: hermes-dashboard
- name: HERMES_DASHBOARD_OIDC_SCOPES
value: openid profile email groups
- name: API_SERVER_ENABLED
value: "true"
- name: API_SERVER_HOST
value: 0.0.0.0
- name: API_SERVER_PORT
value: "8642"
- name: API_SERVER_CORS_ORIGINS
value: https://agent.bstein.dev
- name: VICTORIA_METRICS_URL
value: http://victoria-metrics-single-server.monitoring.svc.cluster.local:8428
- name: ARIADNE_BASE_URL
value: http://ariadne.maintenance.svc.cluster.local
- name: JENKINS_BASE_URL
value: http://jenkins.jenkins.svc.cluster.local:8080
- name: GITEA_BASE_URL
value: https://scm.bstein.dev
- name: GRAFANA_BASE_URL
value: https://metrics.bstein.dev
# Claude subscription OAuth token (sk-ant-oat01...). The anthropic
# provider accepts ANTHROPIC_API_KEY, ANTHROPIC_TOKEN, or this, in
# that order; an OAuth token is not an API key, so it must arrive
# under this name. Optional, so Hermes still starts without it and
# falls back to openai-codex.
# TODO: migrate to Vault alongside the Gitea token; this manual
# Secret was created in the interest of demo time.
volumeMounts:
- name: home
mountPath: /opt/data
- name: tools
mountPath: /usr/local/bin/kubectl
subPath: kubectl
- name: triage-skill
mountPath: /opt/data/workspace/skills/triage-titan-test-failures
readOnly: true
- name: mastery-skill
mountPath: /opt/data/workspace/skills/master-hermes-on-atlas
readOnly: true
- name: service-health-skill
mountPath: /opt/data/workspace/skills/triage-atlas-service-health
readOnly: true
- name: alert-tuning-skill
mountPath: /opt/data/workspace/skills/tune-atlas-alerts
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
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
volumes:
- name: home
persistentVolumeClaim:
claimName: hermes-home
- name: config
configMap:
name: hermes-config
- name: operator-guide
configMap:
name: hermes-operator-guide
- name: tools
emptyDir: {}
- name: triage-skill
configMap:
name: hermes-triage-skill
items:
- key: SKILL.md
path: SKILL.md
- key: openai.yaml
path: agents/openai.yaml
- name: mastery-skill
configMap:
name: hermes-mastery-skill
items:
- key: SKILL.md
path: SKILL.md
- key: openai.yaml
path: agents/openai.yaml
- key: architecture.md
path: references/architecture.md
- key: curriculum.md
path: references/curriculum.md
- key: incident-drills.md
path: references/incident-drills.md
- key: mastery-rubric.md
path: references/mastery-rubric.md
- key: two-hour-proof-sprint.md
path: references/two-hour-proof-sprint.md
- name: service-health-skill
configMap:
name: hermes-service-health-skill
items:
- key: SKILL.md
path: SKILL.md
- key: openai.yaml
path: agents/openai.yaml
- key: service-map.md
path: references/service-map.md
- name: alert-tuning-skill
configMap:
name: hermes-alert-tuning-skill
items:
- key: SKILL.md
path: SKILL.md
- key: openai.yaml
path: agents/openai.yaml
- key: alert-review.md
path: references/alert-review.md