Skip to content

Commit d937ac5

Browse files
Kabuki94claude
andcommitted
feat(ai-stack): Hermes-Agent + Open WebUI Quadlets on shared ai-net bridge
Integrates the deploy-aistack.sh reference flow (Open WebUI -> Hermes- Agent -> Ollama -> GPU) declaratively into MiOS as Quadlets. Existing AI-related containers (mios-ai / LocalAI, mios-ollama, mios-searxng) join the new ai-net bridge alongside their existing mios.network membership, so the agent gateway can address them by container name without leaving the bridge. Architectural Law 5 (UNIFIED-AI-REDIRECTS) unaffected: every AI URL still resolves to localhost. Topology: browser -> mios-webui (host:3030) -> mios-hermes (host:8642) -> mios-ollama (mios-ollama:11434) -> GPU (CDI) agent's web_search tool -> mios-searxng:8080 (on ai-net) agent alt backend -> mios-ai:8080 (LocalAI, on ai-net) Port choice (3030 for Open WebUI, NOT 8080): The upstream deploy-aistack.sh maps Open WebUI to host :8080. That port is taken on MiOS by mios-ai (LocalAI's OpenAI-compatible /v1 endpoint) -- the canonical Architectural-Law-5 surface that aichat / system prompts / agent SDKs all reference. Moving LocalAI off 8080 would break every existing client. So WebUI gets host :3030 instead; internal port stays 8080 to match upstream defaults. Documented in the Quadlet's [Container] header. New files: * etc/containers/systemd/ai-net.network -- 10.90.0.0/24 bridge (one octet beyond mios.network's 10.89.0.0/24). * etc/containers/systemd/mios-hermes.container -- Hermes-Agent gateway, dual-network (ai-net + mios.network), publishes :8642. Exec=gateway run; bypasses upstream's interactive `setup` wizard by pre-seeding /etc/mios/hermes/config.yaml at build time. * etc/containers/systemd/mios-webui.container -- Open WebUI, dual-network, publishes :3030. ENABLE_OLLAMA_API=false so all chat traffic flows through Hermes (keeps tool-use / history / rate-limit decisions in one place). * usr/lib/systemd/system/mios-hermes-firstboot.service + usr/libexec/mios/mios-hermes-firstboot -- generates a 64-char API_SERVER_KEY into /etc/mios/hermes/api.env on first boot via `openssl rand -hex 32`. Both mios-hermes (server side) and mios-webui (client side, OPENAI_API_KEY) read the same file via Quadlet EnvironmentFile= so the auth pair stays in sync without operator copy-paste. * usr/share/mios/hermes/config.yaml -- vendor Hermes config: backend = mios-ollama:11434, model = qwen2.5-coder:7b (matches MIOS_AI_MODEL), web_search tool wired at mios-searxng:8080, operator override file at /etc/hermes/config.local.yaml. * usr/lib/tmpfiles.d/mios-hermes.conf -- /etc/mios/hermes (0750), /var/lib/mios/hermes (0750), copy-if-absent vendor config.yaml. * usr/lib/tmpfiles.d/mios-webui.conf -- /var/lib/mios/webui (0750). Multi-network'd existing units: * etc/containers/systemd/mios-ai.container * etc/containers/systemd/mios-searxng.container * usr/share/containers/systemd/ollama.container Each now lists Network=mios.network AND Network=ai-net.network so they're addressable by container name from BOTH bridges. Identity (next two slots in 810-829 service range): * mios-hermes UID/GID 820 (sysusers.d/50-mios-services.conf) * mios-webui UID/GID 821 (sysusers.d/50-mios-services.conf) Wiring: * usr/share/mios/mios.toml -- new [hermes] and [webui] sections with endpoint / backend / model fields, sitting alongside [ai] and [search]. * usr/share/mios/env.defaults -- MIOS_HERMES_{VERSION,IMAGE,PORT, USER,UID,GID} and MIOS_WEBUI_{VERSION,IMAGE,PORT,USER,UID,GID} for shell-side consumers. * automation/lib/globals.{sh,ps1} -- mirror env.defaults: user/uid/ gid + port + URL + unit name + container image ref. * usr/libexec/mios/mios-dashboard.sh -- "Hermes" and "WebUI" rows in the Self-replication-loop endpoint section, plus mios-hermes and mios-webui entries in the Quadlet status table. * usr/lib/systemd/system-preset/90-mios.preset -- enable mios- hermes-firstboot.service / mios-hermes.service / mios-webui. service. .gitignore: * Add explicit !/etc/containers/systemd/ai-net.network whitelist line (the existing mios* glob doesn't catch it, and we keep the literal "ai-net" name for cross-deploy interop with the upstream deploy-aistack.sh script). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5edf5e0 commit d937ac5

