@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,530 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-tool use hook for Claude Code Agent System
4
+ Implements security policy gates, tier-based command filtering,
5
+ and routing metadata verification with centralized capabilities
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import re
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Tuple, Any
15
+ from tenacity import retry, stop_after_attempt, wait_exponential
16
+
17
+ from pre_kubectl_security import validate_gitops_workflow
18
+
19
+ # Configure logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class SecurityTier:
27
+ """Security tier definitions for command classification"""
28
+
29
+ T0_READ_ONLY = "T0" # describe, get, show, list operations
30
+ T1_VALIDATION = "T1" # validate, plan, template, lint operations
31
+ T2_DRY_RUN = "T2" # --dry-run, --plan-only operations
32
+ T3_BLOCKED = "T3" # apply, reconcile, deploy operations
33
+
34
+ class PolicyEngine:
35
+ """Policy engine for command validation and security enforcement"""
36
+
37
+ def __init__(self):
38
+ # Load agent capabilities for dynamic routing verification
39
+ self.capabilities = self._load_capabilities()
40
+ self.skills = self.capabilities.get("routing_matrix", {}).get("skills", {})
41
+ self.integration_config = self.capabilities.get("integration_metadata", {})
42
+
43
+ self.blocked_commands = [
44
+ # Terraform destructive operations
45
+ r"terraform\s+apply(?!\s+--help)",
46
+ r"terraform\s+destroy",
47
+ r"terragrunt\s+apply(?!\s+--help)",
48
+ r"terragrunt\s+destroy",
49
+
50
+ # Kubernetes write operations
51
+ r"kubectl\s+apply(?!\s+.*--dry-run)",
52
+ r"kubectl\s+create(?!\s+.*--dry-run)",
53
+ r"kubectl\s+delete",
54
+ r"kubectl\s+patch",
55
+ r"kubectl\s+replace(?!\s+.*--dry-run)",
56
+
57
+ # Helm write operations
58
+ r"helm\s+install(?!\s+.*--dry-run)",
59
+ r"helm\s+upgrade(?!\s+.*--dry-run)",
60
+ r"helm\s+uninstall",
61
+ r"helm\s+delete",
62
+
63
+ # Flux write operations
64
+ r"flux\s+reconcile(?!\s+.*--dry-run)",
65
+ r"flux\s+create",
66
+ r"flux\s+delete",
67
+
68
+ # GCP write operations
69
+ r"gcloud\s+.*\s+(create|update|delete|patch)",
70
+ r"gcloud\s+(create|update|delete|patch)",
71
+
72
+ # AWS write operations
73
+ r"aws\s+.*\s+(create|update|delete|put)",
74
+
75
+ # Docker write operations
76
+ r"docker\s+build",
77
+ r"docker\s+push",
78
+ r"docker\s+run(?!\s+.*--rm)",
79
+
80
+ # Git write operations (unless explicitly allowed)
81
+ r"git\s+push(?!\s+--dry-run)",
82
+ r"git\s+commit(?!\s+.*--dry-run)",
83
+ ]
84
+
85
+ self.ask_commands = [
86
+ r"terragrunt\s+apply",
87
+ r"terraform\s+apply",
88
+ r"git\s+push",
89
+ r"git\s+commit",
90
+ ]
91
+
92
+ self.allowed_read_operations = [
93
+ # Terraform read operations
94
+ r"terraform\s+(fmt|validate|plan|show|output|version)",
95
+ r"terragrunt\s+(plan|output|validate)",
96
+
97
+ # Kubernetes read operations
98
+ r"kubectl\s+(get|describe|logs|explain|version)",
99
+ r"kubectl\s+.*--dry-run(=client)?",
100
+
101
+ # Helm read operations
102
+ r"helm\s+(template|lint|list|status|version)",
103
+ r"helm\s+.*--dry-run",
104
+
105
+ # Flux read operations
106
+ r"flux\s+(check|get|version)",
107
+
108
+ # GCP read operations
109
+ r"gcloud\s+.*\s+(describe|list|show|get)",
110
+ r"gcloud\s+(describe|list|show|get)",
111
+
112
+ # AWS read operations
113
+ r"aws\s+.*\s+(describe|list|get)",
114
+
115
+ # General utilities
116
+ r"ls|pwd|cd|cat|head|tail|grep|find|which",
117
+ r"echo|printf",
118
+
119
+ # Python scripts (session management)
120
+ r"python3?\s+.*session.*\.py",
121
+ r"python3?\s+\.claude/session/scripts/.*\.py",
122
+ ]
123
+
124
+ # Commands that require credentials to be loaded
125
+ self.credential_required_patterns = [
126
+ r"kubectl\s+(?!version)", # kubectl (except version)
127
+ r"flux\s+(?!version)", # flux (except version)
128
+ r"helm\s+(?!version)", # helm (except version)
129
+ r"gcloud\s+container\s+", # gcloud container
130
+ r"gcloud\s+sql\s+", # gcloud sql
131
+ r"gcloud\s+redis\s+", # gcloud redis
132
+ ]
133
+
134
+ def check_credentials_required(self, command: str) -> Tuple[bool, str]:
135
+ """
136
+ Check if command requires credentials and provide guidance
137
+
138
+ Returns:
139
+ (requires_creds: bool, warning_message: str)
140
+ """
141
+ # Check if command requires credentials
142
+ for pattern in self.credential_required_patterns:
143
+ if re.search(pattern, command, re.IGNORECASE):
144
+ # Check if credentials are being loaded in the command
145
+ if "gcloud auth" in command or "gcloud config" in command:
146
+ # Auth commands don't need pre-loading
147
+ return False, ""
148
+
149
+ if "source" in command and "load-cluster-credentials.sh" in command:
150
+ # Credentials are being loaded via cluster-specific script
151
+ return False, ""
152
+
153
+ # Check if using optimized scripts that auto-load credentials
154
+ if any(script in command for script in ["k8s-health.sh", "flux-status.sh"]):
155
+ # These scripts auto-load credentials, no warning needed
156
+ return False, ""
157
+
158
+ # Command requires credentials but not loading them
159
+ warning = (
160
+ "โš ๏ธ This command requires GCP/Kubernetes credentials to be loaded.\n\n"
161
+ "Recommended patterns:\n"
162
+ " 1. Load credentials inline:\n"
163
+ " gcloud auth application-default login && kubectl ...\n\n"
164
+ " 2. Use gcloud container clusters get-credentials first:\n"
165
+ " gcloud container clusters get-credentials <cluster> --region <region> && kubectl ...\n\n"
166
+ " 3. Ensure KUBECONFIG is set for kubectl/helm/flux commands\n"
167
+ )
168
+ return True, warning
169
+
170
+ return False, ""
171
+
172
+ def classify_command_tier(self, command: str) -> str:
173
+ """Classify command into security tier"""
174
+
175
+ # Check for blocked operations first
176
+ for pattern in self.blocked_commands:
177
+ if re.search(pattern, command, re.IGNORECASE):
178
+ return SecurityTier.T3_BLOCKED
179
+
180
+ # Check for dry-run operations
181
+ if "--dry-run" in command or "--plan-only" in command:
182
+ return SecurityTier.T2_DRY_RUN
183
+
184
+ # Check for validation operations
185
+ validation_patterns = [
186
+ r"validate", r"plan", r"template", r"lint", r"check", r"fmt"
187
+ ]
188
+ for pattern in validation_patterns:
189
+ if re.search(pattern, command, re.IGNORECASE):
190
+ return SecurityTier.T1_VALIDATION
191
+
192
+ # Check for read operations
193
+ for pattern in self.allowed_read_operations:
194
+ if re.search(pattern, command, re.IGNORECASE):
195
+ return SecurityTier.T0_READ_ONLY
196
+
197
+ # Default to blocked for unknown commands
198
+ return SecurityTier.T3_BLOCKED
199
+
200
+ @retry(
201
+ stop=stop_after_attempt(3),
202
+ wait=wait_exponential(multiplier=1, min=1, max=5),
203
+ reraise=True
204
+ )
205
+ def _load_capabilities_with_retry(self, capabilities_file: Path) -> Dict[str, Any]:
206
+ """Load capabilities file with retry logic"""
207
+ try:
208
+ with open(capabilities_file, 'r', encoding='utf-8') as f:
209
+ data = json.load(f)
210
+
211
+ # Validate structure
212
+ if not isinstance(data, dict):
213
+ raise ValueError("Capabilities file must be a JSON object")
214
+
215
+ logger.debug(f"Successfully loaded capabilities from {capabilities_file}")
216
+ return data
217
+
218
+ except FileNotFoundError:
219
+ logger.error(f"Capabilities file not found: {capabilities_file}")
220
+ raise
221
+ except json.JSONDecodeError as e:
222
+ logger.error(f"Invalid JSON in capabilities file: {e}")
223
+ raise
224
+ except Exception as e:
225
+ logger.error(f"Unexpected error loading capabilities: {e}")
226
+ raise
227
+
228
+ def _load_capabilities(self) -> Dict[str, Any]:
229
+ """Load agent capabilities configuration with robust error handling"""
230
+ capabilities_file = Path(__file__).parent.parent / "tools" / "agent_capabilities.json"
231
+
232
+ try:
233
+ if not capabilities_file.exists():
234
+ logger.warning(f"Capabilities file does not exist: {capabilities_file}")
235
+ return self._get_fallback_capabilities()
236
+
237
+ # Try to load with retry
238
+ return self._load_capabilities_with_retry(capabilities_file)
239
+
240
+ except Exception as e:
241
+ logger.warning(f"Could not load agent capabilities after retries: {e}")
242
+ logger.info("Using fallback capabilities configuration")
243
+ return self._get_fallback_capabilities()
244
+
245
+ def _inspect_script_content(self, script_path: str) -> Tuple[bool, str, Optional[str]]:
246
+ """Inspects script content for blocked or sensitive commands."""
247
+ try:
248
+ with open(script_path, 'r') as f:
249
+ for line_num, line in enumerate(f, 1):
250
+ line = line.strip()
251
+ if not line or line.startswith('#'):
252
+ continue
253
+
254
+ # Check against blocked commands
255
+ for pattern in self.blocked_commands:
256
+ if re.search(pattern, line, re.IGNORECASE):
257
+ return False, SecurityTier.T3_BLOCKED, f"Script contains blocked command on line {line_num}: '{line}'"
258
+
259
+ # Check against commands requiring approval
260
+ for pattern in self.ask_commands:
261
+ if re.search(pattern, line, re.IGNORECASE):
262
+ # This doesn't directly trigger 'ask', but blocks it for safety.
263
+ # The Claude Code environment itself will trigger 'ask' for the direct command.
264
+ # The hook's job is to prevent unnoticed execution inside a script.
265
+ return False, SecurityTier.T3_BLOCKED, f"Script contains sensitive command requiring direct approval on line {line_num}: '{line}'. Execute it directly, not from a script."
266
+
267
+ except FileNotFoundError:
268
+ return False, SecurityTier.T3_BLOCKED, f"Script file not found: {script_path}"
269
+ except Exception as e:
270
+ return False, SecurityTier.T3_BLOCKED, f"Error reading script file {script_path}: {e}"
271
+
272
+ return True, SecurityTier.T0_READ_ONLY, "Script content seems safe."
273
+
274
+ def verify_routing_metadata(self, tool_name: str, command: str, bundle_metadata: Optional[Dict] = None) -> Tuple[bool, str]:
275
+ """Verify routing metadata against bundle expectations"""
276
+
277
+ if not self.integration_config.get("handshake_validation", {}).get("verify_agent_match", False):
278
+ return True, "Routing verification disabled"
279
+
280
+ if not bundle_metadata:
281
+ # Try to load from environment or session
282
+ bundle_metadata = self._get_current_bundle_metadata()
283
+
284
+ if not bundle_metadata:
285
+ return True, "No bundle metadata available"
286
+
287
+ suggested_agent = bundle_metadata.get("suggested_agent")
288
+ expected_tier = bundle_metadata.get("security_tier")
289
+
290
+ # Classify current command
291
+ actual_tier = self.classify_command_tier(command)
292
+
293
+ # Check tier compatibility
294
+ if expected_tier and actual_tier != expected_tier:
295
+ if self.integration_config.get("handshake_validation", {}).get("require_approval_on_mismatch", False):
296
+ return False, f"Tier mismatch: expected {expected_tier}, got {actual_tier}"
297
+ else:
298
+ logger.warning(f"Tier mismatch detected but proceeding: expected {expected_tier}, got {actual_tier}")
299
+
300
+ # Log routing decision for audit
301
+ if self.integration_config.get("handshake_validation", {}).get("log_routing_decisions", False):
302
+ logger.info(f"Routing verification - Agent: {suggested_agent}, Tier: {expected_tier} -> {actual_tier}")
303
+
304
+ return True, "Routing verification passed"
305
+
306
+ def _get_current_bundle_metadata(self) -> Optional[Dict]:
307
+ """Get current bundle metadata from session or environment"""
308
+ try:
309
+ # Try to get from environment variable
310
+ bundle_data = os.environ.get("CLAUDE_TASK_METADATA")
311
+ if bundle_data:
312
+ return json.loads(bundle_data)
313
+
314
+ # Try to get from session active context
315
+ session_dir = Path(__file__).parent.parent / "session" / "active"
316
+ context_file = session_dir / "context.json"
317
+ if context_file.exists():
318
+ with open(context_file) as f:
319
+ context = json.load(f)
320
+ return context.get("current_task_metadata")
321
+ except Exception as e:
322
+ logger.debug(f"Could not load bundle metadata: {e}")
323
+
324
+ return None
325
+
326
+ def validate_command(self, tool_name: str, command: str) -> Tuple[bool, str, str]:
327
+ """
328
+ Validate command against security policies
329
+
330
+ Returns:
331
+ (is_allowed: bool, tier: str, reason: str)
332
+ """
333
+ try:
334
+ # Validate inputs
335
+ if not isinstance(tool_name, str):
336
+ logger.error(f"Invalid tool_name type: {type(tool_name)}")
337
+ return False, SecurityTier.T3_BLOCKED, "Invalid tool name"
338
+
339
+ if not isinstance(command, str):
340
+ logger.error(f"Invalid command type: {type(command)}")
341
+ return False, SecurityTier.T3_BLOCKED, "Invalid command"
342
+
343
+ # Skip validation for non-bash tools
344
+ if tool_name.lower() != "bash":
345
+ return True, SecurityTier.T0_READ_ONLY, "Non-bash tool allowed"
346
+
347
+ # Handle empty commands
348
+ if not command or not command.strip():
349
+ logger.warning("Empty command provided")
350
+ return False, SecurityTier.T3_BLOCKED, "Empty command not allowed"
351
+
352
+ # Check if command is a script execution
353
+ script_match = re.match(r"^\s*(bash|sh)\s+([\w\-\./_]+)", command)
354
+ if script_match:
355
+ script_path = script_match.group(2)
356
+ is_allowed, tier, reason = self._inspect_script_content(script_path)
357
+ if not is_allowed:
358
+ return is_allowed, tier, reason
359
+
360
+ # Enforce GitOps security rules for cluster-related commands
361
+ if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
362
+ try:
363
+ gitops_validation = validate_gitops_workflow(command)
364
+ except Exception as exc:
365
+ logger.error(f"GitOps validation failed: {exc}", exc_info=True)
366
+ return (
367
+ False,
368
+ SecurityTier.T3_BLOCKED,
369
+ "GitOps security validation error; blocking command for safety"
370
+ )
371
+
372
+ if not gitops_validation.get("allowed", False):
373
+ suggestions = gitops_validation.get("suggestions") or []
374
+ suggestion_text = ""
375
+ if suggestions:
376
+ suggestion_text = "\n\nSuggestions:\n - " + "\n - ".join(suggestions)
377
+
378
+ return (
379
+ False,
380
+ SecurityTier.T3_BLOCKED,
381
+ f"GitOps policy violation: {gitops_validation.get('reason', 'operation not permitted')}"
382
+ f"{suggestion_text}"
383
+ )
384
+
385
+ if gitops_validation.get("severity") in ("warning", "high"):
386
+ logger.warning(
387
+ "GitOps validation warning for command '%s': %s",
388
+ command,
389
+ gitops_validation.get("reason", "unspecified reason")
390
+ )
391
+
392
+ # Check if credentials are required (provide warning, don't block)
393
+ requires_creds, creds_warning = self.check_credentials_required(command)
394
+ if requires_creds:
395
+ logger.info(f"Credential warning issued for command: {command[:100]}")
396
+ # Log warning but don't block (allow command to proceed)
397
+ # User will see authentication error if credentials are actually missing
398
+
399
+ tier = self.classify_command_tier(command)
400
+
401
+ if tier == SecurityTier.T3_BLOCKED:
402
+ return False, tier, f"Command blocked by security policy: {command[:100]}"
403
+
404
+ # Verify routing metadata if enabled
405
+ routing_allowed, routing_reason = self.verify_routing_metadata(tool_name, command)
406
+ if not routing_allowed:
407
+ logger.warning(f"Routing verification failed: {routing_reason}")
408
+ return False, tier, f"Routing verification failed: {routing_reason}"
409
+
410
+ # Include credential warning in reason if present
411
+ final_reason = f"Command allowed in tier {tier} ({routing_reason})"
412
+ if requires_creds and creds_warning:
413
+ final_reason = f"{final_reason}\n\n{creds_warning}"
414
+
415
+ return True, tier, final_reason
416
+
417
+ except Exception as e:
418
+ logger.error(f"Error during command validation: {e}", exc_info=True)
419
+ # Fail closed - if validation errors, block the command
420
+ return False, SecurityTier.T3_BLOCKED, f"Validation error: {str(e)}"
421
+
422
+ def pre_tool_use_hook(tool_name: str, parameters: Dict) -> Optional[str]:
423
+ """
424
+ Pre-tool use hook implementation with robust error handling
425
+
426
+ Args:
427
+ tool_name: Name of the tool being invoked
428
+ parameters: Tool parameters
429
+
430
+ Returns:
431
+ None if allowed, error message if blocked
432
+ """
433
+ try:
434
+ # Validate inputs
435
+ if not isinstance(tool_name, str):
436
+ logger.error(f"Invalid tool_name type: {type(tool_name)}")
437
+ return "๐Ÿšซ Error: Invalid tool name provided"
438
+
439
+ if not isinstance(parameters, dict):
440
+ logger.error(f"Invalid parameters type: {type(parameters)}")
441
+ return "๐Ÿšซ Error: Invalid parameters provided"
442
+
443
+ policy_engine = PolicyEngine()
444
+
445
+ # Extract command from parameters
446
+ command = ""
447
+ if tool_name.lower() == "bash":
448
+ command = parameters.get("command", "")
449
+
450
+ # Validate command exists for bash tool
451
+ if not command:
452
+ logger.warning("Bash tool invoked without command")
453
+ return "๐Ÿšซ Error: Bash tool requires a command"
454
+
455
+ # Validate command
456
+ is_allowed, tier, reason = policy_engine.validate_command(tool_name, command)
457
+
458
+ # Log the decision with structured data
459
+ log_data = {
460
+ "tool": tool_name,
461
+ "command": command[:100] if command else "N/A",
462
+ "tier": tier,
463
+ "allowed": is_allowed,
464
+ "reason": reason
465
+ }
466
+
467
+ if not is_allowed:
468
+ logger.warning(f"BLOCKED: {json.dumps(log_data)}")
469
+ return (
470
+ f"๐Ÿšซ Command blocked by security policy\n\n"
471
+ f"Tier: {tier}\n"
472
+ f"Reason: {reason}\n\n"
473
+ f"๐Ÿ’ก Use validation or read-only alternatives instead:"
474
+ f"\n - terraform plan (instead of apply)"
475
+ f"\n - kubectl get/describe (instead of apply/delete)"
476
+ f"\n - --dry-run flag for testing changes\n"
477
+ )
478
+
479
+ logger.info(f"ALLOWED: {json.dumps(log_data)}")
480
+ return None
481
+
482
+ except Exception as e:
483
+ logger.error(f"Unexpected error in pre_tool_use_hook: {e}", exc_info=True)
484
+ # Fail closed - return error message
485
+ return f"๐Ÿšซ Error during security validation: {str(e)}\n\nPlease contact DevOps team."
486
+
487
+ def main():
488
+ """CLI interface for testing the policy engine"""
489
+
490
+ if len(sys.argv) < 2:
491
+ print("Usage: python pre_tool_use.py <command>")
492
+ print(" python pre_tool_use.py --test")
493
+ sys.exit(1)
494
+
495
+ if sys.argv[1] == "--test":
496
+ # Run test cases
497
+ policy_engine = PolicyEngine()
498
+
499
+ test_cases = [
500
+ ("terraform validate", True, SecurityTier.T1_VALIDATION),
501
+ ("terraform apply", False, SecurityTier.T3_BLOCKED),
502
+ ("kubectl get pods", True, SecurityTier.T0_READ_ONLY),
503
+ ("kubectl apply -f manifest.yaml", False, SecurityTier.T3_BLOCKED),
504
+ ("kubectl apply -f manifest.yaml --dry-run=client", True, SecurityTier.T2_DRY_RUN),
505
+ ("helm template myapp", True, SecurityTier.T1_VALIDATION),
506
+ ("helm install myapp", False, SecurityTier.T3_BLOCKED),
507
+ ("gcloud container clusters describe my-cluster", True, SecurityTier.T0_READ_ONLY),
508
+ ]
509
+
510
+ print("๐Ÿงช Testing Policy Engine...")
511
+ for command, expected_allowed, expected_tier in test_cases:
512
+ is_allowed, tier, reason = policy_engine.validate_command("bash", command)
513
+ status = "โœ…" if is_allowed == expected_allowed and tier == expected_tier else "โŒ"
514
+ print(f"{status} {command}: {tier} ({'ALLOWED' if is_allowed else 'BLOCKED'})")
515
+
516
+ print("โœจ Test completed")
517
+
518
+ else:
519
+ # Test specific command
520
+ command = " ".join(sys.argv[1:])
521
+ result = pre_tool_use_hook("bash", {"command": command})
522
+
523
+ if result:
524
+ print(f"โŒ BLOCKED: {result}")
525
+ sys.exit(1)
526
+ else:
527
+ print(f"โœ… ALLOWED: {command}")
528
+
529
+ if __name__ == "__main__":
530
+ main()