@jaguilar87/gaia-ops 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +315 -0
  2. package/CLAUDE.md +154 -0
  3. package/LICENSE +21 -0
  4. package/README.md +221 -0
  5. package/agents/aws-troubleshooter.md +50 -0
  6. package/agents/claude-architect.md +821 -0
  7. package/agents/devops-developer.md +92 -0
  8. package/agents/gcp-troubleshooter.md +50 -0
  9. package/agents/gitops-operator.md +360 -0
  10. package/agents/terraform-architect.md +289 -0
  11. package/bin/gaia-init.js +620 -0
  12. package/commands/architect.md +97 -0
  13. package/commands/restore-session.md +87 -0
  14. package/commands/save-session.md +88 -0
  15. package/commands/session-status.md +61 -0
  16. package/commands/speckit.add-task.md +144 -0
  17. package/commands/speckit.analyze-task.md +65 -0
  18. package/commands/speckit.implement.md +96 -0
  19. package/commands/speckit.init.md +237 -0
  20. package/commands/speckit.plan.md +88 -0
  21. package/commands/speckit.specify.md +161 -0
  22. package/commands/speckit.tasks.md +188 -0
  23. package/config/AGENTS.md +162 -0
  24. package/config/agent-catalog.md +604 -0
  25. package/config/context-contracts.md +682 -0
  26. package/config/git-standards.md +674 -0
  27. package/config/git_standards.json +69 -0
  28. package/config/orchestration-workflow.md +735 -0
  29. package/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  30. package/hooks/__pycache__/pre_kubectl_security.cpython-312.pyc +0 -0
  31. package/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  32. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  33. package/hooks/__pycache__/subagent_stop.cpython-312.pyc +0 -0
  34. package/hooks/post_tool_use.py +463 -0
  35. package/hooks/pre_kubectl_security.py +205 -0
  36. package/hooks/pre_tool_use.py +530 -0
  37. package/hooks/session_start.py +315 -0
  38. package/hooks/subagent_stop.py +549 -0
  39. package/index.js +92 -0
  40. package/package.json +59 -0
  41. package/speckit/README.en.md +648 -0
  42. package/speckit/README.md +353 -0
  43. package/speckit/governance.md +169 -0
  44. package/speckit/scripts/check-prerequisites.sh +194 -0
  45. package/speckit/scripts/common.sh +126 -0
  46. package/speckit/scripts/create-new-feature.sh +131 -0
  47. package/speckit/scripts/init.sh +42 -0
  48. package/speckit/scripts/setup-plan.sh +95 -0
  49. package/speckit/scripts/update-agent-context.sh +718 -0
  50. package/speckit/templates/adr-template.md +118 -0
  51. package/speckit/templates/agent-file-template.md +23 -0
  52. package/speckit/templates/plan-template.md +233 -0
  53. package/speckit/templates/spec-template.md +116 -0
  54. package/speckit/templates/tasks-template-bkp.md +136 -0
  55. package/speckit/templates/tasks-template.md +345 -0
  56. package/templates/CLAUDE.template.md +170 -0
  57. package/templates/code-examples/approval_gate_workflow.py +141 -0
  58. package/templates/code-examples/clarification_workflow.py +94 -0
  59. package/templates/code-examples/commit_validation.py +86 -0
  60. package/templates/project-context.template.json +126 -0
  61. package/templates/settings.template.json +307 -0
  62. package/tools/__pycache__/agent_router.cpython-312.pyc +0 -0
  63. package/tools/__pycache__/approval_gate.cpython-312.pyc +0 -0
  64. package/tools/__pycache__/clarify_engine.cpython-312.pyc +0 -0
  65. package/tools/__pycache__/clarify_patterns.cpython-312.pyc +0 -0
  66. package/tools/__pycache__/commit_validator.cpython-312.pyc +0 -0
  67. package/tools/__pycache__/context_section_reader.cpython-312.pyc +0 -0
  68. package/tools/__pycache__/routing_dashboard.cpython-312.pyc +0 -0
  69. package/tools/__pycache__/routing_feedback.cpython-312.pyc +0 -0
  70. package/tools/__pycache__/semantic_matcher.cpython-312.pyc +0 -0
  71. package/tools/__pycache__/task_manager.cpython-312.pyc +0 -0
  72. package/tools/agent_capabilities.json +231 -0
  73. package/tools/agent_invoker_helper.py +239 -0
  74. package/tools/agent_router.py +730 -0
  75. package/tools/approval_gate.py +318 -0
  76. package/tools/clarify_engine.py +511 -0
  77. package/tools/clarify_patterns.py +356 -0
  78. package/tools/commit_validator.py +338 -0
  79. package/tools/context_provider.py +181 -0
  80. package/tools/context_section_reader.py +301 -0
  81. package/tools/demo_clarify.py +104 -0
  82. package/tools/generate_embeddings.py +168 -0
  83. package/tools/quicktriage_aws_troubleshooter.sh +45 -0
  84. package/tools/quicktriage_devops_developer.sh +38 -0
  85. package/tools/quicktriage_gcp_troubleshooter.sh +51 -0
  86. package/tools/quicktriage_gitops_operator.sh +47 -0
  87. package/tools/quicktriage_terraform_architect.sh +40 -0
  88. package/tools/semantic_matcher.py +222 -0
  89. package/tools/task_manager.py +547 -0
  90. package/tools/task_manager_README.md +395 -0
  91. package/tools/task_manager_example.py +215 -0
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Post-tool use hook for Claude Code Agent System
4
+ Implements audit logging, metrics collection, and session tracking
5
+ """
6
+
7
+ import sys
8
+ import json
9
+ import hashlib
10
+ import logging
11
+ import os
12
+ import time
13
+ from datetime import datetime
14
+ from typing import Dict, List, Optional, Any
15
+ from pathlib import Path
16
+
17
+ # Configure logging
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
21
+ )
22
+ logger = logging.getLogger(__name__)
23
+
24
+ def find_claude_dir() -> Path:
25
+ """Find the .claude directory by searching upward from current location"""
26
+ current = Path.cwd()
27
+
28
+ # If we're already in a .claude directory, return it
29
+ if current.name == ".claude":
30
+ return current
31
+
32
+ # Look for .claude in current directory
33
+ claude_dir = current / ".claude"
34
+ if claude_dir.exists():
35
+ return claude_dir
36
+
37
+ # Search upward through parent directories
38
+ for parent in current.parents:
39
+ claude_dir = parent / ".claude"
40
+ if claude_dir.exists():
41
+ return claude_dir
42
+
43
+ # Fallback - use current directory's .claude (but don't create it yet)
44
+ logger.warning(f"No .claude directory found, using {current}/.claude")
45
+ return current / ".claude"
46
+
47
+ class AuditLogger:
48
+ """Audit logger for tracking all tool executions"""
49
+
50
+ def __init__(self, log_dir: Optional[str] = None):
51
+ if log_dir:
52
+ self.log_dir = Path(log_dir)
53
+ else:
54
+ # Find the correct .claude directory
55
+ claude_dir = find_claude_dir()
56
+ self.log_dir = claude_dir / "logs"
57
+ self.log_dir.mkdir(exist_ok=True, parents=True)
58
+ self.session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
59
+
60
+ def hash_output(self, output: str, max_length: int = 1000) -> str:
61
+ """Create hash of output for audit trail"""
62
+ # Truncate output for hashing
63
+ truncated = output[:max_length] if len(output) > max_length else output
64
+ return hashlib.sha256(truncated.encode()).hexdigest()[:16]
65
+
66
+ def log_execution(self, tool_name: str, parameters: Dict,
67
+ result: Any, duration: float, exit_code: int = 0):
68
+ """Log tool execution details"""
69
+
70
+ timestamp = datetime.now().isoformat()
71
+
72
+ # Extract command for bash tools
73
+ command = ""
74
+ if tool_name.lower() == "bash":
75
+ command = parameters.get("command", "")
76
+
77
+ # Process result
78
+ output_preview = ""
79
+ output_hash = ""
80
+ if result:
81
+ result_str = str(result)
82
+ output_preview = result_str[:200] + "..." if len(result_str) > 200 else result_str
83
+ output_hash = self.hash_output(result_str)
84
+
85
+ # Create audit record
86
+ audit_record = {
87
+ "timestamp": timestamp,
88
+ "session_id": self.session_id,
89
+ "tool_name": tool_name,
90
+ "command": command,
91
+ "parameters": parameters,
92
+ "duration_ms": round(duration * 1000, 2),
93
+ "exit_code": exit_code,
94
+ "output_hash": output_hash,
95
+ "output_preview": output_preview
96
+ }
97
+
98
+ # Write to session log
99
+ session_log_file = self.log_dir / f"session-{self.session_id}.jsonl"
100
+ with open(session_log_file, "a") as f:
101
+ f.write(json.dumps(audit_record) + "\n")
102
+
103
+ # Write to daily audit log
104
+ daily_log_file = self.log_dir / f"audit-{datetime.now().strftime('%Y-%m-%d')}.jsonl"
105
+ with open(daily_log_file, "a") as f:
106
+ f.write(json.dumps(audit_record) + "\n")
107
+
108
+ logger.info(f"Logged execution: {tool_name} - {command[:50]} - {duration:.2f}s")
109
+
110
+ class MetricsCollector:
111
+ """Collect and aggregate execution metrics"""
112
+
113
+ def __init__(self, metrics_dir: Optional[str] = None):
114
+ if metrics_dir:
115
+ self.metrics_dir = Path(metrics_dir)
116
+ else:
117
+ # Find the correct .claude directory
118
+ claude_dir = find_claude_dir()
119
+ self.metrics_dir = claude_dir / "metrics"
120
+ self.metrics_dir.mkdir(exist_ok=True, parents=True)
121
+
122
+ def record_execution(self, tool_name: str, command: str, duration: float,
123
+ success: bool, tier: str = "unknown"):
124
+ """Record execution metrics"""
125
+
126
+ timestamp = datetime.now().isoformat()
127
+
128
+ metrics_record = {
129
+ "timestamp": timestamp,
130
+ "tool_name": tool_name,
131
+ "command_type": self._classify_command(command),
132
+ "duration_ms": round(duration * 1000, 2),
133
+ "success": success,
134
+ "tier": tier
135
+ }
136
+
137
+ # Write to metrics file
138
+ metrics_file = self.metrics_dir / f"metrics-{datetime.now().strftime('%Y-%m')}.jsonl"
139
+ with open(metrics_file, "a") as f:
140
+ f.write(json.dumps(metrics_record) + "\n")
141
+
142
+ def _classify_command(self, command: str) -> str:
143
+ """Classify command type for metrics"""
144
+
145
+ if "terraform" in command.lower():
146
+ return "terraform"
147
+ elif "kubectl" in command.lower():
148
+ return "kubernetes"
149
+ elif "helm" in command.lower():
150
+ return "helm"
151
+ elif "gcloud" in command.lower():
152
+ return "gcp"
153
+ elif "aws" in command.lower():
154
+ return "aws"
155
+ elif "flux" in command.lower():
156
+ return "flux"
157
+ elif "docker" in command.lower():
158
+ return "docker"
159
+ else:
160
+ return "general"
161
+
162
+ def generate_summary(self, days: int = 7) -> Dict[str, Any]:
163
+ """Generate metrics summary for the last N days"""
164
+
165
+ # This would typically read from metrics files and aggregate
166
+ # For now, return a placeholder summary
167
+ return {
168
+ "period_days": days,
169
+ "total_executions": 0,
170
+ "success_rate": 0.0,
171
+ "avg_duration_ms": 0.0,
172
+ "top_commands": [],
173
+ "tier_distribution": {}
174
+ }
175
+
176
+ class NotificationHandler:
177
+ """Handle notifications for threshold breaches or important events"""
178
+
179
+ def __init__(self):
180
+ self.thresholds = {
181
+ "long_execution_seconds": 60,
182
+ "high_failure_rate": 0.3,
183
+ "blocked_command_count": 5
184
+ }
185
+
186
+ def check_thresholds(self, duration: float, success: bool, tool_name: str):
187
+ """Check if execution crosses any notification thresholds"""
188
+
189
+ notifications = []
190
+
191
+ # Long execution time
192
+ if duration > self.thresholds["long_execution_seconds"]:
193
+ notifications.append({
194
+ "type": "long_execution",
195
+ "message": f"Long execution detected: {tool_name} took {duration:.1f}s",
196
+ "severity": "warning"
197
+ })
198
+
199
+ # Command failure
200
+ if not success:
201
+ notifications.append({
202
+ "type": "command_failure",
203
+ "message": f"Command failed: {tool_name}",
204
+ "severity": "error"
205
+ })
206
+
207
+ return notifications
208
+
209
+ class CriticalEventDetector:
210
+ """Detect critical events that warrant context updates"""
211
+
212
+ # Track file modifications within session
213
+ file_modification_count = 0
214
+ file_modification_threshold = 3
215
+
216
+ def __init__(self):
217
+ self.speckit_commands = [
218
+ "/speckit.specify", "/speckit.plan", "/speckit.tasks",
219
+ "/speckit.implement", "/speckit.constitution"
220
+ ]
221
+
222
+ def is_git_commit(self, tool_name: str, parameters: Dict, result: Any, success: bool) -> Optional[Dict]:
223
+ """Detect successful git commit"""
224
+ if not success:
225
+ return None
226
+
227
+ if tool_name.lower() == "bash":
228
+ command = parameters.get("command", "")
229
+ if "git commit" in command and result:
230
+ # Extract commit metadata
231
+ result_str = str(result)
232
+
233
+ # Try to extract commit hash (format: [branch hash] message)
234
+ commit_hash = ""
235
+ commit_message = ""
236
+
237
+ # Look for pattern like "[main 1a2b3c4]"
238
+ import re
239
+ match = re.search(r'\[[\w\-/]+ ([a-f0-9]{7,})\]', result_str)
240
+ if match:
241
+ commit_hash = match.group(1)
242
+
243
+ # Extract message (usually after the hash pattern)
244
+ if commit_hash:
245
+ msg_match = re.search(r'\[[\w\-/]+ [a-f0-9]{7,}\]\s*(.+)', result_str, re.MULTILINE)
246
+ if msg_match:
247
+ commit_message = msg_match.group(1).strip().split('\n')[0]
248
+
249
+ return {
250
+ "event_type": "git_commit",
251
+ "commit_hash": commit_hash,
252
+ "commit_message": commit_message,
253
+ "command": command
254
+ }
255
+ return None
256
+
257
+ def is_git_push(self, tool_name: str, parameters: Dict, result: Any, success: bool) -> Optional[Dict]:
258
+ """Detect successful git push"""
259
+ if not success:
260
+ return None
261
+
262
+ if tool_name.lower() == "bash":
263
+ command = parameters.get("command", "")
264
+ if "git push" in command and result:
265
+ result_str = str(result)
266
+
267
+ # Extract branch info
268
+ branch = ""
269
+ import re
270
+ match = re.search(r'To .+\n\s+[a-f0-9]+\.\.[a-f0-9]+\s+([\w\-/]+)\s+->', result_str)
271
+ if match:
272
+ branch = match.group(1)
273
+
274
+ return {
275
+ "event_type": "git_push",
276
+ "branch": branch,
277
+ "command": command
278
+ }
279
+ return None
280
+
281
+ def should_update_for_file_mods(self, tool_name: str) -> Optional[Dict]:
282
+ """Check if file modification count crosses threshold"""
283
+ if tool_name.lower() in ["edit", "write", "notebookedit"]:
284
+ CriticalEventDetector.file_modification_count += 1
285
+
286
+ if CriticalEventDetector.file_modification_count >= self.file_modification_threshold:
287
+ # Reset counter
288
+ count = CriticalEventDetector.file_modification_count
289
+ CriticalEventDetector.file_modification_count = 0
290
+
291
+ return {
292
+ "event_type": "file_modifications",
293
+ "modification_count": count
294
+ }
295
+ return None
296
+
297
+ def is_speckit_milestone(self, tool_name: str, parameters: Dict) -> Optional[Dict]:
298
+ """Detect spec-kit milestone commands"""
299
+ if tool_name.lower() == "slashcommand":
300
+ command = parameters.get("command", "")
301
+ for speckit_cmd in self.speckit_commands:
302
+ if speckit_cmd in command:
303
+ return {
304
+ "event_type": "speckit_milestone",
305
+ "command": speckit_cmd
306
+ }
307
+ return None
308
+
309
+ class ActiveContextUpdater:
310
+ """Update active session context for critical events"""
311
+
312
+ def __init__(self, context_path: Optional[str] = None):
313
+ if context_path:
314
+ self.context_path = Path(context_path)
315
+ else:
316
+ # Find the correct .claude directory
317
+ claude_dir = find_claude_dir()
318
+ self.context_path = claude_dir / "session" / "active" / "context.json"
319
+ self.context_path.parent.mkdir(exist_ok=True, parents=True)
320
+
321
+ def update_context(self, event_data: Dict) -> None:
322
+ """Update active context with event data"""
323
+ try:
324
+ # Load existing context
325
+ context = {}
326
+ if self.context_path.exists():
327
+ with open(self.context_path, 'r') as f:
328
+ context = json.load(f)
329
+
330
+ # Initialize events list if not exists
331
+ if "critical_events" not in context:
332
+ context["critical_events"] = []
333
+
334
+ # Add timestamp to event
335
+ event_data["timestamp"] = datetime.now().isoformat()
336
+
337
+ # Append new event
338
+ context["critical_events"].append(event_data)
339
+
340
+ # Keep only last 20 events (prevent unbounded growth)
341
+ context["critical_events"] = context["critical_events"][-20:]
342
+
343
+ # Update last_modified
344
+ context["last_modified"] = datetime.now().isoformat()
345
+
346
+ # Write back to file
347
+ with open(self.context_path, 'w') as f:
348
+ json.dump(context, f, indent=2)
349
+
350
+ logger.info(f"Updated active context with event: {event_data['event_type']}")
351
+
352
+ except Exception as e:
353
+ logger.error(f"Error updating active context: {e}")
354
+
355
+ def post_tool_use_hook(tool_name: str, parameters: Dict, result: Any,
356
+ duration: float, success: bool = True) -> None:
357
+ """
358
+ Post-tool use hook implementation
359
+
360
+ Args:
361
+ tool_name: Name of the tool that was invoked
362
+ parameters: Tool parameters
363
+ result: Tool execution result
364
+ duration: Execution duration in seconds
365
+ success: Whether execution was successful
366
+ """
367
+
368
+ try:
369
+ # Initialize components
370
+ audit_logger = AuditLogger()
371
+ metrics_collector = MetricsCollector()
372
+ notification_handler = NotificationHandler()
373
+
374
+ # Determine exit code
375
+ exit_code = 0 if success else 1
376
+
377
+ # Log execution
378
+ audit_logger.log_execution(tool_name, parameters, result, duration, exit_code)
379
+
380
+ # Extract command for metrics
381
+ command = parameters.get("command", "") if tool_name.lower() == "bash" else tool_name
382
+
383
+ # Determine tier (this would ideally come from pre_tool_use)
384
+ tier = "unknown" # Could be enhanced to track tier from pre-hook
385
+
386
+ # Record metrics
387
+ metrics_collector.record_execution(tool_name, command, duration, success, tier)
388
+
389
+ # Check for notifications
390
+ notifications = notification_handler.check_thresholds(duration, success, tool_name)
391
+
392
+ for notification in notifications:
393
+ logger.warning(f"NOTIFICATION: {notification['message']}")
394
+
395
+ # ═══════════════════════════════════════════════════════════════
396
+ # CRITICAL EVENT DETECTION & CONTEXT UPDATES
397
+ # ═══════════════════════════════════════════════════════════════
398
+
399
+ event_detector = CriticalEventDetector()
400
+ context_updater = ActiveContextUpdater()
401
+
402
+ # Detect git commit
403
+ git_commit_event = event_detector.is_git_commit(tool_name, parameters, result, success)
404
+ if git_commit_event:
405
+ context_updater.update_context(git_commit_event)
406
+
407
+ # Detect git push
408
+ git_push_event = event_detector.is_git_push(tool_name, parameters, result, success)
409
+ if git_push_event:
410
+ context_updater.update_context(git_push_event)
411
+
412
+ # Detect file modifications batch
413
+ file_mod_event = event_detector.should_update_for_file_mods(tool_name)
414
+ if file_mod_event:
415
+ context_updater.update_context(file_mod_event)
416
+
417
+ # Detect spec-kit milestones
418
+ speckit_event = event_detector.is_speckit_milestone(tool_name, parameters)
419
+ if speckit_event:
420
+ context_updater.update_context(speckit_event)
421
+
422
+ except Exception as e:
423
+ logger.error(f"Error in post_tool_use_hook: {e}")
424
+
425
+ def main():
426
+ """CLI interface for testing and metrics"""
427
+
428
+ if len(sys.argv) < 2:
429
+ print("Usage: python post_tool_use.py --metrics")
430
+ print(" python post_tool_use.py --test")
431
+ sys.exit(1)
432
+
433
+ if sys.argv[1] == "--metrics":
434
+ # Show current metrics
435
+ metrics_collector = MetricsCollector()
436
+ summary = metrics_collector.generate_summary()
437
+
438
+ print("šŸ“Š Execution Metrics Summary")
439
+ print(f"Period: {summary['period_days']} days")
440
+ print(f"Total executions: {summary['total_executions']}")
441
+ print(f"Success rate: {summary['success_rate']:.1%}")
442
+ print(f"Average duration: {summary['avg_duration_ms']:.1f}ms")
443
+
444
+ elif sys.argv[1] == "--test":
445
+ # Test the post hook
446
+ print("🧪 Testing Post-Tool Use Hook...")
447
+
448
+ test_parameters = {"command": "kubectl get pods"}
449
+ test_result = "pod/test-pod 1/1 Running 0 1m"
450
+
451
+ start_time = time.time()
452
+ post_tool_use_hook("bash", test_parameters, test_result, 0.5, True)
453
+
454
+ print("āœ… Post-hook test completed")
455
+ print("Check .claude/logs/ for audit logs")
456
+ print("Check .claude/metrics/ for metrics")
457
+
458
+ else:
459
+ print(f"Unknown command: {sys.argv[1]}")
460
+ sys.exit(1)
461
+
462
+ if __name__ == "__main__":
463
+ main()
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-execution security hook for kubectl commands to enforce GitOps principles.
4
+ Prevents direct cluster modifications and ensures proper GitOps workflows.
5
+ """
6
+
7
+ import sys
8
+ import re
9
+ import json
10
+ from typing import List, Dict, Any
11
+
12
+ # Forbidden kubectl commands that modify cluster state
13
+ FORBIDDEN_KUBECTL_COMMANDS = [
14
+ r'kubectl\s+apply(?!\s+.*--dry-run)', # kubectl apply without --dry-run
15
+ r'kubectl\s+create(?!\s+.*--dry-run)', # kubectl create without --dry-run
16
+ r'kubectl\s+patch', # kubectl patch (always modifies state)
17
+ r'kubectl\s+replace', # kubectl replace (modifies state)
18
+ r'kubectl\s+delete', # kubectl delete (destructive)
19
+ r'kubectl\s+scale', # kubectl scale (modifies state)
20
+ r'kubectl\s+rollout\s+restart', # kubectl rollout restart (modifies state)
21
+ r'kubectl\s+annotate(?!\s+.*--dry-run)', # kubectl annotate without --dry-run
22
+ r'kubectl\s+label(?!\s+.*--dry-run)', # kubectl label without --dry-run
23
+ ]
24
+
25
+ # Forbidden flux commands that trigger reconciliation
26
+ FORBIDDEN_FLUX_COMMANDS = [
27
+ r'flux\s+create', # flux create (modifies GitOps resources)
28
+ r'flux\s+delete', # flux delete (destructive)
29
+ r'flux\s+suspend', # flux suspend (modifies reconciliation)
30
+ r'flux\s+resume', # flux resume (modifies reconciliation)
31
+ ]
32
+
33
+ # Forbidden helm commands that modify releases
34
+ FORBIDDEN_HELM_COMMANDS = [
35
+ r'helm\s+install(?!\s+.*--dry-run)', # helm install without --dry-run
36
+ r'helm\s+upgrade(?!\s+.*--dry-run)', # helm upgrade without --dry-run
37
+ r'helm\s+uninstall', # helm uninstall (destructive)
38
+ r'helm\s+rollback', # helm rollback (modifies state)
39
+ ]
40
+
41
+ # Safe read-only commands that are always allowed
42
+ SAFE_KUBECTL_COMMANDS = [
43
+ r'kubectl\s+get',
44
+ r'kubectl\s+describe',
45
+ r'kubectl\s+logs',
46
+ r'kubectl\s+top',
47
+ r'kubectl\s+explain',
48
+ r'kubectl\s+version',
49
+ r'kubectl\s+cluster-info',
50
+ r'kubectl\s+config\s+view',
51
+ r'kubectl\s+api-resources',
52
+ r'kubectl\s+api-versions',
53
+ ]
54
+
55
+ SAFE_FLUX_COMMANDS = [
56
+ r'flux\s+get',
57
+ r'flux\s+check',
58
+ r'flux\s+version',
59
+ r'flux\s+logs',
60
+ r'flux\s+stats',
61
+ r'flux\s+tree',
62
+ ]
63
+
64
+ SAFE_HELM_COMMANDS = [
65
+ r'helm\s+list',
66
+ r'helm\s+status',
67
+ r'helm\s+history',
68
+ r'helm\s+template',
69
+ r'helm\s+lint',
70
+ r'helm\s+version',
71
+ r'helm\s+show',
72
+ r'helm\s+search',
73
+ ]
74
+
75
+ class GitOpsSecurityViolation(Exception):
76
+ """Exception raised when a command violates GitOps security principles."""
77
+ pass
78
+
79
+ def is_safe_command(command: str) -> bool:
80
+ """Check if a command is explicitly safe (read-only)."""
81
+ safe_patterns = SAFE_KUBECTL_COMMANDS + SAFE_FLUX_COMMANDS + SAFE_HELM_COMMANDS
82
+
83
+ for pattern in safe_patterns:
84
+ if re.search(pattern, command, re.IGNORECASE):
85
+ return True
86
+ return False
87
+
88
+ def is_forbidden_command(command: str) -> bool:
89
+ """Check if a command is forbidden (modifies cluster state)."""
90
+ forbidden_patterns = FORBIDDEN_KUBECTL_COMMANDS + FORBIDDEN_FLUX_COMMANDS + FORBIDDEN_HELM_COMMANDS
91
+
92
+ for pattern in forbidden_patterns:
93
+ if re.search(pattern, command, re.IGNORECASE):
94
+ return True
95
+ return False
96
+
97
+ def validate_gitops_workflow(command: str, agent_type: str = None) -> Dict[str, Any]:
98
+ """
99
+ Validate command against GitOps security principles.
100
+
101
+ Returns:
102
+ dict: Validation result with status and details
103
+ """
104
+ result = {
105
+ "allowed": False,
106
+ "reason": "",
107
+ "suggestions": [],
108
+ "severity": "info"
109
+ }
110
+
111
+ # Check if command is explicitly safe
112
+ if is_safe_command(command):
113
+ result["allowed"] = True
114
+ result["reason"] = "Read-only operation - safe to execute"
115
+ return result
116
+
117
+ # Check if command is forbidden
118
+ if is_forbidden_command(command):
119
+ result["allowed"] = False
120
+ result["severity"] = "critical"
121
+ result["reason"] = "Command violates GitOps principles - modifies cluster state directly"
122
+
123
+ # Provide specific suggestions based on command type
124
+ if "kubectl apply" in command and "--dry-run" not in command:
125
+ result["suggestions"].extend([
126
+ "Use: kubectl apply --dry-run=client -f <file>",
127
+ "Create manifests in gitops repository first",
128
+ "Commit changes and let Flux CD reconcile"
129
+ ])
130
+ elif "flux reconcile" in command and "--dry-run" not in command:
131
+ result["suggestions"].extend([
132
+ "Use: flux reconcile <resource> --dry-run",
133
+ "Follow GitOps workflow: commit → push → automatic reconciliation"
134
+ ])
135
+ elif "helm install" in command or "helm upgrade" in command:
136
+ result["suggestions"].extend([
137
+ "Use: helm template or helm upgrade --dry-run",
138
+ "Deploy via HelmRelease manifests in gitops repository"
139
+ ])
140
+ else:
141
+ result["suggestions"].append("Use read-only commands or --dry-run alternatives")
142
+
143
+ return result
144
+
145
+ # For gitops-operator agent, be extra strict
146
+ if agent_type == "gitops-operator":
147
+ # Even if not explicitly forbidden, require explicit dry-run for apply operations
148
+ if ("apply" in command or "create" in command) and "--dry-run" not in command:
149
+ result["allowed"] = False
150
+ result["severity"] = "high"
151
+ result["reason"] = "GitOps operator must use --dry-run for all apply operations"
152
+ result["suggestions"].append("Add --dry-run=client flag to command")
153
+ return result
154
+
155
+ # Default: allow but warn about unclear intent
156
+ result["allowed"] = True
157
+ result["severity"] = "warning"
158
+ result["reason"] = "Command not explicitly validated - proceed with caution"
159
+ result["suggestions"].append("Verify command follows GitOps principles")
160
+
161
+ return result
162
+
163
+ def main():
164
+ """Main hook execution."""
165
+ if len(sys.argv) < 2:
166
+ print("Usage: pre_kubectl_security.py <command>")
167
+ sys.exit(1)
168
+
169
+ command = " ".join(sys.argv[1:])
170
+
171
+ # Extract agent type from environment or command context if available
172
+ agent_type = None
173
+ if "gitops-operator" in command or "GITOPS_OPERATOR" in str(sys.argv):
174
+ agent_type = "gitops-operator"
175
+
176
+ try:
177
+ validation = validate_gitops_workflow(command, agent_type)
178
+
179
+ if not validation["allowed"]:
180
+ print(f"🚨 SECURITY VIOLATION: {validation['reason']}")
181
+ print(f"šŸ“‹ Command: {command}")
182
+ print(f"āš ļø Severity: {validation['severity'].upper()}")
183
+
184
+ if validation["suggestions"]:
185
+ print("šŸ’” Suggestions:")
186
+ for suggestion in validation["suggestions"]:
187
+ print(f" • {suggestion}")
188
+
189
+ print("\nšŸ”’ GitOps Security Enforcement Active")
190
+ print("šŸ“– Review: /home/jaguilar/aaxis/rnd/repositories/.claude/agents/gitops-operator.md")
191
+
192
+ sys.exit(1) # Block command execution
193
+
194
+ elif validation["severity"] in ["warning", "high"]:
195
+ print(f"āš ļø WARNING: {validation['reason']}")
196
+ if validation["suggestions"]:
197
+ for suggestion in validation["suggestions"]:
198
+ print(f"šŸ’” {suggestion}")
199
+
200
+ except Exception as e:
201
+ print(f"āŒ Hook execution error: {e}")
202
+ sys.exit(1)
203
+
204
+ if __name__ == "__main__":
205
+ main()