Skip to content

Commit 5c1cd97

Browse files
mios-devclaude
andcommitted
agent env-awareness: CDP ensure + Discord status + OWUI system prompt
Operator chat 2026-05-17 paste -- "launch the crew motor fest and research the newest global trending topics every 15 minutes" -- ran 19 tool calls and produced no answer. Root-causing each failure: 1. CDP control. browser_navigate hit "All CDP discovery methods failed for localhost:9222" twice. mios-hermes-browser existed as a foreground-launch script but had no idempotent up-check. Extended with subcommands: `ensure` (idempotent: noop if up, else background-launch + wait 8s), `status` (probe), `stop` (kill profile-scoped chromedev), `start` (legacy foreground). Default subcommand = ensure. 2. SOUL.md routes the agent to call `terminal: mios-hermes-browser ensure` before ANY browser_* tool call. Skipping it produced the chronic discovery-loop. 3. web_extract failed 4x -- backend is searxng (search-only). SOUL.md now states the fact and routes URL fetches to `terminal: curl -sL "<url>" | ...` instead of looping. 4. memory_save called 4x as a bash line via `terminal:` -- syntax error each time. New "Native tools vs terminal" SOUL section explicitly forbids invoking native tools through the shell; they must be tool_calls. 5. Discord visibility. discord.env has the bot token + default channel/guild, but the agent had no self-check tool. New mios-discord-status helper: parses discord.env via grep (NOT bash source -- comment backticks would mangle it), probes /users/@me + /users/@me/guilds, reports channel_directory mtime, exits 0/1/2 by severity. SOUL.md routes diagnostic asks here. Live probe: bot Agent-1 in "Ding Dongs" guild, default channel #mios -- working. 6. OWUI MiOS-Agent had params={}. Now ships with: * params.system: concise environment-awareness prompt naming canonical verbs (no env values baked in -- operator's "no context injection" rule preserved; only verb names + when to use which tool surface). * temperature 0.2 (predictable verb selection) * top_p 0.9 * stop ["<|im_end|>","<|endoftext|>"] (cut delegate-spawn token leakage from polish input) install-pipe model-create case statement also fixed: OWUI returns 401 (not 409) for "model id already registered" with that detail body. 401 added to the exists-update branch so re-registration actually persists params on an existing host. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 81dc720 commit 5c1cd97

4 files changed

