forked from roger/k3s-cluster
deploy both qwen3.6 versions
This commit is contained in:
139
home-manager/README.md
Normal file
139
home-manager/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 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` | one-shot Job that creates the Hermes cron schedule |
|
||||
| `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 `script.goodnight`, etc.).
|
||||
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. To re-seed after a wipe:
|
||||
`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 door-window-check` —
|
||||
and confirm the message lands in Discord (or `[SILENT]` if all clear).
|
||||
|
||||
---
|
||||
|
||||
## 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`, `cron/jobs.json`, `sessions/`, `memories/`, `skills/`. The ConfigMap
|
||||
only *seeds* these on first boot; after that, edits made via the dashboard or
|
||||
`hermes cron edit` persist on the PVC and Argo will not revert them. For a
|
||||
hard reset, delete the PVC and re-apply.
|
||||
186
home-manager/configmap.yaml
Normal file
186
home-manager/configmap.yaml
Normal file
@@ -0,0 +1,186 @@
|
||||
# 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
|
||||
base_url: "http://litellm-service.litellm:80/v1"
|
||||
api_mode: chat_completions
|
||||
|
||||
auxiliary:
|
||||
compression:
|
||||
provider: openai-api
|
||||
model: qwen3.6
|
||||
base_url: "http://litellm-service.litellm:80/v1"
|
||||
title_generation:
|
||||
provider: openai-api
|
||||
model: qwen3.6
|
||||
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:
|
||||
# REPLACE_WITH_HOME_CHANNEL_ID — set to your 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: 'REPLACE_WITH_HOME_CHANNEL_ID'
|
||||
free_response_channels: 'REPLACE_WITH_HOME_CHANNEL_ID'
|
||||
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. **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.
|
||||
|
||||
## 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.
|
||||
93
home-manager/cron-seed.yaml
Normal file
93
home-manager/cron-seed.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
# One-shot Job that seeds Hermes' built-in cron schedule on first install.
|
||||
# Idempotent: skips job names that already exist.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Tailor the schedules/prompts below to your actual Home Assistant entities.
|
||||
---
|
||||
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"
|
||||
|
||||
exists() { kubectl -n home-manager exec "$POD" -- hermes cron list 2>/dev/null | grep -qi " $1 "; }
|
||||
|
||||
create() {
|
||||
name="$1"; schedule="$2"; deliver="$3"; prompt="$4"
|
||||
if exists "$name"; then
|
||||
echo "cron job '$name' already exists — skipping"
|
||||
else
|
||||
echo "creating cron job '$name' ..."
|
||||
kubectl -n home-manager exec "$POD" -- hermes cron create "$schedule" "$prompt" --name "$name" --deliver "$deliver"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Watchdog checks (silent unless something is wrong) ----
|
||||
create "door-window-check" "every 15m" "discord" \
|
||||
"Check the Home Assistant REST API as documented in your SOUL.md. List all door and window sensor entities (binary_sensor.* for doors, windows, contact sensors). If any are open and either no one is home (check zone.home or presence sensors) or it is between 23:00 and 06:00, notify Roger with which sensor is open. If all closed, reply with exactly [SILENT]."
|
||||
|
||||
create "leak-moisture-check" "every 10m" "discord" \
|
||||
"Check the Home Assistant REST API as documented in your SOUL.md. Look for moisture, water leak, or flood sensor entities (binary_sensor.* moisture/water/leak, sensor.* moisture). If any report wet/active, alert Roger immediately with the sensor name and location. If all dry, reply with exactly [SILENT]."
|
||||
|
||||
create "energy-anomaly" "every 30m" "discord" \
|
||||
"Check the Home Assistant REST API as documented in your SOUL.md. Read the current power draw sensor (sensor.* power, sensor.* current_power). If the total power draw is unusually high for the time of day (over 4000 W during the day, or over 800 W when no one is home at night), alert Roger with the reading. Otherwise reply with exactly [SILENT]."
|
||||
|
||||
create "comfort-check" "every 1h" "discord" \
|
||||
"Check the Home Assistant REST API as documented in your SOUL.md. Read indoor temperature and humidity sensors. If a room is outside a comfortable range (below 18C or above 26C, or humidity above 65 percent), you may toggle a fan entity in that room (safe). Do NOT change climate/HVAC settings yourself — if it is uncomfortable, propose a climate change to Roger on Discord and wait. If everything is comfortable, reply with exactly [SILENT]."
|
||||
|
||||
create "left-on-check" "every 1h" "discord" \
|
||||
"Check the Home Assistant REST API as documented in your SOUL.md. If no one is home (zone.home is away or presence sensors all off), find any lights, media players, fans, or non-critical switches that are on. Turn off the safe non-essential ones (lights, media, fans) and report what you turned off. Do NOT touch anything security-critical. If someone is home, or nothing is left on, reply with exactly [SILENT]."
|
||||
|
||||
# ---- Routines (always delivered) ----
|
||||
create "goodnight-routine" "0 23 * * *" "discord" \
|
||||
"Run the goodnight routine via the Home Assistant REST API as documented in your SOUL.md. Turn off all non-essential lights and pause media players (safe actions). Then propose — but do NOT execute — locking the doors (lock.*), closing the garage (cover.* garage), and arming the alarm (alarm_control_panel.*). Ask Roger on Discord for confirmation before calling any of those security-critical services. Deliver a short summary of what you turned off and what you are waiting for confirmation on."
|
||||
|
||||
create "morning-briefing" "0 7 * * *" "discord" \
|
||||
"Produce a morning home briefing for Roger using the Home Assistant REST API as documented in your SOUL.md. Include: (1) who is home (zone.home / presence), (2) any lights or media still on, (3) any doors or windows open, (4) indoor temperatures, (5) today's calendar events if a calendar entity exists, (6) overnight energy use if an energy sensor exists, (7) any sensors in an alert/unavailable state. Keep it under 1800 chars. Always deliver (no [SILENT])."
|
||||
|
||||
echo "Done. Listing all cron jobs:"
|
||||
kubectl -n home-manager exec "$POD" -- hermes cron list
|
||||
154
home-manager/deployment.yaml
Normal file
154
home-manager/deployment.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
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; 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"
|
||||
# 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 off by default.
|
||||
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
|
||||
24
home-manager/ingress.yaml
Normal file
24
home-manager/ingress.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
4
home-manager/namespace.yaml
Normal file
4
home-manager/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: home-manager
|
||||
11
home-manager/pvc.yaml
Normal file
11
home-manager/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: hermes-data
|
||||
namespace: home-manager
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
41
home-manager/rbac.yaml
Normal file
41
home-manager/rbac.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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 create ...` (the only way to seed Hermes' internal cron).
|
||||
# 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
|
||||
Reference in New Issue
Block a user