19 files changed

Lines changed: 432 additions & 8 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ etc/containers/*
107107
!/etc/containers/systemd/
108108
etc/containers/systemd/*
109109
!/etc/containers/systemd/mios*
110+
# ai-net.network is the AI-stack bridge that mios-hermes / mios-webui /
111+
# mios-ai / mios-ollama / mios-searxng share. Doesn't match the mios*
112+
# glob because the deploy-aistack.sh reference flow names it ai-net,
113+
# and we keep the literal name for cross-deploy interop.
114+
!/etc/containers/systemd/ai-net.network
110115
# storage.conf.d/ -- additionalimagestores bridge so rootless distrobox
111116
# can read images built into rootful podman storage by the system-scope
112117
# .build Quadlets (Universal Blue / Bazzite pattern).

automation/lib/globals.ps1

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ $script:MIOS_SEARXNG_USER = if ($env:MIOS_SEARXNG_USER) { $env:MIOS_SEARXNG_USER
5555
$script:MIOS_SEARXNG_UID = if ($env:MIOS_SEARXNG_UID) { [int]$env:MIOS_SEARXNG_UID } else { 818 }
5656
$script:MIOS_SEARXNG_GID = if ($env:MIOS_SEARXNG_GID) { [int]$env:MIOS_SEARXNG_GID } else { 818 }
5757

58+
$script:MIOS_HERMES_USER = if ($env:MIOS_HERMES_USER) { $env:MIOS_HERMES_USER } else { 'mios-hermes' }
59+
$script:MIOS_HERMES_UID = if ($env:MIOS_HERMES_UID) { [int]$env:MIOS_HERMES_UID } else { 820 }
60+
$script:MIOS_HERMES_GID = if ($env:MIOS_HERMES_GID) { [int]$env:MIOS_HERMES_GID } else { 820 }
61+
62+
$script:MIOS_WEBUI_USER = if ($env:MIOS_WEBUI_USER) { $env:MIOS_WEBUI_USER } else { 'mios-webui' }
63+
$script:MIOS_WEBUI_UID = if ($env:MIOS_WEBUI_UID) { [int]$env:MIOS_WEBUI_UID } else { 821 }
64+
$script:MIOS_WEBUI_GID = if ($env:MIOS_WEBUI_GID) { [int]$env:MIOS_WEBUI_GID } else { 821 }
65+
5866
$script:MIOS_SUBUID_START = if ($env:MIOS_SUBUID_START) { [int]$env:MIOS_SUBUID_START } else { 100000 }
5967
$script:MIOS_SUBUID_COUNT = if ($env:MIOS_SUBUID_COUNT) { [int]$env:MIOS_SUBUID_COUNT } else { 65536 }
6068

@@ -74,6 +82,8 @@ $script:MIOS_PORT_LOCALAI = if ($env:MIOS_PORT_LOCALAI) { [int]$env:
7482
$script:MIOS_PORT_COCKPIT = if ($env:MIOS_PORT_COCKPIT) { [int]$env:MIOS_PORT_COCKPIT } else { 9090 }
7583
$script:MIOS_PORT_OLLAMA = if ($env:MIOS_PORT_OLLAMA) { [int]$env:MIOS_PORT_OLLAMA } else { 11434 }
7684
$script:MIOS_PORT_SEARXNG = if ($env:MIOS_PORT_SEARXNG) { [int]$env:MIOS_PORT_SEARXNG } else { 8888 }
85+
$script:MIOS_PORT_HERMES = if ($env:MIOS_PORT_HERMES) { [int]$env:MIOS_PORT_HERMES } else { 8642 }
86+
$script:MIOS_PORT_WEBUI = if ($env:MIOS_PORT_WEBUI) { [int]$env:MIOS_PORT_WEBUI } else { 3030 }
7787
$script:MIOS_PORT_COCKPIT_LINK = if ($env:MIOS_PORT_COCKPIT_LINK) { [int]$env:MIOS_PORT_COCKPIT_LINK } else { 19090 }
7888

7989
# ── URLS ─────────────────────────────────────────────────────────────
@@ -82,6 +92,8 @@ $script:MIOS_FORGE_URL = if ($env:MIOS_FORGE_URL) { $env:MIOS_FORGE_URL }
8292
$script:MIOS_COCKPIT_URL = if ($env:MIOS_COCKPIT_URL) { $env:MIOS_COCKPIT_URL } else { "https://localhost:$($script:MIOS_PORT_COCKPIT)" }
8393
$script:MIOS_OLLAMA_URL = if ($env:MIOS_OLLAMA_URL) { $env:MIOS_OLLAMA_URL } else { "http://localhost:$($script:MIOS_PORT_OLLAMA)" }
8494
$script:MIOS_SEARXNG_URL = if ($env:MIOS_SEARXNG_URL) { $env:MIOS_SEARXNG_URL } else { "http://localhost:$($script:MIOS_PORT_SEARXNG)" }
95+
$script:MIOS_HERMES_URL = if ($env:MIOS_HERMES_URL) { $env:MIOS_HERMES_URL } else { "http://localhost:$($script:MIOS_PORT_HERMES)/v1" }
96+
$script:MIOS_WEBUI_URL = if ($env:MIOS_WEBUI_URL) { $env:MIOS_WEBUI_URL } else { "http://localhost:$($script:MIOS_PORT_WEBUI)/" }
8597

8698
# ── REPOS ────────────────────────────────────────────────────────────
8799
$script:MIOS_REPO_URL = if ($env:MIOS_REPO_URL) { $env:MIOS_REPO_URL } else { 'https://github.com/mios-dev/mios.git' }
@@ -151,6 +163,9 @@ $script:MIOS_UNIT_AICHAT_BUILD = 'mios-aichat-build.service'
151163
$script:MIOS_UNIT_AICHAT_IMAGE = 'mios-aichat-image.service'
152164
$script:MIOS_UNIT_COCKPIT_LINK = 'mios-cockpit-link.service'
153165
$script:MIOS_UNIT_SEARXNG = 'mios-searxng.service'
166+
$script:MIOS_UNIT_HERMES = 'mios-hermes.service'
167+
$script:MIOS_UNIT_WEBUI = 'mios-webui.service'
168+
$script:MIOS_UNIT_HERMES_FIRSTBOOT = 'mios-hermes-firstboot.service'
154169
$script:MIOS_UNIT_FIRSTBOOT_TARGET = 'mios-firstboot.target'
155170
$script:MIOS_UNIT_OLLAMA_FIRSTBOOT = 'mios-ollama-firstboot.service'
156171
$script:MIOS_UNIT_WSL_FIRSTBOOT = 'mios-wsl-firstboot.service'
@@ -163,6 +178,8 @@ $script:MIOS_CONTAINER_FORGE_IMAGE = 'codeberg.org/forgejo/forgejo:12'
163178
$script:MIOS_CONTAINER_LOCALAI_IMAGE = 'docker.io/localai/localai:latest'
164179
$script:MIOS_CONTAINER_OLLAMA_IMAGE = 'docker.io/ollama/ollama:latest'
165180
$script:MIOS_CONTAINER_SEARXNG_IMAGE = 'docker.io/searxng/searxng:latest'
181+
$script:MIOS_CONTAINER_HERMES_IMAGE = 'docker.io/nousresearch/hermes-agent:latest'
182+
$script:MIOS_CONTAINER_WEBUI_IMAGE = 'docker.io/openwebui/open-webui:latest'
166183

167184
# ── COLOR PALETTE ────────────────────────────────────────────────────
168185
# Hokusai + operator-neutrals palette. Vendor defaults; resolved from

automation/lib/globals.sh

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ export MIOS_VERSION
7474
: "${MIOS_SEARXNG_UID:=818}"
7575
: "${MIOS_SEARXNG_GID:=818}"
7676

77+
: "${MIOS_HERMES_USER:=mios-hermes}"
78+
: "${MIOS_HERMES_UID:=820}"
79+
: "${MIOS_HERMES_GID:=820}"
80+
81+
: "${MIOS_WEBUI_USER:=mios-webui}"
82+
: "${MIOS_WEBUI_UID:=821}"
83+
: "${MIOS_WEBUI_GID:=821}"
84+
7785
# Rootless-container subuid/subgid range. Standard Fedora useradd -m
7886
# allocates 100000:65536; we keep the same so /etc/subuid + /etc/subgid
7987
# stay consistent with stock Fedora workflows.
@@ -86,6 +94,8 @@ export MIOS_AI_USER MIOS_AI_UID MIOS_AI_GID
8694
export MIOS_OLLAMA_USER MIOS_OLLAMA_UID MIOS_OLLAMA_GID
8795
export MIOS_CEPH_USER MIOS_CEPH_UID MIOS_CEPH_GID
8896
export MIOS_SEARXNG_USER MIOS_SEARXNG_UID MIOS_SEARXNG_GID
97+
export MIOS_HERMES_USER MIOS_HERMES_UID MIOS_HERMES_GID
98+
export MIOS_WEBUI_USER MIOS_WEBUI_UID MIOS_WEBUI_GID
8999
export MIOS_SUBUID_START MIOS_SUBUID_COUNT
90100

91101
# ── IMAGES ───────────────────────────────────────────────────────────
@@ -106,10 +116,12 @@ export MIOS_LOCAL_IMAGE MIOS_BASE_IMAGE MIOS_BIB_IMAGE
106116
: "${MIOS_PORT_COCKPIT:=9090}"
107117
: "${MIOS_PORT_OLLAMA:=11434}"
108118
: "${MIOS_PORT_SEARXNG:=8888}"
119+
: "${MIOS_PORT_HERMES:=8642}"
120+
: "${MIOS_PORT_WEBUI:=3030}"
109121
: "${MIOS_PORT_COCKPIT_LINK:=19090}" # podman-desktop discovery shim
110122
export MIOS_PORT_SSH MIOS_PORT_FORGE_HTTP MIOS_PORT_FORGE_SSH
111123
export MIOS_PORT_LOCALAI MIOS_PORT_COCKPIT MIOS_PORT_OLLAMA
112-
export MIOS_PORT_SEARXNG MIOS_PORT_COCKPIT_LINK
124+
export MIOS_PORT_SEARXNG MIOS_PORT_HERMES MIOS_PORT_WEBUI MIOS_PORT_COCKPIT_LINK
113125

114126
# ── URLS ─────────────────────────────────────────────────────────────
115127
# Derived from PORTS so a single port change propagates. MIOS_AI_ENDPOINT
@@ -119,7 +131,10 @@ export MIOS_PORT_SEARXNG MIOS_PORT_COCKPIT_LINK
119131
: "${MIOS_COCKPIT_URL:=https://localhost:${MIOS_PORT_COCKPIT}}"
120132
: "${MIOS_OLLAMA_URL:=http://localhost:${MIOS_PORT_OLLAMA}}"
121133
: "${MIOS_SEARXNG_URL:=http://localhost:${MIOS_PORT_SEARXNG}}"
122-
export MIOS_AI_ENDPOINT MIOS_FORGE_URL MIOS_COCKPIT_URL MIOS_OLLAMA_URL MIOS_SEARXNG_URL
134+
: "${MIOS_HERMES_URL:=http://localhost:${MIOS_PORT_HERMES}/v1}"
135+
: "${MIOS_WEBUI_URL:=http://localhost:${MIOS_PORT_WEBUI}/}"
136+
export MIOS_AI_ENDPOINT MIOS_FORGE_URL MIOS_COCKPIT_URL MIOS_OLLAMA_URL
137+
export MIOS_SEARXNG_URL MIOS_HERMES_URL MIOS_WEBUI_URL
123138

124139
# ── REPOS ────────────────────────────────────────────────────────────
125140
: "${MIOS_REPO_URL:=https://github.com/mios-dev/mios.git}"
@@ -212,6 +227,9 @@ export MIOS_AI_SYSTEM_PROMPT MIOS_MCP_REGISTRY MIOS_BUILD_ENV_FILE
212227
: "${MIOS_UNIT_AICHAT_IMAGE:=mios-aichat-image.service}"
213228
: "${MIOS_UNIT_COCKPIT_LINK:=mios-cockpit-link.service}"
214229
: "${MIOS_UNIT_SEARXNG:=mios-searxng.service}"
230+
: "${MIOS_UNIT_HERMES:=mios-hermes.service}"
231+
: "${MIOS_UNIT_WEBUI:=mios-webui.service}"
232+
: "${MIOS_UNIT_HERMES_FIRSTBOOT:=mios-hermes-firstboot.service}"
215233

216234
# Hand-written units
217235
: "${MIOS_UNIT_FIRSTBOOT_TARGET:=mios-firstboot.target}"
@@ -222,6 +240,7 @@ export MIOS_AI_SYSTEM_PROMPT MIOS_MCP_REGISTRY MIOS_BUILD_ENV_FILE
222240
export MIOS_UNIT_AI MIOS_UNIT_FORGE MIOS_UNIT_FORGE_RUNNER MIOS_UNIT_OLLAMA
223241
export MIOS_UNIT_CEPH MIOS_UNIT_K3S MIOS_UNIT_AICHAT_BUILD MIOS_UNIT_AICHAT_IMAGE
224242
export MIOS_UNIT_COCKPIT_LINK MIOS_UNIT_SEARXNG MIOS_UNIT_FIRSTBOOT_TARGET
243+
export MIOS_UNIT_HERMES MIOS_UNIT_WEBUI MIOS_UNIT_HERMES_FIRSTBOOT
225244
export MIOS_UNIT_OLLAMA_FIRSTBOOT MIOS_UNIT_WSL_FIRSTBOOT MIOS_UNIT_USER_SESSION
226245

227246
# ── CONTAINERS / DISTROBOX ───────────────────────────────────────────
@@ -231,10 +250,13 @@ export MIOS_UNIT_OLLAMA_FIRSTBOOT MIOS_UNIT_WSL_FIRSTBOOT MIOS_UNIT_USER_SESSION
231250
: "${MIOS_CONTAINER_LOCALAI_IMAGE:=docker.io/localai/localai:latest}"
232251
: "${MIOS_CONTAINER_OLLAMA_IMAGE:=docker.io/ollama/ollama:latest}"
233252
: "${MIOS_CONTAINER_SEARXNG_IMAGE:=docker.io/searxng/searxng:latest}"
253+
: "${MIOS_CONTAINER_HERMES_IMAGE:=docker.io/nousresearch/hermes-agent:latest}"
254+
: "${MIOS_CONTAINER_WEBUI_IMAGE:=docker.io/openwebui/open-webui:latest}"
234255

235256
export MIOS_DISTROBOX_AICHAT MIOS_CONTAINER_AICHAT_IMAGE
236257
export MIOS_CONTAINER_FORGE_IMAGE MIOS_CONTAINER_LOCALAI_IMAGE
237258
export MIOS_CONTAINER_OLLAMA_IMAGE MIOS_CONTAINER_SEARXNG_IMAGE
259+
export MIOS_CONTAINER_HERMES_IMAGE MIOS_CONTAINER_WEBUI_IMAGE
238260

239261
# ── COLOR PALETTE ────────────────────────────────────────────────────
240262
# Hokusai + operator-neutrals palette. Resolved from mios.toml [colors]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# /etc/containers/systemd/ai-net.network
2+
# 'MiOS' AI stack network (Hermes-Agent <-> Open WebUI <-> Ollama).
3+
#
4+
# Topology mirrors the upstream deploy-aistack.sh reference flow:
5+
#
6+
# browser -> mios-webui (3030) -> mios-hermes (8642) -> mios-ollama (11434) -> GPU
7+
#
8+
# but extended so the existing MiOS AI containers (mios-ai / LocalAI on 8080,
9+
# mios-searxng on 8888, mios-ollama on 11434) ALSO live on this bridge in
10+
# addition to mios.network. That way:
11+
#
12+
# * The agent (mios-hermes) reaches mios-ollama by container name without
13+
# leaving the bridge.
14+
# * Open WebUI can target either mios-hermes (default) or mios-ai
15+
# (LocalAI) as alternate /v1 backends, both via container DNS.
16+
# * SearXNG is reachable from the agent's `web_search` tool surface as
17+
# mios-searxng:8080 without going through host loopback.
18+
#
19+
# Subnet 10.90.0.0/24 is one octet beyond mios.network's 10.89.0.0/24 to
20+
# stay clear of any podman default ranges, the WSL2 eth0 prefixes
21+
# (10.88.x typical), and the rootful podman default (10.88.0.0/16).
22+
# Quadlet generates the actual podman network as 'systemd-ai-net'.
23+
24+
[Network]
25+
Subnet=10.90.0.0/24
26+
Gateway=10.90.0.1
27+
Label=io.mios.network=ai-stack

etc/containers/systemd/mios-ai.container

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ ConditionPathIsDirectory=/etc/mios/ai
1414
[Container]
1515
Image=docker.io/localai/localai:latest@sha256:a6af99e17a73a92caa134e70ae84492cc47b67645c1676268a7522ad14f4c09d
1616
ContainerName=mios-ai
17-
# Single MiOS internal network so sibling Quadlets (mios-forge,
18-
# mios-aichat, mios-mcp, ollama, etc.) reach LocalAI by container name
19-
# at http://mios-ai:8080/v1 without going through host loopback. Keeps
20-
# the network surface KISS: one bridge, all services on it. PublishPort
21-
# below still maps to the host so external clients can reach :8080.
17+
# MiOS internal networks: mios.network is the historical core bridge
18+
# (mios-forge / mios-cockpit-link / mios-mcp etc.); ai-net.network is
19+
# the dedicated AI-stack bridge that mios-hermes / mios-webui /
20+
# mios-ollama / mios-searxng all share so the gateway can address
21+
# them by container name without leaving the bridge. Multi-network
22+
# membership keeps mios-ai reachable from BOTH worlds.
2223
Network=mios.network
24+
Network=ai-net.network
2325
PublishPort=8080:8080
2426
# LocalAI v4 layout: /build/models is canonical (matches MODELS_PATH /
2527
# upstream Containerfile WORKDIR), but /data/{outputs,collections} is
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# /etc/containers/systemd/mios-hermes.container
2+
# 'MiOS' Hermes-Agent OpenAI-compatible gateway.
3+
#
4+
# Hermes-Agent (Nous Research) sits between an Open WebUI / aichat /
5+
# Claude Code-style frontend and the underlying LLM backend (mios-ollama
6+
# by default). It exposes /v1 OpenAI-shaped endpoints with first-class
7+
# agent / tool-use semantics on top of any backend that lacks them, so
8+
# operators can drive non-Hermes models (qwen2.5-coder, granite, llama)
9+
# through the same agent surface.
10+
#
11+
# Defaults policy: enabled by default. ConditionPathIsDirectory guards
12+
# the config dir; if /etc/mios/hermes/ doesn't exist (incomplete
13+
# bootstrap), the unit no-ops at pre-boot rather than crash-looping.
14+
# mios-hermes-firstboot.service generates a fresh API_SERVER_KEY into
15+
# /etc/mios/hermes/api.env on first boot if missing -- that key is what
16+
# Open WebUI uses as its OPENAI_API_KEY when calling mios-hermes.
17+
18+
[Unit]
19+
Description='MiOS' Hermes-Agent OpenAI-compatible gateway
20+
After=network-online.target mios-hermes-firstboot.service ollama.service
21+
Wants=network-online.target mios-hermes-firstboot.service
22+
ConditionPathIsDirectory=/etc/mios/hermes
23+
ConditionPathExists=/etc/mios/hermes/api.env
24+
25+
[Container]
26+
Image=docker.io/nousresearch/hermes-agent:latest
27+
ContainerName=mios-hermes
28+
# Dual-network: ai-net is the canonical subnet for the AI stack
29+
# (Open WebUI <-> Hermes <-> Ollama), mios.network keeps the unit
30+
# reachable from the rest of MiOS by container name.
31+
Network=ai-net.network
32+
Network=mios.network
33+
PublishPort=8642:8642
34+
AutoUpdate=registry
35+
36+
# Persistent state. /opt/data inside the container is where Hermes
37+
# stores its config.yaml, prompt history, and tool registry. Bind-mount
38+
# the host's /var/lib/mios/hermes there so state survives image rebuilds.
39+
Volume=/var/lib/mios/hermes:/opt/data:Z
40+
# /etc/mios/hermes/config.yaml is a vendor-shipped declarative config
41+
# that wires Hermes at mios-ollama:11434 by default; operators can
42+
# override via /etc/mios/hermes/config.local.yaml (see config.yaml's
43+
# trailing include directive).
44+
Volume=/etc/mios/hermes:/etc/hermes:Z,ro
45+
46+
# API key + server settings come from /etc/mios/hermes/api.env, which
47+
# mios-hermes-firstboot.service generates on first boot via
48+
# `openssl rand -hex 32` if not already present. Open WebUI reads the
49+
# same file so the key flows through declaratively.
50+
EnvironmentFile=/etc/mios/hermes/api.env
51+
52+
# Force the agent to point at MiOS's mios-ollama by container name on
53+
# ai-net. The deploy-aistack.sh first-run wizard prompts for these
54+
# interactively; we bake them in so the unit is fully declarative.
55+
Environment=HERMES_BACKEND_PROVIDER=ollama
56+
Environment=HERMES_BACKEND_BASE_URL=http://mios-ollama:11434
57+
Environment=HERMES_BACKEND_MODEL=qwen2.5-coder:7b
58+
Environment=HERMES_CONFIG_PATH=/etc/hermes/config.yaml
59+
60+
# Identity. UID 820 is pinned in usr/lib/sysusers.d/50-mios-services.conf.
61+
User=820
62+
Group=820
63+
64+
Label=org.opencontainers.image.title=mios-hermes
65+
Label=org.opencontainers.image.url=http://localhost:8642/v1/models
66+
Label=org.opencontainers.image.documentation=https://nousresearch.com/
67+
Label=io.podman_desktop.openInBrowser=http://localhost:8642/v1/models
68+
69+
# `gateway run` is the long-lived Hermes API-server mode. The
70+
# alternative `setup` interactive wizard (used by deploy-aistack.sh on
71+
# first run) is bypassed here -- /etc/mios/hermes/config.yaml is
72+
# pre-shipped at build time so the gateway can start without an
73+
# operator-driven setup pass.
74+
Exec=gateway run
75+
76+
[Service]
77+
Restart=on-failure
78+
RestartSec=10s
79+
TimeoutStartSec=300s
80+
Delegate=yes
81+
82+
[Install]
83+
WantedBy=multi-user.target default.target

etc/containers/systemd/mios-searxng.container

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ ConditionPathIsDirectory=/etc/mios/searxng
2626
# every host shape MiOS targets.
2727
Image=docker.io/searxng/searxng:latest@sha256:885f1cd1bb86d759aa4724a1986a61c000292823ff8eeb17d34de1a8acb74477
2828
ContainerName=mios-searxng
29+
# Dual-network: mios.network keeps the unit reachable for the rest of
30+
# MiOS; ai-net.network puts SearXNG on the same bridge as mios-hermes
31+
# so the agent's `web_search` tool can call mios-searxng:8080 by
32+
# container name without leaving the network.
2933
Network=mios.network
34+
Network=ai-net.network
3035
# 8888 host -> 8080 container. 8080 is taken by mios-ai (LocalAI's
3136
# OpenAI-compatible endpoint), so SearXNG goes on the next conventional
3237
# search-proxy port. Sibling Quadlets on mios.network reach it as
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# /etc/containers/systemd/mios-webui.container
2+
# 'MiOS' Open WebUI browser frontend.
3+
#
4+
# Open WebUI (https://github.com/open-webui/open-webui) is the
5+
# browser-side companion to mios-hermes / mios-ai. By default it points
6+
# at mios-hermes:8642/v1 (the Hermes-Agent gateway), which then proxies
7+
# to mios-ollama:11434 inside the ai-net bridge. Operators can also add
8+
# mios-ai (LocalAI on :8080) as a second OpenAI-compatible backend
9+
# inside the WebUI's settings panel -- both are reachable by container
10+
# name on the ai-net bridge.
11+
#
12+
# Port choice (3030, NOT 8080):
13+
# The upstream deploy-aistack.sh maps Open WebUI to host :8080. MiOS
14+
# already publishes mios-ai (LocalAI's OpenAI-compatible /v1 endpoint)
15+
# on :8080 -- that port is the canonical Architectural-Law-5 surface
16+
# that aichat / system prompts / agent SDKs all reference. Moving
17+
# LocalAI off 8080 would break every existing client. So WebUI gets
18+
# a different host port (3030) instead. Internal port stays 8080 to
19+
# match upstream defaults.
20+
#
21+
# Defaults policy: enabled by default. After= mios-hermes so the
22+
# OPENAI_API_BASE_URL target is reachable when the frontend starts.
23+
24+
[Unit]
25+
Description='MiOS' Open WebUI browser frontend
26+
After=network-online.target mios-hermes.service
27+
Wants=network-online.target
28+
Requires=mios-hermes.service
29+
30+
[Container]
31+
Image=docker.io/openwebui/open-webui:latest
32+
ContainerName=mios-webui
33+
Network=ai-net.network
34+
Network=mios.network
35+
PublishPort=3030:8080
36+
AutoUpdate=registry
37+
38+
Volume=/var/lib/mios/webui:/app/backend/data:Z
39+
40+
# Disable the built-in Ollama API path so Open WebUI ALWAYS goes
41+
# through the Hermes-Agent gateway. That keeps tool-use, prompt
42+
# history, and rate-limiting decisions in one place rather than
43+
# bypassing them when the user picks a non-Hermes-aware model.
44+
Environment=ENABLE_OLLAMA_API=false
45+
Environment=OPENAI_API_BASE_URL=http://mios-hermes:8642/v1
46+
47+
# OPENAI_API_KEY is sourced from the same /etc/mios/hermes/api.env
48+
# that mios-hermes itself reads, so the WebUI <-> Hermes auth pair
49+
# stays in sync without operator copy-paste. The key is generated on
50+
# first boot by mios-hermes-firstboot.service.
51+
EnvironmentFile=/etc/mios/hermes/api.env
52+
Environment=WEBUI_AUTH=true
53+
Environment=WEBUI_NAME=MiOS
54+
55+
User=821
56+
Group=821
57+
58+
Label=org.opencontainers.image.title=mios-webui
59+
Label=org.opencontainers.image.url=http://localhost:3030/
60+
Label=org.opencontainers.image.documentation=https://docs.openwebui.com/
61+
Label=io.podman_desktop.openInBrowser=http://localhost:3030/
62+
63+
[Service]
64+
Restart=on-failure
65+
RestartSec=10s
66+
TimeoutStartSec=300s
67+
Delegate=yes
68+
69+
[Install]
70+
WantedBy=multi-user.target default.target

usr/lib/systemd/system-preset/90-mios.preset

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ enable greenboot-status.service
4747
enable redboot-auto-reboot.service
4848
enable cockpit.socket
4949
enable mios-searxng.service
50+
enable mios-hermes-firstboot.service
51+
enable mios-hermes.service
52+
enable mios-webui.service
5053
# user@1000.service starts the per-user systemd manager + user D-Bus
5154
# session bus for the 'mios' user (uid 1000, pinned in
5255
# /usr/lib/sysusers.d/10-mios.conf). Normally systemd-logind reads

0 commit comments

Comments
 (0)