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>
356 lines
13 KiB
YAML
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
|