forked from Keelrleek/Harbor
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.
This commit is contained in:
commit
dbfd1720c5
43 files changed
+12440
No files matched your search
@@ -0,0 +1,241 @@
|
||||
"""`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)
|
||||
Reference in new issue
Block a user