# services/hermes/chat-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: hermes-chat-tenant
  namespace: hermes
  labels:
    app: hermes-chat-tenant
spec:
  serviceName: hermes-chat-tenant
  replicas: 4
  podManagementPolicy: Parallel
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: hermes-chat-tenant
  template:
    metadata:
      labels:
        app: hermes-chat-tenant
      annotations:
        ai.bstein.dev/role: isolated-user-chat
        ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject
        ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides
        ai.bstein.dev/config-rev: "20260808-webui-telegram"
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: hermes-chat
        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-chat-relay-key: kv/data/atlas/hermes/chat-telegram
        vault.hashicorp.com/agent-inject-template-chat-relay-key: |
          {{- with secret "kv/data/atlas/hermes/chat-telegram" -}}
          {{ .Data.data.relay_key }}
          {{- 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-chat
      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-08, titan-13, titan-14, titan-17]
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: hardware
                    operator: In
                    values: [rpi5]
            - weight: 40
              preference:
                matchExpressions:
                  - key: hardware
                    operator: In
                    values: [rpi4]
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: hermes-chat-tenant
                topologyKey: kubernetes.io/hostname
      initContainers:
        - name: init-config
          image: busybox:1.37
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - |
              set -eu
              mkdir -p /opt/data/home/.local/bin /opt/data/logs /opt/data/workspace
              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
              touch /opt/data/.env
              relay_key=""
              if [ -s /vault/secrets/chat-relay-key ]; then
                relay_key="$(tr -d '\r\n' < /vault/secrets/chat-relay-key)"
              fi
              if [ -z "${relay_key}" ]; then
                api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
                relay_key="${api_key}"
              fi
              { grep -v '^API_SERVER_KEY=' /opt/data/.env || true; printf 'API_SERVER_KEY=%s\n' "${relay_key}"; } > /opt/data/.env.tmp
              mv /opt/data/.env.tmp /opt/data/.env
              if [ -s /vault/secrets/anthropic-token ]; then
                token="$(tr -d '\r\n' < /vault/secrets/anthropic-token)"
                if [ -n "${token}" ]; then
                  { grep -v '^CLAUDE_CODE_OAUTH_TOKEN=' /opt/data/.env || true; printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${token}"; } > /opt/data/.env.tmp
                  mv /opt/data/.env.tmp /opt/data/.env
                fi
              fi
              chmod 0600 /opt/data/.env
              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: 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}
      containers:
        - name: hermes
          image: registry.bstein.dev/bstein/hermes-agent@sha256:15c5c538c0b58686af2e54e10bc870b23284789d485a609349df24ed3053622f
          imagePullPolicy: IfNotPresent
          command: [/opt/hermes/.venv/bin/hermes]
          args: [gateway, run]
          ports:
            - {name: api, containerPort: 8642, 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: 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: "0"}
            - {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://chat.hermes.bstein.dev}
          volumeMounts:
            - {name: home, mountPath: /opt/data}
            - {name: provider-auth, mountPath: /shared-auth}
            - {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
          readinessProbe:
            tcpSocket: {port: api}
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
          livenessProbe:
            tcpSocket: {port: api}
            initialDelaySeconds: 90
            periodSeconds: 30
            timeoutSeconds: 10
          securityContext:
            allowPrivilegeEscalation: false
            runAsUser: 10000
            runAsGroup: 10000
            seccompProfile:
              type: RuntimeDefault
          resources:
            requests: {cpu: 250m, memory: 512Mi}
            limits: {cpu: "1", memory: 2Gi}
        - name: webui
          image: registry.bstein.dev/bstein/hermes-webui@sha256:a771858bd668d25e19c74864baea5425101c8cd5215d1ba3a312f3312ce6c5e1
          imagePullPolicy: IfNotPresent
          command: [/bin/sh, -ec]
          args:
            - |
              api_key="$(sed -n 's/^API_SERVER_KEY=//p' /opt/data/.env | tail -n 1)"
              test -n "${api_key}"
              export API_SERVER_KEY="${api_key}"
              export HERMES_WEBUI_GATEWAY_API_KEY="${api_key}"
              exec /opt/hermes/.venv/bin/python /opt/hermes-webui/server.py
          ports:
            - {name: webui, containerPort: 8787, 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: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
            - {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
            - {name: HERMES_WEBUI_PORT, value: "8787"}
            - {name: HERMES_WEBUI_STATE_DIR, value: /opt/data/webui}
            - {name: HERMES_WEBUI_DEFAULT_WORKSPACE, value: /opt/data/workspace}
            - {name: HERMES_WEBUI_CHAT_BACKEND, value: gateway}
            - {name: HERMES_WEBUI_GATEWAY_BASE_URL, value: http://127.0.0.1:8642}
            - {name: HERMES_WEBUI_GATEWAY_USE_RUNS_API, value: "true"}
            - {name: HERMES_WEBUI_SKIP_ONBOARDING, value: "1"}
            - {name: HERMES_WEBUI_SECURE, value: "1"}
            - {name: HERMES_WEBUI_ALLOWED_ORIGINS, value: https://chat.hermes.bstein.dev}
            - {name: HERMES_WEBUI_TRUST_FORWARDED_HOST, value: "1"}
            - {name: HERMES_WEBUI_TRUST_FORWARDED_PROTO, value: "1"}
          volumeMounts:
            - {name: home, mountPath: /opt/data}
            - {name: provider-auth, mountPath: /shared-auth, readOnly: true}
            - {name: tmp, mountPath: /tmp}
          readinessProbe:
            httpGet: {path: /health, port: webui}
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 5
          livenessProbe:
            httpGet: {path: /health, port: webui}
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 10
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: [ALL]
            readOnlyRootFilesystem: true
            runAsUser: 10000
            runAsGroup: 10000
            seccompProfile:
              type: RuntimeDefault
          resources:
            requests: {cpu: 100m, memory: 256Mi}
            limits: {cpu: 750m, memory: 1Gi}
      volumes:
        - name: provider-auth
          persistentVolumeClaim:
            claimName: hermes-provider-auth
        - name: config
          configMap:
            name: hermes-chat-config
        - name: coordinator
          configMap:
            name: hermes-coordinator
            defaultMode: 0555
        - name: auth-patch
          emptyDir: {}
        - name: tmp
          emptyDir:
            sizeLimit: 256Mi
  volumeClaimTemplates:
    - metadata:
        name: home
        labels:
          app: hermes-chat-tenant
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: astreae
        resources:
          requests:
            storage: 2Gi
