Skip to content

Commit 83dcfb4

Browse files
mios-devclaude
andcommitted
Linux service / process / container verbs
Adds five typed verbs covering the gap between the existing Windows-side surface (Task Manager-style operations via Win32) and the Linux-side MiOS-DEV/host environment: service_status(name) -- systemctl is-active + status snapshot service_restart(name) -- systemctl restart <name> WRITE process_list(filter?, sort="rss", limit=20) -- ps -eo pid,user,rss,pcpu,comm,args sorted by rss (default) or cpu container_status(name?) -- podman ps -a, optional name filter container_restart(name) -- podman restart <name> WRITE Wired through every surface: usr/share/mios/owui/pipes/mios_agent_pipe.py * _dispatch_mios_verb gains 5 case branches. * _ROUTER_SYSTEM verb table lists the new verbs with [READ]/ [WRITE] tags and example service / container names operators typically reference (hermes-agent, mios-daemon, mios-open-webui, mios-surrealdb, ollama, etc.). usr/share/mios/owui/tools/mios_verbs.py * Five new async methods on the Tools class so Hermes can invoke them via OpenAI tool_calls during multi-step paths. All go through _broker_send (same socket as the rest of the verbs) so dispatch context is identical. usr/libexec/mios/mios-owui-install-tools * Five JSONSchema specs registered in the `tool` row of webui.db so the chat model sees them as native typed tool_calls. Live-verified on podman-MiOS-DEV: * service_status mios-daemon -> "active" + status block * process_list filter=ollama -> ollama serve / runner PIDs * container_status -> mios-forge / mios-ollama / mios-code-server / mios-searxng / mios-forgejo-runner / mios-surrealdb all Up * Tool row in webui.db: 26711 -> 31528 chars (+4817 for the 5 verb specs + Tools class methods) Symmetric Linux <-> Windows verb table is now: FS search everything_search <-> fs_search App launch open_app / launch_app (auto-picks env) URL open open_url Window ctrl focus/move/close/list_windows (WSLg = Win32) Service ctrl <-> service_status / restart Process audit <-> process_list Container <-> container_status / restart WRITE verbs (service_restart, container_restart) are tagged in the router prompt so the model only picks them when the operator explicitly asks; READ verbs are free-fire for status questions. process_kill deliberately omitted -- too dangerous as a single- verb dispatch; agent path still has shell-exec for that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 704a741 commit 83dcfb4

3 files changed

Lines changed: 254 additions & 0 deletions

File tree

