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 ──────────────────────────────────────────────────
|
# ── Agent Profiles ──────────────────────────────────────────────────
|
||||||
HERMES_BIN = "/home/oplabs/.hermes/hermes-agent/venv/bin/hermes"
|
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 = {
|
PROFILE_LABELS = {
|
||||||
"coder": "Cody (Coder)",
|
"coder": "Cody (Coder)",
|
||||||
"assistant": "Ashley (Assistant)",
|
"assistant": "Ashley (Assistant)",
|
||||||
"artist": "Artie (Artist)",
|
"artist": "Artie (Artist)",
|
||||||
"kanban": "Kanban Manager",
|
"kanban": "Kanban Manager",
|
||||||
|
"watchdog": "Watchdog (Infra)",
|
||||||
}
|
}
|
||||||
|
|
||||||
PROFILE_ICONS = {
|
PROFILE_ICONS = {
|
||||||
@@ -54,12 +55,37 @@ PROFILE_ICONS = {
|
|||||||
"assistant": "🤖",
|
"assistant": "🤖",
|
||||||
"artist": "🎨",
|
"artist": "🎨",
|
||||||
"kanban": "📋",
|
"kanban": "📋",
|
||||||
|
"watchdog": "🛡️",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_profile_status(profile: str) -> dict:
|
def _get_profile_status(profile: str) -> dict:
|
||||||
"""Check if a Hermes profile gateway process is running."""
|
"""Check if a Hermes profile gateway process is running."""
|
||||||
try:
|
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(
|
result = subprocess.run(
|
||||||
["ps", "aux"],
|
["ps", "aux"],
|
||||||
capture_output=True, text=True, timeout=10,
|
capture_output=True, text=True, timeout=10,
|
||||||
@@ -556,6 +582,8 @@ async def auth_middleware(request: Request, call_next):
|
|||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
if request.url.path.startswith("/api/network/"):
|
if request.url.path.startswith("/api/network/"):
|
||||||
return await call_next(request)
|
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":
|
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)
|
return await call_next(request)
|
||||||
if not _check_auth(request.headers.get("Authorization")):
|
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 ──────────────────────────────────────────────────
|
# ── Agent Chat API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -279,7 +279,7 @@ let chatState = {
|
|||||||
sending: false,
|
sending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROFILE_ICONS = { coder: '💻', assistant: '🤖', artist: '🎨', kanban: '📋' };
|
const PROFILE_ICONS = { coder: '💻', assistant: '🤖', artist: '🎨', kanban: '📋', watchdog: '🛡️' };
|
||||||
|
|
||||||
async function loadAgentProfiles() {
|
async function loadAgentProfiles() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -286,7 +286,7 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
<script src="/static/vault.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user