#!/usr/bin/env python3 # retoor import json import re import sys from pathlib import Path CONFIRMATION_TOKEN = "I-HAVE-CONFIRMED-PRODUCTION-DB-ACCESS" PRODUCTION_PATHS = re.compile( r"data/(devplace\.db|devii_tasks\.db|devii_lessons\.db|keys|uploads" r"|attachments|project_files|backups)\b" ) PRODUCTION_DB_FILE = re.compile(r"\bdevplace\.db\b") MANAGEMENT_CLI = re.compile(r"(?:^|[;&|(]\s*|\s)(?:[\w./-]*/)?devplace\s+(?!-)") PYTHON_INVOCATION = re.compile(r"(?:^|[;&|(\s])(?:[\w./-]*/)?python[0-9.]*(?:\s|$)") DATABASE_OVERRIDE = re.compile(r"DEVPLACE_DATABASE_URL\s*=\s*[\"']?(\S+?)[\"']?(?:\s|$)") DATABASE_ASSIGNMENT = re.compile(r"DEVPLACE_DATABASE_URL[\"'\]\s]*[=,]") MODULE_INVOCATION = re.compile(r"-m\s+devplacepy") INLINE_CODE = re.compile(r"-c\s+(?P[\"'])(?P.*?)(?P=quote)", re.DOTALL) SCRIPT_PATH = re.compile(r"(?:^|\s)(?P[\w./~-]+\.py)(?:\s|$)") IMPORT_GATE = re.compile( r"^from devplacepy\.main import app\s*;?\s*(?:print\([^)]*\)\s*;?\s*)?$" ) TEST_RUNNER = re.compile(r"\bpytest\b|\bmake\s+(test|test-[\w-]+)\b") SERVER_TARGET = re.compile(r"\bmake\s+(dev|prod|docker-[\w-]+|ppy)\b") APPLICATION_IMPORT = re.compile( r"(?:^|[\s;])(?:from|import)\s+devplacepy\b" r"|import_module\s*\(\s*[\"']devplacepy", re.MULTILINE, ) def reaches_application(text: str) -> bool: return bool(APPLICATION_IMPORT.search(text)) def overrides_the_database(text: str) -> bool: match = DATABASE_OVERRIDE.search(text) if not match: return False return "data/devplace.db" not in match.group(1) def source_targets_a_scratch_database(source: str) -> bool: if PRODUCTION_DB_FILE.search(source): return False return bool(DATABASE_ASSIGNMENT.search(source)) def script_is_safe(command: str) -> bool | None: match = SCRIPT_PATH.search(command) if not match: return None path = Path(match.group("path")).expanduser() try: body = path.read_text(errors="replace") except OSError: return None if not reaches_application(body): return True return source_targets_a_scratch_database(body) def hazard_in(command: str) -> str: if CONFIRMATION_TOKEN in command: return "" if TEST_RUNNER.search(command) or SERVER_TARGET.search(command): return "" if PRODUCTION_DB_FILE.search(command) or PRODUCTION_PATHS.search(command): return "it names the production database or a production data directory" if MANAGEMENT_CLI.search(command): return "the devplace management CLI operates on the production database" if not PYTHON_INVOCATION.search(command): return "" if overrides_the_database(command): return "" if MODULE_INVOCATION.search(command): return "it runs a devplacepy module with no DEVPLACE_DATABASE_URL override" inline = INLINE_CODE.search(command) if inline: code = inline.group("code").strip() if not reaches_application(code): return "" if IMPORT_GATE.match(code): return "" return "it imports devplacepy inline with no DEVPLACE_DATABASE_URL override" safe = script_is_safe(command) if safe is None: return "" if safe: return "" return "the script imports devplacepy with no DEVPLACE_DATABASE_URL override" def refuse(reason: str) -> dict: return { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": ( f"Blocked: this command reaches the production database because {reason}. " "The production database is never touched without the user's explicit, " "stated confirmation. Stop, tell the user exactly what the command would " "read or write, and ask them to confirm in their own words. Only after " f"they have done so may the command carry the literal token " f"{CONFIRMATION_TOKEN}, which still raises a permission prompt they must " "approve. Never add that token on your own initiative. Alternatives that " "need no confirmation: set DEVPLACE_DATABASE_URL to a scratch database, " "or run the test suite." ), } } def confirm(reason: str) -> dict: return { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": ( "This command carries the production-database confirmation token and " f"reaches the production database because {reason}. Approve only if you " "asked for this." ), } } def main() -> None: try: payload = json.load(sys.stdin) except (json.JSONDecodeError, ValueError): sys.exit(0) command = (payload.get("tool_input") or {}).get("command") or "" if not command: sys.exit(0) if CONFIRMATION_TOKEN in command: stripped = command.replace(CONFIRMATION_TOKEN, "") reason = hazard_in(stripped) if reason: print(json.dumps(confirm(reason))) sys.exit(0) reason = hazard_in(command) if reason: print(json.dumps(refuse(reason))) sys.exit(0) if __name__ == "__main__": main()