usr/libexec/mios/mios-owui-install-tools

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,89 @@ def main() -> int:
221221
"required": [],
222222
},
223223
},
224+
{
225+
"name": "service_status",
226+
"description": "systemctl status snapshot for a single Linux service (read-only). Returns is-active line + first 20 lines of status. Use this when the operator asks 'is X running' / 'why did X fail' / 'what's the status of <service>'.",
227+
"parameters": {
228+
"type": "object",
229+
"properties": {
230+
"name": {
231+
"type": "string",
232+
"description": "Service unit name. Examples: hermes-agent, mios-daemon, mios-open-webui, mios-surrealdb, ollama, mios-forge.",
233+
}
234+
},
235+
"required": ["name"],
236+
},
237+
},
238+
{
239+
"name": "service_restart",
240+
"description": "systemctl restart <name>. WRITE verb -- visibly restarts a Linux service. Use when the operator asks to restart / reload / kick a service. Confirms with the post-restart is-active state.",
241+
"parameters": {
242+
"type": "object",
243+
"properties": {
244+
"name": {
245+
"type": "string",
246+
"description": "Service unit name (same names as service_status).",
247+
}
248+
},
249+
"required": ["name"],
250+
},
251+
},
252+
{
253+
"name": "process_list",
254+
"description": "ps snapshot of running Linux processes, sorted by RSS (memory) or CPU. Read-only. Use when the operator asks 'what's using memory' / 'what's eating CPU' / 'is <process> running'.",
255+
"parameters": {
256+
"type": "object",
257+
"properties": {
258+
"filter": {
259+
"type": "string",
260+
"description": "Case-insensitive substring on command name. Empty = top processes overall.",
261+
"default": "",
262+
},
263+
"sort": {
264+
"type": "string",
265+
"enum": ["rss", "cpu"],
266+
"default": "rss",
267+
"description": "Sort key: rss (memory) or cpu.",
268+
},
269+
"limit": {
270+
"type": "integer",
271+
"default": 20,
272+
"description": "Max rows.",
273+
},
274+
},
275+
"required": [],
276+
},
277+
},
278+
{
279+
"name": "container_status",
280+
"description": "podman ps -a snapshot. Read-only. Returns one line per container with name, status, image. Use when the operator asks 'is <container> running' / 'list containers' / 'show podman status'.",
281+
"parameters": {
282+
"type": "object",
283+
"properties": {
284+
"name": {
285+
"type": "string",
286+
"description": "Optional case-insensitive substring filter on container name. Empty = all containers (including stopped).",
287+
"default": "",
288+
}
289+
},
290+
"required": [],
291+
},
292+
},
293+
{
294+
"name": "container_restart",
295+
"description": "podman restart <name>. WRITE verb. Use when the operator asks to restart a MiOS-DEV container (mios-open-webui, mios-ollama, mios-surrealdb, mios-forge, etc.).",
296+
"parameters": {
297+
"type": "object",
298+
"properties": {
299+
"name": {
300+
"type": "string",
301+
"description": "Container name (exact). Examples: mios-open-webui, mios-ollama, mios-surrealdb, mios-forge, mios-searxng.",
302+
}
303+
},
304+
"required": ["name"],
305+
},
306+
},
224307
# ── PHASE-3 typed window/launch surface ────────────────────
225308
# Operator directive 2026-05-18 (native OpenAI strict
226309
# function-calling with enum positions; replaces SOUL.md