Lines changed: 327 additions & 6 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/bin/bash
2+
# /usr/libexec/mios/mios-discord-status
3+
#
4+
# Self-check for the Hermes-Agent Discord integration. The agent's
5+
# `discord_send_message` tool quietly returns "channel not configured"
6+
# or "unauthorized" when something is wrong; this helper exposes the
7+
# actual state so the operator (and the agent, via terminal:) can
8+
# diagnose without re-deriving the curl pipeline each time.
9+
#
10+
# Checks:
11+
# 1. /etc/mios/hermes/discord.env present + readable
12+
# 2. DISCORD_BOT_TOKEN parses, length is sane
13+
# 3. GET /users/@me -> bot username + id (proves token live)
14+
# 4. GET /users/@me/guilds -> guild count (proves bot is in a guild)
15+
# 5. MIOS_DISCORD_DEFAULT_CHANNEL + _GUILD env values reported
16+
# 6. /var/lib/mios/hermes/channel_directory.json mtime (last refresh)
17+
#
18+
# Operator directive 2026-05-17: "Fix discord too" -- the chat above
19+
# showed the agent couldn't tell whether discord was up. SOUL.md now
20+
# routes diagnostic questions here.
21+
#
22+
# Exit codes:
23+
# 0 = token works, bot in >=1 guild, default channel/guild set
24+
# 1 = soft failure (token works but config incomplete)
25+
# 2 = hard failure (token missing, invalid, or API unreachable)
26+
27+
set -uo pipefail
28+
29+
ENV_FILE="${MIOS_DISCORD_ENV:-/etc/mios/hermes/discord.env}"
30+
DIRECTORY="${MIOS_HERMES_HOME:-/var/lib/mios/hermes}/channel_directory.json"
31+
API="https://discord.com/api/v10"
32+
33+
red() { printf '\033[31m%s\033[0m\n' "$*"; }
34+
green() { printf '\033[32m%s\033[0m\n' "$*"; }
35+
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
36+
hdr() { printf '\n=== %s ===\n' "$*"; }
37+
38+
# --- 1. env file ---------------------------------------------------
39+
hdr "discord.env"
40+
if [ ! -r "$ENV_FILE" ]; then
41+
red " $ENV_FILE not readable by uid $(id -u)"
42+
exit 2
43+
fi
44+
echo " $(ls -la "$ENV_FILE")"
45+
46+
# Parse via grep (NOT `source`) so comment backticks don't trip bash.
47+
TOKEN=$(grep -E '^DISCORD_BOT_TOKEN=' "$ENV_FILE" | head -1 | cut -d= -f2-)
48+
DEFAULT_CHANNEL=$(grep -E '^MIOS_DISCORD_DEFAULT_CHANNEL=' "$ENV_FILE" | head -1 | cut -d= -f2-)
49+
DEFAULT_GUILD=$(grep -E '^MIOS_DISCORD_DEFAULT_GUILD=' "$ENV_FILE" | head -1 | cut -d= -f2-)
50+
51+
if [ -z "$TOKEN" ]; then
52+
red " DISCORD_BOT_TOKEN not set in $ENV_FILE"
53+
exit 2
54+
fi
55+
echo " DISCORD_BOT_TOKEN: present (${#TOKEN} chars)"
56+
echo " MIOS_DISCORD_DEFAULT_CHANNEL: ${DEFAULT_CHANNEL:-<unset>}"
57+
echo " MIOS_DISCORD_DEFAULT_GUILD: ${DEFAULT_GUILD:-<unset>}"
58+
59+
# --- 2. token live? ------------------------------------------------
60+
hdr "GET $API/users/@me"
61+
TMP=$(mktemp)
62+
HTTP=$(curl -sS --max-time 8 -o "$TMP" -w '%{http_code}' \
63+
-H "Authorization: Bot $TOKEN" "$API/users/@me" 2>/dev/null || echo "000")
64+
case "$HTTP" in
65+
200)
66+
USERNAME=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('username',''))" "$TMP")
67+
USERID=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('id',''))" "$TMP")
68+
green " OK $HTTP -- bot username='$USERNAME' id='$USERID'"
69+
;;
70+
401)
71+
red " $HTTP -- token rejected (rotate via Discord Developer Portal -> Bot tab -> Reset Token, paste into $ENV_FILE)"
72+
rm -f "$TMP"; exit 2 ;;
73+
000|"")
74+
red " $HTTP -- no response (DNS failure? network down? proxy?)"
75+
rm -f "$TMP"; exit 2 ;;
76+
*)
77+
red " $HTTP -- $(head -c 200 "$TMP")"
78+
rm -f "$TMP"; exit 2 ;;
79+
esac
80+
rm -f "$TMP"
81+
82+
# --- 3. guild membership ------------------------------------------
83+
hdr "GET $API/users/@me/guilds"
84+
TMP=$(mktemp)
85+
HTTP=$(curl -sS --max-time 8 -o "$TMP" -w '%{http_code}' \
86+
-H "Authorization: Bot $TOKEN" "$API/users/@me/guilds" 2>/dev/null || echo "000")
87+
if [ "$HTTP" = "200" ]; then
88+
COUNT=$(python3 -c "import json,sys; print(len(json.load(open(sys.argv[1]))))" "$TMP")
89+
green " OK $HTTP -- bot is in $COUNT guild(s):"
90+
python3 -c "
91+
import json,sys
92+
for g in json.load(open(sys.argv[1])):
93+
print(' -', g.get('name'), '(id=' + g.get('id','?') + ')')
94+
" "$TMP"
95+
else
96+
yellow " $HTTP -- $(head -c 200 "$TMP")"
97+
fi
98+
rm -f "$TMP"
99+
100+
# --- 4. channel_directory.json freshness ---------------------------
101+
hdr "channel_directory.json"
102+
if [ -r "$DIRECTORY" ]; then
103+
echo " $(ls -la "$DIRECTORY")"
104+
AGE_S=$(( $(date +%s) - $(stat -c %Y "$DIRECTORY") ))
105+
AGE_MIN=$(( AGE_S / 60 ))
106+
if [ "$AGE_MIN" -gt 1440 ]; then
107+
yellow " age: ${AGE_MIN}min -- stale (>24h). Restart hermes-agent.service to refresh."
108+
else
109+
echo " age: ${AGE_MIN}min"
110+
fi
111+
DCOUNT=$(python3 -c "import json; print(len(json.load(open('$DIRECTORY')).get('platforms',{}).get('discord',[])))" 2>/dev/null || echo "?")
112+
echo " discord entries cached: $DCOUNT"
113+
else
114+
yellow " $DIRECTORY not present yet (hermes hasn't fetched the directory yet)"
115+
fi
116+
117+
# --- 5. default channel/guild consistency --------------------------
118+
hdr "default channel/guild check"
119+
if [ -z "$DEFAULT_CHANNEL" ] || [ -z "$DEFAULT_GUILD" ]; then
120+
yellow " Default channel/guild not set in $ENV_FILE -- discord_send_message"
121+
yellow " will require an explicit 'channel' arg every call."
122+
exit 1
123+
fi
124+
green " default channel=$DEFAULT_CHANNEL guild=$DEFAULT_GUILD"
125+
echo
126+
green "All checks passed."

