feat: add health watchdog, key auto-sync, analytics, Discord notifications, trivial-PR merge
- health-check.py: model health watchdog (silent when healthy, alert on 2+ consecutive failures) — catches stale-key/model-breakage like 2026-08-04 - sync-key.py: systemd ExecStartPre syncs 9router key from BWS to disk, prevents silent 401s after key rotation - run_server.py: CONFIG__ANALYTICS_FOLDER + /api/metrics (Prometheus) + /api/analytics (JSON) + /api/v1/notify_review (Discord webhook) - trivial_merge.py: trivial-PR fast-path (docs/deps/tiny diffs) approve+merge - auto_merge_bot.py: Discord notifications on merge, trivial integration - flake.nix: ship pr-agent-sync-key + pr-agent-health-check binaries
This commit is contained in:
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR-Agent key auto-sync — fetch the working 9router key from Bitwarden Secrets
|
||||
Manager (BWS) and write it to the on-disk omniroute_key file IF it differs.
|
||||
|
||||
Why: the on-disk key file is the single source of truth for the running
|
||||
server (run_server.py reads it at startup). If BWS gets updated (key
|
||||
rotation) and the file isn't refreshed, the server silently starts failing
|
||||
with 401s — exactly what happened 2026-08-04 (stale 35-char key for 14h).
|
||||
|
||||
This script is invoked by systemd ExecStartPre= so every service start /
|
||||
restart re-syncs the key before uvicorn boots. It is idempotent and
|
||||
fail-open (on any BWS error it leaves the existing file untouched so the
|
||||
service can still start).
|
||||
"""
|
||||
import os, sys, subprocess, hashlib
|
||||
from pathlib import Path
|
||||
|
||||
APP_DIR = Path(os.environ.get("PR_AGENT_APP_DIR", "/var/lib/pr-agent-server"))
|
||||
KEYFILE = APP_DIR / "omniroute_key"
|
||||
# BWS secret that holds the working router key
|
||||
BWS_SECRET_ID = os.environ.get("BWS_ROUTER_KEY_SECRET_ID", "2aef2194-971d-4dae-99dd-b49a0041f97c")
|
||||
PROJECT_ID = "27210268-6134-47b3-9a68-b4980079d1ec"
|
||||
|
||||
|
||||
def sha(s: str) -> str:
|
||||
return hashlib.sha256(s.encode()).hexdigest()
|
||||
|
||||
|
||||
def bws_get_secret_value(secret_id: str) -> str:
|
||||
"""Fetch a BWS secret value. Returns '' on any failure (fail-open)."""
|
||||
token = os.environ.get("BWS_ACCESS_TOKEN", "")
|
||||
if not token and Path("/etc/bws-token").is_file():
|
||||
token = Path("/etc/bws-token").read_text().strip()
|
||||
if not token:
|
||||
print("sync-key: no BWS_ACCESS_TOKEN", file=sys.stderr)
|
||||
return ""
|
||||
env = {**os.environ, "BWS_ACCESS_TOKEN": token}
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["/usr/local/bin/bws", "secret", "get", secret_id, "--output", "env"],
|
||||
capture_output=True, text=True, timeout=30, env=env,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
print(f"sync-key: bws get failed rc={r.returncode}: {r.stderr[:200]}", file=sys.stderr)
|
||||
return ""
|
||||
# Value is shell-quoted KEY="value" — take first line, strip quotes
|
||||
line = r.stdout.split("\n")[0]
|
||||
if "=" not in line:
|
||||
return ""
|
||||
val = line.split("=", 1)[1].strip()
|
||||
# Remove wrapping quotes (shlex could be used; simple strip is fine for keys)
|
||||
if val.startswith('"') and val.endswith('"'):
|
||||
val = val[1:-1]
|
||||
elif val.startswith("'") and val.endswith("'"):
|
||||
val = val[1:-1]
|
||||
return val
|
||||
except Exception as e:
|
||||
print(f"sync-key: bws error {type(e).__name__}: {str(e)[:200]}", file=sys.stderr)
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
new_key = bws_get_secret_value(BWS_SECRET_ID).strip()
|
||||
if not new_key:
|
||||
print("sync-key: no key from BWS, leaving existing file", file=sys.stderr)
|
||||
return 0 # fail-open
|
||||
|
||||
if KEYFILE.exists():
|
||||
old = KEYFILE.read_text().strip()
|
||||
if old == new_key:
|
||||
print("sync-key: key already up to date (no change)")
|
||||
return 0
|
||||
|
||||
# Write key with correct owner/perms (pr-agent user)
|
||||
try:
|
||||
import pwd
|
||||
pw = pwd.getpwnam("pr-agent")
|
||||
KEYFILE.write_text(new_key + "\n")
|
||||
os.chmod(KEYFILE, 0o600)
|
||||
os.chown(KEYFILE, pw.pw_uid, pw.pw_gid)
|
||||
print(f"sync-key: updated {KEYFILE} ({sha(new_key)[:12]}...)")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"sync-key: write failed {type(e).__name__}: {str(e)[:200]}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user