Add watchdog agent profile to command center

- Add 'watchdog' to KNOWN_PROFILES (5th agent)
- Custom status check via watchdog state file + kanban gateway fallback
- New /api/watchdog/status endpoint with scripts, plugins, heartbeat info
- Watchdog endpoint bypasses auth (public, non-sensitive)
- Update frontend PROFILE_ICONS + cache buster
This commit is contained in:
2026-06-02 17:45:03 -04:00
parent 3f086b7849
commit 5839bc47ce
3 changed files with 107 additions and 3 deletions
+105 -1
View File
@@ -40,13 +40,14 @@ _unauthorized = Response(
# ── Agent Profiles ──────────────────────────────────────────────────
HERMES_BIN = "/home/oplabs/.hermes/hermes-agent/venv/bin/hermes"
KNOWN_PROFILES = ["coder", "assistant", "artist", "kanban"]
KNOWN_PROFILES = ["coder", "assistant", "artist", "kanban", "watchdog"]
PROFILE_LABELS = {
"coder": "Cody (Coder)",
"assistant": "Ashley (Assistant)",
"artist": "Artie (Artist)",
"kanban": "Kanban Manager",
"watchdog": "Watchdog (Infra)",
}
PROFILE_ICONS = {
@@ -54,12 +55,37 @@ PROFILE_ICONS = {
"assistant": "🤖",
"artist": "🎨",
"kanban": "📋",
"watchdog": "🛡️",
}
def _get_profile_status(profile: str) -> dict:
"""Check if a Hermes profile gateway process is running."""
try:
# Watchdog runs as a plugin inside the kanban gateway; check its state file
if profile == "watchdog":
watchdog_state = Path(os.path.expanduser("~/.hermes/workspace/watchdog-state.json"))
if watchdog_state.exists():
try:
state = json.loads(watchdog_state.read_text())
last_beat = state.get("last_heartbeat", 0)
now = time.time()
# Consider alive if heartbeat within the last 3 hours
if last_beat and (now - last_beat) < 10800:
return {"status": "online", "profile": profile, "last_heartbeat": last_beat}
except (json.JSONDecodeError, OSError):
pass
# Fallback: check if kanban gateway is running (watchdog rides on it)
result = subprocess.run(
["ps", "aux"],
capture_output=True, text=True, timeout=10,
)
for line in result.stdout.splitlines():
if "--profile kanban" in line and "gateway run" in line:
return {"status": "online", "profile": profile,
"note": "running via kanban gateway (plugin)"}
return {"status": "offline", "profile": profile}
result = subprocess.run(
["ps", "aux"],
capture_output=True, text=True, timeout=10,
@@ -556,6 +582,8 @@ async def auth_middleware(request: Request, call_next):
return await call_next(request)
if request.url.path.startswith("/api/network/"):
return await call_next(request)
if request.url.path.startswith("/api/watchdog/"):
return await call_next(request)
if request.url.path == "/" or request.url.path.startswith("/static/") or request.url.path == "/api/agents" or request.url.path.startswith("/api/agents/") and request.method == "GET":
return await call_next(request)
if not _check_auth(request.headers.get("Authorization")):
@@ -715,6 +743,82 @@ async def vault_search(q: str = Query(..., min_length=1, description="Search que
})
# ── Watchdog API ─────────────────────────────────────────────────────
WATCHDOG_STATE_PATH = Path(os.path.expanduser("~/.hermes/workspace/watchdog-state.json"))
WATCHDOG_SCRIPTS_DIR = Path(os.path.expanduser("~/.hermes/scripts"))
def _get_watchdog_scripts() -> list[dict]:
"""List watchdog scripts and their basic info."""
scripts = []
try:
for f in sorted(WATCHDOG_SCRIPTS_DIR.glob("*watchdog*")):
if f.suffix in (".sh", ".py"):
scripts.append({
"name": f.name,
"path": str(f),
"size": f.stat().st_size,
"modified": int(f.stat().st_mtime),
})
except OSError:
pass
return scripts
def _get_watchdog_plugins() -> list[dict]:
"""List watchdog-related plugins."""
plugins = []
plugin_dir = Path(os.path.expanduser("~/.hermes/plugins"))
try:
for p in sorted(plugin_dir.glob("*watchdog*")):
plugin_yaml = p / "plugin.yaml"
if plugin_yaml.exists():
try:
meta = yaml.safe_load(plugin_yaml.read_text()) or {}
except Exception:
meta = {}
plugins.append({
"name": p.name,
"description": meta.get("description", ""),
"version": meta.get("version", ""),
})
except OSError:
pass
return plugins
@app.get("/api/watchdog/status")
async def watchdog_status():
"""Return detailed watchdog agent status (heartbeat, scripts, plugins)."""
state = {}
state_file = WATCHDOG_STATE_PATH
if state_file.exists():
try:
state = json.loads(state_file.read_text())
except (json.JSONDecodeError, OSError):
state = {}
profile_status = _get_profile_status("watchdog")
# Age of last heartbeat
last_heartbeat = state.get("last_heartbeat", 0)
if last_heartbeat:
age = time.time() - last_heartbeat
age_str = f"{age/60:.0f}m ago" if age < 3600 else f"{age/3600:.1f}h ago"
state["heartbeat_age_seconds"] = int(age)
state["heartbeat_age_str"] = age_str
return JSONResponse({
"status": profile_status["status"],
"note": profile_status.get("note", ""),
"state": state,
"scripts": _get_watchdog_scripts(),
"plugins": _get_watchdog_plugins(),
"cached_at": time.time(),
})
# ── Agent Chat API ──────────────────────────────────────────────────
+1 -1
View File
@@ -279,7 +279,7 @@ let chatState = {
sending: false,
};
const PROFILE_ICONS = { coder: '💻', assistant: '🤖', artist: '🎨', kanban: '📋' };
const PROFILE_ICONS = { coder: '💻', assistant: '🤖', artist: '🎨', kanban: '📋', watchdog: '🛡️' };
async function loadAgentProfiles() {
try {
+1 -1
View File
@@ -286,7 +286,7 @@
</main>
</div>
<script src="/static/app.js?v=2"></script>
<script src="/static/app.js?v=3"></script>
<script src="/static/vault.js?v=2"></script>
</body>
</html>