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:
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user