usr/libexec/mios/mios-hermes-browser

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,115 @@
1818
# is still available for cron / scripted use by setting
1919
# HERMES_BROWSER_HEADLESS=1 in the calling environment.
2020
#
21+
# Operator directive 2026-05-17: "fix CDP control". Browser_navigate
22+
# from inside Hermes was hitting "All CDP discovery methods failed"
23+
# because nothing was listening on :9222 and the agent had no way to
24+
# bring it up. This script now supports an idempotent subcommand
25+
# surface so the agent can ensure the port is up before any
26+
# browser_* call:
27+
#
28+
# mios-hermes-browser ensure # idempotent: noop if up, else launch in bg
29+
# mios-hermes-browser status # exit 0 if CDP responding, 1 otherwise
30+
# mios-hermes-browser stop # kill the ChromeDev process tree
31+
# mios-hermes-browser start # explicit foreground launch (the legacy path)
32+
# mios-hermes-browser # default = ensure (safest for the agent)
33+
#
34+
# `ensure` waits up to 8s for CDP to respond; SOUL.md tells the agent
35+
# to call it before any browser_navigate / browser_screenshot / etc.
36+
#
2137
# Profile data lives under /var/lib/mios/hermes-browser/profile so the
2238
# Hermes session is fully isolated from the operator's interactive
2339
# Chrome session (separate cookies, separate signed-in accounts,
2440
# separate extensions).
25-
set -euo pipefail
41+
set -uo pipefail
2642

