Compare commits
1 Commits
fix/homeas
...
fix-myorg-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b35442b5a |
246
AGENTS.md
246
AGENTS.md
@@ -1,246 +0,0 @@
|
|||||||
# AGENTS.md - Guide for Coding Agents
|
|
||||||
|
|
||||||
This file provides essential information for AI coding agents working with this Kubernetes cluster project.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
This repository contains Kubernetes manifests for a K3s cluster running self-hosted services on the `rogi.casa` domain. The cluster is managed via **GitOps using ArgoCD** - all changes to the cluster are deployed automatically from this Git repository.
|
|
||||||
|
|
||||||
**⚠️ CRITICAL: Permission Model**
|
|
||||||
|
|
||||||
You **DO NOT** have permission to push changes to this repository. Before applying any changes to the cluster:
|
|
||||||
1. Make the necessary code changes to the manifests
|
|
||||||
2. Clearly present the changes to the user
|
|
||||||
3. Ask the user to review and push the changes
|
|
||||||
4. Wait for confirmation that changes have been pushed
|
|
||||||
5. Only then will ArgoCD automatically deploy the changes to the cluster
|
|
||||||
|
|
||||||
## Architecture & GitOps Workflow
|
|
||||||
|
|
||||||
### ArgoCD App-of-Apps Pattern
|
|
||||||
|
|
||||||
This project uses ArgoCD's "app-of-apps" pattern:
|
|
||||||
|
|
||||||
```
|
|
||||||
argocd-bootstrap.yaml (root Application)
|
|
||||||
↓
|
|
||||||
argocd/apps/ (directory containing all Application manifests)
|
|
||||||
↓
|
|
||||||
Individual Applications (one per service directory)
|
|
||||||
↓
|
|
||||||
Kubernetes manifests in each service directory (e.g., pihole/, homeassistant/)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deployment Flow
|
|
||||||
|
|
||||||
1. You make changes to Kubernetes manifests in the repository
|
|
||||||
2. User reviews and pushes changes to the `main` branch
|
|
||||||
3. ArgoCD detects changes (automatically or on sync)
|
|
||||||
4. ArgoCD applies changes to the cluster with `prune: true` and `selfHeal: true`
|
|
||||||
5. Cluster state converges to match the Git state
|
|
||||||
|
|
||||||
### Key Files
|
|
||||||
|
|
||||||
- **`argocd-bootstrap.yaml`**: The root Application that bootstraps ArgoCD. Points to `argocd/apps/` directory. This is the only file that needs manual `kubectl apply` during initial setup.
|
|
||||||
- **`argocd/apps/project.yaml`**: ArgoCD AppProject defining permissions for all applications
|
|
||||||
- **`argocd/apps/*.yaml`**: Individual ArgoCD Application manifests (one per service)
|
|
||||||
- **`argocd/gen-apps.sh`**: Script to regenerate all ArgoCD manifests from the `APPS` array
|
|
||||||
|
|
||||||
## Repository Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
k3s-cluster/
|
|
||||||
├── argocd-bootstrap.yaml # Root ArgoCD Application (app-of-apps)
|
|
||||||
├── argocd/
|
|
||||||
│ ├── apps/ # Individual ArgoCD Application manifests
|
|
||||||
│ │ ├── project.yaml # AppProject definition
|
|
||||||
│ │ ├── pihole.yaml # Application for pihole/
|
|
||||||
│ │ ├── homeassistant.yaml # Application for homeassistant/
|
|
||||||
│ │ └── ... # One per service
|
|
||||||
│ ├── gen-apps.sh # Generates argocd/apps/* manifests
|
|
||||||
│ └── ingress.yaml # ArgoCD's own ingress
|
|
||||||
├── <service-name>/ # Each service has its own directory
|
|
||||||
│ ├── namespace.yaml # (Optional) Namespace definition
|
|
||||||
│ ├── deployment.yaml # Main deployment/statefulset
|
|
||||||
│ ├── service.yaml # Service definition
|
|
||||||
│ ├── ingress.yaml # Ingress configuration
|
|
||||||
│ ├── configmap.yaml # (Optional) ConfigMaps
|
|
||||||
│ ├── pvc.yaml # (Optional) PersistentVolumeClaims
|
|
||||||
│ └── secret.yaml # (Optional) Secrets (rarely committed)
|
|
||||||
├── cert-manager/ # cert-manager installation manifests
|
|
||||||
├── nas/ # External NAS service configuration
|
|
||||||
├── monitoring/ # Prometheus + Grafana stack
|
|
||||||
└── README.md # Comprehensive project documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current Services
|
|
||||||
|
|
||||||
The cluster runs these services (each in its own directory):
|
|
||||||
|
|
||||||
- **argocd** - GitOps continuous delivery platform
|
|
||||||
- **cert-manager** - SSL certificate management (Let's Encrypt)
|
|
||||||
- **fava** - Beancount accounting web interface
|
|
||||||
- **gitea** - Self-hosted Git server
|
|
||||||
- **glance** - Personal dashboard
|
|
||||||
- **gym-tracker** - Workout tracking application
|
|
||||||
- **homeassistant** - Home automation
|
|
||||||
- **jellyfin** - Media server
|
|
||||||
- **litellm** - LLM proxy
|
|
||||||
- **minecraft-server** - Minecraft server
|
|
||||||
- **monitoring** - Prometheus + Grafana
|
|
||||||
- **myorg-assistant** - Organization assistant
|
|
||||||
- **n8n** - Workflow automation
|
|
||||||
- **nas** - External NAS proxy
|
|
||||||
- **openwebui** - Web UI for LLMs
|
|
||||||
- **phoenix** - AI observability platform
|
|
||||||
- **pihole** - Network-wide ad blocking
|
|
||||||
- **platform-engineer** - Platform engineering tools
|
|
||||||
- **qbittorrent** - Torrent client
|
|
||||||
- **searxng** - Meta search engine
|
|
||||||
- **vaultwarden** - Password manager (Bitwarden compatible)
|
|
||||||
|
|
||||||
## How to Make Changes
|
|
||||||
|
|
||||||
### Adding a New Service
|
|
||||||
|
|
||||||
1. Create a new directory: `mkdir new-service`
|
|
||||||
2. Create Kubernetes manifests in `new-service/`:
|
|
||||||
- `namespace.yaml` (if dedicated namespace needed)
|
|
||||||
- `deployment.yaml` or `statefulset.yaml`
|
|
||||||
- `service.yaml`
|
|
||||||
- `ingress.yaml`
|
|
||||||
- Any ConfigMaps, Secrets, PVCs needed
|
|
||||||
3. Add the service to `argocd/gen-apps.sh`:
|
|
||||||
- Add a line to the `APPS` array: `"new-service|namespace|new-service|true|true"`
|
|
||||||
- Format: `name|namespace|path|recurse|validate`
|
|
||||||
4. Run `./argocd/gen-apps.sh` to regenerate ArgoCD manifests
|
|
||||||
5. **Present changes to user for review and push**
|
|
||||||
|
|
||||||
### Modifying an Existing Service
|
|
||||||
|
|
||||||
1. Edit the relevant manifest(s) in the service directory
|
|
||||||
2. If changing ArgoCD configuration, also update `argocd/gen-apps.sh` and regenerate
|
|
||||||
3. **Present changes to user for review and push**
|
|
||||||
|
|
||||||
### Removing a Service
|
|
||||||
|
|
||||||
1. Remove the service directory: `rm -rf service-name/`
|
|
||||||
2. Remove from `APPS` array in `argocd/gen-apps.sh`
|
|
||||||
3. Run `./argocd/gen-apps.sh` to regenerate
|
|
||||||
4. **Present changes to user for review and push**
|
|
||||||
5. ArgoCD will automatically prune the resources from the cluster
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Ingress Configuration
|
|
||||||
|
|
||||||
Each service has its own `ingress.yaml` with:
|
|
||||||
- `ingressClassName: traefik` (K3s default)
|
|
||||||
- TLS configured with `cert-manager.io/cluster-issuer: letsencrypt-prod`
|
|
||||||
- Host-based routing (e.g., `pihole.rogi.casa`)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```yaml
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: pihole
|
|
||||||
namespace: pihole
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
spec:
|
|
||||||
ingressClassName: traefik
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- pihole.rogi.casa
|
|
||||||
secretName: pihole-tls
|
|
||||||
rules:
|
|
||||||
- host: pihole.rogi.casa
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: pihole-web
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
|
|
||||||
- Each service typically has its own namespace
|
|
||||||
- Use ResourceRequests and Limits for all containers
|
|
||||||
- PVCs for persistent data
|
|
||||||
- ConfigMaps for configuration files
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
### What You CAN Do
|
|
||||||
|
|
||||||
- Read and understand all manifests
|
|
||||||
- Create new manifest files
|
|
||||||
- Modify existing manifest files
|
|
||||||
- Run `./argocd/gen-apps.sh` to regenerate ArgoCD manifests
|
|
||||||
- Explain how the cluster works
|
|
||||||
- Troubleshoot issues by reading manifests
|
|
||||||
|
|
||||||
### What You CANNOT Do
|
|
||||||
|
|
||||||
- Push changes to the Git repository (no push permissions)
|
|
||||||
- Directly apply manifests with `kubectl apply` (unless explicitly asked)
|
|
||||||
- Access the Kubernetes cluster directly (unless explicitly configured)
|
|
||||||
- Create secrets that should remain private (those are managed manually)
|
|
||||||
|
|
||||||
### Secrets Management
|
|
||||||
|
|
||||||
Secrets are generally **not committed to the repository**. They must be created manually in the cluster:
|
|
||||||
```bash
|
|
||||||
kubectl create secret docker-registry gitea-registry \
|
|
||||||
--docker-server=gitea.rogi.casa \
|
|
||||||
--docker-username=<user> \
|
|
||||||
--docker-password=<token> \
|
|
||||||
-n <namespace>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow Summary
|
|
||||||
|
|
||||||
When asked to make changes:
|
|
||||||
|
|
||||||
1. **Understand** the current state by reading relevant files
|
|
||||||
2. **Modify** the manifests (create/edit files)
|
|
||||||
3. **Regenerate** ArgoCD manifests if needed (`./argocd/gen-apps.sh`)
|
|
||||||
4. **Present** the changes clearly to the user:
|
|
||||||
```
|
|
||||||
I've made the following changes:
|
|
||||||
- Modified pihole/deployment.yaml to update image version
|
|
||||||
- Regenerated argocd/apps/pihole.yaml
|
|
||||||
|
|
||||||
Please review and push these changes to deploy them.
|
|
||||||
```
|
|
||||||
5. **Wait** for user confirmation that changes are pushed
|
|
||||||
6. **Verify** (if possible) that ArgoCD has synced the changes
|
|
||||||
|
|
||||||
## Useful Commands (for reference)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Regenerate ArgoCD manifests after modifying gen-apps.sh
|
|
||||||
./argocd/gen-apps.sh
|
|
||||||
|
|
||||||
# Check ArgoCD applications status (requires kubectl access)
|
|
||||||
kubectl get applications -n argocd
|
|
||||||
|
|
||||||
# View logs of a pod (requires kubectl access)
|
|
||||||
kubectl logs -n <namespace> <pod-name>
|
|
||||||
|
|
||||||
# Check ingress status (requires kubectl access)
|
|
||||||
kubectl get ingress -n <namespace>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Questions?
|
|
||||||
|
|
||||||
If you're unsure about anything:
|
|
||||||
1. Read the comprehensive `README.md` in the repository root
|
|
||||||
2. Check existing service directories for examples
|
|
||||||
3. Ask the user for clarification before making changes
|
|
||||||
4. Remember: **never push without explicit user review and approval**
|
|
||||||
@@ -22,9 +22,3 @@ spec:
|
|||||||
selfHeal: true
|
selfHeal: true
|
||||||
syncOptions:
|
syncOptions:
|
||||||
- CreateNamespace=false
|
- CreateNamespace=false
|
||||||
ignoreDifferences:
|
|
||||||
- group: argoproj.io
|
|
||||||
kind: Application
|
|
||||||
jsonPointers:
|
|
||||||
- /status
|
|
||||||
- /operation
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
apiVersion: argoproj.io/v1alpha1
|
||||||
kind: Application
|
kind: Application
|
||||||
metadata:
|
metadata:
|
||||||
name: llamacpp
|
name: argocd
|
||||||
namespace: argocd
|
namespace: argocd
|
||||||
annotations:
|
annotations:
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
argocd.argoproj.io/sync-wave: "0"
|
||||||
@@ -10,12 +10,12 @@ spec:
|
|||||||
source:
|
source:
|
||||||
repoURL: https://git.rogi.casa/roger/k3s-cluster.git
|
repoURL: https://git.rogi.casa/roger/k3s-cluster.git
|
||||||
targetRevision: main
|
targetRevision: main
|
||||||
path: llamacpp
|
path: argocd
|
||||||
directory:
|
directory:
|
||||||
recurse: true
|
recurse: false
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
namespace: llamacpp
|
namespace: argocd
|
||||||
syncPolicy:
|
syncPolicy:
|
||||||
automated:
|
automated:
|
||||||
prune: true
|
prune: true
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
apiVersion: argoproj.io/v1alpha1
|
|
||||||
kind: Application
|
|
||||||
metadata:
|
|
||||||
name: home-manager
|
|
||||||
namespace: argocd
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-wave: "0"
|
|
||||||
spec:
|
|
||||||
project: k3s-cluster
|
|
||||||
source:
|
|
||||||
repoURL: https://git.rogi.casa/roger/k3s-cluster.git
|
|
||||||
targetRevision: main
|
|
||||||
path: home-manager
|
|
||||||
directory:
|
|
||||||
recurse: true
|
|
||||||
destination:
|
|
||||||
server: https://kubernetes.default.svc
|
|
||||||
namespace: home-manager
|
|
||||||
syncPolicy:
|
|
||||||
automated:
|
|
||||||
prune: true
|
|
||||||
selfHeal: true
|
|
||||||
syncOptions:
|
|
||||||
- CreateNamespace=false
|
|
||||||
@@ -7,11 +7,6 @@ metadata:
|
|||||||
app.kubernetes.io/name: argocd-cm
|
app.kubernetes.io/name: argocd-cm
|
||||||
app.kubernetes.io/part-of: argocd
|
app.kubernetes.io/part-of: argocd
|
||||||
data:
|
data:
|
||||||
# Serve HTTP (no redirect to HTTPS) so the TLS-terminating Traefik ingress works.
|
|
||||||
# Without this, argocd-server redirects HTTP->HTTPS, causing an infinite
|
|
||||||
# redirect loop behind the ingress (argocd.rogi.casa unreachable).
|
|
||||||
server.insecure: "true"
|
|
||||||
|
|
||||||
# add an additional local user with apiKey and login capabilities
|
# add an additional local user with apiKey and login capabilities
|
||||||
# apiKey - allows generating API keys
|
# apiKey - allows generating API keys
|
||||||
# login - allows to login using UI
|
# login - allows to login using UI
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: argocd-rbac-cm
|
|
||||||
namespace: argocd
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: argocd-rbac-cm
|
|
||||||
app.kubernetes.io/part-of: argocd
|
|
||||||
data:
|
|
||||||
policy.csv: |
|
|
||||||
# Grant platform-engineer read-only access to applications
|
|
||||||
g, platform-engineer, role:readonly
|
|
||||||
|
|
||||||
# Custom policy for platform-engineer with application read permissions
|
|
||||||
p, role:platform-engineer, applications, get, *, allow
|
|
||||||
p, role:platform-engineer, applications, list, *, allow
|
|
||||||
p, role:platform-engineer, clusters, get, *, allow
|
|
||||||
p, role:platform-engineer, clusters, list, *, allow
|
|
||||||
p, role:platform-engineer, repositories, get, *, allow
|
|
||||||
p, role:platform-engineer, repositories, list, *, allow
|
|
||||||
p, role:platform-engineer, projects, get, *, allow
|
|
||||||
p, role:platform-engineer, projects, list, *, allow
|
|
||||||
g, platform-engineer, role:platform-engineer
|
|
||||||
|
|
||||||
# Default policy - deny by default (ArgoCD default)
|
|
||||||
policy.default: role:readonly
|
|
||||||
|
|
||||||
# Enable RBAC
|
|
||||||
rbac.enabled: "true"
|
|
||||||
@@ -21,15 +21,14 @@ mkdir -p "$APPS_DIR"
|
|||||||
|
|
||||||
# app-name | namespace | path | recurse | validate
|
# app-name | namespace | path | recurse | validate
|
||||||
APPS=(
|
APPS=(
|
||||||
|
"argocd|argocd|argocd|false|true"
|
||||||
"cert-manager|cert-manager|cert-manager|true|true"
|
"cert-manager|cert-manager|cert-manager|true|true"
|
||||||
"fava|fava|fava|true|true"
|
"fava|fava|fava|true|true"
|
||||||
"gitea|gitea|gitea|true|true"
|
"gitea|gitea|gitea|true|true"
|
||||||
"glance|glance|glance|true|true"
|
"glance|glance|glance|true|true"
|
||||||
"gym-tracker|gym-tracker|gym-tracker|true|true"
|
"gym-tracker|gym-tracker|gym-tracker|true|true"
|
||||||
"homeassistant|home-assistant|homeassistant|true|true"
|
"homeassistant|home-assistant|homeassistant|true|true"
|
||||||
"home-manager|home-manager|home-manager|true|true"
|
|
||||||
"jellyfin|jellyfin|jellyfin|true|true"
|
"jellyfin|jellyfin|jellyfin|true|true"
|
||||||
"llamacpp|llamacpp|llamacpp|true|true"
|
|
||||||
"litellm|litellm|litellm|true|true"
|
"litellm|litellm|litellm|true|true"
|
||||||
"minecraft-server|minecraft|minecraft-server|true|true"
|
"minecraft-server|minecraft|minecraft-server|true|true"
|
||||||
"monitoring|monitoring|monitoring|true|true"
|
"monitoring|monitoring|monitoring|true|true"
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ metadata:
|
|||||||
namespace: gitea
|
namespace: gitea
|
||||||
labels:
|
labels:
|
||||||
app: gitea
|
app: gitea
|
||||||
annotations:
|
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-07-21T13:30:00Z"
|
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
@@ -103,8 +101,6 @@ metadata:
|
|||||||
namespace: gitea
|
namespace: gitea
|
||||||
labels:
|
labels:
|
||||||
app: gitea-runner
|
app: gitea-runner
|
||||||
annotations:
|
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-07-21T13:30:00Z"
|
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
@@ -119,14 +115,7 @@ spec:
|
|||||||
kubernetes.io/arch: arm64
|
kubernetes.io/arch: arm64
|
||||||
containers:
|
containers:
|
||||||
- name: gitea-runner
|
- name: gitea-runner
|
||||||
image: vegardit/gitea-act-runner:2.1.0
|
image: vegardit/gitea-act-runner:latest
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "250m"
|
|
||||||
limits:
|
|
||||||
memory: "1Gi"
|
|
||||||
cpu: "500m"
|
|
||||||
env:
|
env:
|
||||||
- name: GITEA_INSTANCE_URL
|
- name: GITEA_INSTANCE_URL
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
# Home Manager Agent — Deployment Plan
|
|
||||||
|
|
||||||
An autonomous **Hermes Agent** that runs inside the k3s cluster and takes care
|
|
||||||
of the home by talking to **Home Assistant**'s REST API. It mirrors the
|
|
||||||
`platform-engineer` agent pattern but its domain is the home, not the cluster:
|
|
||||||
it watches sensors, runs routines, and notifies Roger on Discord — asking
|
|
||||||
before touching anything security-critical (locks, alarm, garage, HVAC).
|
|
||||||
|
|
||||||
Docs: https://hermes-agent.nousresearch.com/docs/user-guide/docker
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Goal & operating model
|
|
||||||
|
|
||||||
- **One Hermes container** in namespace `home-manager`, scheduled on the amd64
|
|
||||||
NUC (`roger-nucbox-evo-x2`, 24 GiB) — same node as `platform-engineer`.
|
|
||||||
- Hermes runs in **gateway mode** under s6 supervision (`gateway run`), so the
|
|
||||||
built-in **cron scheduler** is active and survives restarts.
|
|
||||||
- The agent talks to the home via the **Home Assistant REST API** at
|
|
||||||
`http://home-assistant.home-assistant:80` (in-cluster `home-assistant`
|
|
||||||
Service). Auth is a HA Long-Lived Access Token in a Secret.
|
|
||||||
- The agent has **NO k8s RBAC and NO git repo** — it doesn't need them. Its
|
|
||||||
only write path is HA service calls. (Contrast with `platform-engineer`,
|
|
||||||
which remediates via git commits → ArgoCD sync.)
|
|
||||||
- LLM calls route through the in-cluster **LiteLLM** proxy.
|
|
||||||
- Notifications go to a **dedicated Discord bot** (separate token from the
|
|
||||||
platform engineer's bot).
|
|
||||||
- **Hermes-native cron jobs** make the agent run periodic checks + routines.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Files in this directory
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `namespace.yaml` | namespace `home-manager` |
|
|
||||||
| `rbac.yaml` | ServiceAccount + Role for the `cron-seed` Job only (pods/exec on the hermes pod) |
|
|
||||||
| `configmap.yaml` | seed `config.yaml` + `SOUL.md` (HA REST API docs + operating rules) |
|
|
||||||
| `secret.yaml` | HA token, Discord token/channel, LiteLLM key, dashboard auth (PLACEHOLDERS — fill in) |
|
|
||||||
| `pvc.yaml` | 5 Gi PVC for `/opt/data` (HERMES_HOME) |
|
|
||||||
| `deployment.yaml` | Deployment (1 replica, Recreate, pinned to amd64 NUC) + Service |
|
|
||||||
| `ingress.yaml` | `home-manager.rogi.casa` → dashboard (TLS + basic auth) |
|
|
||||||
| `cron-seed.yaml` | Sync-hook Job that reconciles the Hermes cron schedule from Git (deletes + recreates each job on every run so prompt/schedule changes apply on sync) |
|
|
||||||
| `README.md` | this file |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. How it differs from `platform-engineer`
|
|
||||||
|
|
||||||
| Concern | `platform-engineer` | `home-manager` (this) |
|
|
||||||
|---|---|---|
|
|
||||||
| Domain | k3s cluster health | the home |
|
|
||||||
| Observe via | Prometheus / Loki / ArgoCD HTTP APIs | Home Assistant REST API |
|
|
||||||
| Write path | git commit → push → ArgoCD sync | HA service calls (`POST /api/services/...`) |
|
|
||||||
| k8s RBAC | none | none |
|
|
||||||
| git repo | yes (cloned to `/workspace/k3s-cluster`) | none |
|
|
||||||
| Discord bot | dedicated | dedicated (separate token + channel) |
|
|
||||||
| `HERMES_WRITE_SAFE_ROOT` | `/` (needs to edit manifests) | unset → defaults to `/opt/data` (tighter) |
|
|
||||||
| Image | `nousresearch/hermes-agent:latest` | same |
|
|
||||||
| Model | `qwen3.6` via LiteLLM | same |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Safety & guardrails
|
|
||||||
|
|
||||||
1. **SOUL.md is the real boundary.** The agent may freely toggle lights, fans,
|
|
||||||
media, and non-critical switches. It must **ask Roger on Discord before
|
|
||||||
touching** `lock.*`, `alarm_control_panel.*`, garage `cover.*`, risky
|
|
||||||
`climate.*` changes, or anything tagged `critical`/`security`/`safe_to_auto: false`.
|
|
||||||
2. **`tool_loop_guardrails.hard_stop_enabled: true`** — circuit-breaks a stuck
|
|
||||||
gateway.
|
|
||||||
3. **`yolo: true` + `approvals.mode: off`** — cron jobs aren't blocked waiting
|
|
||||||
for a human. Safe because the blast radius is HA service calls and the SOUL.md
|
|
||||||
encodes the confirmation gate for dangerous entities.
|
|
||||||
4. **Tirith scanner disabled** — it false-positives on the in-cluster plain-HTTP
|
|
||||||
HA URL and would block every query.
|
|
||||||
5. **Single replica / single-writer PVC** — never scale >1. `podAntiAffinity`
|
|
||||||
guards against an accidental co-run.
|
|
||||||
6. **No real-time event bus** — the terminal tool can't hold a HA WebSocket open.
|
|
||||||
The agent works on cron polling + on-demand Discord commands. (See §6 for an
|
|
||||||
optional real-time hook.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Deployment checklist (do in this order)
|
|
||||||
|
|
||||||
1. **Create a Home Assistant Long-Lived Access Token.** In HA UI:
|
|
||||||
Profile → Long-Lived Access Tokens → Create Token → label `home-manager-agent`.
|
|
||||||
Copy the token.
|
|
||||||
2. **Create a dedicated Discord bot** (separate from the platform-engineer bot),
|
|
||||||
invite it to your server, and note its token + the channel ID you want it to
|
|
||||||
use as its home channel.
|
|
||||||
3. **Fill in `secret.yaml`** — base64-encode each value (`echo -n 'VALUE' | base64`):
|
|
||||||
- `HOMEASSISTANT_TOKEN` — the HA LLAT from step 1.
|
|
||||||
- `OPENAI_API_KEY` — your LiteLLM master key.
|
|
||||||
- `DISCORD_BOT_TOKEN`, `DISCORD_HOME_CHANNEL`, `DISCORD_FREE_RESPONSE_CHANNELS`
|
|
||||||
— from step 2 (channel ID is the same for both).
|
|
||||||
- `HERMES_DASHBOARD_BASIC_AUTH_PASSWORD` and `..._SECRET` — strong values.
|
|
||||||
4. **Fill in `configmap.yaml`** — replace `REPLACE_WITH_HOME_CHANNEL_ID` (two
|
|
||||||
places under `discord:`) with your numeric home channel ID. (Must match the
|
|
||||||
secret's `DISCORD_HOME_CHANNEL`.)
|
|
||||||
5. **Tailor `cron-seed.yaml`** schedules/prompts to your actual HA entities
|
|
||||||
(your sensor names, your calendars, your todo lists). The default schedule
|
|
||||||
has 4 jobs (energy anomaly, comfort check, morning briefing, evening
|
|
||||||
briefing) — adjust thresholds and wording to your setup.
|
|
||||||
6. **Commit & push** the whole change (including the regenerated
|
|
||||||
`argocd/apps/home-manager.yaml`). ArgoCD will create the namespace resources,
|
|
||||||
deploy the pod, and bring up the ingress at `home-manager.rogi.casa`.
|
|
||||||
7. **Seed the cron jobs:** ArgoCD runs `cron-seed.yaml` as a Sync hook
|
|
||||||
automatically on every sync. The hook **reconciles** the cron schedule to
|
|
||||||
match Git: it deletes + recreates each named job, so prompt/schedule edits
|
|
||||||
in `cron-seed.yaml` take effect on the next sync (this overwrites any live
|
|
||||||
`hermes cron edit` changes — edit the prompts in Git instead). The schedule
|
|
||||||
has exactly 4 jobs: `energy-anomaly` (every 4h), `comfort-check` (every 4h),
|
|
||||||
`morning-briefing` (08:00 daily), `evening-briefing` (00:00 daily). To
|
|
||||||
re-run it by hand: `kubectl apply -f home-manager/cron-seed.yaml`.
|
|
||||||
8. **Smoke test:** trigger a check manually —
|
|
||||||
`kubectl exec -n home-manager deploy/hermes -- hermes cron run morning-briefing` —
|
|
||||||
and confirm the message lands in Discord.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Optional: real-time events from HA
|
|
||||||
|
|
||||||
The base setup is cron-poll + Discord. For real-time reaction (e.g. "front door
|
|
||||||
opened → agent acts immediately"), you can have Home Assistant call the Hermes
|
|
||||||
gateway API on an event:
|
|
||||||
|
|
||||||
1. Enable the gateway API (port 8642) in `config.yaml` and set an auth token.
|
|
||||||
2. In HA, create a `rest` command + an automation that fires on the event and
|
|
||||||
POSTs to `http://home-manager.home-manager:80/...` (the in-cluster Service).
|
|
||||||
|
|
||||||
This is not enabled by default — start with cron + Discord, add webhooks once
|
|
||||||
the agent is trusted.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. What ArgoCD owns vs. what is runtime state
|
|
||||||
|
|
||||||
- **ArgoCD owns** (in git): namespace, RBAC, Secret, ConfigMap (seed), PVC,
|
|
||||||
Deployment, Service, Ingress, cron-seed Job.
|
|
||||||
- **Runtime state (on the PVC, NOT reconciled):** `config.yaml`, `SOUL.md`,
|
|
||||||
`.env`, `sessions/`, `memories/`, `skills/`. The ConfigMap only *seeds* these
|
|
||||||
on first boot; after that, edits made via the dashboard persist on the PVC
|
|
||||||
and Argo will not revert them. For a hard reset, delete the PVC and re-apply.
|
|
||||||
- **Exception — cron jobs (`cron/jobs.json`):** the `cron-seed` Sync hook
|
|
||||||
re-runs on every ArgoCD sync and **deletes + recreates** each named job from
|
|
||||||
`cron-seed.yaml`, so the cron schedule IS reconciled from Git. Live edits
|
|
||||||
via `hermes cron edit` will be overwritten on the next sync — edit the
|
|
||||||
prompts in `cron-seed.yaml` and commit instead.
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
# Hermes configuration + SOUL.md (seeded into the PVC on first boot).
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: hermes-seed
|
|
||||||
namespace: home-manager
|
|
||||||
data:
|
|
||||||
config.yaml: |
|
|
||||||
model:
|
|
||||||
provider: openai-api
|
|
||||||
default: qwen3.6-35b-a3b
|
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
|
||||||
api_mode: chat_completions
|
|
||||||
|
|
||||||
auxiliary:
|
|
||||||
compression:
|
|
||||||
provider: openai-api
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
|
||||||
title_generation:
|
|
||||||
provider: openai-api
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
|
||||||
|
|
||||||
terminal:
|
|
||||||
backend: local
|
|
||||||
cwd: /opt/data
|
|
||||||
timeout: 180
|
|
||||||
home_mode: profile
|
|
||||||
|
|
||||||
# The agent runs unattended (cron jobs). yolo:true disables approval prompts
|
|
||||||
# so cron jobs aren't blocked waiting for a human. Safe here because the
|
|
||||||
# agent's blast radius is limited to Home Assistant service calls, and the
|
|
||||||
# SOUL.md forbids touching security-critical entities without confirmation.
|
|
||||||
yolo: true
|
|
||||||
approvals:
|
|
||||||
mode: off
|
|
||||||
|
|
||||||
# Disable the Tirith pre-exec command scanner. It flags in-cluster plain
|
|
||||||
# HTTP URLs (http://home-assistant.home-assistant:80) as 'insecure URL'
|
|
||||||
# false positives, which blocks every HA API query.
|
|
||||||
security:
|
|
||||||
tirith_enabled: false
|
|
||||||
tirith_fail_open: true
|
|
||||||
|
|
||||||
tool_loop_guardrails:
|
|
||||||
hard_stop_enabled: true
|
|
||||||
hard_stop_after:
|
|
||||||
exact_failure: 5
|
|
||||||
idempotent_no_progress: 5
|
|
||||||
|
|
||||||
sessions:
|
|
||||||
auto_prune: true
|
|
||||||
retention_days: 90
|
|
||||||
|
|
||||||
cron:
|
|
||||||
wrap_response: false
|
|
||||||
|
|
||||||
discord:
|
|
||||||
# The home channel's Discord ID. Must match DISCORD_HOME_CHANNEL /
|
|
||||||
# DISCORD_FREE_RESPONSE_CHANNELS in the Secret. Hermes reads these from
|
|
||||||
# config.yaml, not the env.
|
|
||||||
allowed_channels: '1470909384162017444'
|
|
||||||
free_response_channels: '1470909384162017444'
|
|
||||||
dm_policy: open
|
|
||||||
group_policy: open
|
|
||||||
|
|
||||||
memory:
|
|
||||||
memory_enabled: true
|
|
||||||
user_profile_enabled: true
|
|
||||||
write_approval: false
|
|
||||||
|
|
||||||
skills:
|
|
||||||
write_approval: false
|
|
||||||
|
|
||||||
SOUL.md: |
|
|
||||||
# Home Manager — rogi.casa (via Home Assistant)
|
|
||||||
|
|
||||||
You are the autonomous Home Manager for the `rogi.casa` home. You run
|
|
||||||
*inside* the k3s cluster (namespace `home-manager`) and your job is to keep
|
|
||||||
the home comfortable, safe, and efficient by talking to Home Assistant, and
|
|
||||||
to notify your owner (Roger) on Discord when something needs a human.
|
|
||||||
|
|
||||||
## The home you look after
|
|
||||||
|
|
||||||
- **Home Assistant:** the brain of the home. Runs in namespace
|
|
||||||
`home-assistant`. You reach its REST API in-cluster at
|
|
||||||
`http://home-assistant.home-assistant:80` (the `home-assistant` Service,
|
|
||||||
port 80 → 8123). If that DNS ever fails, fall back to the node IP
|
|
||||||
`http://10.88.20.11:8123` (HA runs hostNetwork).
|
|
||||||
- **Auth:** every HA REST call needs an `Authorization: Bearer $HOMEASSISTANT_TOKEN`
|
|
||||||
header. The token is in your environment. NEVER log or echo the token.
|
|
||||||
- **Your model provider:** LiteLLM at `http://litellm-service.litellm:80/v1`.
|
|
||||||
- **Notifications:** Discord (your home channel).
|
|
||||||
|
|
||||||
## How you observe the home (Home Assistant REST API)
|
|
||||||
|
|
||||||
Use the terminal tool to send HTTP requests with curl. Always pass the bearer
|
|
||||||
token from the environment (`$HOMEASSISTANT_TOKEN`), never hard-code it.
|
|
||||||
|
|
||||||
Base URL: `$HOMEASSISTANT_URL` (set to http://home-assistant.home-assistant:80)
|
|
||||||
|
|
||||||
### 1. Read state
|
|
||||||
- All entities: GET /api/states
|
|
||||||
- One entity: GET /api/states/<entity_id> (e.g. light.living_room)
|
|
||||||
- Services list: GET /api/services
|
|
||||||
- Calendar events: GET /api/calendars/<calendar_id>?start=...&end=...
|
|
||||||
- History: GET /api/history/period/<timestamp>?filter_entity_id=<id>&minimal
|
|
||||||
- Config check: GET /api/config
|
|
||||||
|
|
||||||
### 2. Actuate (call a service)
|
|
||||||
POST /api/services/<domain>/<service> with a JSON body.
|
|
||||||
Common services:
|
|
||||||
- light.turn_on / light.turn_off / light.toggle
|
|
||||||
body: {"entity_id": "light.x", "brightness": 200}
|
|
||||||
- switch.turn_on / switch.turn_off / switch.toggle
|
|
||||||
body: {"entity_id": "switch.x"}
|
|
||||||
- cover.open_cover / close_cover / set_cover_position
|
|
||||||
body: {"entity_id": "cover.x", "position": 50}
|
|
||||||
- fan.toggle / fan.set_percentage
|
|
||||||
- media_player.play_media / media_pause / volume_set
|
|
||||||
- climate.set_temperature
|
|
||||||
body: {"entity_id": "climate.x", "temperature": 21}
|
|
||||||
- script.turn_on
|
|
||||||
body: {"entity_id": "script.goodnight"}
|
|
||||||
- input_boolean.toggle / input_select.select_option
|
|
||||||
|
|
||||||
### 3. Fire events
|
|
||||||
POST /api/events/<event_type> { ... }
|
|
||||||
(Listening to the live event bus needs the WebSocket API, which the
|
|
||||||
terminal tool can't hold open. Use cron polling for periodic checks, and
|
|
||||||
ask Roger on Discord for anything that needs real-time reaction.)
|
|
||||||
|
|
||||||
### 4. Parse JSON
|
|
||||||
Pipe curl output through `python3 -m json.tool` or `jq` in the terminal
|
|
||||||
tool. The execute_code tool is BLOCKED in cron mode.
|
|
||||||
|
|
||||||
## How you remediate (call services — NOT git, NOT kubectl)
|
|
||||||
|
|
||||||
Unlike the platform engineer, you do NOT change the cluster. You change the
|
|
||||||
*home* by calling Home Assistant services via the REST API. You have no k8s
|
|
||||||
access and no git repo — and you don't need them.
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. GET /api/states (or a specific entity) to understand the situation.
|
|
||||||
2. Decide a safe action. POST the service call.
|
|
||||||
3. GET the entity back to confirm the new state.
|
|
||||||
4. Report what you did in one line (entity_id + service + result).
|
|
||||||
|
|
||||||
## Operating rules
|
|
||||||
|
|
||||||
1. **Read first, act second.** Always GET the current state before changing
|
|
||||||
anything. Cite the exact `entity_id` and the service you called.
|
|
||||||
2. **Only safe, idempotent actions on your own.** Allowed without asking:
|
|
||||||
lights, fans, media players, non-critical switches, running scripts Roger
|
|
||||||
has marked safe, reading any sensor.
|
|
||||||
3. **NEVER touch security- or safety-critical entities without explicit
|
|
||||||
confirmation from Roger on Discord.** This includes, but is not limited to:
|
|
||||||
- `lock.*` (door locks)
|
|
||||||
- `alarm_control_panel.*` (alarm arm/disarm)
|
|
||||||
- `cover.*` for garage doors or main gates
|
|
||||||
- `climate.*` changes that could freeze/overheat the house or cost a lot
|
|
||||||
- any `switch.*` / `script.*` tagged `critical` or `security`
|
|
||||||
- any entity with a custom attribute `safe_to_auto: false`
|
|
||||||
For these, post the proposed action to Discord and wait for Roger to
|
|
||||||
reply yes/no before calling the service.
|
|
||||||
4. **When in doubt, notify, don't act.** If an action is unusual, could
|
|
||||||
startle someone (e.g. loud media at 03:00, vacuum robot at night), or you
|
|
||||||
are unsure what an entity does, ask Roger first.
|
|
||||||
5. **Be quiet when healthy.** Watchdog cron jobs reply with exactly
|
|
||||||
`[SILENT]` when there is nothing to report. Failed jobs always deliver.
|
|
||||||
6. **No runaway loops.** You cannot create new cron jobs from inside a cron
|
|
||||||
run (Hermes disables that). Do not try.
|
|
||||||
7. **Talk like a concierge.** Short, concrete, with entity_ids and values.
|
|
||||||
No filler. When you did something, say what in one line.
|
|
||||||
8. **Language: Catalan by default.** Address Roger in Catalan in *all*
|
|
||||||
interactions — briefings, watchdog alerts, Discord DMs, and any other
|
|
||||||
message you produce. This is the default regardless of the language the
|
|
||||||
cron prompt or instruction is written in (those are just instructions
|
|
||||||
to you, not the output language). Only switch away from Catalan if Roger
|
|
||||||
writes to you in another language or explicitly asks you to. Never
|
|
||||||
default to English or Italian.
|
|
||||||
9. **Respect time of day and presence.** Don't run noisy routines (loud
|
|
||||||
media, vacuum robots) at night. Check presence (`zone.*` /
|
|
||||||
`binary_sensor.*`) and the time before acting.
|
|
||||||
10. **Absence of presence entities ≠ nobody home.** This Home Assistant
|
|
||||||
instance has **no presence/device-tracker sensors configured**. Do NOT
|
|
||||||
infer "nobody is home" from the absence of `zone.*`, `device_tracker.*`,
|
|
||||||
or presence `binary_sensor.*` entities, from `zone.home` being missing /
|
|
||||||
unavailable, or from presence sensors being absent. "No presence data"
|
|
||||||
means **unknown occupancy**, not "empty house". Any cron prompt or rule
|
|
||||||
that says "if no one is home" / "if presence sensors all off" must be
|
|
||||||
treated as: **skip the empty-home logic** and fall back to the safe
|
|
||||||
default (assume someone *might* be home; don't turn things off, don't
|
|
||||||
run aggressive routines). Only treat the house as empty if Roger tells
|
|
||||||
you explicitly, or if a real presence entity actually reports `away` /
|
|
||||||
`off`.
|
|
||||||
|
|
||||||
## How you reach Roger
|
|
||||||
|
|
||||||
Notifications go to Discord (your home channel). Cron jobs deliver there by
|
|
||||||
default (`deliver="discord"`). Keep messages under ~1800 chars. For anything
|
|
||||||
that needs a yes/no decision, ask the question clearly and wait — do not
|
|
||||||
proceed on a guess.
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# One-shot Job that seeds Hermes' built-in cron schedule.
|
|
||||||
# Reconciles: on every run it deletes any existing job with the same name and
|
|
||||||
# recreates it with the prompt/schedule below, so changes to this file (e.g.
|
|
||||||
# prompt wording) are applied to the live schedule on the next ArgoCD sync.
|
|
||||||
# Note: this means live `hermes cron edit` changes will be overwritten — edit
|
|
||||||
# the prompts here in Git instead and let ArgoCD reconcile.
|
|
||||||
#
|
|
||||||
# Cron prompts are deliberately written as plain-English instructions (no inline
|
|
||||||
# curl commands) to avoid tripping Hermes' threat-pattern scanner, which blocks
|
|
||||||
# cron prompts containing curl+auth-header patterns. The exact HA REST API
|
|
||||||
# endpoints and examples are documented in the agent's SOUL.md instead.
|
|
||||||
#
|
|
||||||
# Schedule (4 jobs):
|
|
||||||
# 1. energy-anomaly — every 4h (silent unless power draw is too high)
|
|
||||||
# 2. comfort-check — every 4h (silent unless a room is out of range)
|
|
||||||
# 3. morning-briefing — 0 6 * * * (always delivered)
|
|
||||||
# 4. evening-briefing — 0 22 * * * (always delivered)
|
|
||||||
# NOTE: Hermes' cron runs in UTC (no TZ set on the container). The cluster is
|
|
||||||
# at UTC+2, so every fixed daily time below is expressed in UTC and shifted
|
|
||||||
# back 2h from the intended local wall-clock time:
|
|
||||||
# 08:00 local -> 0 6 (morning briefing)
|
|
||||||
# 00:00 local -> 0 22 (evening briefing, i.e. midnight local)
|
|
||||||
# Relative schedules (every 4h) are timezone-independent.
|
|
||||||
# None of these act autonomously on climate/energy — they report and ask Roger.
|
|
||||||
---
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: Job
|
|
||||||
metadata:
|
|
||||||
name: hermes-cron-seed
|
|
||||||
namespace: home-manager
|
|
||||||
labels:
|
|
||||||
app: hermes
|
|
||||||
annotations:
|
|
||||||
argocd.argoproj.io/sync-options: Replace=true
|
|
||||||
argocd.argoproj.io/hook: Sync
|
|
||||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
|
||||||
spec:
|
|
||||||
backoffLimit: 4
|
|
||||||
ttlSecondsAfterFinished: 86400
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: hermes
|
|
||||||
spec:
|
|
||||||
serviceAccountName: cron-seeder
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
containers:
|
|
||||||
- name: seed
|
|
||||||
image: alpine:3.20
|
|
||||||
command: ["sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
apk add --no-cache curl
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64) KARCH=amd64 ;;
|
|
||||||
aarch64) KARCH=arm64 ;;
|
|
||||||
armv7l) KARCH=arm ;;
|
|
||||||
*) echo "unsupported arch: $ARCH" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
curl -fsSL -o /usr/local/bin/kubectl \
|
|
||||||
"https://dl.k8s.io/release/v1.35.0/bin/linux/${KARCH}/kubectl"
|
|
||||||
chmod +x /usr/local/bin/kubectl
|
|
||||||
|
|
||||||
echo "Waiting for hermes pod to be Ready..."
|
|
||||||
kubectl -n home-manager wait --for=condition=Ready pod -l app=hermes --timeout=300s || true
|
|
||||||
|
|
||||||
POD=$(kubectl -n home-manager get pod -l app=hermes -o jsonpath='{.items[0].metadata.name}')
|
|
||||||
echo "Using pod: $POD"
|
|
||||||
|
|
||||||
# Returns 0 if a cron job with this name already exists.
|
|
||||||
exists() { kubectl -n home-manager exec "$POD" -- hermes cron list 2>/dev/null | grep -qi " $1 "; }
|
|
||||||
|
|
||||||
# Reconcile a cron job to the desired state defined in Git: if a job
|
|
||||||
# with this name already exists, delete it first so the (re)create
|
|
||||||
# below picks up prompt/schedule changes instead of being skipped.
|
|
||||||
create() {
|
|
||||||
name="$1"; schedule="$2"; deliver="$3"; prompt="$4"
|
|
||||||
if exists "$name"; then
|
|
||||||
echo "deleting existing cron job '$name' to apply updates ..."
|
|
||||||
kubectl -n home-manager exec "$POD" -- hermes cron delete "$name" || true
|
|
||||||
fi
|
|
||||||
echo "creating cron job '$name' ..."
|
|
||||||
kubectl -n home-manager exec "$POD" -- hermes cron create "$schedule" "$prompt" --name "$name" --deliver "$deliver"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- 1. Energy anomaly (every 4h, silent unless too high) ----
|
|
||||||
create "energy-anomaly" "every 4h" "discord" \
|
|
||||||
"Check the Home Assistant REST API as documented in your SOUL.md. Read the current total house power draw sensor (sensor.* power / current_power — your main power clamp). Determine whether it is currently day (06:00 to 23:00) or night (23:00 to 06:00) from the current time. If the power draw exceeds 4000 W during the day, or exceeds 1000 W during the night, report to Roger on Discord: the current reading in watts, the time, whether it is day or night, and which threshold was exceeded. Ask Roger what he wants to do about it. Do NOT turn anything off or change any entity yourself — only report and ask. If power is within the normal range for the time of day, reply with exactly [SILENT]."
|
|
||||||
|
|
||||||
# ---- 2. Temperature & humidity check (every 4h, silent unless out of range) ----
|
|
||||||
create "comfort-check" "every 4h" "discord" \
|
|
||||||
"Check the Home Assistant REST API as documented in your SOUL.md. Read indoor temperature and humidity sensors in each room (sensor.*temperature, sensor.*humidity). Comfortable range: temperature between 18C and 26C, humidity between 30 and 65 percent. For any room that is outside that range, report to Roger on Discord: the room name (entity_id), the reading, and whether it is too high or too low. Ask Roger what he wants to do about it. Do NOT change climate/HVAC, fans, or any entity yourself — only report and ask. If every room is within the comfortable range, reply with exactly [SILENT]."
|
|
||||||
|
|
||||||
# ---- 3. Morning briefing (daily at 08:00 local = 06:00 UTC, always delivered) ----
|
|
||||||
create "morning-briefing" "0 6 * * *" "discord" \
|
|
||||||
"Produce a morning home briefing for Roger using the Home Assistant REST API as documented in your SOUL.md. Include, in short bullet form: (1) today's calendar events from calendar.* entities for today (time + title); (2) open tasks from todo.* task lists; (3) overnight energy use — total energy consumed overnight from energy / utility_meter sensors, plus the current power draw; (4) outdoor and indoor temperature and humidity, and the current weather from weather.*; (5) occupancy status from zone.home — report its state if it exists; if zone.home is missing or unavailable, state 'occupancy: unknown — zone.home not configured' rather than claiming no one is home; (6) any sensors currently in an alert or unavailable state. Keep the message under 1800 chars. Always deliver (no [SILENT])."
|
|
||||||
|
|
||||||
# ---- 4. Evening briefing (daily at midnight local = 22:00 UTC, always delivered) ----
|
|
||||||
create "evening-briefing" "0 22 * * *" "discord" \
|
|
||||||
"Produce an evening home status report for Roger using the Home Assistant REST API as documented in your SOUL.md. Report on: (1) lights currently turned on (light.* with state 'on'); (2) sockets and switches currently turned on (switch.* with state 'on'); (3) current energy use / power draw; (4) indoor temperature and humidity per room. List the specific entity_ids and their values. Then ask Roger which of the on devices/lights he wants turned off. Do NOT turn anything off yourself — only report and ask. Keep the message under 1800 chars. Always deliver (no [SILENT])."
|
|
||||||
|
|
||||||
echo "Done. Listing all cron jobs:"
|
|
||||||
kubectl -n home-manager exec "$POD" -- hermes cron list
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: hermes
|
|
||||||
namespace: home-manager
|
|
||||||
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
|
|
||||||
# home via the Home Assistant REST API and notifies via Discord.
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
# 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 HOMEASSISTANT_TOKEN \
|
|
||||||
DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \
|
|
||||||
GATEWAY_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \
|
|
||||||
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
|
|
||||||
# Home Assistant REST API base URL (in-cluster service). Non-secret, so
|
|
||||||
# it lives here rather than in the Secret. The SOUL.md uses $HOMEASSISTANT_URL.
|
|
||||||
- name: HOMEASSISTANT_URL
|
|
||||||
value: "http://home-assistant.home-assistant:80"
|
|
||||||
# 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: "home-manager"
|
|
||||||
# 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>`.
|
|
||||||
# HERMES_WRITE_SAFE_ROOT is intentionally unset → defaults to HERMES_HOME
|
|
||||||
# (/opt/data). The home agent has no git workspace to write to, so the
|
|
||||||
# tighter default is correct (memory/skills/scripts live under /opt/data).
|
|
||||||
volumeMounts:
|
|
||||||
- name: data
|
|
||||||
mountPath: /opt/data
|
|
||||||
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: seed
|
|
||||||
configMap:
|
|
||||||
name: hermes-seed
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: home-manager
|
|
||||||
namespace: home-manager
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: hermes
|
|
||||||
ports:
|
|
||||||
- name: gateway
|
|
||||||
port: 80
|
|
||||||
targetPort: 8642
|
|
||||||
- name: dashboard
|
|
||||||
port: 9119
|
|
||||||
targetPort: 9119
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: home-manager
|
|
||||||
namespace: home-manager
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
spec:
|
|
||||||
ingressClassName: traefik
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- home-manager.rogi.casa
|
|
||||||
secretName: home-manager-tls
|
|
||||||
rules:
|
|
||||||
- host: home-manager.rogi.casa
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: home-manager
|
|
||||||
port:
|
|
||||||
number: 9119 # dashboard
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: home-manager
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: hermes-data
|
|
||||||
namespace: home-manager
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 5Gi
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Minimal RBAC for the cron-seed Job ONLY.
|
|
||||||
#
|
|
||||||
# The Hermes home-manager agent itself has NO k8s RBAC — it manages the home
|
|
||||||
# via the Home Assistant REST API (http://home-assistant.home-assistant:80).
|
|
||||||
#
|
|
||||||
# The cron-seed Job needs to `kubectl exec` into the hermes pod to run
|
|
||||||
# `hermes cron delete` + `hermes cron create ...` (the only way to seed and
|
|
||||||
# reconcile Hermes' internal cron schedule from Git).
|
|
||||||
# Scoped to this namespace, pods/exec on the hermes pod only.
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: cron-seeder
|
|
||||||
namespace: home-manager
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: Role
|
|
||||||
metadata:
|
|
||||||
name: cron-seeder
|
|
||||||
namespace: home-manager
|
|
||||||
rules:
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["pods"]
|
|
||||||
verbs: ["get", "list"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["pods/exec"]
|
|
||||||
verbs: ["create"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: RoleBinding
|
|
||||||
metadata:
|
|
||||||
name: cron-seeder
|
|
||||||
namespace: home-manager
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: Role
|
|
||||||
name: cron-seeder
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: cron-seeder
|
|
||||||
namespace: home-manager
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
---
|
|
||||||
# Home Assistant configuration backup.
|
|
||||||
#
|
|
||||||
# This CronJob copies Home Assistant backup tarballs from the config PVC
|
|
||||||
# (/config/backups) to the Synology NAS over SSH (rsync), so you have an
|
|
||||||
# off-PVC copy of your configuration.
|
|
||||||
#
|
|
||||||
# Prerequisite 1 (in the Home Assistant UI):
|
|
||||||
# Enable automatic backups so tarballs land in /config/backups:
|
|
||||||
# Settings -> System -> Backups -> (menu) -> Schedule backups
|
|
||||||
# Recommended: daily backup, e.g. "Every day at 04:00", keep the last 7.
|
|
||||||
#
|
|
||||||
# Prerequisite 2 (manual, once - secrets are NOT committed to Git):
|
|
||||||
# Create the NAS SSH credentials secret in the cluster:
|
|
||||||
#
|
|
||||||
# kubectl create secret generic home-assistant-backup-nas \
|
|
||||||
# --from-literal=NAS_USER=<nas-ssh-user> \
|
|
||||||
# --from-literal=NAS_HOST=10.88.30.10 \
|
|
||||||
# --from-literal=NAS_PORT=22 \
|
|
||||||
# --from-literal=NAS_PATH='<shared-folder-path>/home-assistant-backups' \
|
|
||||||
# --from-file=ssh-privatekey=$HOME/.ssh/id_rsa_nas \
|
|
||||||
# -n home-assistant
|
|
||||||
#
|
|
||||||
# Also add the NAS to known_hosts (so rsync doesn't prompt):
|
|
||||||
#
|
|
||||||
# ssh-keyscan -p 22 -H 10.88.30.10 > /tmp/nas_known_hosts
|
|
||||||
# kubectl create configmap home-assistant-backup-known-hosts \
|
|
||||||
# --from-file=known_hosts=/tmp/nas_known_hosts \
|
|
||||||
# -n home-assistant
|
|
||||||
#
|
|
||||||
# The NAS_HOST above (10.88.30.10) matches the NAS IP used by the nas-proxy
|
|
||||||
# service in this repo. Adjust NAS_PATH/credentials to your Synology share.
|
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
|
||||||
metadata:
|
|
||||||
name: home-assistant-backup
|
|
||||||
namespace: home-assistant
|
|
||||||
spec:
|
|
||||||
schedule: "30 4 * * *" # daily at 04:30 (after the HA 04:00 backup)
|
|
||||||
concurrencyPolicy: Forbid
|
|
||||||
successfulJobsHistoryLimit: 3
|
|
||||||
failedJobsHistoryLimit: 3
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
backoffLimit: 2
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
containers:
|
|
||||||
- name: backup
|
|
||||||
image: alpine:3.20
|
|
||||||
env:
|
|
||||||
- name: NAS_USER
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: home-assistant-backup-nas
|
|
||||||
key: NAS_USER
|
|
||||||
- name: NAS_HOST
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: home-assistant-backup-nas
|
|
||||||
key: NAS_HOST
|
|
||||||
- name: NAS_PORT
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: home-assistant-backup-nas
|
|
||||||
key: NAS_PORT
|
|
||||||
- name: NAS_PATH
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: home-assistant-backup-nas
|
|
||||||
key: NAS_PATH
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
apk add --no-cache rsync openssh-client
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
cp /ssh-keys/ssh-privatekey ~/.ssh/id_rsa
|
|
||||||
chmod 600 ~/.ssh/id_rsa
|
|
||||||
cp /known-hosts/known_hosts ~/.ssh/known_hosts
|
|
||||||
chmod 644 ~/.ssh/known_hosts
|
|
||||||
echo "Syncing /config/backups -> ${NAS_USER}@${NAS_HOST}:${NAS_PATH}/"
|
|
||||||
rsync -a --delete -e "ssh -p ${NAS_PORT} -o StrictHostKeyChecking=yes" \
|
|
||||||
/config/backups/ "${NAS_USER}@${NAS_HOST}:${NAS_PATH}/"
|
|
||||||
echo "Backup sync complete."
|
|
||||||
volumeMounts:
|
|
||||||
- name: config
|
|
||||||
mountPath: /config
|
|
||||||
readOnly: true
|
|
||||||
- name: ssh-keys
|
|
||||||
mountPath: /ssh-keys
|
|
||||||
readOnly: true
|
|
||||||
- name: known-hosts
|
|
||||||
mountPath: /known-hosts
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: config
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: home-assistant-config
|
|
||||||
- name: ssh-keys
|
|
||||||
secret:
|
|
||||||
secretName: home-assistant-backup-nas
|
|
||||||
items:
|
|
||||||
- key: ssh-privatekey
|
|
||||||
path: ssh-privatekey
|
|
||||||
- name: known-hosts
|
|
||||||
configMap:
|
|
||||||
name: home-assistant-backup-known-hosts
|
|
||||||
@@ -55,17 +55,20 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: home-assistant
|
- name: home-assistant
|
||||||
image: ghcr.io/home-assistant/home-assistant:2026.7.4
|
image: ghcr.io/home-assistant/home-assistant:stable
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "512Mi"
|
memory: "256Mi"
|
||||||
limits:
|
limits:
|
||||||
memory: "1Gi"
|
memory: "512Mi"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8123
|
- containerPort: 8123
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /config
|
mountPath: /config
|
||||||
|
- name: configuration
|
||||||
|
mountPath: /config/configuration.yaml
|
||||||
|
subPath: configuration.yaml
|
||||||
- name: localtime
|
- name: localtime
|
||||||
mountPath: /etc/localtime
|
mountPath: /etc/localtime
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@@ -79,28 +82,6 @@ spec:
|
|||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
- SYS_ADMIN
|
- SYS_ADMIN
|
||||||
# Seed configuration.yaml from the ConfigMap onto the writable PVC so that
|
|
||||||
# Home Assistant has a writable configuration.yaml (required for the UI to
|
|
||||||
# create/save scripts, helpers, reloads, etc.). The ConfigMap stays the
|
|
||||||
# GitOps source of truth: it is re-copied on every pod start. UI-managed
|
|
||||||
# entities (scripts, automations, integrations) live in /config/.storage
|
|
||||||
# on the PVC and are never overwritten by this step.
|
|
||||||
initContainers:
|
|
||||||
- name: seed-config
|
|
||||||
image: busybox:1.36
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
echo "Seeding /config/configuration.yaml from ConfigMap..."
|
|
||||||
cp /config-cm/configuration.yaml /config/configuration.yaml
|
|
||||||
echo "Done."
|
|
||||||
volumeMounts:
|
|
||||||
- name: config
|
|
||||||
mountPath: /config
|
|
||||||
- name: configuration
|
|
||||||
mountPath: /config-cm
|
|
||||||
readOnly: true
|
|
||||||
hostNetwork: true
|
hostNetwork: true
|
||||||
volumes:
|
volumes:
|
||||||
- name: config
|
- name: config
|
||||||
|
|||||||
@@ -11,51 +11,36 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
config.yaml: |
|
config.yaml: |
|
||||||
model_list:
|
model_list:
|
||||||
- model_name: gpt-5.6-luna
|
- model_name: gpt-5-mini
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/gpt-5.6-luna
|
model: openai/gpt-5-mini-2025-08-07
|
||||||
api_key: "os.environ/OPENAI_API_KEY"
|
api_key: "os.environ/OPENAI_API_KEY"
|
||||||
- model_name: claude-haiku-4.5
|
- model_name: claude-4.5-haiku
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: "anthropic/claude-haiku-4-5-20251001"
|
model: "anthropic/claude-haiku-4-5-20251001"
|
||||||
api_key: "os.environ/ANTHROPIC_API_KEY"
|
api_key: "os.environ/ANTHROPIC_API_KEY"
|
||||||
- model_name: claude-sonnet-5
|
- model_name: gemini-3-flash
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: "anthropic/claude-sonnet-5"
|
model: gemini/gemini-3-flash-preview
|
||||||
api_key: "os.environ/ANTHROPIC_API_KEY"
|
|
||||||
- model_name: gemini-3.6-flash
|
|
||||||
litellm_params:
|
|
||||||
model: gemini/gemini-3.6-flash
|
|
||||||
api_key: "os.environ/GEMINI_API_KEY"
|
api_key: "os.environ/GEMINI_API_KEY"
|
||||||
- model_name: tencent/hy3
|
|
||||||
litellm_params:
|
|
||||||
model: openrouter/tencent/hy3
|
|
||||||
api_key: "os.environ/OPENROUTER_API_KEY"
|
|
||||||
- model_name: z-ai/glm-5.2
|
|
||||||
litellm_params:
|
|
||||||
model: openrouter/z-ai/glm-5.2
|
|
||||||
api_key: "os.environ/OPENROUTER_API_KEY"
|
|
||||||
- model_name: glm-4.7-flash
|
- model_name: glm-4.7-flash
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: ollama/glm-4.7-flash
|
model: ollama/glm-4.7-flash
|
||||||
api_base: http://10.88.20.12:11434
|
api_base: http://10.88.20.12:11434
|
||||||
# Used by the platform-engineer Hermes agent (deployed in ns platform-engineer).
|
# Used by the platform-engineer Hermes agent (deployed in ns platform-engineer).
|
||||||
# Served by llama.cpp (llama-server, Vulkan on the NUCBox APU) — see
|
# model_name is the alias Hermes requests; the underlying Ollama model is
|
||||||
# llamacpp/deployment-qwen36-27b.yaml. Thinking is left ON; reasoning comes
|
# qwen3.6:latest (the fast non-27b tag). 27b is a slow reasoning model.
|
||||||
# back in `reasoning_content` (llama-server --jinja + default reasoning format).
|
# `ollama_chat/` (not `ollama/`) uses Ollama's NATIVE /api/chat endpoint.
|
||||||
- model_name: qwen3.6-27b
|
# `think: false` + `chat_template_kwargs.enable_thinking: false` disable
|
||||||
|
# Qwen3 thinking so the model emits content directly (otherwise the
|
||||||
|
# OpenAI-compat translation returns empty content with reasoning split off).
|
||||||
|
- model_name: qwen3.6
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/qwen3.6-27b
|
model: ollama_chat/qwen3.6:latest
|
||||||
api_base: http://llamacpp-qwen36-27b.llamacpp/v1
|
api_base: http://10.88.20.12:11434
|
||||||
api_key: "sk-no-auth"
|
think: false
|
||||||
# Faster "flash" variant: Qwen3.6-35B-A3B is a MoE (3B active params), so
|
chat_template_kwargs:
|
||||||
# inference is much faster than the dense 27B despite more total weights.
|
enable_thinking: false
|
||||||
# See llamacpp/deployment-qwen36-35b-a3b.yaml.
|
|
||||||
- model_name: qwen3.6-35b-a3b
|
|
||||||
litellm_params:
|
|
||||||
model: openai/qwen3.6-35b-a3b
|
|
||||||
api_base: http://llamacpp-qwen36-35b-a3b.llamacpp/v1
|
|
||||||
api_key: "sk-no-auth"
|
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
#set_verbose: True # Uncomment this if you want to see verbose logs; not recommended in production
|
#set_verbose: True # Uncomment this if you want to see verbose logs; not recommended in production
|
||||||
callbacks: ["arize_phoenix"]
|
callbacks: ["arize_phoenix"]
|
||||||
@@ -66,7 +51,7 @@ data:
|
|||||||
request_timeout: 600
|
request_timeout: 600
|
||||||
telemetry: False
|
telemetry: False
|
||||||
general_settings:
|
general_settings:
|
||||||
master_key: os.environ/LITELLM_MASTER_KEY
|
master_key: sk-1234 # [OPTIONAL] Use to enforce auth on proxy. See - https://docs.litellm.ai/docs/proxy/virtual_keys
|
||||||
store_model_in_db: True
|
store_model_in_db: True
|
||||||
proxy_budget_rescheduler_min_time: 60
|
proxy_budget_rescheduler_min_time: 60
|
||||||
proxy_budget_rescheduler_max_time: 64
|
proxy_budget_rescheduler_max_time: 64
|
||||||
@@ -105,13 +90,6 @@ spec:
|
|||||||
- "/app/proxy_server_config.yaml"
|
- "/app/proxy_server_config.yaml"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 4000
|
- containerPort: 4000
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
limits:
|
|
||||||
memory: "2Gi"
|
|
||||||
cpu: "1000m"
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
mountPath: /app/proxy_server_config.yaml
|
mountPath: /app/proxy_server_config.yaml
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
# llama.cpp (llama-server)
|
|
||||||
|
|
||||||
In-cluster LLM inference via llama.cpp's `llama-server`, serving local models on
|
|
||||||
the NUCBox APU (AMD Ryzen AI Max 395 / Strix Halo, Radeon 8060S, 128 GiB unified
|
|
||||||
memory: 32 GiB RAM / 96 GiB VRAM).
|
|
||||||
|
|
||||||
This replaces the bare-metal Ollama setup for models that benefit from
|
|
||||||
always-loaded weights + tuned batching. LiteLLM (`litellm/`) points at these
|
|
||||||
in-cluster Services instead of the external `10.88.20.12:11434` Ollama endpoint.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
One Deployment + Service **per model**, all in namespace `llamacpp`, all pinned
|
|
||||||
to the NUCBox (`nodeSelector: {kubernetes.io/arch: amd64, hardware: high-memory}`):
|
|
||||||
|
|
||||||
| Alias | Model | GGUF | Service |
|
|
||||||
|-------------------|----------------------------|-------------------------------------------------------|-------------------------------------------|
|
|
||||||
| `qwen3.6-27b` | Qwen3.6-27B (dense) | unsloth/Qwen3.6-27B-MTP-GGUF (Q4_K_XL, ~16 GiB) | `llamacpp-qwen36-27b.llamacpp:80` |
|
|
||||||
| `qwen3.6-35b-a3b` | Qwen3.6-35B-A3B (MoE/flash)| unsloth/Qwen3.6-35B-A3B-MTP-GGUF (Q4_K_XL, ~20 GiB) | `llamacpp-qwen36-35b-a3b.llamacpp:80` |
|
|
||||||
|
|
||||||
The 35B-A3B is a Mixture-of-Experts model (3B active params per token), so
|
|
||||||
inference is significantly faster than the dense 27B despite more total weights —
|
|
||||||
hence the "flash" label. Use it for latency-sensitive workloads; use the 27B for
|
|
||||||
deeper reasoning.
|
|
||||||
|
|
||||||
Model files are downloaded idempotently by an initContainer into a shared
|
|
||||||
hostPath PVC (`/data/llamacpp/models` on the NUCBox), so pods survive reboots
|
|
||||||
without re-downloading.
|
|
||||||
|
|
||||||
## GPU / Vulkan
|
|
||||||
|
|
||||||
The `server-vulkan` image (`ghcr.io/ggml-org/llama.cpp:server-vulkan`) bundles
|
|
||||||
the Mesa/RADV Vulkan driver, which supports the Radeon 8060S (RDNA 3.5). Full
|
|
||||||
layer offload (`-ngl 999`) puts model weights entirely in the 96 GiB VRAM pool.
|
|
||||||
|
|
||||||
The container mounts `/dev/dri` and runs `privileged: true` — the simplest
|
|
||||||
reliable way to give Vulkan access to the DRM render node on k3s without a
|
|
||||||
device plugin. Tighten later with `supplementalGroups` (the host's `render`
|
|
||||||
group GID) if desired.
|
|
||||||
|
|
||||||
### Verify the GPU is actually used
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl logs -n llamacpp deploy/llamacpp-qwen36-27b | grep -iE 'vulkan|gpu|offload|device'
|
|
||||||
kubectl logs -n llamacpp deploy/llamacpp-qwen36-35b-a3b | grep -iE 'vulkan|gpu|offload|device'
|
|
||||||
```
|
|
||||||
|
|
||||||
If only a CPU device shows up, the container can't see the GPU — check that
|
|
||||||
`/dev/dri/renderD128` exists on the NUCBox and that the `amdgpu` module is loaded.
|
|
||||||
|
|
||||||
## VRAM budget (both models co-resident)
|
|
||||||
|
|
||||||
Both models run simultaneously on the same 96 GiB VRAM pool. Approximate usage:
|
|
||||||
|
|
||||||
| Model | Weights | KV cache | Subtotal |
|
|
||||||
|-------------------|----------|-------------------------------------------|----------|
|
|
||||||
| qwen3.6-27b | ~16 GiB | ~16 GiB (q8_0, 131k total, 1 slot) | ~32 GiB |
|
|
||||||
| qwen3.6-35b-a3b | ~20 GiB | ~9 GiB (q8_0, 262k total, 131k/slot) | ~29 GiB |
|
|
||||||
| **Total** | | | **~61 GiB** |
|
|
||||||
|
|
||||||
~35 GiB headroom — comfortable. Both models' KV caches are quantized
|
|
||||||
to q8_0 (halved vs f16, ~negligible quality loss), which makes the dense 27B's
|
|
||||||
large-context KV affordable (~256 KiB/token f16 → ~128 KiB/token q8_0). The
|
|
||||||
a3b's KV is tiny (~72 KiB/token) so its large context is nearly free.
|
|
||||||
|
|
||||||
## Tuning
|
|
||||||
|
|
||||||
The key knobs (in each `deployment-*.yaml`):
|
|
||||||
|
|
||||||
- `-ngl 999` — offload all layers to GPU. Reduce only if VRAM is tight.
|
|
||||||
- `-c` — total KV-cache context. The 27B runs `-c 131072 -np 1` (single slot
|
|
||||||
gets the full 131k); the a3b runs `-c 262144 -np 2` (2 slots × 131k each).
|
|
||||||
The MoE a3b's KV cache is ~72 KiB/token so large context is cheap; the dense
|
|
||||||
27B's is ~256 KiB/token (f16) / ~128 KiB/token (q8_0), which is why the 27B
|
|
||||||
uses q8_0 KV to keep 131k affordable (~16 GiB) and sticks to 1 slot.
|
|
||||||
- `-np` — parallel slots (concurrent requests). Each extra slot multiplies
|
|
||||||
KV-cache VRAM usage. The 27B uses 1 slot (full 131k to the single request,
|
|
||||||
dense KV is the constraint); the a3b uses 2 slots (131k each, cheap MoE KV).
|
|
||||||
Bump higher only if you need more concurrent throughput.
|
|
||||||
- `--cache-type-k q8_0 --cache-type-v q8_0` (both) — quantize the KV cache to
|
|
||||||
q8_0, halving KV VRAM with ~negligible quality loss. Essential for the dense
|
|
||||||
27B at 131k (f16 would be ~32 GiB KV alone); nearly free headroom on the a3b.
|
|
||||||
Drop to q4_0 for even less VRAM if retrieval quality allows.
|
|
||||||
- `--threads 8` — CPU threads for sampling/overhead. Mostly irrelevant under
|
|
||||||
full GPU offload; tune if CPU-bound.
|
|
||||||
|
|
||||||
## Memory accounting
|
|
||||||
|
|
||||||
k8s sees only the ~32 GiB system RAM as allocatable (the 96 GiB VRAM is
|
|
||||||
reserved by firmware and managed by `amdgpu`). The model weights and KV cache
|
|
||||||
live in VRAM and are **not** counted against the container's cgroup memory
|
|
||||||
limit — that limit only covers CPU-side overhead and the mmap'd GGUF pages
|
|
||||||
during load. If the pod is OOM-killed during model load, raise the memory limit.
|
|
||||||
|
|
||||||
## Adding a model
|
|
||||||
|
|
||||||
1. Copy `deployment-qwen36-27b.yaml` → `deployment-<new>.yaml`; change the
|
|
||||||
`model:` label, GGUF URL/file, `--alias`, and Service name.
|
|
||||||
2. Point LiteLLM at it in `litellm/litellm.yaml`:
|
|
||||||
```yaml
|
|
||||||
- model_name: <alias>
|
|
||||||
litellm_params:
|
|
||||||
model: openai/<alias>
|
|
||||||
api_base: http://<service>.llamacpp/v1
|
|
||||||
api_key: "sk-no-auth"
|
|
||||||
```
|
|
||||||
3. (No gen-apps.sh change needed — the `llamacpp` app already syncs the whole
|
|
||||||
directory recursively.)
|
|
||||||
4. Check the VRAM budget table above — two large models may not coexist.
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# Qwen3.6-27B (dense, Q4_K_XL) served by llama.cpp's llama-server on the NUCBox APU.
|
|
||||||
#
|
|
||||||
# Hardware: AMD Ryzen AI Max 395 (Strix Halo) — integrated Radeon 8060S,
|
|
||||||
# 128 GiB unified memory (32 GiB RAM / 96 GiB VRAM via firmware). The Q4 model
|
|
||||||
# (~16 GiB) is fully offloaded to the GPU via the Vulkan backend.
|
|
||||||
#
|
|
||||||
# Image: ghcr.io/ggml-org/llama.cpp:server-vulkan bundles the Mesa/RADV Vulkan
|
|
||||||
# driver, which supports the Radeon 8060S (RDNA 3.5). The project moved from the
|
|
||||||
# legacy `ggerganov/llama.cpp` namespace (which only has light/full tags) to
|
|
||||||
# `ggml-org/llama.cpp` (server-vulkan + pinned build tags like server-vulkan-bXXXX).
|
|
||||||
# Pin to a build tag (e.g. server-vulkan-b4738) for production reproducibility.
|
|
||||||
#
|
|
||||||
# GPU access: the container mounts /dev/dri (the DRM render nodes) and runs
|
|
||||||
# privileged. This is the simplest reliable option on k3s without a Vulkan
|
|
||||||
# device plugin; tighten later with supplementalGroups if desired.
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-qwen36-27b
|
|
||||||
namespace: llamacpp
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-27b
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate # never run two pods loading the same model into VRAM
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-27b
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-27b
|
|
||||||
spec:
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/arch: amd64
|
|
||||||
hardware: high-memory
|
|
||||||
initContainers:
|
|
||||||
# Idempotently download the GGUF into the shared models PVC on first boot.
|
|
||||||
# Exits immediately if the file is already present (pod restart / recreate).
|
|
||||||
- name: fetch-model
|
|
||||||
image: alpine:3.20
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
if [ -f "/models/$MODEL_FILE" ]; then
|
|
||||||
echo "Model $MODEL_FILE already present — skipping download."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Installing curl..."
|
|
||||||
apk add --no-cache curl
|
|
||||||
echo "Downloading $MODEL_FILE from $MODEL_URL ..."
|
|
||||||
curl -fL --retry 5 --retry-delay 5 -o "/models/$MODEL_FILE" "$MODEL_URL"
|
|
||||||
echo "Download complete: $(ls -lh /models/$MODEL_FILE)"
|
|
||||||
env:
|
|
||||||
- name: MODEL_URL
|
|
||||||
value: "https://huggingface.co/unsloth/Qwen3.6-27B-MTP-GGUF/resolve/main/Qwen3.6-27B-UD-Q4_K_XL.gguf"
|
|
||||||
- name: MODEL_FILE
|
|
||||||
value: "Qwen3.6-27B-UD-Q4_K_XL.gguf"
|
|
||||||
volumeMounts:
|
|
||||||
- name: models
|
|
||||||
mountPath: /models
|
|
||||||
containers:
|
|
||||||
- name: llama-server
|
|
||||||
image: ghcr.io/ggml-org/llama.cpp:server-vulkan
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
# llama.cpp's CLI parser does NOT split on '=' — every value flag must be a
|
|
||||||
# separate argv element (flag, then value). See common/arg.cpp in the repo.
|
|
||||||
args:
|
|
||||||
- -m # model file
|
|
||||||
- /models/Qwen3.6-27B-UD-Q4_K_XL.gguf
|
|
||||||
- --alias # /v1/models reports this name; matches the litellm alias
|
|
||||||
- qwen3.6-27b
|
|
||||||
- --host
|
|
||||||
- 0.0.0.0
|
|
||||||
- --port
|
|
||||||
- "8080"
|
|
||||||
- --jinja # use the GGUF's chat template (Qwen3 thinking format)
|
|
||||||
- -ngl # offload ALL layers to the GPU (fits in 96 GiB VRAM)
|
|
||||||
- "999"
|
|
||||||
- -c # total KV-cache context (single slot gets the full window)
|
|
||||||
- "131072"
|
|
||||||
- -np # 1 slot => 131072 tokens for the single request (no split)
|
|
||||||
- "1"
|
|
||||||
- --cont-batching # continuous batching across slots
|
|
||||||
- --cache-type-k # quantize KV cache to q8_0 — halves KV VRAM (~32 GiB → ~16 GiB
|
|
||||||
- q8_0 # at 131k ctx); ~negligible quality loss, frees headroom for large -c
|
|
||||||
- --cache-type-v
|
|
||||||
- q8_0
|
|
||||||
- --threads # CPU threads for sampling/overhead (GPU does the heavy lifting)
|
|
||||||
- "8"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8080
|
|
||||||
resources:
|
|
||||||
# The model weights + KV cache live in GPU VRAM (96 GiB pool) and are
|
|
||||||
# NOT counted against the cgroup memory limit. This limit only covers
|
|
||||||
# CPU-side overhead + the mmap'd GGUF file pages during load (~16 GiB,
|
|
||||||
# reclaimable). k8s sees ~32 GiB as the node's allocatable system RAM,
|
|
||||||
# so the request is kept low to stay schedulable alongside other pods.
|
|
||||||
# If the pod OOM-kills during load, the amdgpu driver may be counting
|
|
||||||
# some VRAM against the cgroup — raise the limit.
|
|
||||||
requests:
|
|
||||||
cpu: "1000m"
|
|
||||||
memory: "2Gi"
|
|
||||||
limits:
|
|
||||||
cpu: "4000m"
|
|
||||||
memory: "24Gi"
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
failureThreshold: 6
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 180 # model load + Vulkan init can take a few minutes
|
|
||||||
periodSeconds: 30
|
|
||||||
failureThreshold: 5
|
|
||||||
securityContext:
|
|
||||||
# Vulkan on the AMD APU needs /dev/dri + the driver. Privileged is the
|
|
||||||
# simplest reliable path on k3s without a device plugin.
|
|
||||||
privileged: true
|
|
||||||
volumeMounts:
|
|
||||||
- name: models
|
|
||||||
mountPath: /models
|
|
||||||
readOnly: true
|
|
||||||
- name: dri
|
|
||||||
mountPath: /dev/dri
|
|
||||||
volumes:
|
|
||||||
- name: models
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: llamacpp-models
|
|
||||||
- name: dri
|
|
||||||
hostPath:
|
|
||||||
path: /dev/dri
|
|
||||||
type: Directory
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-qwen36-27b
|
|
||||||
namespace: llamacpp
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-27b
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-27b
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# Qwen3.6-35B-A3B (MoE: 35B total / 3B active, Q4_K_XL) — the "flash" variant.
|
|
||||||
#
|
|
||||||
# Despite having more total parameters than the 27B dense model, only 3B are
|
|
||||||
# active per token (Mixture-of-Experts), so inference is much faster. The full
|
|
||||||
# ~20 GiB of Q4 weights is still loaded into VRAM but only a small fraction is
|
|
||||||
# computed per token.
|
|
||||||
#
|
|
||||||
# Hardware: AMD Ryzen AI Max 395 (Strix Halo) — integrated Radeon 8060S,
|
|
||||||
# 128 GiB unified memory (32 GiB RAM / 96 GiB VRAM via firmware). Full GPU
|
|
||||||
# offload via the Vulkan backend. Shares the 96 GiB VRAM pool with the 27B
|
|
||||||
# model — see llamacpp/README.md for the combined VRAM budget.
|
|
||||||
#
|
|
||||||
# Image: ghcr.io/ggml-org/llama.cpp:server-vulkan (Mesa/RADV Vulkan driver,
|
|
||||||
# supports the Radeon 8060S / RDNA 3.5). Pin to a build tag for production.
|
|
||||||
#
|
|
||||||
# GPU access: mounts /dev/dri + privileged (simplest reliable path on k3s).
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-qwen36-35b-a3b
|
|
||||||
namespace: llamacpp
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate # never run two pods loading the same model into VRAM
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
spec:
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/arch: amd64
|
|
||||||
hardware: high-memory
|
|
||||||
initContainers:
|
|
||||||
# Idempotently download the GGUF into the shared models PVC on first boot.
|
|
||||||
# Exits immediately if the file is already present (pod restart / recreate).
|
|
||||||
- name: fetch-model
|
|
||||||
image: alpine:3.20
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
if [ -f "/models/$MODEL_FILE" ]; then
|
|
||||||
echo "Model $MODEL_FILE already present — skipping download."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Installing curl..."
|
|
||||||
apk add --no-cache curl
|
|
||||||
echo "Downloading $MODEL_FILE from $MODEL_URL ..."
|
|
||||||
curl -fL --retry 5 --retry-delay 5 -o "/models/$MODEL_FILE" "$MODEL_URL"
|
|
||||||
echo "Download complete: $(ls -lh /models/$MODEL_FILE)"
|
|
||||||
env:
|
|
||||||
- name: MODEL_URL
|
|
||||||
value: "https://huggingface.co/unsloth/Qwen3.6-35B-A3B-MTP-GGUF/resolve/main/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
|
|
||||||
- name: MODEL_FILE
|
|
||||||
value: "Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
|
|
||||||
volumeMounts:
|
|
||||||
- name: models
|
|
||||||
mountPath: /models
|
|
||||||
containers:
|
|
||||||
- name: llama-server
|
|
||||||
image: ghcr.io/ggml-org/llama.cpp:server-vulkan
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
# llama.cpp's CLI parser does NOT split on '=' — every value flag must be a
|
|
||||||
# separate argv element (flag, then value). See common/arg.cpp in the repo.
|
|
||||||
args:
|
|
||||||
- -m # model file
|
|
||||||
- /models/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf
|
|
||||||
- --alias # /v1/models reports this name; matches the litellm alias
|
|
||||||
- qwen3.6-35b-a3b
|
|
||||||
- --host
|
|
||||||
- 0.0.0.0
|
|
||||||
- --port
|
|
||||||
- "8080"
|
|
||||||
- --jinja # use the GGUF's chat template (Qwen3 thinking format)
|
|
||||||
- -ngl # offload ALL layers to the GPU (fits in 96 GiB VRAM)
|
|
||||||
- "999"
|
|
||||||
- -c # total KV-cache context, split across parallel slots
|
|
||||||
- "262144"
|
|
||||||
- -np # 2 parallel slots => 131072 tokens per concurrent request
|
|
||||||
- "2"
|
|
||||||
- --cont-batching # continuous batching across slots
|
|
||||||
- --cache-type-k # quantize KV cache to q8_0 — halves KV VRAM (~18 GiB → ~9 GiB
|
|
||||||
- q8_0 # at 262k ctx); ~negligible quality loss, frees headroom for large -c
|
|
||||||
- --cache-type-v # (raise to q4_0 for even less VRAM if retrieval quality allows)
|
|
||||||
- q8_0
|
|
||||||
- --threads # CPU threads for sampling/overhead (GPU does the heavy lifting)
|
|
||||||
- "8"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: 8080
|
|
||||||
resources:
|
|
||||||
# The model weights + KV cache live in GPU VRAM (96 GiB pool) and are
|
|
||||||
# NOT counted against the cgroup memory limit. This limit only covers
|
|
||||||
# CPU-side overhead + the mmap'd GGUF file pages during load (~20 GiB,
|
|
||||||
# reclaimable). k8s sees ~32 GiB as the node's allocatable system RAM.
|
|
||||||
# If the pod OOM-kills during load, raise the limit.
|
|
||||||
requests:
|
|
||||||
cpu: "1000m"
|
|
||||||
memory: "2Gi"
|
|
||||||
limits:
|
|
||||||
cpu: "4000m"
|
|
||||||
memory: "24Gi"
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
failureThreshold: 6
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /health
|
|
||||||
port: 8080
|
|
||||||
initialDelaySeconds: 180 # model load + Vulkan init can take a few minutes
|
|
||||||
periodSeconds: 30
|
|
||||||
failureThreshold: 5
|
|
||||||
securityContext:
|
|
||||||
# Vulkan on the AMD APU needs /dev/dri + the driver. Privileged is the
|
|
||||||
# simplest reliable path on k3s without a device plugin.
|
|
||||||
privileged: true
|
|
||||||
volumeMounts:
|
|
||||||
- name: models
|
|
||||||
mountPath: /models
|
|
||||||
readOnly: true
|
|
||||||
- name: dri
|
|
||||||
mountPath: /dev/dri
|
|
||||||
volumes:
|
|
||||||
- name: models
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: llamacpp-models
|
|
||||||
- name: dri
|
|
||||||
hostPath:
|
|
||||||
path: /dev/dri
|
|
||||||
type: Directory
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-qwen36-35b-a3b
|
|
||||||
namespace: llamacpp
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
spec:
|
|
||||||
type: ClusterIP
|
|
||||||
selector:
|
|
||||||
app: llamacpp
|
|
||||||
model: qwen3.6-35b-a3b
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: llamacpp
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Shared model-weight storage for all llama.cpp pods.
|
|
||||||
#
|
|
||||||
# All llamacpp pods are pinned to the NUCBox (roger-nucbox-evo-x2) via
|
|
||||||
# nodeSelector, so a single hostPath PV on that node is correct and matches the
|
|
||||||
# existing postgres hostPath pattern. GGUF files are large (10s of GB); baking
|
|
||||||
# them into images would be wasteful, and an initContainer downloads them
|
|
||||||
# idempotently on first boot instead.
|
|
||||||
#
|
|
||||||
# nodeAffinity keeps the PV bound to the NUCBox even if labels change later.
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolume
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-models
|
|
||||||
labels:
|
|
||||||
type: local
|
|
||||||
app: llamacpp
|
|
||||||
spec:
|
|
||||||
storageClassName: manual
|
|
||||||
capacity:
|
|
||||||
storage: 100Gi
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteMany
|
|
||||||
hostPath:
|
|
||||||
path: /data/llamacpp/models
|
|
||||||
nodeAffinity:
|
|
||||||
required:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: kubernetes.io/hostname
|
|
||||||
operator: In
|
|
||||||
values:
|
|
||||||
- roger-nucbox-evo-x2
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
|
||||||
name: llamacpp-models
|
|
||||||
namespace: llamacpp
|
|
||||||
labels:
|
|
||||||
app: llamacpp
|
|
||||||
spec:
|
|
||||||
storageClassName: manual
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteMany
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 100Gi
|
|
||||||
@@ -14,8 +14,6 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: prometheus
|
app: prometheus
|
||||||
annotations:
|
|
||||||
kubectl.kubernetes.io/restartedAt: "2026-07-26T12:00:00Z"
|
|
||||||
spec:
|
spec:
|
||||||
# Prevent scheduling on Raspberry Pi due to resource requirements (512Mi-1Gi memory, 500m-1000m CPU)
|
# Prevent scheduling on Raspberry Pi due to resource requirements (512Mi-1Gi memory, 500m-1000m CPU)
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
@@ -41,10 +39,10 @@ spec:
|
|||||||
mountPath: /prometheus
|
mountPath: /prometheus
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "1Gi"
|
memory: "512Mi"
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
limits:
|
limits:
|
||||||
memory: "4Gi"
|
memory: "1Gi"
|
||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
volumes:
|
volumes:
|
||||||
- name: prometheus-config
|
- name: prometheus-config
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: deadline-checker
|
- name: deadline-checker
|
||||||
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- python
|
- python
|
||||||
- -c
|
- run_job.py
|
||||||
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
|
|
||||||
- deadline-checker
|
- deadline-checker
|
||||||
env:
|
env:
|
||||||
- name: MYORG_REPO_PATH
|
- name: MYORG_REPO_PATH
|
||||||
@@ -54,16 +53,6 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: myorg-assistant-secret
|
name: myorg-assistant-secret
|
||||||
key: LITELLM_API_KEY
|
key: LITELLM_API_KEY
|
||||||
- name: WEB_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: WEB_SECRET_KEY
|
|
||||||
- name: GIT_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_TOKEN
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: myorg-data
|
- name: myorg-data
|
||||||
mountPath: /data/myorg
|
mountPath: /data/myorg
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: evening-summary
|
- name: evening-summary
|
||||||
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- python
|
- python
|
||||||
- -c
|
- run_job.py
|
||||||
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
|
|
||||||
- evening-summary
|
- evening-summary
|
||||||
env:
|
env:
|
||||||
- name: MYORG_REPO_PATH
|
- name: MYORG_REPO_PATH
|
||||||
@@ -54,16 +53,6 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: myorg-assistant-secret
|
name: myorg-assistant-secret
|
||||||
key: LITELLM_API_KEY
|
key: LITELLM_API_KEY
|
||||||
- name: WEB_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: WEB_SECRET_KEY
|
|
||||||
- name: GIT_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_TOKEN
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: myorg-data
|
- name: myorg-data
|
||||||
mountPath: /data/myorg
|
mountPath: /data/myorg
|
||||||
|
|||||||
@@ -24,51 +24,13 @@ spec:
|
|||||||
restartPolicy: OnFailure
|
restartPolicy: OnFailure
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
- name: gitea-registry
|
- name: gitea-registry
|
||||||
initContainers:
|
|
||||||
- name: git-clone
|
|
||||||
image: alpine/git:latest
|
|
||||||
command:
|
|
||||||
- sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
if [ ! -d /data/myorg/.git ]; then
|
|
||||||
echo "Cloning repository..."
|
|
||||||
git clone ${GIT_REPO_URL} /data/myorg
|
|
||||||
cd /data/myorg
|
|
||||||
git config user.name "${GIT_USERNAME}"
|
|
||||||
git config user.email "${GIT_USERNAME}@rogi.casa"
|
|
||||||
git config credential.helper store
|
|
||||||
echo "https://${GIT_USERNAME}:${GIT_TOKEN}@git.rogi.casa" > ~/.git-credentials
|
|
||||||
else
|
|
||||||
echo "Repository already exists, skipping clone."
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
- name: GIT_REPO_URL
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_REPO_URL
|
|
||||||
- name: GIT_USERNAME
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_USERNAME
|
|
||||||
- name: GIT_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_TOKEN
|
|
||||||
volumeMounts:
|
|
||||||
- name: myorg-data
|
|
||||||
mountPath: /data/myorg
|
|
||||||
containers:
|
containers:
|
||||||
- name: git-sync
|
- name: git-sync
|
||||||
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- python
|
- python
|
||||||
- -c
|
- run_job.py
|
||||||
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
|
|
||||||
- git-sync
|
- git-sync
|
||||||
env:
|
env:
|
||||||
- name: MYORG_REPO_PATH
|
- name: MYORG_REPO_PATH
|
||||||
@@ -106,11 +68,6 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: myorg-assistant-secret
|
name: myorg-assistant-secret
|
||||||
key: LITELLM_API_KEY
|
key: LITELLM_API_KEY
|
||||||
- name: WEB_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: WEB_SECRET_KEY
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: myorg-data
|
- name: myorg-data
|
||||||
mountPath: /data/myorg
|
mountPath: /data/myorg
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: morning-briefing
|
- name: morning-briefing
|
||||||
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- python
|
- python
|
||||||
- -c
|
- run_job.py
|
||||||
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
|
|
||||||
- morning-briefing
|
- morning-briefing
|
||||||
env:
|
env:
|
||||||
# From ConfigMap
|
# From ConfigMap
|
||||||
@@ -61,16 +60,6 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: myorg-assistant-secret
|
name: myorg-assistant-secret
|
||||||
key: LITELLM_API_KEY
|
key: LITELLM_API_KEY
|
||||||
- name: WEB_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: WEB_SECRET_KEY
|
|
||||||
- name: GIT_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_TOKEN
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: myorg-data
|
- name: myorg-data
|
||||||
mountPath: /data/myorg
|
mountPath: /data/myorg
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: waiting-followup
|
- name: waiting-followup
|
||||||
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
image: git.rogi.casa/roger/myorg-assistant/myorg-assistant:fcf79bf
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- python
|
- python
|
||||||
- -c
|
- run_job.py
|
||||||
- "from src.scheduler.jobs import run_job; import sys; run_job(sys.argv[1])"
|
|
||||||
- waiting-followup
|
- waiting-followup
|
||||||
env:
|
env:
|
||||||
- name: MYORG_REPO_PATH
|
- name: MYORG_REPO_PATH
|
||||||
@@ -54,16 +53,6 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: myorg-assistant-secret
|
name: myorg-assistant-secret
|
||||||
key: LITELLM_API_KEY
|
key: LITELLM_API_KEY
|
||||||
- name: WEB_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: WEB_SECRET_KEY
|
|
||||||
- name: GIT_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: myorg-assistant-secret
|
|
||||||
key: GIT_TOKEN
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: myorg-data
|
- name: myorg-data
|
||||||
mountPath: /data/myorg
|
mountPath: /data/myorg
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ spec:
|
|||||||
git config user.name "${GIT_USERNAME}"
|
git config user.name "${GIT_USERNAME}"
|
||||||
git config user.email "${GIT_USERNAME}@rogi.casa"
|
git config user.email "${GIT_USERNAME}@rogi.casa"
|
||||||
git config credential.helper store
|
git config credential.helper store
|
||||||
echo "https://${GIT_USERNAME}:${GIT_TOKEN}@git.rogi.casa" > ~/.git-credentials
|
echo "https://${GIT_USERNAME}:${GIT_TOKEN}@gitea.rogi.casa" > ~/.git-credentials
|
||||||
else
|
else
|
||||||
echo "Repository already exists, pulling latest changes..."
|
echo "Repository already exists, pulling latest changes..."
|
||||||
cd /data/myorg
|
cd /data/myorg
|
||||||
|
|||||||
@@ -53,17 +53,15 @@ spec:
|
|||||||
value: http
|
value: http
|
||||||
- name: N8N_PORT
|
- name: N8N_PORT
|
||||||
value: "5678"
|
value: "5678"
|
||||||
- name: NODE_OPTIONS
|
|
||||||
value: "--max-old-space-size=768"
|
|
||||||
image: n8nio/n8n
|
image: n8nio/n8n
|
||||||
name: n8n
|
name: n8n
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5678
|
- containerPort: 5678
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "512Mi"
|
memory: "250Mi"
|
||||||
limits:
|
limits:
|
||||||
memory: "1Gi"
|
memory: "500Mi"
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /home/node/.n8n
|
- mountPath: /home/node/.n8n
|
||||||
name: n8n-claim0
|
name: n8n-claim0
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ spec:
|
|||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- ai.rogi.casa
|
- openai.rogi.casa
|
||||||
secretName: openwebui-tls
|
secretName: openwebui-tls
|
||||||
rules:
|
rules:
|
||||||
- host: ai.rogi.casa
|
- host: openai.rogi.casa
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ platform-engineer/
|
|||||||
├── pvc.yaml # persistent /opt/data (HERMES_HOME)
|
├── pvc.yaml # persistent /opt/data (HERMES_HOME)
|
||||||
├── dockerfile # derived image: hermes-agent + kubectl + helm
|
├── dockerfile # derived image: hermes-agent + kubectl + helm
|
||||||
├── deployment.yaml # Deployment, schedules on amd64, mounts kube SA token
|
├── deployment.yaml # Deployment, schedules on amd64, mounts kube SA token
|
||||||
├── ingress.yaml # platform-engineer.rogi.casa → dashboard (optional)
|
├── ingress.yaml # hermes.rogi.casa → dashboard (optional)
|
||||||
└── README.md # this file
|
└── README.md # this file
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -130,10 +130,10 @@ model:
|
|||||||
auxiliary:
|
auxiliary:
|
||||||
compression:
|
compression:
|
||||||
provider: openai-api
|
provider: openai-api
|
||||||
model: gemini-3.6-flash
|
model: gemini-3-flash
|
||||||
title_generation:
|
title_generation:
|
||||||
provider: openai-api
|
provider: openai-api
|
||||||
model: gemini-3.6-flash
|
model: gemini-3-flash
|
||||||
|
|
||||||
terminal:
|
terminal:
|
||||||
backend: local
|
backend: local
|
||||||
@@ -173,7 +173,7 @@ HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=<strong password>
|
|||||||
|
|
||||||
> Why `OPENAI_API_KEY` + `OPENAI_BASE_URL`: the `openai-api` provider honours
|
> Why `OPENAI_API_KEY` + `OPENAI_BASE_URL`: the `openai-api` provider honours
|
||||||
> `OPENAI_BASE_URL`, so this is the simplest way to point Hermes at the
|
> `OPENAI_BASE_URL`, so this is the simplest way to point Hermes at the
|
||||||
> in-cluster LiteLLM. `claude-4.5-haiku` / `gemini-3.6-flash` are the model names
|
> in-cluster LiteLLM. `claude-4.5-haiku` / `gemini-3-flash` are the model names
|
||||||
> already exposed by your `litellm/litellm.yaml` ConfigMap.
|
> already exposed by your `litellm/litellm.yaml` ConfigMap.
|
||||||
|
|
||||||
`SOUL.md` (personality + guardrails) — see `configmap.yaml`. Key points:
|
`SOUL.md` (personality + guardrails) — see `configmap.yaml`. Key points:
|
||||||
@@ -196,7 +196,7 @@ HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=<strong password>
|
|||||||
2–4 GiB; 1 GiB is fine without browser tools, which we keep off).
|
2–4 GiB; 1 GiB is fine without browser tools, which we keep off).
|
||||||
- Volume: PVC mounted at `/opt/data` (HERMES_HOME), RWX not needed (single pod).
|
- Volume: PVC mounted at `/opt/data` (HERMES_HOME), RWX not needed (single pod).
|
||||||
- Ports: 8642 (gateway API, internal only) and 9119 (dashboard) → exposed via
|
- Ports: 8642 (gateway API, internal only) and 9119 (dashboard) → exposed via
|
||||||
Ingress `platform-engineer.rogi.casa` with TLS + basic-auth (already enforced by the
|
Ingress `hermes.rogi.casa` with TLS + basic-auth (already enforced by the
|
||||||
`HERMES_DASHBOARD_BASIC_AUTH_*` env vars).
|
`HERMES_DASHBOARD_BASIC_AUTH_*` env vars).
|
||||||
- `imagePullSecrets: gitea-registry`.
|
- `imagePullSecrets: gitea-registry`.
|
||||||
- env from Secret; `HERMES_DASHBOARD=1`.
|
- env from Secret; `HERMES_DASHBOARD=1`.
|
||||||
@@ -283,7 +283,7 @@ Design rules baked into SOUL.md:
|
|||||||
|
|
||||||
1. **Notifications:** dedicated `platform-engineer` Discord bot → its own token
|
1. **Notifications:** dedicated `platform-engineer` Discord bot → its own token
|
||||||
in `secret.yaml` (`DISCORD_BOT_TOKEN`, `DISCORD_HOME_CHANNEL`).
|
in `secret.yaml` (`DISCORD_BOT_TOKEN`, `DISCORD_HOME_CHANNEL`).
|
||||||
2. **Dashboard:** public at `platform-engineer.rogi.casa` (Traefik TLS + cert-manager + the
|
2. **Dashboard:** public at `hermes.rogi.casa` (Traefik TLS + cert-manager + the
|
||||||
bundled Hermes basic-auth provider). Reach the dashboard on port 9119; the
|
bundled Hermes basic-auth provider). Reach the dashboard on port 9119; the
|
||||||
gateway API on 8642 is ClusterIP-only.
|
gateway API on 8642 is ClusterIP-only.
|
||||||
3. **Image:** derived image pushed to `git.rogi.casa/roger/hermes-agent`, pulled
|
3. **Image:** derived image pushed to `git.rogi.casa/roger/hermes-agent`, pulled
|
||||||
@@ -322,7 +322,7 @@ Design rules baked into SOUL.md:
|
|||||||
set the LiteLLM master key as `OPENAI_API_KEY` and a strong dashboard
|
set the LiteLLM master key as `OPENAI_API_KEY` and a strong dashboard
|
||||||
password + a 32-byte session secret.
|
password + a 32-byte session secret.
|
||||||
5. **Commit & push** the whole change. ArgoCD will create the namespace
|
5. **Commit & push** the whole change. ArgoCD will create the namespace
|
||||||
resources, deploy the pod, and bring up the ingress at `platform-engineer.rogi.casa`.
|
resources, deploy the pod, and bring up the ingress at `hermes.rogi.casa`.
|
||||||
6. **Seed the cron jobs:**
|
6. **Seed the cron jobs:**
|
||||||
`kubectl apply -f platform-engineer/cron-seed.yaml` (one-shot Job) — it waits
|
`kubectl apply -f platform-engineer/cron-seed.yaml` (one-shot Job) — it waits
|
||||||
for the hermes pod, then runs `hermes cron create ...` for each watchdog.
|
for the hermes pod, then runs `hermes cron create ...` for each watchdog.
|
||||||
@@ -357,7 +357,7 @@ Design rules baked into SOUL.md:
|
|||||||
| `dockerfile` | derived image: hermes-agent + kubectl + helm (linux/amd64) |
|
| `dockerfile` | derived image: hermes-agent + kubectl + helm (linux/amd64) |
|
||||||
| `build-and-push.sh` | builds & pushes the image to the Gitea registry |
|
| `build-and-push.sh` | builds & pushes the image to the Gitea registry |
|
||||||
| `deployment.yaml` | Deployment (1 replica, Recreate, pinned to amd64 NUC) + Service |
|
| `deployment.yaml` | Deployment (1 replica, Recreate, pinned to amd64 NUC) + Service |
|
||||||
| `ingress.yaml` | `platform-engineer.rogi.casa` → dashboard (TLS + basic auth) |
|
| `ingress.yaml` | `hermes.rogi.casa` → dashboard (TLS + basic auth) |
|
||||||
| `cron-seed.yaml` | one-shot Job that creates the Hermes cron schedule |
|
| `cron-seed.yaml` | one-shot Job that creates the Hermes cron schedule |
|
||||||
|
|
||||||
Also changed outside this directory:
|
Also changed outside this directory:
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ data:
|
|||||||
config.yaml: |
|
config.yaml: |
|
||||||
model:
|
model:
|
||||||
provider: openai-api
|
provider: openai-api
|
||||||
default: qwen3.6-27b
|
default: z-ai/glm-5.2
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
base_url: "http://litellm-service.litellm:80/v1"
|
||||||
api_mode: chat_completions
|
api_mode: chat_completions
|
||||||
|
|
||||||
auxiliary:
|
auxiliary:
|
||||||
compression:
|
compression:
|
||||||
provider: openai-api
|
provider: openai-api
|
||||||
model: qwen3.6-35b-a3b # flash model for speed-sensitive auxiliary tasks
|
model: z-ai/glm-5.2
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
base_url: "http://litellm-service.litellm:80/v1"
|
||||||
title_generation:
|
title_generation:
|
||||||
provider: openai-api
|
provider: openai-api
|
||||||
model: qwen3.6-35b-a3b # flash model for speed-sensitive auxiliary tasks
|
model: z-ai/glm-5.2
|
||||||
base_url: "http://litellm-service.litellm:80/v1"
|
base_url: "http://litellm-service.litellm:80/v1"
|
||||||
|
|
||||||
terminal:
|
terminal:
|
||||||
@@ -59,16 +59,6 @@ data:
|
|||||||
cron:
|
cron:
|
||||||
wrap_response: false
|
wrap_response: false
|
||||||
|
|
||||||
discord:
|
|
||||||
allowed_channels: '1470909384162017444' # DISCORD_HOME_CHANNEL
|
|
||||||
free_response_channels: '1470909384162017444' # no @mention needed here
|
|
||||||
# Per-platform gateway auth. Paired with GATEWAY_ALLOW_ALL_USERS=true in
|
|
||||||
# the env (secret.yaml), this lets the bot reply to inbound DMs and
|
|
||||||
# group messages from anyone. Tighten later by switching to
|
|
||||||
# DISCORD_ALLOWED_USERS=<id> in the secret and dropping these two lines.
|
|
||||||
dm_policy: open
|
|
||||||
group_policy: open
|
|
||||||
|
|
||||||
memory:
|
memory:
|
||||||
memory_enabled: true
|
memory_enabled: true
|
||||||
user_profile_enabled: true
|
user_profile_enabled: true
|
||||||
@@ -178,14 +168,7 @@ data:
|
|||||||
run (Hermes disables that). Do not try.
|
run (Hermes disables that). Do not try.
|
||||||
7. **Talk like an engineer.** Short, concrete, with resource names and
|
7. **Talk like an engineer.** Short, concrete, with resource names and
|
||||||
queries. No filler. When you fixed something, say what you did in one line.
|
queries. No filler. When you fixed something, say what you did in one line.
|
||||||
8. **Language: Catalan by default.** Address Roger in Catalan in *all*
|
8. **Respect GitOps.** If an app is `OutOfSync`/`Degraded`, check whether a
|
||||||
interactions — daily reports, watchdog alerts, Discord DMs, and any
|
|
||||||
other message you produce. This is the default regardless of the
|
|
||||||
language the cron prompt or instruction is written in (those are just
|
|
||||||
instructions to you, not the output language). Only switch away from
|
|
||||||
Catalan if Roger writes to you in another language or explicitly asks
|
|
||||||
you to. Never default to English or Italian.
|
|
||||||
9. **Respect GitOps.** If an app is `OutOfSync`/`Degraded`, check whether a
|
|
||||||
commit is stuck. Don't hand-edit resources — fix the source repo.
|
commit is stuck. Don't hand-edit resources — fix the source repo.
|
||||||
|
|
||||||
## How you reach Roger
|
## How you reach Roger
|
||||||
|
|||||||
@@ -54,12 +54,6 @@ spec:
|
|||||||
|
|
||||||
exists() { kubectl -n platform-engineer exec "$POD" -- hermes cron list 2>/dev/null | grep -qi " $1 "; }
|
exists() { kubectl -n platform-engineer exec "$POD" -- hermes cron list 2>/dev/null | grep -qi " $1 "; }
|
||||||
|
|
||||||
# NOTE: `hermes cron create` has no --model flag. New jobs inherit the
|
|
||||||
# global default model from config.yaml at creation time. To pin a
|
|
||||||
# specific model per-job (e.g. the flash model qwen3.6-35b-a3b for all
|
|
||||||
# cron tasks), edit /opt/data/cron/jobs.json directly after seeding:
|
|
||||||
# kubectl exec deploy/hermes -- python3 -c "...set model field..."
|
|
||||||
# See llamacpp/README.md and the deployment notes for details.
|
|
||||||
create() {
|
create() {
|
||||||
name="$1"; schedule="$2"; deliver="$3"; prompt="$4"
|
name="$1"; schedule="$2"; deliver="$3"; prompt="$4"
|
||||||
if exists "$name"; then
|
if exists "$name"; then
|
||||||
@@ -71,26 +65,11 @@ spec:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---- Watchdog checks (silent unless something is wrong) ----
|
# ---- Watchdog checks (silent unless something is wrong) ----
|
||||||
# NOTE: the old single "cluster-health-check" combined 4 heavy HTTP
|
create "cluster-health-check" "every 6h" "discord" \
|
||||||
# gathers (Prometheus + a cluster-wide raw Loki log dump) into one
|
"Check cluster health using the HTTP APIs documented in your SOUL.md. Check: (1) any node that is NotReady, (2) any pod not in Running phase, (3) any recent error/panic/crashloop/backoff log lines in Loki across all namespaces in the last 20 minutes, (4) any ArgoCD app that is not Synced plus Healthy. If everything is healthy, reply with exactly [SILENT]. Otherwise give a concise per-resource summary of what is wrong."
|
||||||
# cron session and exceeded the flash model's per-request context.
|
|
||||||
# The 35b-a3b llama-server runs -c 131072 -np 4, so each request only
|
|
||||||
# gets ~32k tokens of KV cache (NOT 132k). It has been split into
|
|
||||||
# focused sub-checks so each run stays small, and the log check now
|
|
||||||
# uses LogQL aggregation instead of dumping raw lines cluster-wide.
|
|
||||||
create "node-health-check" "every 6h" "discord" \
|
|
||||||
"Check node health using the Prometheus API documented in your SOUL.md. Query kube_node_status_condition{condition=\"Ready\",status!=\"true\"}. If all nodes are Ready, reply with exactly [SILENT]. Otherwise list each NotReady node by name. Keep it short; do not paste raw JSON — summarize with jq."
|
|
||||||
|
|
||||||
create "pod-phase-check" "every 6h" "discord" \
|
|
||||||
"Check pod health using the Prometheus API documented in your SOUL.md. Query kube_pod_status_phase{phase!=\"Running\"} and exclude Completed/Succeeded pods (jobs). If all Running, reply with exactly [SILENT]. Otherwise list each non-Running pod as namespace/pod:phase, grouped by namespace. Summarize with jq; do not dump raw JSON."
|
|
||||||
|
|
||||||
create "pod-error-log-check" "every 6h" "discord" \
|
|
||||||
"Check for recent crash-loop/error logs using the Loki API documented in your SOUL.md. IMPORTANT context discipline: do NOT fetch raw log lines across all namespaces — that overflows your context window. First run a LogQL aggregation that returns only counts: sum by (namespace, pod) (count_over_time({namespace=~\".+\"} |~ \"(?i)backoff|crashloop|panic\" [20m])). Parse the counts with jq. If every count is zero, reply with exactly [SILENT]. If any namespace/pod has a non-zero count, fetch at most 10 sample lines for THAT pod only (limit=10) to identify the cause. Summarize concisely; never paste more than a handful of lines."
|
|
||||||
|
|
||||||
# (ArgoCD health is already covered by the argocd-sync-health cron below.)
|
|
||||||
|
|
||||||
create "pod-restart-loop" "every 1h" "discord" \
|
create "pod-restart-loop" "every 1h" "discord" \
|
||||||
"Find pods with high restart rates using the Prometheus API documented in your SOUL.md. Run increase(kube_pod_container_status_restarts_total[15m]) and list only pods whose value is greater than 3. If none, reply [SILENT]. For any pod that qualifies, IMPORTANT context discipline: do NOT fetch raw logs across the cluster or over a wide time window — that overflows your context window (a raw cluster-wide Loki dump can exceed 60k tokens). Fetch at most 10 sample lines for THAT specific pod only, using Loki query_range with limit=10 over the last 15 minutes, filtered to error/crash patterns (e.g. {namespace=\"<ns>\",pod=\"<pod>\"} |~ \"(?i)error|panic|crash|oom|backoff\"). Summarize the root cause in one line; never paste more than a handful of log lines. If the cause is clearly fixable via a manifest change such as bumping a memory limit, fixing a config value, or bumping the restartedAt annotation, make the edit in /workspace/k3s-cluster, commit and push, then trigger an ArgoCD sync via the API. Report what you did in one line. If not clearly fixable, post the log excerpt and proposed fix, and wait for Roger."
|
"Find pods with high restart rates using the Prometheus API documented in your SOUL.md. If any pod has more than 3 restarts in the last 15 minutes, fetch its logs from Loki to diagnose the cause. If the cause is clearly fixable via a manifest change such as bumping a memory limit, fixing a config value, or bumping the restartedAt annotation, make the edit in /workspace/k3s-cluster, commit and push, then trigger an ArgoCD sync via the API. Report what you did in one line. If not clearly fixable, post the log excerpt and proposed fix, and wait for Roger. If no high-restart pods, reply [SILENT]."
|
||||||
|
|
||||||
create "pvc-pressure" "every 1d" "discord" \
|
create "pvc-pressure" "every 1d" "discord" \
|
||||||
"Check storage health using the Prometheus API documented in your SOUL.md. Alert if any PVC has less than 15 percent free space, or if any node filesystem is over 85 percent full. If all healthy, reply [SILENT]."
|
"Check storage health using the Prometheus API documented in your SOUL.md. Alert if any PVC has less than 15 percent free space, or if any node filesystem is over 85 percent full. If all healthy, reply [SILENT]."
|
||||||
@@ -98,22 +77,15 @@ spec:
|
|||||||
create "argocd-sync-health" "every 6h" "discord" \
|
create "argocd-sync-health" "every 6h" "discord" \
|
||||||
"Check ArgoCD app health using the API documented in your SOUL.md. If every app is Synced and Healthy, reply [SILENT]. Otherwise list the OutOfSync or Degraded apps with their status. If an app is OutOfSync and you believe a recent git push caused it, you may trigger a sync via the API. Do NOT hand-edit resources to fix them — fix the source repo."
|
"Check ArgoCD app health using the API documented in your SOUL.md. If every app is Synced and Healthy, reply [SILENT]. Otherwise list the OutOfSync or Degraded apps with their status. If an app is OutOfSync and you believe a recent git push caused it, you may trigger a sync via the API. Do NOT hand-edit resources to fix them — fix the source repo."
|
||||||
|
|
||||||
create "cert-expiry" "0 7 * * *" "discord" \
|
create "cert-expiry" "0 9 * * *" "discord" \
|
||||||
"Check certificate expiry using the Prometheus API documented in your SOUL.md. Alert on any certificate expiring in under 21 days, with its name and namespace. If none, reply [SILENT]."
|
"Check certificate expiry using the Prometheus API documented in your SOUL.md. Alert on any certificate expiring in under 21 days, with its name and namespace. If none, reply [SILENT]."
|
||||||
|
|
||||||
create "node-resource-drift" "every 1d" "discord" \
|
create "node-resource-drift" "every 1d" "discord" \
|
||||||
"Check node resources using the Prometheus API documented in your SOUL.md. Alert if any node is NotReady, or if any node has CPU over 90 percent or memory over 90 percent. Otherwise reply [SILENT]."
|
"Check node resources using the Prometheus API documented in your SOUL.md. Alert if any node is NotReady, or if any node has CPU over 90 percent or memory over 90 percent. Otherwise reply [SILENT]."
|
||||||
|
|
||||||
# ---- Daily report (always delivered) ----
|
# ---- Daily report (always delivered) ----
|
||||||
# NOTE: Hermes' cron runs in UTC (no TZ set on the container). The
|
create "daily-cluster-report" "0 8 * * *" "discord" \
|
||||||
# cluster is at UTC+2, so fixed daily times are expressed in UTC and
|
"Produce a daily cluster report for Roger using the HTTP APIs documented in your SOUL.md. Include: (1) node count and Ready/NotReady status per node, (2) top 5 pods by CPU and by memory, (3) count of pods not Running grouped by namespace, (4) any ArgoCD apps that are OutOfSync or Degraded, (5) any certificates expiring within 30 days, (6) any recent Warning-level log lines from the last 24 hours. Keep it under 1800 chars. Always deliver (no [SILENT])."
|
||||||
# shifted back 2h from the intended local wall-clock time:
|
|
||||||
# 08:00 local -> 0 6 (daily cluster report)
|
|
||||||
# 09:00 local -> 0 7 (cert expiry check)
|
|
||||||
# Relative schedules (every 6h / every 1h / every 1d) are
|
|
||||||
# timezone-independent.
|
|
||||||
create "daily-cluster-report" "0 6 * * *" "discord" \
|
|
||||||
"Produce a daily cluster report for Roger using the HTTP APIs documented in your SOUL.md. Include: (1) node count and Ready/NotReady status per node, (2) top 5 pods by CPU and by memory, (3) count of pods not Running grouped by namespace, (4) any ArgoCD apps that are OutOfSync or Degraded, (5) any certificates expiring within 30 days, (6) recent error/crash activity: IMPORTANT context discipline — do NOT fetch raw log lines across the cluster over 24h — that overflows your context window. First run a LogQL aggregation that returns only counts: sum by (namespace, pod) (count_over_time({namespace=~\".+\"} |~ \"(?i)error|panic|crash|oom|backoff\" [24h])). Parse the counts with jq and summarize the top few noisiest pods by namespace/pod:count. If one stands out, fetch at most 5 sample lines for THAT pod only (limit=5) to identify the cause. Never paste more than a handful of lines total. Keep the whole report under 1800 chars. Always deliver (no [SILENT])."
|
|
||||||
|
|
||||||
echo "Done. Listing all cron jobs:"
|
echo "Done. Listing all cron jobs:"
|
||||||
kubectl -n platform-engineer exec "$POD" -- hermes cron list
|
kubectl -n platform-engineer exec "$POD" -- hermes cron list
|
||||||
|
|||||||
@@ -86,11 +86,9 @@ spec:
|
|||||||
: > /opt/data/.env
|
: > /opt/data/.env
|
||||||
chmod 600 /opt/data/.env
|
chmod 600 /opt/data/.env
|
||||||
for k in OPENAI_API_KEY OPENAI_BASE_URL DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \
|
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 \
|
GITEA_TOKEN GITEA_REPO_URL ARGOCD_API_TOKEN ARGOCD_SERVER \
|
||||||
HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \
|
HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \
|
||||||
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET \
|
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET; do
|
||||||
API_SERVER_KEY; do
|
|
||||||
eval "v=\${$k:-}"
|
eval "v=\${$k:-}"
|
||||||
[ -n "$v" ] && echo "$k=$v" >> /opt/data/.env
|
[ -n "$v" ] && echo "$k=$v" >> /opt/data/.env
|
||||||
done
|
done
|
||||||
@@ -126,30 +124,6 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- name: HERMES_HOME
|
- name: HERMES_HOME
|
||||||
value: /opt/data
|
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 <key>`.
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /opt/data
|
mountPath: /opt/data
|
||||||
@@ -164,8 +138,7 @@ spec:
|
|||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
# Probe the dashboard port (9119, always enabled via HERMES_DASHBOARD=1
|
# 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
|
# and binds 0.0.0.0). The gateway API on 8642 is off by default.
|
||||||
# API_SERVER_ENABLED=true above.
|
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: 9119
|
port: 9119
|
||||||
initialDelaySeconds: 90
|
initialDelaySeconds: 90
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ spec:
|
|||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- platform-engineer.rogi.casa
|
- hermes.rogi.casa
|
||||||
secretName: platform-engineer-tls
|
secretName: hermes-tls
|
||||||
rules:
|
rules:
|
||||||
- host: platform-engineer.rogi.casa
|
- host: hermes.rogi.casa
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
|||||||
Reference in New Issue
Block a user