From e632862bc8c36fab08ea04358c19ac4909da6111 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Jun 2026 00:03:24 +0000 Subject: [PATCH] fix: case registry persistence/locking and override proof verification - allocate_case_no: create registry when missing instead of returning a fixed DEL-001; add fcntl exclusive lock and fsync to prevent duplicate Case Nos under concurrent transcript saves; reset counters on year rollover - watchdog: require OverrideProof.verify() for override actions instead of accepting any non-empty proof string or action_type containing 'override' - Add regression tests for missing registry, concurrency, year rollover, invalid override proofs Co-authored-by: Jack J Burleson // LJM --- executive/watchdog.py | 22 +++++-- litigation/run.py | 76 +++++++++++++++++----- tests/test_executive_watchdog.py | 54 ++++++++++++++++ tests/test_litigation_run.py | 104 +++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 19 deletions(-) diff --git a/executive/watchdog.py b/executive/watchdog.py index 7123215..c65bc63 100644 --- a/executive/watchdog.py +++ b/executive/watchdog.py @@ -119,13 +119,27 @@ def run_checks() -> tuple[bool, list[str]]: for a in actions: rid = a.get("ruling_id", "") - if rid and rid not in approved and "override" not in a.get("action_type", "").lower(): - # Action references ruling not in judicial log - proof = a.get("override_proof") + action_type = a.get("action_type", "").lower() + proof = a.get("override_proof") + + if "override" in action_type: if not proof: + alerts.append(f"Override action without proof: ruling_id={rid}") + continue + try: + from executive.proof import OverrideProof + + reason = a.get("description", "") + if OverrideProof.verify(proof, rid, reason) is None: + alerts.append(f"Invalid override proof: ruling_id={rid}") + except FileNotFoundError: alerts.append( - f"Action without judicial approval and no override proof: ruling_id={rid}" + f"Cannot verify override proof (secret missing): ruling_id={rid}" ) + elif rid and rid not in approved: + alerts.append( + f"Action without judicial approval and no override proof: ruling_id={rid}" + ) # (c) Override frequency overrides = [a for a in actions if a.get("override_proof")] diff --git a/litigation/run.py b/litigation/run.py index c86de43..90a0c54 100644 --- a/litigation/run.py +++ b/litigation/run.py @@ -12,6 +12,11 @@ from datetime import datetime from pathlib import Path +try: + import fcntl +except ImportError: # Windows — no cross-process locking + fcntl = None # type: ignore[assignment,misc] + # Add project root for imports REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT)) @@ -62,30 +67,71 @@ def slugify(text: str) -> str: REGISTRY_PATH = REPO_ROOT / "courtroom" / "case-registry.yaml" +def _lock_registry_file(file_obj) -> None: + if fcntl is not None: + fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX) + + +def _unlock_registry_file(file_obj) -> None: + if fcntl is not None: + fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN) + + def allocate_case_no(category: str = "DEL", *, deliberation: int = 1) -> str: """ Allocate the next Case No. from courtroom/case-registry.yaml. Per core/case-format.md: registry holds next available NNN per category. + Uses an exclusive file lock so concurrent saves cannot corrupt the registry + or assign duplicate Case Nos. Creates the registry when missing and resets + per-category counters when the calendar year advances past the registry year. """ import yaml - year = datetime.now().strftime("%Y") + calendar_year = datetime.now().strftime("%Y") + REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) if not REGISTRY_PATH.exists(): - return f"{year}-{category}-001-{deliberation:03d}" - - data = yaml.safe_load(REGISTRY_PATH.read_text(encoding="utf-8")) or {} - year = str(data.get("year", year)) - categories = data.setdefault("categories", {}) - nnn = int(categories.get(category, 1)) - case_no = f"{year}-{category}-{nnn:03d}-{deliberation:03d}" - categories[category] = nnn + 1 - data["categories"] = categories - data["last_updated"] = datetime.now().strftime("%Y-%m-%d") - REGISTRY_PATH.write_text( - yaml.dump(data, default_flow_style=False, sort_keys=False), - encoding="utf-8", - ) + REGISTRY_PATH.write_text( + yaml.dump( + { + "year": int(calendar_year), + "categories": {}, + "last_updated": datetime.now().strftime("%Y-%m-%d"), + }, + default_flow_style=False, + sort_keys=False, + ), + encoding="utf-8", + ) + + with open(REGISTRY_PATH, "r+", encoding="utf-8") as registry_file: + _lock_registry_file(registry_file) + try: + registry_file.seek(0) + raw = registry_file.read() + data = yaml.safe_load(raw) or {} + year = str(data.get("year", calendar_year)) + if year != calendar_year: + data["year"] = int(calendar_year) + data["categories"] = {cat: 1 for cat in (data.get("categories") or {})} + year = calendar_year + categories = data.setdefault("categories", {}) + # Registry may seed unused categories at 0; NNN must be 001-999 per case-format.md + nnn = max(int(categories.get(category, 1)), 1) + case_no = f"{year}-{category}-{nnn:03d}-{deliberation:03d}" + categories[category] = nnn + 1 + data["categories"] = categories + data["year"] = int(data.get("year", calendar_year)) + data["last_updated"] = datetime.now().strftime("%Y-%m-%d") + serialized = yaml.dump(data, default_flow_style=False, sort_keys=False) + registry_file.seek(0) + registry_file.write(serialized) + registry_file.truncate() + registry_file.flush() + os.fsync(registry_file.fileno()) + finally: + _unlock_registry_file(registry_file) + return case_no diff --git a/tests/test_executive_watchdog.py b/tests/test_executive_watchdog.py index 72e66d3..618fdc7 100644 --- a/tests/test_executive_watchdog.py +++ b/tests/test_executive_watchdog.py @@ -87,6 +87,60 @@ def test_run_checks_alerts_on_tampered_judicial_log( assert any("Log integrity" in a for a in alerts) +def test_run_checks_alerts_on_override_without_proof( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + log_path = tmp_path / "judicial_decisions.log" + actions_path = tmp_path / "executive_actions.log" + + JudicialLog(log_path=log_path) # empty approvals + + _write_action( + actions_path, + action_type="override", + ruling_id="2026-UNKNOWN-999", + description="rogue override", + ) + + monkeypatch.setattr(watchdog, "JudicialLog", lambda: JudicialLog(log_path=log_path)) + monkeypatch.setattr(watchdog, "_actions_log_path", lambda: actions_path) + + ok, alerts = watchdog.run_checks() + assert ok is False + assert any("Override action without proof" in a for a in alerts) + + +def test_run_checks_alerts_on_invalid_override_proof( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + log_path = tmp_path / "judicial_decisions.log" + actions_path = tmp_path / "executive_actions.log" + secret_path = tmp_path / ".executive_secret" + secret_path.write_bytes(bytes.fromhex("99" * 32)) + + proof = OverrideProof.generate( + "2026-DEL-001", + "emergency", + timestamp="2026-05-25T12:00:00+00:00", + secret_path=secret_path, + ) + _write_action( + actions_path, + action_type="override", + ruling_id="2026-DEL-002", + description="emergency", + override_proof=proof, + ) + + monkeypatch.setattr(watchdog, "JudicialLog", lambda: JudicialLog(log_path=log_path)) + monkeypatch.setattr(watchdog, "_actions_log_path", lambda: actions_path) + monkeypatch.setenv("EXECUTIVE_PROOF_SECRET", "99" * 64) + + ok, alerts = watchdog.run_checks() + assert ok is False + assert any("Invalid override proof" in a for a in alerts) + + def test_run_checks_override_frequency_threshold( tmp_path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_litigation_run.py b/tests/test_litigation_run.py index 8e46431..d5e4bfa 100644 --- a/tests/test_litigation_run.py +++ b/tests/test_litigation_run.py @@ -2,6 +2,9 @@ from __future__ import annotations +import concurrent.futures +import datetime as dt_module +import re from pathlib import Path import pytest @@ -10,6 +13,79 @@ from litigation import run as litigation_run +def _patch_calendar_year(monkeypatch: pytest.MonkeyPatch, year: int) -> None: + class FixedDatetime: + @staticmethod + def now(tz=None): + return dt_module.datetime(year, 6, 14) + + @staticmethod + def strftime(fmt: str) -> str: + return dt_module.datetime(year, 6, 14).strftime(fmt) + + monkeypatch.setattr(litigation_run, "datetime", FixedDatetime) + + +def _case_no(content: str) -> str: + match = re.search(r"\*\*Case No\.:\*\*\s*(.+)", content) + assert match is not None, content + return match.group(1).strip() + + +def test_allocate_case_no_creates_registry_when_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _patch_calendar_year(monkeypatch, 2026) + registry = tmp_path / "case-registry.yaml" + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + + first = litigation_run.allocate_case_no("DEL") + second = litigation_run.allocate_case_no("DEL") + + assert first == "2026-DEL-001-001" + assert second == "2026-DEL-002-001" + assert registry.exists() + data = yaml.safe_load(registry.read_text(encoding="utf-8")) + assert data["categories"]["DEL"] == 3 + + +def test_allocate_case_no_resets_sequences_on_year_rollover( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _patch_calendar_year(monkeypatch, 2026) + registry = tmp_path / "case-registry.yaml" + registry.write_text( + yaml.dump({"year": 2025, "categories": {"DEL": 9, "ARCH": 4}}), + encoding="utf-8", + ) + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + + case_no = litigation_run.allocate_case_no("DEL") + + assert case_no == "2026-DEL-001-001" + data = yaml.safe_load(registry.read_text(encoding="utf-8")) + assert data["year"] == 2026 + assert data["categories"]["DEL"] == 2 + assert data["categories"]["ARCH"] == 1 + + +def test_allocate_case_no_treats_zero_category_as_first_case( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + registry = tmp_path / "case-registry.yaml" + registry.write_text( + yaml.dump({"year": 2026, "categories": {"FEAT": 0}}), + encoding="utf-8", + ) + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + + case_no = litigation_run.allocate_case_no("FEAT") + + assert case_no == "2026-FEAT-001-001" + data = yaml.safe_load(registry.read_text(encoding="utf-8")) + assert data["categories"]["FEAT"] == 2 + + def test_allocate_case_no_increments_registry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: registry = tmp_path / "case-registry.yaml" registry.write_text( @@ -76,3 +152,31 @@ def test_save_transcript_filename_suffix_independent_of_case_no( assert path.name.endswith("-1.md") assert "**Case No.:** 2026-DEL-020-001" in content + + +def test_allocate_case_no_concurrent_assignments_are_unique( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + registry = tmp_path / "case-registry.yaml" + registry.write_text( + yaml.dump({"year": 2026, "categories": {"DEL": 40}}), + encoding="utf-8", + ) + monkeypatch.setattr(litigation_run, "REGISTRY_PATH", registry) + + fake_module = tmp_path / "litigation_pkg" + fake_module.mkdir() + (fake_module / "run.py").write_text("", encoding="utf-8") + transcripts_dir = fake_module / "transcripts" + transcripts_dir.mkdir() + monkeypatch.setattr(litigation_run, "__file__", str(fake_module / "run.py")) + + def save_one(i: int) -> str: + path = litigation_run.save_transcript(f"matter {i}", f"body {i}") + return _case_no(path.read_text(encoding="utf-8")) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + case_numbers = list(executor.map(save_one, range(8))) + + assert len(set(case_numbers)) == len(case_numbers) + assert yaml.safe_load(registry.read_text(encoding="utf-8"))["categories"]["DEL"] == 48