2743
CDP_PORT="${HERMES_BROWSER_CDP_PORT:-9222}"
2844
PROFILE_DIR="${HERMES_BROWSER_PROFILE_DIR:-/var/lib/mios/hermes-browser/profile}"
2945
# Default 0 (visible) per operator directive 2026-05-16. Operators
3046
# wanting headless set HERMES_BROWSER_HEADLESS=1.
3147
HEADLESS="${HERMES_BROWSER_HEADLESS:-0}"
3248
APP_ID="${HERMES_BROWSER_APP_ID:-com.google.ChromeDev}"
49+
LOG_FILE="${HERMES_BROWSER_LOG:-/var/lib/mios/hermes-browser/launch.log}"
50+
51+
mkdir -p "$PROFILE_DIR" "$(dirname "$LOG_FILE")"
52+
53+
# --- helpers -------------------------------------------------------
54+
55+
cdp_responding() {
56+
# Hit the CDP discovery endpoint with a short timeout. 200 = up.
57+
local code
58+
code=$(curl -sS --max-time 2 -o /dev/null -w "%{http_code}" \
59+
"http://127.0.0.1:${CDP_PORT}/json/version" 2>/dev/null || echo "000")
60+
[ "$code" = "200" ]
61+
}
62+
63+
wait_for_cdp() {
64+
local deadline=$(( $(date +%s) + ${1:-8} ))
65+
while [ "$(date +%s)" -lt "$deadline" ]; do
66+
cdp_responding && return 0
67+
sleep 0.25
68+
done
69+
return 1
70+
}
71+
72+
kill_existing() {
73+
# ChromeDev flatpak processes that bound 9222; scoped to our profile
74+
# so we don't clobber the operator's interactive Chrome.
75+
pkill -f "user-data-dir=${PROFILE_DIR}" >/dev/null 2>&1 || true
76+
}
77+
78+
# --- subcommand dispatch -------------------------------------------
79+
80+
SUBCMD="${1:-ensure}"
81+
82+
case "$SUBCMD" in
83+
status)
84+
if cdp_responding; then
85+
echo "CDP up on 127.0.0.1:${CDP_PORT}"
86+
exit 0
87+
fi
88+
echo "CDP not responding on 127.0.0.1:${CDP_PORT}"
89+
exit 1
90+
;;
91+
stop)
92+
kill_existing
93+
echo "stopped (any process bound to profile ${PROFILE_DIR})"
94+
exit 0
95+
;;
96+
ensure|"")
97+
if cdp_responding; then
98+
echo "CDP already up on 127.0.0.1:${CDP_PORT}"
99+
exit 0
100+
fi
101+
echo "CDP not responding -- launching ChromeDev in background..."
102+
# Self-launch via `start` subcommand, backgrounded + detached.
103+
nohup "$0" start >>"$LOG_FILE" 2>&1 &
104+
disown || true
105+
if wait_for_cdp 8; then
106+
echo "CDP up on 127.0.0.1:${CDP_PORT} (log: $LOG_FILE)"
107+
exit 0
108+
fi
109+
echo "ERROR: CDP did not come up within 8s -- check $LOG_FILE"
110+
echo " hint: flatpak run $APP_ID may need a display / dbus session"
111+
tail -n 20 "$LOG_FILE" 2>/dev/null | sed 's/^/ /' || true
112+
exit 1
113+
;;
114+
start)
115+
# Legacy foreground launch path -- exec'd by `ensure` and also
116+
# callable directly when operator wants to see the launch
117+
# output live.
118+
;;
119+
*)
120+
echo "usage: $0 {ensure|status|stop|start}" >&2
121+
exit 64
122+
;;
123+
esac
124+
125+
# --- foreground launch (start subcommand) --------------------------
33126

34-
mkdir -p "$PROFILE_DIR"
127+
# Strict mode for the actual launch. The dispatch above used -uo
128+
# pipefail (no -e) so subcommand checks could fail gracefully.
129+
set -e
35130

