@jaguilar87/gaia-ops 2.2.1 → 2.2.2

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 (32) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/config/embeddings_info.json +14 -0
  3. package/config/intent_embeddings.json +2002 -0
  4. package/config/intent_embeddings.npy +0 -0
  5. package/package.json +2 -1
  6. package/templates/CLAUDE.template.md +3 -11
  7. package/tests/README.en.md +224 -0
  8. package/tests/README.md +338 -0
  9. package/tests/fixtures/project-context.aws.json +53 -0
  10. package/tests/fixtures/project-context.gcp.json +53 -0
  11. package/tests/integration/RUN_TESTS.md +185 -0
  12. package/tests/integration/__init__.py +0 -0
  13. package/tests/integration/test_hooks_integration.py +473 -0
  14. package/tests/integration/test_hooks_workflow.py +397 -0
  15. package/tests/permissions-validation/MANUAL_VALIDATION.md +434 -0
  16. package/tests/permissions-validation/test_permissions_validation.py +527 -0
  17. package/tests/system/__init__.py +0 -0
  18. package/tests/system/permissions_helpers.py +318 -0
  19. package/tests/system/test_agent_definitions.py +166 -0
  20. package/tests/system/test_configuration_files.py +121 -0
  21. package/tests/system/test_directory_structure.py +231 -0
  22. package/tests/system/test_permissions_system.py +1006 -0
  23. package/tests/tools/__init__.py +0 -0
  24. package/tests/tools/test_agent_router.py +266 -0
  25. package/tests/tools/test_clarify_engine.py +413 -0
  26. package/tests/tools/test_context_provider.py +157 -0
  27. package/tests/validators/__init__.py +0 -0
  28. package/tests/validators/test_approval_gate.py +415 -0
  29. package/tests/validators/test_commit_validator.py +446 -0
  30. package/tools/context_provider.py +4 -4
  31. package/tools/generate_embeddings.py +3 -3
  32. package/tools/semantic_matcher.py +2 -2
