200 lines
6.8 KiB
YAML
200 lines
6.8 KiB
YAML
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"
|
|
# 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 <key>`.
|
|
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
|