36131
CHROME_ARGS=(
37132
--remote-debugging-port="$CDP_PORT"

usr/libexec/mios/mios-owui-install-pipe

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,43 @@ BASE_MODEL="mios_agent.mios-agent" # <function_id>.<pipe_id> from pipes()
150150
MODEL_PAYLOAD=$(python3 - "$MODEL_ID" "$MODEL_NAME" "$BASE_MODEL" <<'PYEOF'
151151
import json, sys
152152
mid, mname, base = sys.argv[1], sys.argv[2], sys.argv[3]
153+
154+
# Operator directive 2026-05-17: "could use a system prompt for
155+
# MiOS-Agent in OWUI and configure the settings too as well".
156+
#
157+
# This is the OWUI-side system prompt operators see + can edit
158+
# under Settings -> Models -> MiOS-Agent -> Advanced. It's
159+
# prepended by OWUI before the pipe receives the messages, so:
160+
# - the CPU refine step sees it (helps refine route phrasing)
161+
# - hermes sees it (reinforces SOUL.md from the user-prompt side)
162+
# - OWUI's task generators (title, tags, follow-ups) see it
163+
#
164+
# Kept short -- the full operating manual lives in SOUL.md inside
165+
# hermes. This is the "what is this agent" intro. NO env values
166+
# baked in (operator: "no context injection for environment
167+
# detection") -- only canonical verb names.
168+
SYSTEM_PROMPT = (
169+
"You are MiOS-Agent, a local-first agent running on the operator's "
170+
"MiOS host. You speak the user's language and mirror their tone. "
171+
"For any visible-to-operator action (launch app, open URL, install "
172+
"software, take screenshot, control window) you ALWAYS go through "
173+
"the canonical MiOS verb -- never bare PowerShell, never browser "
174+
"automation for visible browsing, never vendor download pages. "
175+
"Canonical verbs: mios-find (launch resolver), mios-open-url "
176+
"(visible browser), mios-installer (cross-platform install), "
177+
"mios-windows (Windows dispatch), mios-window (focus/close/move), "
178+
"mios-screenshot, mios-steamcmd, mios-system-status (single source "
179+
"of truth for hardware/services/models), mios-hermes-browser ensure "
180+
"(bring CDP up before any browser_* tool call). Use web_search not "
181+
"web_extract (the extract backend is search-only in this MiOS). "
182+
"Native tools (memory_save, kanban_*, discord_send_message, "
183+
"delegate_task, etc.) are tool_calls -- never call them as bash "
184+
"lines through terminal. You can fork and improve your own tools "
185+
"(mios-tool-clone) and skills (mios-skill-clone) when a recurring "
186+
"task hits the same friction twice. Report tool stdout verbatim; "
187+
"guessing confidently is a defect."
188+
)
189+
153190
print(json.dumps({
154191
"id": mid,
155192
"base_model_id": base,
@@ -169,7 +206,25 @@ print(json.dumps({
169206
# recent state + chat language (operator directive 2026-05-17:
170207
# "no hardcoded answers or anything hardcoded for english").
171208
},
172-
"params": {},
209+
"params": {
210+
# The system prompt OWUI prepends to every chat. Edit in
211+
# the OWUI admin UI (Settings -> Models -> MiOS-Agent) or
212+
# re-run this installer.
213+
"system": SYSTEM_PROMPT,
214+
# Sampling defaults tuned for the refine -> hermes -> polish
215+
# chain. Low temperature: agent should be predictable about
216+
# which canonical verb to call, not "creative". top_p left
217+
# at OWUI default. Hermes internally overrides these on its
218+
# delegate spawns -- this is just the outer chat-completion
219+
# default.
220+
"temperature": 0.2,
221+
"top_p": 0.9,
222+
# Stop tokens: hermes occasionally emits these as text inside
223+
# delegate-spawn tool args; cut them so they don't leak into
224+
# the polished answer.
225+
"stop": ["<|im_end|>", "<|endoftext|>"],
226+
# OWUI streams tokens incrementally; no need to override.
227+
},
173228
"is_active": True,
174229
}))
175230
PYEOF
@@ -187,7 +242,10 @@ case "$RESP3" in
187242
200|201)
188243
echo "[mios-owui-install-pipe] ✓ created Model entry $MODEL_ID"
189244
;;
190-
400|409)
245+
400|401|409)
246+
# OWUI returns 401 (not 409) for "model id already registered"
247+
# -- detail body "Uh-oh! This model id is already registered."
248+
# Treat all three as "exists -- need to update".
191249
echo "[mios-owui-install-pipe] (Model exists; updating)"
192250
RESP4=$(curl -s -X POST "$OWUI_URL/api/v1/models/model/update?id=$MODEL_ID" \
193251
-H "Authorization: Bearer $TOKEN" \

usr/share/mios/ai/hermes-soul.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ never from training data.
4242
`Get-StartApps`, never bash `which`, never a vendor download URL.
4343

4444
4. **"Open a browser to `<url>`" / "go to `<url>`" → `terminal: mios-open-url "<url>"`.**
45-
NEVER `browser_navigate` for visible browsing (it drives a HEADLESS
46-
CDP session the user can't see).
45+
NEVER `browser_navigate` for visible browsing — it drives an
46+
isolated ChromeDev profile the operator can't see. `browser_*`
47+
is for SCRAPING/INSPECTION only. Before ANY `browser_*` call,
48+
ALWAYS run `terminal: mios-hermes-browser ensure` first — it
49+
idempotently brings the CDP port up. Skipping it produces the
50+
chronic "All CDP discovery methods failed for localhost:9222"
51+
loop (operator-flagged 2026-05-17).
4752

4853
5. **"Install `<X>` on Windows" → `terminal: mios-installer install <id> --backend winget --no-confirm`.**
4954
`mios-installer search "<X>" --backend winget` first to confirm
@@ -188,6 +193,43 @@ in the user's tone (1-2 sentences). DO NOT:
188193
"what GPU do I have", "list ollama models", "what services are
189194
running", "how much disk left", "system status".
190195

196+
## Native tools vs `terminal` — don't confuse the surfaces
197+
198+
Native tools (`memory_save`, `kanban_create`, `web_search`,
199+
`discord_send_message`, `delegate_task`, ...) are called via the
200+
**gateway's tool_call schema** with a JSON args object. They are
201+
NOT bash commands. If you emit `terminal: memory_save(key="x",
202+
value="y")` the shell tries to evaluate it and dies with `syntax
203+
error near unexpected token` — that's a chronic defect
204+
(operator-flagged 2026-05-17: agent called `memory_save(...)` four
205+
times through `terminal:` before giving up).
206+
207+
When you want to invoke a native tool, emit it as a tool call,
208+
not as a shell line. The terminal tool is ONLY for bash commands
209+
(MiOS helpers, system probes, file ops).
210+
211+
## Web search vs extract — searxng is search-only
212+
213+
`web_search` works (local SearXNG provider, no API key). `web_extract`
214+
DOES NOT in this MiOS — the backend is searxng which only indexes,
215+
it can't fetch full page content. Calling it returns "SearXNG is a
216+
search-only backend".
217+
218+
For URL content: `terminal: curl -sL "<url>" | sed -e 's/<[^>]*>//g'
219+
| head -c 4000` (quick text), or `terminal: mios-html-extract
220+
"<url>"` if installed. Don't loop on web_extract — it will never
221+
succeed against searxng.
222+
223+
## Discord messaging
224+
225+
`discord_send_message` posts to the operator's default channel set
226+
in `~/.config/mios/identity.toml [discord]`. The bot token is in
227+
the same file. If the call returns "channel not configured" or
228+
"unauthorized", do NOT invent a workaround — surface the error
229+
verbatim and tell the operator to check
230+
`terminal: mios-discord-status` (probes the token + lists reachable
231+
channels). Operator-confirmed 2026-05-17.
232+
191233
## Window-close path — drilled (you keep missing this)
192234

193235
"close <X>" / "quit <X>" / "exit <X>" / "shut down <X>" where X is

0 commit comments

Comments
 (0)