Files
HarborFork/harbor/doctor.py
T
KeelrLeek dbfd1720c5 Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and
stop flags for a team of AI agents; the chat page and board; host hooks and
MCP server; autopilot; tools/watch.py. Put under version control after the
weekend's changes so they have history and can be rolled back. Local only.
2026-10-05 10:44:23 -05:00

242 lines
12 KiB
Python

"""`harbor doctor`: is the project healthy, and is every agent's app really connected?
Each check is {area, check, status (ok|warn|fail|info), detail, fix}. Host checks read the host configuration files
that `harbor install` writes; "live" checks use what Harbor recorded when agents last reached it.
"""
import json
import os
import re
import tomllib
import urllib.request
from pathlib import Path
from . import install
def _check(area, check, status, detail="", fix=""):
return {"area": area, "check": check, "status": status, "detail": detail, "fix": fix}
def _text(path):
try:
return Path(path).read_text(encoding="utf-8")
except OSError:
return None
def _block(text, start=install.START, end=install.END):
return bool(text) and start in text and end in text
def codex_checks(agent):
home = install.user_home() / ".codex"
out = []
config = _text(home / "config.toml")
server = None
try:
server = tomllib.loads(config or "").get("mcp_servers", {}).get("harbor")
except tomllib.TOMLDecodeError as error:
out.append(_check("Codex", "config.toml parses", "fail", str(error), "fix the TOML by hand"))
if server and server.get("env", {}).get("HARBOR_AGENT") == agent:
out.append(_check("Codex", "MCP server", "ok", f"[mcp_servers.harbor] as {agent}"))
else:
out.append(_check("Codex", "MCP server", "fail", "missing or for another agent", "harbor install codex"))
hooks = _text(home / "hooks.json")
events = set()
if hooks:
try:
events = {e for e, entries in json.loads(hooks).get("hooks", {}).items()
if any(install._is_ours(x) for x in entries)}
except ValueError:
pass
wanted = {"SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"}
out.append(_check("Codex", "hooks", "ok" if wanted <= events else "fail",
", ".join(sorted(events)) or "none", "" if wanted <= events else "harbor install codex"))
trusted = len(re.findall(r"trusted_hash", config or ""))
out.append(_check("Codex", "hooks trusted", "ok" if trusted else "warn",
f"{trusted} trusted hook entr{'y' if trusted == 1 else 'ies'} in config.toml",
"" if trusted else "Codex: Settings > Hooks > trust the Harbor hooks"))
out.append(_check("Codex", "AGENTS.md", "ok" if _block(_text(home / "AGENTS.md"), install.MD_START, install.MD_END)
else "warn", str(home / "AGENTS.md"), "harbor install codex"))
return out
def opencode_checks(agent):
home = install.user_home() / ".config" / "opencode"
out = []
config = next((p for p in (home / "opencode.jsonc", home / "opencode.json") if p.exists()), None)
try:
server = json.loads(_text(config) or "{}").get("mcp", {}).get("harbor") if config else None
except ValueError:
server = None
ok = bool(server) and server.get("environment", {}).get("HARBOR_AGENT") == agent and server.get("enabled", True)
out.append(_check("OpenCode", "MCP server", "ok" if ok else "fail", str(config or home), "" if ok else
"harbor install opencode"))
plugin = _text(home / "plugin" / "harbor.js")
ok = bool(plugin) and f'const AGENT = "{agent}";' in plugin
out.append(_check("OpenCode", "plugin", "ok" if ok else "fail", str(home / "plugin" / "harbor.js"),
"" if ok else "harbor install opencode"))
out.append(_check("OpenCode", "AGENTS.md", "ok" if _block(_text(home / "AGENTS.md"), install.MD_START,
install.MD_END) else "warn", "", "harbor install opencode"))
return out
def deepseek_checks(agent):
from . import projects
home = install.user_home() / ".dsh"
out = []
patch = _text(home / "profiles" / "desktop" / "cordis.patch.yml")
rows = []
if patch:
try:
for row in install._TolerantLoader.load(patch):
if isinstance(row, dict) and "insert" in row:
rows += [r for r in row["insert"] if isinstance(r, dict)]
except Exception as error: # noqa: BLE001 - report any YAML problem
out.append(_check("DeepSeek", "cordis.patch.yml parses", "fail", str(error), "restore it from the backup"))
mcp = next((r for r in rows if r.get("id") == "mcp-harbor"), None)
ok = bool(mcp) and (mcp.get("config") or {}).get("env", {}).get("HARBOR_AGENT") == agent
out.append(_check("DeepSeek", "MCP row", "ok" if ok else "fail", "mcp-harbor", "" if ok else "harbor install deepseek"))
hooks_row = next((r for r in rows if r.get("id") == "hooks-harbor"), None)
hooks_file = projects.harbor_home() / "integrations" / "deepseek" / "hooks.json"
ok = bool(hooks_row) and hooks_file.exists()
out.append(_check("DeepSeek", "hooks row and file", "ok" if ok else "fail", str(hooks_file),
"" if ok else "harbor install deepseek"))
out.append(_check("DeepSeek", "AGENTS.md", "ok" if _block(_text(home / "AGENTS.md"), install.MD_START, install.MD_END)
else "warn", "", "harbor install deepseek"))
return out
def kimi_checks(agent, project):
from . import projects
out = []
name = f"harbor-{project.name}"
folder = projects.harbor_home() / "integrations" / "kimi" / name
manifest = _text(folder / "kimi.plugin.json")
ok = bool(manifest) and f'"HARBOR_AGENT": "{agent}"' in manifest
out.append(_check("Kimi", "plugin folder", "ok" if ok else "fail", str(folder),
"" if ok else f"harbor --project {project.name} install kimi"))
share = install.kimi_share() / "daimon"
registered = (share / "plugin-market" / "personal" / f"{name}.json").exists()
out.append(_check("Kimi", "registered", "ok" if registered else "fail", "Personal market",
"" if registered else f"harbor --project {project.name} install kimi"))
installed, enabled = False, False
record = share / "runtime" / "kimi-code" / "home" / "plugins" / "installed.json"
try:
def walk(node):
if isinstance(node, dict):
if node.get("id") == name:
yield node
for value in node.values():
yield from walk(value)
elif isinstance(node, list):
for value in node:
yield from walk(value)
entry = next(walk(json.loads(_text(record) or "{}")), None)
installed, enabled = entry is not None, bool(entry and entry.get("enabled", True))
except ValueError:
pass
installed = installed or (share / "plugin-packages" / name).exists()
out.append(_check("Kimi", "installed", "ok" if installed and enabled else "warn",
f"{name} " + ("enabled" if enabled else "installed but disabled" if installed else "not installed"),
"" if installed and enabled else "Kimi: sidebar Plugins > Personal > install (and enable) Harbor"))
return out
def running_mcp_servers():
"""{agent: count} of Harbor MCP server processes running right now (started by the agents' apps)."""
import subprocess
try:
out = subprocess.run(["powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | "
"ForEach-Object { $_.CommandLine }"], capture_output=True, text=True, timeout=20).stdout
except (OSError, subprocess.SubprocessError):
return {}
found = {}
for line in out.splitlines():
m = re.search(r"(?:run|harbor_mcp|harbor)\.py.*?--as\s+(\w+)\s+mcp\b", line) or (
re.search(r"harbor_mcp\.py", line) and None)
if m:
found[m.group(1)] = found.get(m.group(1), 0) + 1
return found
HOST_CHECKS = {"codex": codex_checks, "opencode": opencode_checks, "deepseek": deepseek_checks}
def server_check(project, port, server_known):
url = f"http://127.0.0.1:{port}"
if server_known:
return _check("Server", url, "ok", "this page is served by it")
try:
with urllib.request.urlopen(f"{url}/api/projects", timeout=2) as response:
names = [p["name"] for p in json.loads(response.read())]
except OSError:
return _check("Server", url, "warn", "not running (agents still work)", "start it: harbor serve, or Chat.cmd")
except (ValueError, KeyError, TypeError):
return _check("Server", url, "warn", "something else answers on this port", "free the port or pick another")
ok = project.name in names
return _check("Server", url, "ok" if ok else "warn", "serving " + ", ".join(names), "" if ok else "restart the server")
def run(project, harbor, port=None, server_known=False):
"""server_known: the caller is the server itself (asking it over HTTP would wait on its own lock)."""
port = port or int(os.environ.get("HARBOR_PORT", "8766"))
checks = [_check("Project", "file", "ok", str(project.source)),
_check("Project", "roster", "ok", ", ".join(project.names))]
try:
quick = harbor.conn.execute("PRAGMA quick_check").fetchone()[0]
count = harbor.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
checks.append(_check("Database", "integrity", "ok" if quick == "ok" else "fail",
f"{project.db} ({count} messages): {quick}", "" if quick == "ok" else "restore a backup"))
except Exception as error: # noqa: BLE001
checks.append(_check("Database", "integrity", "fail", str(error), "restore a backup"))
backups = sorted(project.backups.glob("harbor-*.db")) if project.backups.exists() else []
if backups:
age = (harbor.clock() - backups[-1].stat().st_mtime) / 3600
checks.append(_check("Database", "backups", "ok" if age < 24 else "warn",
f"{len(backups)} copies, newest {age:.1f} h old", "" if age < 24 else "harbor backup"))
else:
checks.append(_check("Database", "backups", "warn", "none yet", "harbor backup (the server also makes them)"))
if project.mirror:
try:
with open(project.mirror, "ab"):
pass
checks.append(_check("Mirror", "writable", "ok", str(project.mirror)))
except OSError as error:
checks.append(_check("Mirror", "writable", "fail", str(error), "check the file's permissions"))
checks.append(server_check(project, port, server_known))
links = harbor.connections()
servers = running_mcp_servers()
for spec in project.agents:
if spec["kind"] == "human":
continue
host = spec.get("host")
if host in HOST_CHECKS:
checks += HOST_CHECKS[host](spec["name"])
elif host == "kimi":
checks += kimi_checks(spec["name"], project)
seen = {(r["channel"], r["host"]): r for r in links.get(spec["name"], [])}
live = ", ".join(f"{ch}{('/' + h) if h else ''} {r['minutes']} min ago" for (ch, h), r in sorted(seen.items()))
running = servers.get(spec["name"], 0)
if running:
live = f"Harbor tools running in its app now ({running} process{'es' if running > 1 else ''})" + (
f"; {live}" if live else "")
status = "ok" if running or any(ch in ("mcp", "hook") for ch, _ in seen) else "info"
checks.append(_check(spec["name"], "live connection", status, live or "not seen through MCP or hooks yet",
"" if status == "ok" else "shows once the app starts a session with Harbor installed"))
return checks
def render(checks):
mark = {"ok": "OK ", "warn": "WARN", "fail": "FAIL", "info": "info"}
lines = []
for c in checks:
lines.append(f"{mark[c['status']]} {c['area']:<9} {c['check']:<22} {c['detail']}")
if c["fix"] and c["status"] in ("warn", "fail"):
lines.append(f"{'':6}{'':<9} -> {c['fix']}")
bad = sum(c["status"] == "fail" for c in checks)
warn = sum(c["status"] == "warn" for c in checks)
lines.append(f"\n{bad} failing, {warn} warnings.")
return "\n".join(lines)