Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

285 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Pre-commit hook to validate hardcoded settings key strings against the
allow/block lists defined in settings_routes.py.
Catches new settings keys that would be rejected by the runtime namespace
gate before they reach the release server.
"""
import ast
import re
import sys
from pathlib import Path
SETTINGS_ROUTES = (
Path(__file__).resolve().parent.parent
/ "src"
/ "local_deep_research"
/ "web"
/ "routes"
/ "settings_routes.py"
)
WRITE_FUNC_NAMES = {"set_setting"}
# Regex patterns for JS files
JS_SAVE_SETTING_RE = re.compile(r"saveSetting\s*\(\s*['\"]([^'\"]+)['\"]")
JS_SETTINGS_API_URL_RE = re.compile(
r"['\"]\/settings\/api\/([a-z][a-z0-9_]*\.[a-z0-9_.]+)['\"]"
)
JS_INLINE_COMMENT_RE = re.compile(r"/\*.*?\*/")
# Excluded paths/directories
SKIP_PATH_SEGMENTS = {
"tests",
"test",
".pre-commit-hooks",
"migrations",
"node_modules",
"dist",
"build",
"vendor",
"defaults", # JSON schema files define keys — not write call sites
}
SKIP_NAME_PREFIXES = ("test_", "conftest")
SKIP_NAME_SUFFIXES = ("_test.py", "_test.js", ".min.js")
SKIP_EXACT_NAMES = {"settings_routes.py"}
def load_prefixes():
"""Parse ALLOWED_SETTING_PREFIXES and BLOCKED_SETTING_PREFIXES
from settings_routes.py using AST."""
try:
source = SETTINGS_ROUTES.read_text(encoding="utf-8")
except FileNotFoundError:
print(
f"FATAL: Cannot find settings_routes.py at {SETTINGS_ROUTES}\n"
"The hook cannot validate settings keys without it."
)
sys.exit(1)
tree = ast.parse(source, filename=str(SETTINGS_ROUTES))
allowed = None
blocked = None
for node in ast.walk(tree):
if not isinstance(node, ast.Assign):
continue
for target in node.targets:
if not isinstance(target, ast.Name):
continue
if target.id == "ALLOWED_SETTING_PREFIXES":
allowed = _extract_frozenset_strings(node.value)
elif target.id == "BLOCKED_SETTING_PREFIXES":
blocked = _extract_frozenset_strings(node.value)
if allowed is None or blocked is None:
print(
"FATAL: Could not parse ALLOWED/BLOCKED_SETTING_PREFIXES "
f"from {SETTINGS_ROUTES}"
)
sys.exit(1)
return allowed, blocked
def _extract_frozenset_strings(node):
"""Extract string values from frozenset({...}) or frozenset((...,))."""
if not isinstance(node, ast.Call):
return None
if not (isinstance(node.func, ast.Name) and node.func.id == "frozenset"):
return None
if not node.args:
return None
collection = node.args[0]
strings = set()
if isinstance(collection, (ast.Set, ast.Tuple, ast.List)):
for elt in collection.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
strings.add(elt.value)
return strings if strings else None
def is_key_allowed(key, allowed, blocked):
"""Mirrors _is_allowed_new_setting_key from settings_routes.py."""
if not isinstance(key, str) or not key or ".." in key:
return False
key_lower = key.lower()
for prefix in blocked:
if key_lower.startswith(prefix):
return False
for prefix in allowed:
if key_lower.startswith(prefix):
return True
return False
def should_skip(filepath):
"""Check if a file should be skipped."""
p = Path(filepath)
name = p.name
if name in SKIP_EXACT_NAMES:
return True
if name.startswith(SKIP_NAME_PREFIXES):
return True
if name.endswith(SKIP_NAME_SUFFIXES):
return True
if SKIP_PATH_SEGMENTS.intersection(p.parts):
return True
return False
class SettingsKeyChecker(ast.NodeVisitor):
"""AST visitor for Python files — detects set_setting("key", ...) calls."""
def __init__(self, filename, allowed, blocked):
self.filename = filename
self.allowed = allowed
self.blocked = blocked
self.errors = []
def visit_Call(self, node):
key = self._extract_write_key(node)
if key is not None and not is_key_allowed(
key, self.allowed, self.blocked
):
self.errors.append((node.lineno, key))
self.generic_visit(node)
def _extract_write_key(self, node):
"""Extract a hardcoded settings key from a set_setting call.
Returns the key string if found, or None if the call is not
applicable or uses a dynamic key.
"""
# Pattern 1: set_setting("key", ...)
if isinstance(node.func, ast.Name) and node.func.id in WRITE_FUNC_NAMES:
return self._first_string_arg(node)
# Pattern 2: obj.set_setting("key", ...)
if (
isinstance(node.func, ast.Attribute)
and node.func.attr in WRITE_FUNC_NAMES
):
return self._first_string_arg(node)
return None
@staticmethod
def _first_string_arg(node):
"""Return the first positional arg if it's a settings-key-like string.
A settings key contains at least one dot (e.g. "llm.provider") or
underscore (e.g. "local_search_embedding_model"). Bare words like
"value" or "config" are not settings keys and are skipped to avoid
false positives on non-settings call sites.
"""
if node.args and isinstance(node.args[0], ast.Constant):
val = node.args[0].value
if (
isinstance(val, str)
and len(val) > 2
and ("." in val or "_" in val)
):
return val
return None
def check_python_file(filepath, allowed, blocked):
"""Check a Python file for settings key namespace violations."""
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except Exception:
return []
try:
tree = ast.parse(content, filename=filepath)
except SyntaxError:
return []
checker = SettingsKeyChecker(filepath, allowed, blocked)
checker.visit(tree)
return checker.errors
def check_js_file(filepath, allowed, blocked):
"""Check a JS file for settings key namespace violations."""
errors = []
try:
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
except Exception:
return []
for i, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("//") or stripped.startswith("*"):
continue
# Strip inline /* ... */ comments to avoid false positives
line = JS_INLINE_COMMENT_RE.sub("", line)
# saveSetting('key', ...)
for m in JS_SAVE_SETTING_RE.finditer(line):
key = m.group(1)
if not is_key_allowed(key, allowed, blocked):
errors.append((i, key))
# '/settings/api/key' literal URLs
for m in JS_SETTINGS_API_URL_RE.finditer(line):
key = m.group(1)
if not is_key_allowed(key, allowed, blocked):
errors.append((i, key))
return errors
def main():
if len(sys.argv) < 2:
print("Usage: check-settings-key-namespace.py <file1> <file2> ...")
sys.exit(1)
allowed, blocked = load_prefixes()
all_errors = []
for filepath in sys.argv[1:]:
if should_skip(filepath):
continue
if filepath.endswith(".py"):
errors = check_python_file(filepath, allowed, blocked)
elif filepath.endswith((".js", ".mjs")):
errors = check_js_file(filepath, allowed, blocked)
else:
continue
if errors:
all_errors.append((filepath, errors))
if all_errors:
allowed_sorted = sorted(allowed)
print("\nSettings key namespace violations found:\n")
for filepath, errors in all_errors:
print(f"{filepath}:")
for line_num, key in errors:
print(
f" Line {line_num}: '{key}' does not match any allowed prefix"
)
print(
f"\nAllowed prefixes: {', '.join(allowed_sorted)}\n"
"Fix: Add the prefix to ALLOWED_SETTING_PREFIXES in "
"settings_routes.py, or rename the key."
)
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()