"""monitor — static status page builder. Reads the shared database and renders a self-contained HTML file (no external assets, no JS, everything escaped). Contains NO sensitive data: daemon health, proposal queue counts, and recent audit activity (actor/action/result only — no email bodies, no detail payloads, no URLs). """ from __future__ import annotations import html from datetime import UTC, datetime from sqlalchemy import func, select from cayceops.config import Config from cayceops.db import session_scope from cayceops.models import ( AuditEntry, ContentItem, DaemonRun, MailMessage, Proposal, ProposalStatus, ) DAEMONS = ("scanner", "executor", "maild", "publisher", "monitor") _STYLE = """""" def _fmt_ts(dt) -> str: return dt.strftime("%Y-%m-%d %H:%M:%SZ") if dt else "—" def _daemon_rows(sess) -> str: rows = [] for name in DAEMONS: last = ( sess.query(DaemonRun) .filter(DaemonRun.daemon == name) .order_by(DaemonRun.started_at.desc()) .first() ) if last is None: rows.append( "
| time (utc) | actor | action | result |
|---|
generated {generated} · static page, no sensitive data by design
audit entries: {sess.scalar(select(func.count(AuditEntry.id)))} total
""" import os os.makedirs(os.path.dirname(cfg.monitor_output) or ".", exist_ok=True) with open(cfg.monitor_output, "w", encoding="utf-8") as fh: fh.write(page) return cfg.monitor_output def _anchor(cfg: Config, sess) -> int: """Anchor core (injectable session for tests). Returns 0 ok / 0 skip.""" from cayceops.components.publisher import nostr from cayceops.config import ConfigError from cayceops.models import AuditEntry, audit if not cfg.nostr_private_key or not cfg.nostr_relays: raise ConfigError( "anchor requires CAYCE_OPS_NOSTR_PRIVATE_KEY and CAYCE_OPS_NOSTR_RELAYS" ) if not cfg.deployment_id: raise ConfigError("anchor requires CAYCE_OPS_DEPLOYMENT_ID") last = ( sess.query(AuditEntry) .filter(AuditEntry.entry_hash.isnot(None)) .order_by(AuditEntry.id.desc()) .first() ) if last is None: print("nothing to anchor: chain empty") return 0 today = datetime.now(UTC).date() last_anchor = ( sess.query(AuditEntry) .filter(AuditEntry.action == "anchor") .order_by(AuditEntry.id.desc()) .first() ) if last_anchor is not None and last_anchor.ts.date() == today: print("already anchored today") return 0 secret = nostr.normalize_key(cfg.nostr_private_key) event = nostr.build_event( secret, f"cayce-ops audit anchor deployment={cfg.deployment_id} " f"last_hash={last.entry_hash} entries={last.id}", tags=[["t", "cayce-ops"], ["t", "audit-anchor"]], ) if not nostr.verify_event(event): raise RuntimeError("anchor event failed self-verification") results = nostr.publish(cfg.nostr_relays, event) audit( sess, actor="daemon:monitor", action="anchor", detail={ "last_hash": last.entry_hash, "last_entry": last.id, "event_id": event["id"], "relays": results, "deployment": cfg.deployment_id, }, ) print(f"anchored: {event['id']}") return 0 def anchor(cfg: Config | None = None) -> int: """CLI: anchor once per day (external anchoring, audit round 2 point 4).""" from cayceops.config import ConfigError, load from cayceops.db import engine cfg = cfg or load() try: with session_scope() as sess: try: return _anchor(cfg, sess) except ConfigError as exc: print(f"anchor refused: {exc}") return 2 except Exception as exc: # noqa: BLE001 — CLI boundary print(f"anchor failed: {str(exc)[:200]}") return 1 finally: engine().dispose() def build(cfg: Config | None = None) -> int: """CLI entry: render the status page inside a DaemonRun bookkeeping wrap.""" from cayceops.config import load from cayceops.db import engine cfg = cfg or load() try: with session_scope() as sess: run_row = DaemonRun(daemon="monitor") sess.add(run_row) sess.flush() try: out = _build(cfg, sess) except Exception as exc: # noqa: BLE001 — daemon boundary run_row.status = "error" run_row.detail = {"error": str(exc)[:300]} run_row.finished_at = datetime.now(UTC) return 1 run_row.status = "ok" run_row.detail = {"output": out} run_row.finished_at = datetime.now(UTC) print(out) return 0 finally: engine().dispose()