HumboldtJoker commited on
Commit
5f2247e
·
verified ·
1 Parent(s): 9967267

Move scaffold/scaffold/discipline_gate.py -> scaffold/discipline_gate.py

Browse files
Files changed (1) hide show
  1. scaffold/discipline_gate.py +175 -0
scaffold/discipline_gate.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rivet Discipline Gate — pre-flight check on every suggestion.
2
+
3
+ Before any code suggestion reaches the user, it passes through this gate.
4
+ The gate checks for dangerous patterns, flags security implications, and
5
+ assigns a confidence level. If a suggestion fails the gate, it's blocked
6
+ or annotated with warnings.
7
+ """
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+
14
+ class Confidence(str, Enum):
15
+ HIGH = "HIGH" # Traced the full code path
16
+ MEDIUM = "MEDIUM" # Read the code but not run it
17
+ LOW = "LOW" # Reasoning from architecture, not source
18
+
19
+
20
+ class Severity(str, Enum):
21
+ BLOCK = "BLOCK" # Do not suggest this
22
+ WARN = "WARN" # Suggest with prominent warning
23
+ INFO = "INFO" # Note for awareness
24
+
25
+
26
+ @dataclass
27
+ class GateResult:
28
+ passed: bool = True
29
+ flags: list = field(default_factory=list)
30
+ confidence: Confidence = Confidence.MEDIUM
31
+ requires_review: list = field(default_factory=list)
32
+
33
+ def add_flag(self, severity: Severity, message: str):
34
+ self.flags.append({"severity": severity.value, "message": message})
35
+ if severity == Severity.BLOCK:
36
+ self.passed = False
37
+
38
+ def format_warnings(self) -> str:
39
+ if not self.flags:
40
+ return ""
41
+ lines = ["\n⚠️ **Discipline Gate Flags:**"]
42
+ for f in self.flags:
43
+ icon = "🛑" if f["severity"] == "BLOCK" else "⚠️" if f["severity"] == "WARN" else "ℹ️"
44
+ lines.append(f" {icon} [{f['severity']}] {f['message']}")
45
+ if self.requires_review:
46
+ lines.append(f"\n 📋 Requires review from: {', '.join(self.requires_review)}")
47
+ lines.append(f"\n 📊 Confidence: {self.confidence.value}")
48
+ return "\n".join(lines)
49
+
50
+
51
+ # Pattern matchers for dangerous operations
52
+ DESTRUCTIVE_MIGRATION_PATTERNS = [
53
+ r'\bDROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT)',
54
+ r'\bALTER\s+TABLE\s+\w+\s+DROP',
55
+ r'\bALTER\s+TABLE\s+\w+\s+ALTER\s+COLUMN\s+\w+\s+TYPE',
56
+ r'\bTRUNCATE\s+TABLE',
57
+ r'\bDELETE\s+FROM\s+\w+\s*;', # Unqualified DELETE
58
+ ]
59
+
60
+ AUTH_PATTERNS = [
61
+ r'\bJWT_SECRET\b',
62
+ r'\bverifyToken\b',
63
+ r'\brequireAdmin\b',
64
+ r'\brequireModerator\b',
65
+ r'\bauth\s*middleware\b',
66
+ r'\bsession\s*cookie\b',
67
+ r'\brejectIfIneligible\b',
68
+ r'\bBearer\b',
69
+ ]
70
+
71
+ WEBHOOK_BYPASS_PATTERNS = [
72
+ r"process\.env\.\w+\s*\|\|\s*['\"]", # env fallback to empty string
73
+ r"if\s*\(\s*!secret\s*\)\s*return", # skip on missing secret
74
+ r"if\s*\(\s*!.*SECRET.*\)\s*return",
75
+ ]
76
+
77
+ TRANSACTION_GAPS = [
78
+ r'await\s+\w+\.(debit|credit|transfer|purchase|delete)\(', # Multi-step without tx
79
+ ]
80
+
81
+
82
+ def check_migration_safety(content: str) -> list:
83
+ """Check for destructive migration patterns."""
84
+ flags = []
85
+ for pattern in DESTRUCTIVE_MIGRATION_PATTERNS:
86
+ matches = re.findall(pattern, content, re.IGNORECASE)
87
+ if matches:
88
+ flags.append((Severity.BLOCK,
89
+ f"Destructive migration detected: {pattern}. "
90
+ f"Staging and prod share the database — additive only."))
91
+ return flags
92
+
93
+
94
+ def check_auth_impact(content: str) -> list:
95
+ """Flag anything touching authentication."""
96
+ flags = []
97
+ for pattern in AUTH_PATTERNS:
98
+ if re.search(pattern, content, re.IGNORECASE):
99
+ flags.append((Severity.WARN,
100
+ "This touches authentication. Review with security before merging."))
101
+ break
102
+ return flags
103
+
104
+
105
+ def check_webhook_safety(content: str) -> list:
106
+ """Check for webhook signature bypass patterns (Audit C2)."""
107
+ flags = []
108
+ for pattern in WEBHOOK_BYPASS_PATTERNS:
109
+ if re.search(pattern, content):
110
+ flags.append((Severity.WARN,
111
+ "Webhook signature bypass pattern detected (Audit C2). "
112
+ "Never skip verification when env var is missing — reject instead."))
113
+ break
114
+ return flags
115
+
116
+
117
+ def check_transaction_safety(content: str) -> list:
118
+ """Flag multi-step DB operations that may need transaction wrapping (Audit H3)."""
119
+ flags = []
120
+ matches = re.findall(r'await\s+\w+\.\w+\(', content)
121
+ if len(matches) >= 2:
122
+ for pattern in TRANSACTION_GAPS:
123
+ if re.search(pattern, content):
124
+ flags.append((Severity.WARN,
125
+ "Multi-step DB operation detected. Ensure these are wrapped "
126
+ "in a single transaction (Audit H3: gem debit without tx protection)."))
127
+ break
128
+ return flags
129
+
130
+
131
+ def run_gate(suggestion: str, context: str = "") -> GateResult:
132
+ """Run the full discipline gate on a suggestion.
133
+
134
+ Args:
135
+ suggestion: The code/text being suggested to the user
136
+ context: Optional surrounding context (the question, file being edited)
137
+
138
+ Returns:
139
+ GateResult with pass/fail, flags, and confidence
140
+ """
141
+ result = GateResult()
142
+ full_content = f"{context}\n{suggestion}"
143
+
144
+ # Run all checks
145
+ for severity, message in check_migration_safety(full_content):
146
+ result.add_flag(severity, message)
147
+
148
+ for severity, message in check_auth_impact(full_content):
149
+ result.add_flag(severity, message)
150
+ result.requires_review.append("security")
151
+
152
+ for severity, message in check_webhook_safety(full_content):
153
+ result.add_flag(severity, message)
154
+ result.requires_review.append("security")
155
+
156
+ for severity, message in check_transaction_safety(full_content):
157
+ result.add_flag(severity, message)
158
+
159
+ return result
160
+
161
+
162
+ if __name__ == "__main__":
163
+ # Quick test
164
+ test_migration = "ALTER TABLE students DROP COLUMN legacy_score;"
165
+ result = run_gate(test_migration)
166
+ print(f"Migration test: passed={result.passed}")
167
+ print(result.format_warnings())
168
+
169
+ test_webhook = """
170
+ const secret = process.env.STRIPE_SECRET || ''
171
+ if (!secret) return next()
172
+ """
173
+ result = run_gate(test_webhook)
174
+ print(f"\nWebhook test: passed={result.passed}")
175
+ print(result.format_warnings())