#!/usr/bin/env python3 """Board-local integrity records for Hermes PR continuation cards.""" from __future__ import annotations import hashlib import fcntl import os import sqlite3 import tempfile from contextlib import contextmanager from pathlib import Path from typing import Any, Iterator from supervisor_lineage import Lineage KANBAN_ROOT = Path( os.environ.get("HERMES_KANBAN_HOME", os.environ.get("HERMES_HOME", "/opt/data")) ) / "kanban/boards" STATE_FILE = "supervisor-state.db" class SupervisorStateError(ValueError): """A present integrity record cannot be decoded or read safely.""" def _path(board: str, path: Path | None) -> Path: """Keep integrity rows board-local without sharing Kanban's SQLite file.""" if path is not None: return path if not board or "/" in board or "\\" in board: raise ValueError("board name is invalid") return KANBAN_ROOT / board / STATE_FILE def _schema(connection: sqlite3.Connection) -> None: """Install the small coordinator schema in its isolated state database.""" connection.executescript( """ CREATE TABLE IF NOT EXISTS supervisor_roots ( board TEXT NOT NULL, root_task_id TEXT NOT NULL, project TEXT NOT NULL, branch TEXT NOT NULL, pull_request TEXT NOT NULL, base_branch TEXT NOT NULL, live_pr_head TEXT NOT NULL, ready_for_human_merge INTEGER NOT NULL DEFAULT 0, ready_commit TEXT NOT NULL DEFAULT '', PRIMARY KEY (board, root_task_id) ); CREATE TABLE IF NOT EXISTS supervisor_children ( board TEXT NOT NULL, child_task_id TEXT NOT NULL, root_task_id TEXT NOT NULL, parent_task_id TEXT NOT NULL, kind TEXT NOT NULL, head_commit TEXT NOT NULL, objective_digest TEXT NOT NULL, cycle INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (board, child_task_id), UNIQUE (board, root_task_id, head_commit, objective_digest) ); """ ) def _legacy_rows(board: str, database: Path) -> tuple[list[tuple[Any, ...]], list[tuple[Any, ...]]]: """Read a complete, healthy legacy state table set or refuse migration.""" if not database.exists(): return [], [] try: source = sqlite3.connect(f"file:{database}?mode=ro", uri=True) try: checked = [str(row[0]) for row in source.execute("PRAGMA integrity_check")] if checked != ["ok"]: raise SupervisorStateError("legacy supervisor state is unreadable") names = {str(row[0]) for row in source.execute( "SELECT name FROM sqlite_master WHERE type='table'" )} required = {"supervisor_roots", "supervisor_children"} if not names & required: return [], [] if not required <= names: raise SupervisorStateError("legacy supervisor state is malformed") root_columns = {str(row[1]) for row in source.execute("PRAGMA table_info(supervisor_roots)")} child_columns = {str(row[1]) for row in source.execute("PRAGMA table_info(supervisor_children)")} root_base = {"board", "root_task_id", "project", "branch", "pull_request", "base_branch", "live_pr_head"} child_base = {"board", "child_task_id", "root_task_id", "parent_task_id", "kind", "head_commit", "objective_digest"} if not root_base <= root_columns or not child_base <= child_columns: raise SupervisorStateError("legacy supervisor state schema is malformed") ready = "ready_for_human_merge" if "ready_for_human_merge" in root_columns else "0" commit = "ready_commit" if "ready_commit" in root_columns else "''" cycle = "cycle" if "cycle" in child_columns else "1" roots = source.execute( "SELECT board,root_task_id,project,branch,pull_request,base_branch,live_pr_head," f"{ready},{commit} FROM supervisor_roots" ).fetchall() children = source.execute( "SELECT board,child_task_id,root_task_id,parent_task_id,kind,head_commit,objective_digest," f"{cycle} FROM supervisor_children" ).fetchall() finally: source.close() except sqlite3.Error as error: raise SupervisorStateError("legacy supervisor state is unreadable") from error root_ids = {str(row[1]) for row in roots} if (any(not row or row[0] != board for row in [*roots, *children]) or any(str(row[2]) not in root_ids for row in children)): raise SupervisorStateError("legacy supervisor state is malformed") return roots, children def _bootstrap_sidecar(board: str, state: Path) -> None: """Atomically import valid legacy rows once, never creating an empty fallback.""" lock = state.with_name(f".{STATE_FILE}.lock") state.parent.mkdir(mode=0o700, parents=True, exist_ok=True) with lock.open("a+") as handle: fcntl.flock(handle.fileno(), fcntl.LOCK_EX) if state.exists(): return roots, children = _legacy_rows(board, state.with_name("kanban.db")) descriptor, temporary = tempfile.mkstemp(prefix=f".{STATE_FILE}.", dir=state.parent) os.close(descriptor) replacement = Path(temporary) try: connection = sqlite3.connect(replacement) try: _schema(connection) if roots: connection.executemany("INSERT INTO supervisor_roots VALUES(?,?,?,?,?,?,?,?,?)", roots) connection.executemany("INSERT INTO supervisor_children VALUES(?,?,?,?,?,?,?,?)", children) connection.commit() finally: connection.close() descriptor = os.open(replacement, os.O_RDONLY) try: os.fsync(descriptor) finally: os.close(descriptor) os.chmod(replacement, 0o600) os.replace(replacement, state) directory = os.open(state.parent, os.O_RDONLY) try: os.fsync(directory) finally: os.close(directory) except Exception: replacement.unlink(missing_ok=True) raise @contextmanager def _connect(board: str, path: Path | None = None) -> Iterator[sqlite3.Connection]: """Open one sidecar transaction and always close its SQLite handle.""" default_path = path is None path = _path(board, path) if default_path and not path.exists(): _bootstrap_sidecar(board, path) path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) connection = sqlite3.connect(path, timeout=5) try: connection.execute("PRAGMA busy_timeout=5000") connection.execute("PRAGMA foreign_keys=ON") _schema(connection) columns = {row[1] for row in connection.execute("PRAGMA table_info(supervisor_roots)")} if "ready_for_human_merge" not in columns: connection.execute( "ALTER TABLE supervisor_roots ADD COLUMN ready_for_human_merge INTEGER NOT NULL DEFAULT 0" ) if "ready_commit" not in columns: connection.execute("ALTER TABLE supervisor_roots ADD COLUMN ready_commit TEXT NOT NULL DEFAULT ''") child_columns = {row[1] for row in connection.execute("PRAGMA table_info(supervisor_children)")} if "cycle" not in child_columns: connection.execute("ALTER TABLE supervisor_children ADD COLUMN cycle INTEGER NOT NULL DEFAULT 1") if path.name == STATE_FILE: os.chmod(path, 0o600) yield connection connection.commit() except Exception: connection.rollback() raise finally: connection.close() def _lineage(row: tuple[Any, ...] | None) -> Lineage | None: if row is None: return None try: values = tuple(str(value) for value in row[:5]) except (TypeError, ValueError): return None if not all(values): return None return Lineage(*values) def get_root(board: str, root_task_id: str, *, path: Path | None = None) -> Lineage | None: """Return one coordinator-issued root lineage, never task-body claims.""" try: with _connect(board, path) as connection: row = connection.execute( "SELECT root_task_id, branch, pull_request, project, base_branch " "FROM supervisor_roots WHERE board=? AND root_task_id=?", (board, root_task_id) ).fetchone() except (OSError, sqlite3.Error) as error: raise SupervisorStateError("supervisor root state is unreadable") from error if row is None: return None lineage = _lineage(row) if lineage is None: raise SupervisorStateError("supervisor root state is malformed") return lineage def get_live_head(board: str, root_task_id: str, *, path: Path | None = None) -> str: """Return the broker-confirmed head associated with a trusted root.""" try: with _connect(board, path) as connection: row = connection.execute( "SELECT live_pr_head FROM supervisor_roots WHERE board=? AND root_task_id=?", (board, root_task_id), ).fetchone() except (OSError, sqlite3.Error) as error: raise SupervisorStateError("supervisor root state is unreadable") from error if row is None: return "" if not isinstance(row[0], str) or not row[0]: raise SupervisorStateError("supervisor root state is malformed") return row[0] def get_child(board: str, child_task_id: str, *, path: Path | None = None) -> dict[str, Any] | None: """Return the only durable authority record for a continuation child.""" try: with _connect(board, path) as connection: row = connection.execute( "SELECT root_task_id, parent_task_id, kind, head_commit, objective_digest, cycle " "FROM supervisor_children WHERE board=? AND child_task_id=?", (board, child_task_id) ).fetchone() except (OSError, sqlite3.Error) as error: raise SupervisorStateError("supervisor child state is unreadable") from error if row is None: return None root, parent, kind, head, digest, cycle = (str(value) for value in row) lineage = get_root(board, root, path=path) if lineage is None or kind not in {"repair", "review"} or not all((parent, head, digest)): raise SupervisorStateError("supervisor child state is malformed") try: parsed_cycle = int(cycle) except (TypeError, ValueError) as error: raise SupervisorStateError("supervisor child state is malformed") from error if parsed_cycle < 1: raise SupervisorStateError("supervisor child state is malformed") return {"lineage": lineage, "root_task_id": root, "parent_task_id": parent, "kind": kind, "head_commit": head, "objective_digest": digest, "cycle": parsed_cycle} def record_submission( board: str, task_id: str, lineage: Lineage, live_pr_head: str, *, path: Path | None = None ) -> None: """Record a coordinator-verified submission against its immutable root. ``task_id`` identifies the completing root or child. The coordinator has already authenticated that task's signed assignment before calling this function; the record deliberately remains keyed by ``root_task_id``. """ if not board or not task_id or not live_pr_head: raise ValueError("submission does not name a task and live head") with _connect(board, path) as connection: existing = connection.execute( "SELECT project,branch,pull_request,base_branch,live_pr_head FROM supervisor_roots " "WHERE board=? AND root_task_id=?", (board, lineage.root_task_id) ).fetchone() identity = (lineage.project, lineage.branch, lineage.pull_request, lineage.base_branch) if existing is None: connection.execute( "INSERT INTO supervisor_roots(board,root_task_id,project,branch,pull_request,base_branch,live_pr_head) " "VALUES(?,?,?,?,?,?,?)", (board, lineage.root_task_id, *identity, live_pr_head), ) elif tuple(existing[:4]) != identity: raise ValueError("submission attempts to rewrite immutable root lineage") elif existing[4] != live_pr_head: connection.execute( "UPDATE supervisor_roots SET live_pr_head=?, ready_for_human_merge=0, ready_commit='' " "WHERE board=? AND root_task_id=?", (live_pr_head, board, lineage.root_task_id), ) def record_live_head( board: str, root_task_id: str, live_pr_head: str, *, path: Path | None = None ) -> None: """Refresh a root's head only after the continuation CLI verified its PR.""" if not board or not root_task_id or not live_pr_head: raise ValueError("root and live head are required") with _connect(board, path) as connection: changed = connection.execute( "UPDATE supervisor_roots SET live_pr_head=?, ready_for_human_merge=0, ready_commit='' " "WHERE board=? AND root_task_id=?", (live_pr_head, board, root_task_id) ).rowcount if changed != 1: raise ValueError("unknown continuation root") def set_ready( board: str, root_task_id: str, head_commit: str, *, path: Path | None = None ) -> None: """Publish human-ready state only for the verified current root head.""" with _connect(board, path) as connection: changed = connection.execute( "UPDATE supervisor_roots SET ready_for_human_merge=1, ready_commit=? " "WHERE board=? AND root_task_id=? AND live_pr_head=?", (head_commit, board, root_task_id, head_commit), ).rowcount if changed != 1: raise ValueError("ready head is not the trusted live root head") def clear_ready(board: str, root_task_id: str, *, path: Path | None = None) -> None: """Invalidate approval whenever a continuation is queued or head changes.""" with _connect(board, path) as connection: connection.execute( "UPDATE supervisor_roots SET ready_for_human_merge=0, ready_commit='' " "WHERE board=? AND root_task_id=?", (board, root_task_id) ) def record_child( board: str, child_task_id: str, root_task_id: str, parent_task_id: str, kind: str, head_commit: str, objective: str, cycle: int = 1, *, path: Path | None = None ) -> None: """Bind a created child to an existing trusted root and exact continuation.""" if not get_root(board, root_task_id, path=path) or kind not in {"repair", "review"}: raise ValueError("continuation root or kind is invalid") digest = objective_digest(objective) if cycle < 1: raise ValueError("continuation cycle is invalid") values = (root_task_id, parent_task_id, kind, head_commit, digest, cycle) with _connect(board, path) as connection: existing = connection.execute( "SELECT root_task_id,parent_task_id,kind,head_commit,objective_digest,cycle " "FROM supervisor_children WHERE board=? AND child_task_id=?", (board, child_task_id) ).fetchone() if existing is None: collision = connection.execute( "SELECT child_task_id FROM supervisor_children WHERE board=? AND root_task_id=? " "AND head_commit=? AND objective_digest=?", (board, root_task_id, head_commit, digest), ).fetchone() if collision is not None: raise ValueError("continuation already maps this root/head/objective to another task") connection.execute( "INSERT INTO supervisor_children VALUES(?,?,?,?,?,?,?,?)", (board, child_task_id, *values) ) elif tuple(existing) != values: raise ValueError("continuation child lineage conflicts with its existing record") def objective_digest(objective: str) -> str: """Make duplicate user follow-ups idempotent without retaining extra prose.""" return hashlib.sha256(objective.strip().encode()).hexdigest() def existing_child( board: str, root_task_id: str, head_commit: str, objective: str, *, path: Path | None = None ) -> str: """Return an existing same-objective continuation, if one was created.""" with _connect(board, path) as connection: row = connection.execute( "SELECT child_task_id FROM supervisor_children WHERE board=? AND root_task_id=? " "AND head_commit=? AND objective_digest=?", (board, root_task_id, head_commit, objective_digest(objective)), ).fetchone() return str(row[0]) if row else ""