# 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: anthropic/claude-opus-5, falling back to openai-codex/gpt-5.6-terra then local gpt-oss:20b
        ai.bstein.dev/role: testing-triage
        ai.bstein.dev/placement: titan-21 preferred, Jetson preferred, arm64 fallback
        ai.bstein.dev/config-rev: "20260808-dedicated-triage"
        # 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"
        # Without this the agent init container is appended after the
        # pod's own init containers, so init-config runs before the
        # secret file exists and silently falls back to the Secret -
        # the migration looks complete while Vault is never read.
        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-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-08
                      - titan-13
                      - titan-14
                      - titan-17
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 80
              preference:
                matchExpressions:
                  - key: atlas.bstein.dev/spillover
                    operator: DoesNotExist
            - weight: 60
              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
              mkdir -p /shared-auth
              if [ ! -s /shared-auth/auth.json ] && [ -s /opt/data/auth.json ]; then
                cp /opt/data/auth.json /shared-auth/auth.json
                chmod 0600 /shared-auth/auth.json
              fi
              chown -R 10000:10000 /opt/data
              chown -R 10000:10000 /shared-auth
          securityContext:
            runAsUser: 0
            runAsGroup: 0
          volumeMounts:
            - name: home
              mountPath: /opt/data
            - name: config
              mountPath: /config
            - name: operator-guide
              mountPath: /guide
            - name: provider-auth
              mountPath: /shared-auth
          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
        - 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
          command: [/opt/hermes/.venv/bin/hermes]
          args:
            - gateway
            - run
          ports:
            - name: api
              containerPort: 8642
              protocol: TCP
            - 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: 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://triage.hermes.bstein.dev
            - 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://triage.hermes.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.

          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 10000
            runAsGroup: 10000
            capabilities:
              drop: [ALL]
            seccompProfile:
              type: RuntimeDefault
          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
            - 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
          startupProbe:
            httpGet:
              path: /api/status
              port: dashboard
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 60
          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: provider-auth
          persistentVolumeClaim:
            claimName: hermes-provider-auth
        - name: config
          configMap:
            name: hermes-config
        - name: operator-guide
          configMap:
            name: hermes-operator-guide
        - name: tools
          emptyDir: {}
        - name: coordinator
          configMap:
            name: hermes-coordinator
            defaultMode: 0555
        - name: auth-patch
          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
