From 25d02aedeb5c4af4195dbfee40db0cb4fc272762 Mon Sep 17 00:00:00 2001 From: CC Worker Date: Sun, 7 Jun 2026 23:49:53 +0000 Subject: [PATCH] fix(reset): default-deny destructive reset against prod target /admin/reset and reset_environment.reset() act on os.environ['SUPABASE_URL']. A platform-admin call on a prod-deployed API would wipe prod data + exam corpus + storage. Refuse when the target matches a known prod marker (.156 / supabase.classroomcopilot) unless RESET_ALLOW_PROD=1 is set. Addresses overwatch review finding #1 on feature/exam-seeding-overhaul. Co-Authored-By: Claude Opus 4.8 --- run/initialization/reset_environment.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/run/initialization/reset_environment.py b/run/initialization/reset_environment.py index 17d2aa9..b06e076 100644 --- a/run/initialization/reset_environment.py +++ b/run/initialization/reset_environment.py @@ -129,6 +129,28 @@ def _sb_headers(): } +# Markers that identify a production Supabase target. Destructive reset against any of these is +# refused by default (project rule: ".94 only; .156 human-gated") — set RESET_ALLOW_PROD=1 to override. +PROD_TARGET_MARKERS = ("192.168.0.156", "supabase.classroomcopilot") + + +def _assert_reset_allowed(url: str, scope: str) -> None: + """Default-deny destructive reset against a production-looking Supabase target. + + The /admin/reset route and this module both act on os.environ['SUPABASE_URL']; without this guard + a platform-admin call on a prod-deployed API would wipe prod data + exam corpus + storage. We refuse + when the target matches a known prod marker unless an explicit RESET_ALLOW_PROD opt-in is set. + """ + target = (url or "").lower() + looks_prod = any(m in target for m in PROD_TARGET_MARKERS) + override = os.environ.get("RESET_ALLOW_PROD", "").strip().lower() in ("1", "true", "yes") + if looks_prod and not override: + raise RuntimeError( + f"refusing destructive reset (scope={scope}) against production-looking target {target!r}; " + f"this is human-gated — set RESET_ALLOW_PROD=1 to override." + ) + + # ─── Neo4j helpers ──────────────────────────────────────────────────────────── def _neo4j_drop_all_non_system() -> Dict[str, List[str]]: @@ -243,6 +265,7 @@ def reset(scope: str = "all") -> Dict[str, Any]: if scope not in ("all", "exam-corpus", "timetable"): raise ValueError(f"invalid scope {scope!r} (want all|exam-corpus|timetable)") url, headers = _sb_headers() + _assert_reset_allowed(url, scope) if scope == "exam-corpus": logger.info("RESET (scope=exam-corpus) — exam tables + cc.examboards storage")