308 lines
17 KiB
Python
308 lines
17 KiB
Python
"""Durable, account-bound human release gate inbox."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sqlite3
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from src.private_state import connect_private_sqlite
|
|
|
|
|
|
_HASH = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
|
_PROJECT = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
_STATES = {"pending", "released", "held", "superseded"}
|
|
_CHECKLIST = {"exact_hash", "artifacts_reviewed", "provenance_reviewed"}
|
|
|
|
|
|
class GateConflict(RuntimeError):
|
|
"""The gate or idempotency revision no longer matches."""
|
|
|
|
|
|
class GateValidationError(ValueError):
|
|
"""The producer or reviewer payload is not safe to persist."""
|
|
|
|
|
|
class GateNotFound(LookupError):
|
|
"""No gate exists for this account."""
|
|
|
|
|
|
def _canonical(value: object) -> str:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
|
|
|
|
def _fingerprint(value: object) -> str:
|
|
return hashlib.sha256(_canonical(value).encode()).hexdigest()
|
|
|
|
|
|
class HumanGateStore:
|
|
def __init__(self, path: str | Path, *, clock: Callable[[], float]):
|
|
self.path = Path(path)
|
|
self.clock = clock
|
|
self._initialize()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
connection = connect_private_sqlite(self.path, timeout=2.0)
|
|
connection.row_factory = sqlite3.Row
|
|
return connection
|
|
|
|
def _initialize(self) -> None:
|
|
with self._connect() as connection:
|
|
connection.execute("PRAGMA journal_mode=WAL")
|
|
connection.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS human_gates (
|
|
id TEXT PRIMARY KEY, login TEXT NOT NULL, source TEXT NOT NULL,
|
|
project TEXT NOT NULL, candidate_hash TEXT NOT NULL,
|
|
title TEXT NOT NULL, priority INTEGER NOT NULL,
|
|
payload_json TEXT NOT NULL, state TEXT NOT NULL,
|
|
revision INTEGER NOT NULL, created_at REAL NOT NULL,
|
|
updated_at REAL NOT NULL, superseded_by TEXT,
|
|
decision_reason TEXT NOT NULL DEFAULT '',
|
|
override_reason TEXT NOT NULL DEFAULT '',
|
|
checklist_json TEXT NOT NULL DEFAULT '{}',
|
|
UNIQUE(login, source, project, candidate_hash)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS human_gates_queue
|
|
ON human_gates(login, state, priority DESC, created_at, id);
|
|
CREATE TABLE IF NOT EXISTS human_gate_intake_keys (
|
|
login TEXT NOT NULL, idempotency_key TEXT NOT NULL,
|
|
fingerprint TEXT NOT NULL, gate_id TEXT NOT NULL,
|
|
PRIMARY KEY(login, idempotency_key)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS human_gate_history (
|
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT, gate_id TEXT NOT NULL,
|
|
action TEXT NOT NULL, at REAL NOT NULL, details_json TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS human_gate_receipts (
|
|
receipt_id TEXT PRIMARY KEY, login TEXT NOT NULL,
|
|
idempotency_key TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
gate_id TEXT NOT NULL, receipt_json TEXT NOT NULL,
|
|
UNIQUE(login, idempotency_key)
|
|
);
|
|
"""
|
|
)
|
|
|
|
@staticmethod
|
|
def _login(value: str) -> str:
|
|
value = str(value).strip().lower()
|
|
if not value or len(value) > 255:
|
|
raise GateValidationError("login is required")
|
|
return value
|
|
|
|
@staticmethod
|
|
def _key(value: str) -> str:
|
|
value = str(value).strip()
|
|
if not value or len(value) > 128:
|
|
raise GateValidationError("Idempotency key is required")
|
|
return value
|
|
|
|
@staticmethod
|
|
def _candidate(raw: dict) -> dict:
|
|
if not isinstance(raw, dict):
|
|
raise GateValidationError("Candidate must be an object")
|
|
source = str(raw.get("source", "")).strip()
|
|
project = str(raw.get("project", "")).strip()
|
|
candidate_hash = str(raw.get("candidate_hash", "")).strip()
|
|
title = " ".join(str(raw.get("title", "")).split())
|
|
priority = raw.get("priority", 0)
|
|
if not source or len(source) > 128:
|
|
raise GateValidationError("source is invalid")
|
|
if not _PROJECT.fullmatch(project):
|
|
raise GateValidationError("project is invalid")
|
|
if not _HASH.fullmatch(candidate_hash):
|
|
raise GateValidationError("candidate hash is invalid")
|
|
if not title or len(title) > 300:
|
|
raise GateValidationError("title is invalid")
|
|
if isinstance(priority, bool) or not isinstance(priority, int) or not 0 <= priority <= 100:
|
|
raise GateValidationError("priority is invalid")
|
|
normalized = {
|
|
"source": source, "project": project, "candidate_hash": candidate_hash,
|
|
"title": title, "priority": priority,
|
|
"artifacts": HumanGateStore._references(raw.get("artifacts", []), "name"),
|
|
"links": HumanGateStore._references(raw.get("links", []), "label"),
|
|
"checks": HumanGateStore._checks(raw.get("checks", [])),
|
|
"score": raw.get("score") if isinstance(raw.get("score"), dict) else {},
|
|
"provenance": raw.get("provenance") if isinstance(raw.get("provenance"), dict) else {},
|
|
}
|
|
if len(_canonical(normalized)) > 100_000:
|
|
raise GateValidationError("Candidate payload is too large")
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _references(raw: object, label: str) -> list[dict]:
|
|
if not isinstance(raw, list) or len(raw) > 50:
|
|
raise GateValidationError("references are invalid")
|
|
result = []
|
|
for item in raw:
|
|
if not isinstance(item, dict):
|
|
raise GateValidationError("reference is invalid")
|
|
name, url = str(item.get(label, "")).strip(), str(item.get("url", "")).strip()
|
|
if not name or len(name) > 200 or not url.startswith("https://") or len(url) > 2048:
|
|
raise GateValidationError("reference is invalid")
|
|
result.append({label: name, "url": url})
|
|
return result
|
|
|
|
@staticmethod
|
|
def _checks(raw: object) -> list[dict]:
|
|
if not isinstance(raw, list) or len(raw) > 100:
|
|
raise GateValidationError("checks are invalid")
|
|
result = []
|
|
for item in raw:
|
|
if not isinstance(item, dict):
|
|
raise GateValidationError("check is invalid")
|
|
name, state = str(item.get("name", "")).strip(), item.get("state")
|
|
if not name or len(name) > 200 or state not in {"success", "failure", "pending", "skipped"}:
|
|
raise GateValidationError("check is invalid")
|
|
result.append({"name": name, "state": state, "required": bool(item.get("required", True))})
|
|
return result
|
|
|
|
def _history(self, connection: sqlite3.Connection, gate_id: str) -> list[dict]:
|
|
return [
|
|
{"sequence": row[0], "action": row[1], "at": row[2], **json.loads(row[3])}
|
|
for row in connection.execute(
|
|
"SELECT sequence, action, at, details_json FROM human_gate_history WHERE gate_id=? ORDER BY sequence",
|
|
(gate_id,),
|
|
)
|
|
]
|
|
|
|
def _present(self, connection: sqlite3.Connection, row: sqlite3.Row, *, history: bool = False) -> dict:
|
|
payload = json.loads(row["payload_json"])
|
|
item = {
|
|
"id": row["id"], **payload, "state": row["state"], "revision": row["revision"],
|
|
"created_at": row["created_at"], "updated_at": row["updated_at"],
|
|
"superseded_by": row["superseded_by"],
|
|
}
|
|
if row["state"] in {"released", "held"}:
|
|
item.update({
|
|
"reason": row["decision_reason"], "override_reason": row["override_reason"],
|
|
"checklist": json.loads(row["checklist_json"]),
|
|
})
|
|
if history:
|
|
item["history"] = self._history(connection, row["id"])
|
|
return item
|
|
|
|
def has_intake_key(self, login: str, idempotency_key: str) -> bool:
|
|
with self._connect() as connection:
|
|
return connection.execute(
|
|
"SELECT 1 FROM human_gate_intake_keys WHERE login=? AND idempotency_key=?",
|
|
(self._login(login), self._key(idempotency_key)),
|
|
).fetchone() is not None
|
|
|
|
def intake(self, login: str, raw: dict, *, idempotency_key: str) -> dict:
|
|
login, key, payload = self._login(login), self._key(idempotency_key), self._candidate(raw)
|
|
fingerprint = _fingerprint(payload)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
prior = connection.execute(
|
|
"SELECT fingerprint, gate_id FROM human_gate_intake_keys WHERE login=? AND idempotency_key=?",
|
|
(login, key),
|
|
).fetchone()
|
|
if prior:
|
|
if prior["fingerprint"] != fingerprint:
|
|
raise GateConflict("Idempotency key was already used for another candidate")
|
|
row = connection.execute("SELECT * FROM human_gates WHERE id=? AND login=?", (prior["gate_id"], login)).fetchone()
|
|
return self._present(connection, row, history=True)
|
|
existing = connection.execute(
|
|
"SELECT * FROM human_gates WHERE login=? AND source=? AND project=? AND candidate_hash=?",
|
|
(login, payload["source"], payload["project"], payload["candidate_hash"]),
|
|
).fetchone()
|
|
if existing:
|
|
if _fingerprint(json.loads(existing["payload_json"])) != fingerprint:
|
|
raise GateConflict("Candidate hash is already bound to different facts")
|
|
connection.execute("INSERT INTO human_gate_intake_keys VALUES (?,?,?,?)", (login, key, fingerprint, existing["id"]))
|
|
return self._present(connection, existing, history=True)
|
|
now, gate_id = float(self.clock()), str(uuid.uuid4())
|
|
old_rows = connection.execute(
|
|
"SELECT id, revision FROM human_gates WHERE login=? AND source=? AND project=? AND state='pending'",
|
|
(login, payload["source"], payload["project"]),
|
|
).fetchall()
|
|
connection.execute(
|
|
"INSERT INTO human_gates(id,login,source,project,candidate_hash,title,priority,payload_json,state,revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?, 'pending',1,?,?)",
|
|
(gate_id, login, payload["source"], payload["project"], payload["candidate_hash"], payload["title"], payload["priority"], _canonical(payload), now, now),
|
|
)
|
|
connection.execute("INSERT INTO human_gate_history(gate_id,action,at,details_json) VALUES (?,?,?,?)", (gate_id, "intake", now, _canonical({"candidate_hash": payload["candidate_hash"]})))
|
|
for old in old_rows:
|
|
connection.execute("UPDATE human_gates SET state='superseded', revision=?, updated_at=?, superseded_by=? WHERE id=? AND state='pending'", (old["revision"] + 1, now, gate_id, old["id"]))
|
|
connection.execute("INSERT INTO human_gate_history(gate_id,action,at,details_json) VALUES (?,?,?,?)", (old["id"], "superseded", now, _canonical({"superseded_by": gate_id, "candidate_hash": payload["candidate_hash"]})))
|
|
connection.execute("INSERT INTO human_gate_intake_keys VALUES (?,?,?,?)", (login, key, fingerprint, gate_id))
|
|
row = connection.execute("SELECT * FROM human_gates WHERE id=?", (gate_id,)).fetchone()
|
|
return self._present(connection, row, history=True)
|
|
|
|
def list(self, login: str, *, state: str = "pending", limit: int = 100) -> dict:
|
|
login = self._login(login)
|
|
if state not in _STATES and state != "all":
|
|
raise GateValidationError("state is invalid")
|
|
limit = min(max(int(limit), 1), 100)
|
|
with self._connect() as connection:
|
|
pending_count = connection.execute("SELECT COUNT(*) FROM human_gates WHERE login=? AND state='pending'", (login,)).fetchone()[0]
|
|
where, args = ("login=?", [login]) if state == "all" else ("login=? AND state=?", [login, state])
|
|
rows = connection.execute(f"SELECT * FROM human_gates WHERE {where} ORDER BY CASE WHEN state='pending' THEN 0 ELSE 1 END, priority DESC, created_at, id LIMIT ?", (*args, limit)).fetchall()
|
|
return {"pending_count": pending_count, "items": [self._present(connection, row) for row in rows]}
|
|
|
|
def detail(self, login: str, gate_id: str) -> dict:
|
|
with self._connect() as connection:
|
|
row = connection.execute("SELECT * FROM human_gates WHERE login=? AND id=?", (self._login(login), gate_id)).fetchone()
|
|
if row is None:
|
|
raise GateNotFound("Gate not found")
|
|
return self._present(connection, row, history=True)
|
|
|
|
def decide(self, login: str, gate_id: str, *, expected_revision: int, decision: str, reason: str, override_reason: str, checklist: dict, idempotency_key: str) -> dict:
|
|
login, key = self._login(login), self._key(idempotency_key)
|
|
reason, override_reason = str(reason).strip(), str(override_reason).strip()
|
|
if decision not in {"release", "hold"}:
|
|
raise GateValidationError("decision is invalid")
|
|
if decision == "hold" and not reason:
|
|
raise GateValidationError("Hold reason is required")
|
|
if decision == "release" and (
|
|
not isinstance(checklist, dict)
|
|
or set(checklist) != _CHECKLIST
|
|
or not all(value is True for value in checklist.values())
|
|
):
|
|
raise GateValidationError("A complete release checklist is required")
|
|
if decision == "hold":
|
|
checklist = {}
|
|
request = {"gate_id": gate_id, "expected_revision": expected_revision, "decision": decision, "reason": reason, "override_reason": override_reason, "checklist": checklist}
|
|
fingerprint = _fingerprint(request)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
prior = connection.execute("SELECT fingerprint, receipt_json FROM human_gate_receipts WHERE login=? AND idempotency_key=?", (login, key)).fetchone()
|
|
if prior:
|
|
if prior["fingerprint"] != fingerprint:
|
|
raise GateConflict("Idempotency key was already used for another decision")
|
|
return json.loads(prior["receipt_json"])
|
|
row = connection.execute("SELECT * FROM human_gates WHERE login=? AND id=?", (login, gate_id)).fetchone()
|
|
if row is None:
|
|
raise GateNotFound("Gate not found")
|
|
if row["state"] != "pending" or row["revision"] != expected_revision:
|
|
raise GateConflict("Gate revision is stale")
|
|
payload = json.loads(row["payload_json"])
|
|
unmet = [check["name"] for check in payload["checks"] if check["required"] and check["state"] != "success"]
|
|
if decision == "release" and unmet and not override_reason:
|
|
raise GateValidationError("An explicit override reason is required for unmet checks")
|
|
now, receipt_id, revision = float(self.clock()), str(uuid.uuid4()), row["revision"] + 1
|
|
state = "released" if decision == "release" else "held"
|
|
connection.execute("UPDATE human_gates SET state=?,revision=?,updated_at=?,decision_reason=?,override_reason=?,checklist_json=? WHERE id=?", (state, revision, now, reason, override_reason, _canonical(checklist), gate_id))
|
|
receipt = {"receipt_id": receipt_id, "gate_id": gate_id, "candidate_hash": row["candidate_hash"], "state": state, "revision": revision, "decided_at": now, "reason": reason, "override_reason": override_reason, "checklist": checklist, "unmet_required_checks": unmet}
|
|
connection.execute("INSERT INTO human_gate_history(gate_id,action,at,details_json) VALUES (?,?,?,?)", (gate_id, state, now, _canonical({"receipt_id": receipt_id, "reason": reason, "override_reason": override_reason, "unmet_required_checks": unmet})))
|
|
connection.execute("INSERT INTO human_gate_receipts VALUES (?,?,?,?,?,?)", (receipt_id, login, key, fingerprint, gate_id, _canonical(receipt)))
|
|
return receipt
|
|
|
|
def has_receipt_key(self, login: str, idempotency_key: str) -> bool:
|
|
with self._connect() as connection:
|
|
return connection.execute(
|
|
"SELECT 1 FROM human_gate_receipts WHERE login=? AND idempotency_key=?",
|
|
(self._login(login), self._key(idempotency_key)),
|
|
).fetchone() is not None
|
|
|
|
def receipt(self, login: str, receipt_id: str) -> dict:
|
|
with self._connect() as connection:
|
|
row = connection.execute("SELECT receipt_json FROM human_gate_receipts WHERE login=? AND receipt_id=?", (self._login(login), receipt_id)).fetchone()
|
|
if row is None:
|
|
raise GateNotFound("Receipt not found")
|
|
return json.loads(row[0])
|