usr/share/mios/owui/pipes/mios_agent_pipe.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,18 @@ def _render_tool_history_for_compose(self, msgs: list[dict]) -> str:
10471047
' /root /var/log /usr/share/mios. type: "f" files only,\n'
10481048
' "d" directories only, omit for both.\n'
10491049
' [READ ] system_status()\n'
1050+
' [READ ] service_status(name)\n'
1051+
' -- systemctl is-active + status snapshot for a Linux\n'
1052+
' service (hermes-agent, mios-daemon, mios-open-webui, ...).\n'
1053+
' [WRITE] service_restart(name)\n'
1054+
' -- systemctl restart <name>. Use for live patches.\n'
1055+
' [READ ] process_list(filter?, sort=\"rss\", limit=20)\n'
1056+
' -- ps snapshot sorted by rss (default) or cpu.\n'
1057+
' filter = case-insensitive substring on command name.\n'
1058+
' [READ ] container_status(name?)\n'
1059+
' -- podman ps -a snapshot. name = optional substring filter.\n'
1060+
' [WRITE] container_restart(name)\n'
1061+
' -- podman restart <name> (or substring of a container name).\n'
10501062
"\n"
10511063
"Verb-pick priority (most common cases first):\n"
10521064
' "open X" / "launch X" / "start X" / "run X" -> open_app(name=X)\n'
@@ -1243,6 +1255,55 @@ async def _dispatch_mios_verb(
12431255
cmd += f" -type {type_filter}"
12441256
elif tool == "system_status":
12451257
cmd = "mios-system-status"
1258+
elif tool == "service_status":
1259+
# systemctl is-active + status snapshot for a Linux service.
1260+
# Read-only. Picks system bus by default; passes through
1261+
# whatever the operator named.
1262+
name = shlex.quote(str(args.get("name", "")))
1263+
cmd = (
1264+
f"echo \"=== is-active ===\"; systemctl is-active {name}; "
1265+
f"echo; echo \"=== status ===\"; "
1266+
f"systemctl --no-pager status {name} | head -20"
1267+
)
1268+
elif tool == "service_restart":
1269+
# systemctl restart <name>. WRITE verb -- visible side
1270+
# effect on the operator's system. Returns the post-restart
1271+
# is-active line so the agent can confirm.
1272+
name = shlex.quote(str(args.get("name", "")))
1273+
cmd = (
1274+
f"systemctl restart {name} && "
1275+
f"echo \"restarted; is-active=$(systemctl is-active {name})\""
1276+
)
1277+
elif tool == "process_list":
1278+
# ps snapshot sorted by RSS (default) or CPU. limit caps lines.
1279+
# filter is a case-insensitive substring on the command name.
1280+
limit = int(args.get("limit", 20))
1281+
sort = str(args.get("sort", "rss")).lower()
1282+
sort_arg = "--sort=-pcpu" if sort == "cpu" else "--sort=-rss"
1283+
filt = str(args.get("filter", "")).strip()
1284+
base = (
1285+
f"ps -eo pid,user,rss,pcpu,comm,args {sort_arg} --no-headers"
1286+
)
1287+
if filt:
1288+
base += f" | grep -i -- {shlex.quote(filt)}"
1289+
cmd = f"{base} | head -{limit}"
1290+
elif tool == "container_status":
1291+
# podman ps -a snapshot (all containers, including stopped).
1292+
# No filter = all; filter = case-insensitive substring on name.
1293+
filt = str(args.get("name", "")).strip()
1294+
base = "podman ps -a --format '{{.Names}}\\t{{.Status}}\\t{{.Image}}'"
1295+
if filt:
1296+
base += f" | grep -i -- {shlex.quote(filt)}"
1297+
cmd = base
1298+
elif tool == "container_restart":
1299+
# podman restart <name>. WRITE verb. Confirms by showing the
1300+
# post-restart status line.
1301+
name = shlex.quote(str(args.get("name", "")))
1302+
cmd = (
1303+
f"podman restart {name} && "
1304+
f"podman ps --filter name={name} "
1305+
f"--format '{{.Names}}\\t{{.Status}}'"
1306+
)
12461307
else:
12471308
return json.dumps({"success": False,
12481309
"stderr": f"router emitted unknown tool {tool!r}"})

usr/share/mios/owui/tools/mios_verbs.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,113 @@ async def open_url(
592592
"browser": browser or "(default)",
593593
"stderr": result.get("stderr", ""),
594594
})
595+
596+
# ─── service_status ─────────────────────────────────────────────
597+
async def service_status(self, name: str, __user__: Optional[dict] = None) -> str:
598+
"""systemctl status snapshot for a Linux service. Read-only.
599+
Returns is-active + first 20 lines of status output.
600+
"""
601+
if not self.valves.ENABLED:
602+
return json.dumps({"success": False, "stderr": "disabled"})
603+
q = shlex.quote(name)
604+
cmd = (
605+
f"echo '=== is-active ==='; systemctl is-active {q}; "
606+
f"echo; echo '=== status ==='; "
607+
f"systemctl --no-pager status {q} | head -20"
608+
)
609+
result = _broker_send(cmd, timeout=self.valves.SEARCH_TIMEOUT_S, capture=True)
610+
return json.dumps({
611+
"success": result["success"],
612+
"name": name,
613+
"output": (result.get("stdout") or "")[:4000],
614+
"stderr": result.get("stderr", ""),
615+
})
616+
617+
# ─── service_restart ────────────────────────────────────────────
618+
async def service_restart(self, name: str, __user__: Optional[dict] = None) -> str:
619+
"""systemctl restart <name>. WRITE verb -- visible side effect.
620+
Confirms via post-restart is-active line.
621+
"""
622+
if not self.valves.ENABLED:
623+
return json.dumps({"success": False, "stderr": "disabled"})
624+
q = shlex.quote(name)
625+
cmd = (
626+
f"systemctl restart {q} && "
627+
f"echo \"restarted; is-active=$(systemctl is-active {q})\""
628+
)
629+
result = _broker_send(cmd, timeout=self.valves.LAUNCH_TIMEOUT_S, capture=True)
630+
return json.dumps({
631+
"success": result["success"],
632+
"name": name,
633+
"output": (result.get("stdout") or "")[:1000],
634+
"stderr": result.get("stderr", ""),
635+
})
636+
637+
# ─── process_list ───────────────────────────────────────────────
638+
async def process_list(
639+
self,
640+
filter: str = "",
641+
sort: str = "rss",
642+
limit: int = 20,
643+
__user__: Optional[dict] = None,
644+
) -> str:
645+
"""ps snapshot, sorted by rss (default) or cpu. Read-only.
646+
647+
:param filter: case-insensitive substring on command name.
648+
:param sort: "rss" (memory, default) or "cpu".
649+
:param limit: max lines (default 20).
650+
"""
651+
if not self.valves.ENABLED:
652+
return json.dumps({"success": False, "stderr": "disabled"})
653+
sort_arg = "--sort=-pcpu" if sort.lower() == "cpu" else "--sort=-rss"
654+
base = f"ps -eo pid,user,rss,pcpu,comm,args {sort_arg} --no-headers"
655+
if filter.strip():
656+
base += f" | grep -i -- {shlex.quote(filter.strip())}"
657+
cmd = f"{base} | head -{int(limit)}"
658+
result = _broker_send(cmd, timeout=self.valves.SEARCH_TIMEOUT_S, capture=True)
659+
lines = [ln for ln in (result.get("stdout") or "").splitlines() if ln.strip()]
660+
return json.dumps({
661+
"success": result["success"],
662+
"lines": lines,
663+
"count": len(lines),
664+
"stderr": result.get("stderr", ""),
665+
})
666+
667+
# ─── container_status ───────────────────────────────────────────
668+
async def container_status(self, name: str = "", __user__: Optional[dict] = None) -> str:
669+
"""podman ps -a snapshot. Read-only.
670+
671+
:param name: optional case-insensitive substring filter on container name.
672+
"""
673+
if not self.valves.ENABLED:
674+
return json.dumps({"success": False, "stderr": "disabled"})
675+
base = "podman ps -a --format '{{.Names}}\\t{{.Status}}\\t{{.Image}}'"
676+
if name.strip():
677+
base += f" | grep -i -- {shlex.quote(name.strip())}"
678+
result = _broker_send(base, timeout=self.valves.SEARCH_TIMEOUT_S, capture=True)
679+
return json.dumps({
680+
"success": result["success"],
681+
"output": (result.get("stdout") or "")[:4000],
682+
"stderr": result.get("stderr", ""),
683+
})
684+
685+
# ─── container_restart ──────────────────────────────────────────
686+
async def container_restart(self, name: str, __user__: Optional[dict] = None) -> str:
687+
"""podman restart <name>. WRITE verb.
688+
689+
:param name: container name (exact or substring -- podman resolves).
690+
"""
691+
if not self.valves.ENABLED:
692+
return json.dumps({"success": False, "stderr": "disabled"})
693+
q = shlex.quote(name)
694+
cmd = (
695+
f"podman restart {q} && "
696+
f"podman ps --filter name={q} --format '{{{{.Names}}}}\\t{{{{.Status}}}}'"
697+
)
698+
result = _broker_send(cmd, timeout=self.valves.LAUNCH_TIMEOUT_S, capture=True)
699+
return json.dumps({
700+
"success": result["success"],
701+
"name": name,
702+
"output": (result.get("stdout") or "")[:1000],
703+
"stderr": result.get("stderr", ""),
704+
})

0 commit comments

Comments
 (0)