apiVersion: apps/v1 kind: Deployment metadata: name: hermes namespace: platform-engineer labels: app: hermes spec: replicas: 1 # MUST be 1 — Hermes' /opt/data is single-writer. strategy: type: Recreate # never run two pods against the same PVC selector: matchLabels: app: hermes template: metadata: labels: app: hermes spec: # No serviceAccountName — the agent has NO k8s API access. It manages the # cluster via git commits (→ ArgoCD sync) and reads via Loki/Prometheus/ArgoCD. # Pin to the powerful amd64 node (image is linux/amd64; the NUC has 24 GiB). nodeSelector: kubernetes.io/arch: amd64 affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: hardware operator: In values: ["high-memory"] podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: hermes topologyKey: kubernetes.io/hostname initContainers: # Clone the k3s-cluster repo into a persistent workspace so the agent can # commit + push remediations. The token is injected via envFrom. - name: git-clone image: alpine/git:2.43.0 command: ["sh", "-c"] args: - | set -e cd /workspace if [ -d k3s-cluster/.git ]; then echo "Repo exists, pulling latest..." cd k3s-cluster && git pull --rebase || true else echo "Cloning repo..." git clone "${GITEA_REPO_URL}" k3s-cluster cd k3s-cluster git config user.name "Platform Engineer" git config user.email "platform-engineer@rogi.casa" fi envFrom: - secretRef: name: hermes-env volumeMounts: - name: workspace mountPath: /workspace # Seed /opt/data with config.yaml + SOUL.md + .env on first boot only. # ArgoCD owns the manifests; the PVC is runtime state and is NOT reconciled. - name: seed-data image: busybox:1.36 command: ["sh", "-c"] args: - | set -e if [ ! -f /opt/data/config.yaml ]; then echo "First boot: seeding /opt/data from ConfigMap + env..." cp /seed/config.yaml /opt/data/config.yaml cp /seed/SOUL.md /opt/data/SOUL.md chmod 600 /opt/data/config.yaml # Write .env from the injected Secret env vars so the s6 gateway # finds API keys (the hermes container reads keys from /opt/data/.env). : > /opt/data/.env chmod 600 /opt/data/.env for k in OPENAI_API_KEY OPENAI_BASE_URL DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \ DISCORD_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \ GITEA_TOKEN GITEA_REPO_URL ARGOCD_API_TOKEN ARGOCD_SERVER \ HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \ HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET \ API_SERVER_KEY; do eval "v=\${$k:-}" [ -n "$v" ] && echo "$k=$v" >> /opt/data/.env done else echo "/opt/data already initialized — leaving runtime state intact." fi mkdir -p /opt/data/home/.kube /opt/data/cron/output /opt/data/scripts envFrom: - secretRef: name: hermes-env volumeMounts: - name: data mountPath: /opt/data - name: seed mountPath: /seed containers: - name: hermes image: nousresearch/hermes-agent:latest imagePullPolicy: Always # IMPORTANT: do NOT set `command:` — it would override the image's # ENTRYPOINT (/init, s6-overlay), which sets up the hermes user, seeds # config on first boot, and supervises the gateway. args: ["gateway", "run"] ports: - name: gateway containerPort: 8642 - name: dashboard containerPort: 9119 envFrom: - secretRef: name: hermes-env env: - name: HERMES_HOME value: /opt/data # Hermes' file-write tool refuses any path outside HERMES_WRITE_SAFE_ROOT. # When unset it defaults to HERMES_HOME (/opt/data), which blocks the # agent's only GitOps remediation path (editing manifests under # /workspace/k3s-cluster). Whitelist the whole filesystem — consistent # with yolo:true, approvals.mode:off, and the agent having no k8s RBAC. - name: HERMES_WRITE_SAFE_ROOT value: "/" # Expose the Hermes gateway HTTP API on port 8642 (the Service routes # 80 → 8642). Used by Open WebUI and other in-cluster clients to talk # to the agent as an OpenAI-compatible endpoint. - name: API_SERVER_ENABLED value: "true" # Bind the gateway API on all interfaces so other pods can reach it # via the Service (default is 127.0.0.1/loopback, which is unreachable # cross-pod). Auth is still enforced via API_SERVER_KEY. - name: API_SERVER_HOST value: "0.0.0.0" # Distinct model id advertised on GET /v1/models. Without this both # agents report "hermes-agent" and Open WebUI dedupes them, hiding one. - name: API_SERVER_MODEL_NAME value: "platform-engineer" # NOTE: API_SERVER_KEY comes from the hermes-env Secret (via envFrom) # and is also seeded into /opt/data/.env by the init container. Clients # (e.g. Open WebUI) authenticate with `Authorization: Bearer `. volumeMounts: - name: data mountPath: /opt/data - name: workspace mountPath: /workspace resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "2Gi" cpu: "1000m" livenessProbe: # Probe the dashboard port (9119, always enabled via HERMES_DASHBOARD=1 # and binds 0.0.0.0). The gateway API on 8642 is enabled via # API_SERVER_ENABLED=true above. tcpSocket: port: 9119 initialDelaySeconds: 90 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 5 securityContext: allowPrivilegeEscalation: false volumes: - name: data persistentVolumeClaim: claimName: hermes-data - name: workspace emptyDir: {} - name: seed configMap: name: hermes-seed --- apiVersion: v1 kind: Service metadata: name: hermes namespace: platform-engineer spec: type: ClusterIP selector: app: hermes ports: - name: gateway port: 80 targetPort: 8642 - name: dashboard port: 9119 targetPort: 9119