@@ -0,0 +1,473 @@
1
+ """
2
+ Integration tests for hooks and permissions system.
3
+
4
+ Tests the integration between:
5
+ - pre_tool_use hook and PolicyEngine
6
+ - post_tool_use hook and AuditLogger
7
+ - Settings permissions and pattern matching
8
+ - GitOps security validation
9
+ - Tier-based command classification
10
+ """
11
+
12
+ import pytest
13
+ import sys
14
+ import json
15
+ import tempfile
16
+ import shutil
17
+ from pathlib import Path
18
+ from typing import Dict, Any
19
+
20
+ # Add parent directories to path for imports
21
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "hooks"))
22
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tests" / "system"))
23
+
24
+ try:
25
+ from pre_tool_use import PolicyEngine, SecurityTier, pre_tool_use_hook
26
+ PRE_HOOK_AVAILABLE = True
27
+ except ImportError as e:
28
+ print(f"⚠️ pre_tool_use hook not available: {e}")
29
+ PRE_HOOK_AVAILABLE = False
30
+
31
+ try:
32
+ from post_tool_use import post_tool_use_hook, AuditLogger, MetricsCollector
33
+ POST_HOOK_AVAILABLE = True
34
+ except ImportError as e:
35
+ print(f"⚠️ post_tool_use hook not available: {e}")
36
+ POST_HOOK_AVAILABLE = False
37
+
38
+ from permissions_helpers import (
39
+ get_permission_decision,
40
+ matches_any_pattern,
41
+ get_permission_level,
42
+ merge_settings,
43
+ load_merged_settings
44
+ )
45
+
46
+
47
+ @pytest.mark.skipif(not PRE_HOOK_AVAILABLE, reason="pre_tool_use hook not available")
48
+ class TestPreToolUseHook:
49
+ """Test pre_tool_use hook integration"""
50
+
51
+ def test_hook_allows_read_operations(self):
52
+ """Test that read operations are allowed"""
53
+ result = pre_tool_use_hook("bash", {"command": "kubectl get pods"})
54
+ assert result is None, "Read operations should be allowed"
55
+
56
+ def test_hook_blocks_write_operations(self):
57
+ """Test that write operations are blocked"""
58
+ result = pre_tool_use_hook("bash", {"command": "kubectl apply -f manifest.yaml"})
59
+ assert result is not None, "Write operations should be blocked"
60
+ assert "blocked" in result.lower()
61
+
62
+ def test_hook_allows_dry_run_operations(self):
63
+ """Test that dry-run operations are allowed"""
64
+ result = pre_tool_use_hook("bash", {"command": "kubectl apply -f manifest.yaml --dry-run=client"})
65
+ assert result is None, "Dry-run operations should be allowed"
66
+
67
+ def test_hook_blocks_terraform_apply(self):
68
+ """Test that terraform apply is blocked"""
69
+ result = pre_tool_use_hook("bash", {"command": "terraform apply"})
70
+ assert result is not None
71
+ assert "blocked" in result.lower()
72
+
73
+ def test_hook_allows_terraform_plan(self):
74
+ """Test that terraform plan is allowed"""
75
+ result = pre_tool_use_hook("bash", {"command": "terraform plan"})
76
+ assert result is None
77
+
78
+ def test_hook_handles_empty_command(self):
79
+ """Test that empty commands are rejected"""
80
+ result = pre_tool_use_hook("bash", {"command": ""})
81
+ assert result is not None
82
+ assert "empty" in result.lower() or "error" in result.lower()
83
+
84
+ def test_hook_allows_non_bash_tools(self):
85
+ """Test that non-bash tools are allowed"""
86
+ result = pre_tool_use_hook("read", {"file_path": "/tmp/test.txt"})
87
+ assert result is None, "Non-bash tools should be allowed"
88
+
89
+ def test_hook_blocks_git_push(self):
90
+ """Test that git push is blocked"""
91
+ result = pre_tool_use_hook("bash", {"command": "git push origin main"})
92
+ assert result is not None
93
+ assert "blocked" in result.lower()
94
+
95
+ def test_hook_blocks_git_status(self):
96
+ """Test that git status is blocked (not in allowed patterns)"""
97
+ result = pre_tool_use_hook("bash", {"command": "git status"})
98
+ # git status is not in allowed_read_operations, so it's blocked by default
99
+ assert result is not None
100
+
101
+ def test_hook_blocks_helm_install(self):
102
+ """Test that helm install is blocked"""
103
+ result = pre_tool_use_hook("bash", {"command": "helm install myapp ./chart"})
104
+ assert result is not None
105
+
106
+ def test_hook_allows_helm_template(self):
107
+ """Test that helm template is allowed"""
108
+ result = pre_tool_use_hook("bash", {"command": "helm template myapp ./chart"})
109
+ assert result is None
110
+
111
+ def test_hook_blocks_flux_reconcile(self):
112
+ """Test that flux reconcile is blocked"""
113
+ result = pre_tool_use_hook("bash", {"command": "flux reconcile kustomization flux-system"})
114
+ assert result is not None
115
+
116
+ def test_hook_allows_flux_get(self):
117
+ """Test that flux get is allowed"""
118
+ result = pre_tool_use_hook("bash", {"command": "flux get kustomizations"})
119
+ assert result is None
120
+
121
+ def test_hook_blocks_gcloud_create(self):
122
+ """Test that gcloud create operations are blocked"""
123
+ result = pre_tool_use_hook("bash", {"command": "gcloud compute instances create test-vm"})
124
+ assert result is not None
125
+
126
+ def test_hook_allows_gcloud_describe(self):
127
+ """Test that gcloud describe operations are allowed"""
128
+ result = pre_tool_use_hook("bash", {"command": "gcloud compute instances describe test-vm"})
129
+ assert result is None
130
+
131
+ def test_hook_blocks_docker_build(self):
132
+ """Test that docker build is blocked"""
133
+ result = pre_tool_use_hook("bash", {"command": "docker build -t myapp:latest ."})
134
+ assert result is not None
135
+
136
+ def test_hook_blocks_docker_ps(self):
137
+ """Test that docker ps is blocked (not in allowed patterns)"""
138
+ result = pre_tool_use_hook("bash", {"command": "docker ps"})
139
+ # docker ps is not in allowed_read_operations, so it's blocked by default
140
+ assert result is not None
141
+
142
+ def test_hook_provides_helpful_error_messages(self):
143
+ """Test that blocked commands get helpful error messages"""
144
+ result = pre_tool_use_hook("bash", {"command": "kubectl delete pod test-pod"})
145
+ assert result is not None
146
+ assert ("alternative" in result.lower() or "instead" in result.lower()), \
147
+ "Error message should suggest alternatives"
148
+
149
+
150
+ @pytest.mark.skipif(not PRE_HOOK_AVAILABLE, reason="PolicyEngine not available")
151
+ class TestPolicyEngine:
152
+ """Test PolicyEngine command classification"""
153
+
154
+ @pytest.fixture
155
+ def policy_engine(self):
156
+ """Create a PolicyEngine instance"""
157
+ return PolicyEngine()
158
+
159
+ def test_classify_read_operations(self, policy_engine):
160
+ """Test classification of read operations"""
161
+ tier = policy_engine.classify_command_tier("kubectl get pods")
162
+ assert tier == SecurityTier.T0_READ_ONLY
163
+
164
+ def test_classify_validation_operations(self, policy_engine):
165
+ """Test classification of validation operations"""
166
+ tier = policy_engine.classify_command_tier("terraform plan")
167
+ assert tier == SecurityTier.T1_VALIDATION
168
+
169
+ def test_classify_dry_run_operations(self, policy_engine):
170
+ """Test classification of dry-run operations"""
171
+ tier = policy_engine.classify_command_tier("kubectl apply -f test.yaml --dry-run=client")
172
+ assert tier == SecurityTier.T2_DRY_RUN
173
+
174
+ def test_classify_blocked_operations(self, policy_engine):
175
+ """Test classification of blocked operations"""
176
+ tier = policy_engine.classify_command_tier("terraform apply")
177
+ assert tier == SecurityTier.T3_BLOCKED
178
+
179
+ def test_validate_command_returns_tuple(self, policy_engine):
180
+ """Test that validate_command returns proper tuple"""
181
+ is_allowed, tier, reason = policy_engine.validate_command("bash", "kubectl get pods")
182
+ assert isinstance(is_allowed, bool)
183
+ assert isinstance(tier, str)
184
+ assert isinstance(reason, str)
185
+
186
+ def test_validate_allows_safe_commands(self, policy_engine):
187
+ """Test that safe commands are allowed"""
188
+ is_allowed, tier, reason = policy_engine.validate_command("bash", "ls -la")
189
+ assert is_allowed is True
190
+
191
+ def test_validate_blocks_dangerous_commands(self, policy_engine):
192
+ """Test that dangerous commands are blocked"""
193
+ is_allowed, tier, reason = policy_engine.validate_command("bash", "kubectl delete namespace production")
194
+ assert is_allowed is False
195
+ assert tier == SecurityTier.T3_BLOCKED
196
+
197
+ def test_validate_handles_invalid_tool_name(self, policy_engine):
198
+ """Test handling of invalid tool names"""
199
+ is_allowed, tier, reason = policy_engine.validate_command(123, "test")
200
+ assert is_allowed is False
201
+ assert "invalid" in reason.lower()
202
+
203
+ def test_validate_handles_invalid_command(self, policy_engine):
204
+ """Test handling of invalid commands"""
205
+ is_allowed, tier, reason = policy_engine.validate_command("bash", None)
206
+ assert is_allowed is False
207
+
208
+ def test_check_credentials_required(self, policy_engine):
209
+ """Test credential requirement detection"""
210
+ requires, warning = policy_engine.check_credentials_required("kubectl get pods")
211
+ assert isinstance(requires, bool)
212
+ assert isinstance(warning, str)
213
+
214
+
215
+ @pytest.mark.skipif(not PRE_HOOK_AVAILABLE, reason="PolicyEngine not available")
216
+ class TestGitOpsSecurityValidation:
217
+ """Test GitOps-specific security validation"""
218
+
219
+ @pytest.fixture
220
+ def policy_engine(self):
221
+ return PolicyEngine()
222
+
223
+ def test_kubectl_write_blocked(self, policy_engine):
224
+ """Test that kubectl write operations are blocked"""
225
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "kubectl apply -f deployment.yaml")
226
+ assert is_allowed is False
227
+
228
+ def test_kubectl_read_allowed(self, policy_engine):
229
+ """Test that kubectl read operations are allowed"""
230
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "kubectl get deployments")
231
+ assert is_allowed is True
232
+
233
+ def test_helm_upgrade_blocked(self, policy_engine):
234
+ """Test that helm upgrade is blocked"""
235
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "helm upgrade myapp ./chart")
236
+ assert is_allowed is False
237
+
238
+ def test_helm_template_allowed(self, policy_engine):
239
+ """Test that helm template is allowed"""
240
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "helm template myapp ./chart")
241
+ assert is_allowed is True
242
+
243
+ def test_flux_reconcile_blocked(self, policy_engine):
244
+ """Test that flux reconcile is blocked"""
245
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "flux reconcile helmrelease myapp")
246
+ assert is_allowed is False
247
+
248
+ def test_flux_check_allowed(self, policy_engine):
249
+ """Test that flux check is allowed"""
250
+ is_allowed, tier, _ = policy_engine.validate_command("bash", "flux check")
251
+ assert is_allowed is True
252
+
253
+ def test_dry_run_kubectl_allowed(self, policy_engine):
254
+ """Test that kubectl --dry-run is allowed"""
255
+ is_allowed, tier, _ = policy_engine.validate_command(
256
+ "bash",
257
+ "kubectl apply -f deployment.yaml --dry-run=client"
258
+ )
259
+ assert is_allowed is True
260
+ assert tier == SecurityTier.T2_DRY_RUN
261
+
262
+ def test_dry_run_helm_allowed(self, policy_engine):
263
+ """Test that helm --dry-run is allowed"""
264
+ is_allowed, tier, _ = policy_engine.validate_command(
265
+ "bash",
266
+ "helm install myapp ./chart --dry-run"
267
+ )
268
+ assert is_allowed is True
269
+
270
+ def test_namespace_delete_blocked(self, policy_engine):
271
+ """Test that namespace deletion is blocked"""
272
+ is_allowed, tier, _ = policy_engine.validate_command(
273
+ "bash",
274
+ "kubectl delete namespace production"
275
+ )
276
+ assert is_allowed is False
277
+
278
+
279
+ class TestSettingsPermissionMatching:
280
+ """Test settings-based permission matching"""
281
+
282
+ @pytest.fixture
283
+ def sample_settings(self):
284
+ """Create sample settings for testing"""
285
+ return {
286
+ "permissions": {
287
+ "bash": {
288
+ "deny": [
289
+ "rm -rf",
290
+ "terraform apply",
291
+ "git push"
292
+ ],
293
+ "ask": {
294
+ "terraform plan": "Confirm terraform plan execution?",
295
+ "kubectl apply.*--dry-run": "Confirm dry-run execution?"
296
+ },
297
+ "allow": [
298
+ "kubectl get",
299
+ "kubectl describe",
300
+ "terraform validate",
301
+ "ls",
302
+ "cat"
303
+ ]
304
+ }
305
+ }
306
+ }
307
+
308
+ def test_deny_priority_highest(self, sample_settings):
309
+ """Test that deny has highest priority"""
310
+ decision = get_permission_decision("rm -rf /tmp", "bash", sample_settings)
311
+ assert decision == "deny"
312
+
313
+ def test_ask_priority_over_allow(self, sample_settings):
314
+ """Test that ask has priority over allow"""
315
+ # Even if "terraform" might match allow patterns, specific ask should win
316
+ decision = get_permission_decision("terraform plan", "bash", sample_settings)
317
+ assert decision == "ask"
318
+
319
+ def test_allow_works_when_no_higher_priority(self, sample_settings):
320
+ """Test that allow works when no deny/ask matches"""
321
+ decision = get_permission_decision("kubectl get pods", "bash", sample_settings)
322
+ assert decision == "allow"
323
+
324
+ def test_default_deny_when_no_match(self, sample_settings):
325
+ """Test default deny when no patterns match"""
326
+ decision = get_permission_decision("unknown-command", "bash", sample_settings)
327
+ assert decision == "default_deny"
328
+
329
+ def test_pattern_matching_with_wildcards(self):
330
+ """Test pattern matching with wildcards"""
331
+ patterns = ["kubectl get*", "helm template*"]
332
+ assert matches_any_pattern("kubectl get pods", patterns) is True
333
+ assert matches_any_pattern("helm template mychart", patterns) is True
334
+ assert matches_any_pattern("kubectl apply", patterns) is False
335
+
336
+ def test_pattern_matching_with_regex(self):
337
+ """Test pattern matching with regex patterns"""
338
+ patterns = [r"kubectl\s+apply.*--dry-run"]
339
+ assert matches_any_pattern("kubectl apply -f test.yaml --dry-run=client", patterns) is True
340
+ assert matches_any_pattern("kubectl apply -f test.yaml", patterns) is False
341
+
342
+ def test_settings_without_permissions(self):
343
+ """Test handling of settings without permissions section"""
344
+ settings = {"other": "config"}
345
+ decision = get_permission_decision("any command", "bash", settings)
346
+ assert decision == "default_deny"
347
+
348
+ def test_settings_without_tool(self):
349
+ """Test handling when tool not in permissions"""
350
+ settings = {"permissions": {"other_tool": {}}}
351
+ decision = get_permission_decision("any command", "bash", settings)
352
+ assert decision == "default_deny"
353
+
354
+
355
+ class TestAskPermissionTriggers:
356
+ """Test that 'ask' permissions are properly triggered"""
357
+
358
+ @pytest.fixture
359
+ def ask_settings(self):
360
+ return {
361
+ "permissions": {
362
+ "bash": {
363
+ "ask": {
364
+ "terraform apply": "Confirm terraform apply?",
365
+ "git push": "Confirm git push?",
366
+ "kubectl apply -f": "Confirm kubectl apply?"
367
+ },
368
+ "allow": []
369
+ }
370
+ }
371
+ }
372
+
373
+ def test_terraform_apply_triggers_ask(self, ask_settings):
374
+ """Test that terraform apply triggers ask"""
375
+ decision = get_permission_decision("terraform apply", "bash", ask_settings)
376
+ assert decision == "ask"
377
+
378
+ def test_git_push_triggers_ask(self, ask_settings):
379
+ """Test that git push triggers ask"""
380
+ decision = get_permission_decision("git push origin main", "bash", ask_settings)
381
+ assert decision == "ask"
382
+
383
+ def test_kubectl_apply_triggers_ask(self, ask_settings):
384
+ """Test that kubectl apply triggers ask"""
385
+ decision = get_permission_decision("kubectl apply -f deployment.yaml", "bash", ask_settings)
386
+ assert decision == "ask"
387
+
388
+ def test_other_commands_default_deny(self, ask_settings):
389
+ """Test that non-ask commands get default deny"""
390
+ decision = get_permission_decision("ls -la", "bash", ask_settings)
391
+ assert decision == "default_deny"
392
+
393
+
394
+ class TestPermissionWorkflow:
395
+ """Test complete permission workflow scenarios"""
396
+
397
+ @pytest.fixture
398
+ def complex_settings(self):
399
+ """Settings with all permission types"""
400
+ return {
401
+ "permissions": {
402
+ "bash": {
403
+ "deny": ["rm -rf", "terraform destroy"],
404
+ "ask": {
405
+ "terraform apply": "Confirm?",
406
+ "git push": "Confirm?"
407
+ },
408
+ "allow": ["terraform plan", "kubectl get", "ls"]
409
+ }
410
+ }
411
+ }
412
+
413
+ def test_workflow_deny_blocks_immediately(self, complex_settings):
414
+ """Test that deny blocks without asking"""
415
+ decision = get_permission_decision("rm -rf /tmp", "bash", complex_settings)
416
+ assert decision == "deny"
417
+
418
+ def test_workflow_ask_prompts_user(self, complex_settings):
419
+ """Test that ask returns ask decision"""
420
+ decision = get_permission_decision("terraform apply", "bash", complex_settings)
421
+ assert decision == "ask"
422
+
423
+ def test_workflow_allow_permits_immediately(self, complex_settings):
424
+ """Test that allow permits without asking"""
425
+ decision = get_permission_decision("terraform plan", "bash", complex_settings)
426
+ assert decision == "allow"
427
+
428
+ def test_workflow_default_deny_for_unknown(self, complex_settings):
429
+ """Test that unknown commands get default deny"""
430
+ decision = get_permission_decision("unknown-tool --do-something", "bash", complex_settings)
431
+ assert decision == "default_deny"
432
+
433
+
434
+ @pytest.mark.skipif(not POST_HOOK_AVAILABLE, reason="post_tool_use hook not available")
435
+ class TestPostToolUseHook:
436
+ """Test post_tool_use hook integration"""
437
+
438
+ def test_hook_logs_execution(self):
439
+ """Test that post hook logs execution"""
440
+ with tempfile.TemporaryDirectory() as tmpdir:
441
+ # Test logging functionality
442
+ audit_logger = AuditLogger(log_dir=tmpdir)
443
+ audit_logger.log_execution(
444
+ "bash",
445
+ {"command": "kubectl get pods"},
446
+ "pod/test-pod 1/1 Running",
447
+ 0.5,
448
+ 0
449
+ )
450
+
451
+ # Check that log file was created
452
+ log_files = list(Path(tmpdir).glob("*.jsonl"))
453
+ assert len(log_files) > 0, "Log files should be created"
454
+
455
+ def test_hook_records_metrics(self):
456
+ """Test that post hook records metrics"""
457
+ with tempfile.TemporaryDirectory() as tmpdir:
458
+ metrics_collector = MetricsCollector(metrics_dir=tmpdir)
459
+ metrics_collector.record_execution(
460
+ "bash",
461
+ "kubectl get pods",
462
+ 0.5,
463
+ True,
464
+ "T0"
465
+ )
466
+
467
+ # Check that metrics file was created
468
+ metrics_files = list(Path(tmpdir).glob("*.jsonl"))
469
+ assert len(metrics_files) > 0, "Metrics files should be created"
470
+
471
+
472
+ if __name__ == "__main__":
473
+ pytest.main([__file__, "-v", "--tb=short"])