@jaguilar87/gaia-ops 2.2.0 → 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 (41) hide show
  1. package/CHANGELOG.md +137 -1
  2. package/README.en.md +29 -23
  3. package/README.md +24 -17
  4. package/agents/{claude-architect.md → gaia.md} +6 -6
  5. package/commands/{architect.md → gaia.md} +6 -6
  6. package/config/AGENTS.md +5 -5
  7. package/config/agent-catalog.md +14 -14
  8. package/config/context-contracts.md +4 -4
  9. package/config/embeddings_info.json +14 -0
  10. package/config/intent_embeddings.json +2002 -0
  11. package/config/intent_embeddings.npy +0 -0
  12. package/index.js +3 -1
  13. package/package.json +3 -2
  14. package/speckit/README.en.md +20 -69
  15. package/templates/CLAUDE.template.md +5 -13
  16. package/tests/README.en.md +224 -0
  17. package/tests/README.md +338 -0
  18. package/tests/fixtures/project-context.aws.json +53 -0
  19. package/tests/fixtures/project-context.gcp.json +53 -0
  20. package/tests/integration/RUN_TESTS.md +185 -0
  21. package/tests/integration/__init__.py +0 -0
  22. package/tests/integration/test_hooks_integration.py +473 -0
  23. package/tests/integration/test_hooks_workflow.py +397 -0
  24. package/tests/permissions-validation/MANUAL_VALIDATION.md +434 -0
  25. package/tests/permissions-validation/test_permissions_validation.py +527 -0
  26. package/tests/system/__init__.py +0 -0
  27. package/tests/system/permissions_helpers.py +318 -0
  28. package/tests/system/test_agent_definitions.py +166 -0
  29. package/tests/system/test_configuration_files.py +121 -0
  30. package/tests/system/test_directory_structure.py +231 -0
  31. package/tests/system/test_permissions_system.py +1006 -0
  32. package/tests/tools/__init__.py +0 -0
  33. package/tests/tools/test_agent_router.py +266 -0
  34. package/tests/tools/test_clarify_engine.py +413 -0
  35. package/tests/tools/test_context_provider.py +157 -0
  36. package/tests/validators/__init__.py +0 -0
  37. package/tests/validators/test_approval_gate.py +415 -0
  38. package/tests/validators/test_commit_validator.py +446 -0
  39. package/tools/context_provider.py +28 -7
  40. package/tools/generate_embeddings.py +3 -3
  41. package/tools/semantic_matcher.py +2 -2
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test suite for permissions validation in settings.json and settings.local.json
4
+
5
+ This test validates:
6
+ 1. settings.json has strict, standard configurations
7
+ 2. settings.local.json has more open, query-focused configurations
8
+ 3. All deny rules are properly configured
9
+ 4. All allow rules (gets, queries) are properly configured
10
+ 5. Ask rules require user approval
11
+ """
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Dict, List, Tuple
18
+ from dataclasses import dataclass, field
19
+
20
+
21
+ @dataclass
22
+ class PermissionValidationResult:
23
+ """Result of a permission validation"""
24
+ rule_type: str # 'allow', 'deny', 'ask'
25
+ pattern: str
26
+ is_valid: bool
27
+ reason: str = ""
28
+ examples: List[str] = field(default_factory=list)
29
+ file_source: str = "" # 'settings.json' or 'settings.local.json'
30
+
31
+
32
+ @dataclass
33
+ class ValidationSummary:
34
+ """Summary of all validation results"""
35
+ total_rules: int = 0
36
+ valid_rules: int = 0
37
+ invalid_rules: int = 0
38
+ allow_rules: int = 0
39
+ deny_rules: int = 0
40
+ ask_rules: int = 0
41
+ results: List[PermissionValidationResult] = field(default_factory=list)
42
+
43
+
44
+ class PermissionsValidator:
45
+ """Validator for permissions configuration"""
46
+
47
+ # Read-only operations that should be in 'allow'
48
+ READ_ONLY_OPERATIONS = [
49
+ 'get', 'describe', 'logs', 'show', 'list', 'status',
50
+ 'diff', 'branch', 'top', 'version', 'config get', 'explain',
51
+ 'wait', 'check'
52
+ ]
53
+
54
+ # Dangerous operations that should be in 'deny'
55
+ DANGEROUS_OPERATIONS = [
56
+ 'delete', 'apply', 'destroy', 'create', 'patch', 'scale',
57
+ 'reset --hard', 'push --force', 'push -f', 'mv'
58
+ ]
59
+
60
+ # Operations that should require approval ('ask')
61
+ APPROVAL_OPERATIONS = [
62
+ 'Edit', 'Write', 'NotebookEdit', 'rm', 'rmdir',
63
+ 'install', 'upgrade', 'uninstall', 'rollback',
64
+ 'commit', 'push'
65
+ ]
66
+
67
+ def __init__(self, settings_path: str, settings_local_path: str = None):
68
+ self.settings_path = Path(settings_path)
69
+ self.settings_local_path = Path(settings_local_path) if settings_local_path else None
70
+ self.settings = self._load_json(self.settings_path)
71
+ self.settings_local = self._load_json(self.settings_local_path) if self.settings_local_path else {}
72
+ self.summary = ValidationSummary()
73
+
74
+ def _load_json(self, path: Path) -> Dict:
75
+ """Load JSON file"""
76
+ if not path or not path.exists():
77
+ return {}
78
+ with open(path, 'r') as f:
79
+ return json.load(f)
80
+
81
+ def _extract_operation(self, pattern: str) -> str:
82
+ """Extract the operation from a permission pattern"""
83
+ # Pattern format: Tool(operation:args) or Tool(*)
84
+ match = re.search(r'\(([^:)]+)', pattern)
85
+ if match:
86
+ return match.group(1).lower()
87
+ return ""
88
+
89
+ def _is_read_only_pattern(self, pattern: str) -> bool:
90
+ """Check if pattern matches read-only operations"""
91
+ pattern_lower = pattern.lower()
92
+ return any(op in pattern_lower for op in self.READ_ONLY_OPERATIONS)
93
+
94
+ def _is_dangerous_pattern(self, pattern: str) -> bool:
95
+ """Check if pattern matches dangerous operations"""
96
+ pattern_lower = pattern.lower()
97
+ return any(op in pattern_lower for op in self.DANGEROUS_OPERATIONS)
98
+
99
+ def _is_approval_pattern(self, pattern: str) -> bool:
100
+ """Check if pattern matches operations requiring approval"""
101
+ return any(op in pattern for op in self.APPROVAL_OPERATIONS)
102
+
103
+ def _generate_example_commands(self, pattern: str) -> List[str]:
104
+ """Generate example commands for a permission pattern"""
105
+ examples = []
106
+
107
+ if 'Read(*)' in pattern:
108
+ examples = [
109
+ 'Read("/path/to/file.txt")',
110
+ 'Read("/home/user/config.yaml")'
111
+ ]
112
+ elif 'Glob(*)' in pattern:
113
+ examples = [
114
+ 'Glob("**/*.py")',
115
+ 'Glob("src/**/*.ts")'
116
+ ]
117
+ elif 'Grep(*)' in pattern:
118
+ examples = [
119
+ 'Grep("pattern", "file.txt")',
120
+ 'Grep("error", "**/*.log")'
121
+ ]
122
+ elif 'kubectl get' in pattern:
123
+ examples = [
124
+ 'kubectl get pods -n default',
125
+ 'kubectl get services -A',
126
+ 'kubectl get deployments'
127
+ ]
128
+ elif 'kubectl describe' in pattern:
129
+ examples = [
130
+ 'kubectl describe pod my-pod',
131
+ 'kubectl describe service my-service'
132
+ ]
133
+ elif 'kubectl logs' in pattern:
134
+ examples = [
135
+ 'kubectl logs my-pod',
136
+ 'kubectl logs my-pod -f'
137
+ ]
138
+ elif 'kubectl delete' in pattern:
139
+ examples = [
140
+ 'kubectl delete pod my-pod',
141
+ 'kubectl delete deployment my-deployment'
142
+ ]
143
+ elif 'kubectl apply' in pattern:
144
+ examples = [
145
+ 'kubectl apply -f manifest.yaml',
146
+ 'kubectl apply -k ./kustomize'
147
+ ]
148
+ elif 'git status' in pattern:
149
+ examples = ['git status']
150
+ elif 'git diff' in pattern:
151
+ examples = ['git diff', 'git diff HEAD~1']
152
+ elif 'git commit' in pattern:
153
+ examples = ['git commit -m "feat: add feature"']
154
+ elif 'git push' in pattern and '--force' in pattern:
155
+ examples = ['git push --force', 'git push -f']
156
+ elif 'git push' in pattern:
157
+ examples = ['git push origin main']
158
+ elif 'Edit(*)' in pattern:
159
+ examples = [
160
+ 'Edit("file.py", "old_text", "new_text")',
161
+ 'Edit("config.yaml", "key: old", "key: new")'
162
+ ]
163
+ elif 'Write(*)' in pattern:
164
+ examples = [
165
+ 'Write("new_file.py", "content")',
166
+ 'Write("output.txt", "data")'
167
+ ]
168
+ elif 'terraform destroy' in pattern:
169
+ examples = ['terraform destroy', 'terraform destroy -auto-approve']
170
+ elif 'flux delete' in pattern:
171
+ examples = ['flux delete kustomization my-app']
172
+ elif 'helm install' in pattern:
173
+ examples = ['helm install my-release stable/nginx']
174
+ elif 'helm upgrade' in pattern:
175
+ examples = ['helm upgrade my-release stable/nginx']
176
+
177
+ return examples
178
+
179
+ def validate_allow_rules(self, permissions: Dict, source: str) -> List[PermissionValidationResult]:
180
+ """Validate 'allow' rules"""
181
+ results = []
182
+ allow_rules = permissions.get('allow', [])
183
+
184
+ for pattern in allow_rules:
185
+ is_valid = True
186
+ reason = "Valid read-only/query operation"
187
+
188
+ # Check if it's actually a dangerous operation
189
+ if self._is_dangerous_pattern(pattern):
190
+ is_valid = False
191
+ reason = "Dangerous operation in 'allow' section - should be in 'deny'"
192
+
193
+ # Check if it requires approval
194
+ elif self._is_approval_pattern(pattern) and 'Read' not in pattern and 'Glob' not in pattern and 'Grep' not in pattern:
195
+ is_valid = False
196
+ reason = "Operation requires approval - should be in 'ask'"
197
+
198
+ # Validate it's a read-only operation
199
+ elif not self._is_read_only_pattern(pattern) and pattern not in ['Read(*)', 'Glob(*)', 'Grep(*)', 'Task(*)']:
200
+ is_valid = False
201
+ reason = "Not a clear read-only operation - validate pattern"
202
+
203
+ examples = self._generate_example_commands(pattern)
204
+
205
+ result = PermissionValidationResult(
206
+ rule_type='allow',
207
+ pattern=pattern,
208
+ is_valid=is_valid,
209
+ reason=reason,
210
+ examples=examples,
211
+ file_source=source
212
+ )
213
+ results.append(result)
214
+
215
+ self.summary.allow_rules += 1
216
+ if is_valid:
217
+ self.summary.valid_rules += 1
218
+ else:
219
+ self.summary.invalid_rules += 1
220
+
221
+ return results
222
+
223
+ def validate_deny_rules(self, permissions: Dict, source: str) -> List[PermissionValidationResult]:
224
+ """Validate 'deny' rules"""
225
+ results = []
226
+ deny_rules = permissions.get('deny', [])
227
+
228
+ for pattern in deny_rules:
229
+ is_valid = True
230
+ reason = "Valid dangerous operation blocked"
231
+
232
+ # Check if it's actually a safe operation
233
+ if self._is_read_only_pattern(pattern):
234
+ is_valid = False
235
+ reason = "Read-only operation in 'deny' section - should be in 'allow'"
236
+
237
+ # Validate it's a dangerous operation
238
+ elif not self._is_dangerous_pattern(pattern):
239
+ is_valid = False
240
+ reason = "Not clearly a dangerous operation - validate pattern"
241
+
242
+ examples = self._generate_example_commands(pattern)
243
+
244
+ result = PermissionValidationResult(
245
+ rule_type='deny',
246
+ pattern=pattern,
247
+ is_valid=is_valid,
248
+ reason=reason,
249
+ examples=examples,
250
+ file_source=source
251
+ )
252
+ results.append(result)
253
+
254
+ self.summary.deny_rules += 1
255
+ if is_valid:
256
+ self.summary.valid_rules += 1
257
+ else:
258
+ self.summary.invalid_rules += 1
259
+
260
+ return results
261
+
262
+ def validate_ask_rules(self, permissions: Dict, source: str) -> List[PermissionValidationResult]:
263
+ """Validate 'ask' rules"""
264
+ results = []
265
+ ask_rules = permissions.get('ask', [])
266
+
267
+ for pattern in ask_rules:
268
+ is_valid = True
269
+ reason = "Valid operation requiring approval"
270
+
271
+ # Check if it's a dangerous operation that should be denied
272
+ if self._is_dangerous_pattern(pattern) and not any(op in pattern for op in ['commit', 'push', 'install', 'upgrade']):
273
+ is_valid = False
274
+ reason = "Too dangerous - should be in 'deny'"
275
+
276
+ # Check if it's a read-only operation that should be allowed
277
+ elif self._is_read_only_pattern(pattern):
278
+ is_valid = False
279
+ reason = "Read-only operation - should be in 'allow'"
280
+
281
+ examples = self._generate_example_commands(pattern)
282
+
283
+ result = PermissionValidationResult(
284
+ rule_type='ask',
285
+ pattern=pattern,
286
+ is_valid=is_valid,
287
+ reason=reason,
288
+ examples=examples,
289
+ file_source=source
290
+ )
291
+ results.append(result)
292
+
293
+ self.summary.ask_rules += 1
294
+ if is_valid:
295
+ self.summary.valid_rules += 1
296
+ else:
297
+ self.summary.invalid_rules += 1
298
+
299
+ return results
300
+
301
+ def validate_settings_philosophy(self) -> Tuple[bool, str]:
302
+ """
303
+ Validate that settings.json is strict and settings.local.json is more open
304
+
305
+ Returns:
306
+ (is_valid, reason)
307
+ """
308
+ if not self.settings_local:
309
+ return True, "No settings.local.json found - skipping philosophy check"
310
+
311
+ settings_perms = self.settings.get('permissions', {})
312
+ local_perms = self.settings_local.get('permissions', {})
313
+
314
+ # Check that local has more 'allow' rules (more open)
315
+ settings_allow = len(settings_perms.get('allow', []))
316
+ local_allow = len(local_perms.get('allow', []))
317
+
318
+ # Check that local has fewer 'deny' rules (more permissive)
319
+ settings_deny = len(settings_perms.get('deny', []))
320
+ local_deny = len(local_perms.get('deny', []))
321
+
322
+ issues = []
323
+
324
+ if local_allow <= settings_allow:
325
+ issues.append(f"settings.local.json should have MORE allow rules than settings.json ({local_allow} <= {settings_allow})")
326
+
327
+ if local_deny >= settings_deny:
328
+ issues.append(f"settings.local.json should have FEWER deny rules than settings.json ({local_deny} >= {settings_deny})")
329
+
330
+ if issues:
331
+ return False, "; ".join(issues)
332
+
333
+ return True, "Philosophy check passed: settings.local.json is more permissive"
334
+
335
+ def run_validation(self) -> ValidationSummary:
336
+ """Run complete validation"""
337
+ print("=" * 80)
338
+ print("PERMISSIONS VALIDATION TEST")
339
+ print("=" * 80)
340
+ print()
341
+
342
+ # Validate settings.json
343
+ print(f"📋 Validating: {self.settings_path}")
344
+ print("-" * 80)
345
+ settings_perms = self.settings.get('permissions', {})
346
+
347
+ self.summary.results.extend(self.validate_allow_rules(settings_perms, 'settings.json'))
348
+ self.summary.results.extend(self.validate_deny_rules(settings_perms, 'settings.json'))
349
+ self.summary.results.extend(self.validate_ask_rules(settings_perms, 'settings.json'))
350
+
351
+ # Validate settings.local.json if exists
352
+ if self.settings_local:
353
+ print(f"\n📋 Validating: {self.settings_local_path}")
354
+ print("-" * 80)
355
+ local_perms = self.settings_local.get('permissions', {})
356
+
357
+ self.summary.results.extend(self.validate_allow_rules(local_perms, 'settings.local.json'))
358
+ self.summary.results.extend(self.validate_deny_rules(local_perms, 'settings.local.json'))
359
+ self.summary.results.extend(self.validate_ask_rules(local_perms, 'settings.local.json'))
360
+
361
+ # Validate philosophy
362
+ print("\n" + "=" * 80)
363
+ print("PHILOSOPHY VALIDATION")
364
+ print("=" * 80)
365
+ philosophy_valid, philosophy_reason = self.validate_settings_philosophy()
366
+ print(f"{'✅' if philosophy_valid else '❌'} {philosophy_reason}")
367
+
368
+ self.summary.total_rules = len(self.summary.results)
369
+
370
+ return self.summary
371
+
372
+ def print_summary(self):
373
+ """Print validation summary"""
374
+ print("\n" + "=" * 80)
375
+ print("VALIDATION SUMMARY")
376
+ print("=" * 80)
377
+ print(f"Total rules validated: {self.summary.total_rules}")
378
+ print(f"Valid rules: {self.summary.valid_rules} ✅")
379
+ print(f"Invalid rules: {self.summary.invalid_rules} ❌")
380
+ print()
381
+ print(f"Allow rules: {self.summary.allow_rules}")
382
+ print(f"Deny rules: {self.summary.deny_rules}")
383
+ print(f"Ask rules: {self.summary.ask_rules}")
384
+ print()
385
+
386
+ # Print invalid rules
387
+ invalid_results = [r for r in self.summary.results if not r.is_valid]
388
+ if invalid_results:
389
+ print("⚠️ INVALID RULES FOUND:")
390
+ print("-" * 80)
391
+ for result in invalid_results:
392
+ print(f"[{result.file_source}] {result.rule_type.upper()}: {result.pattern}")
393
+ print(f" Reason: {result.reason}")
394
+ print()
395
+ else:
396
+ print("✅ All rules are valid!")
397
+
398
+ return self.summary.invalid_rules == 0
399
+
400
+ def generate_manual_validation_markdown(self, output_path: str):
401
+ """Generate markdown file with manual validation instructions"""
402
+ from datetime import datetime
403
+
404
+ output = []
405
+ output.append("# Manual Permissions Validation Guide")
406
+ output.append(f"\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
407
+ output.append("This guide provides step-by-step instructions to manually validate all permission rules.\n")
408
+
409
+ # Group by rule type
410
+ allow_results = [r for r in self.summary.results if r.rule_type == 'allow']
411
+ deny_results = [r for r in self.summary.results if r.rule_type == 'deny']
412
+ ask_results = [r for r in self.summary.results if r.rule_type == 'ask']
413
+
414
+ # ALLOW section
415
+ output.append("## 1. ALLOW Rules - Should Execute Automatically\n")
416
+ output.append("These commands should execute WITHOUT asking for approval.\n")
417
+ output.append("**Expected behavior:** Commands run immediately and return results.\n")
418
+
419
+ for i, result in enumerate(allow_results, 1):
420
+ output.append(f"### Test {i}: {result.pattern}")
421
+ output.append(f"**Source:** `{result.file_source}`")
422
+ output.append(f"**Status:** {'✅ Valid' if result.is_valid else '❌ Invalid'}")
423
+ if not result.is_valid:
424
+ output.append(f"**Issue:** {result.reason}")
425
+ output.append("\n**Example commands:**")
426
+ for example in result.examples:
427
+ output.append(f"```bash\n{example}\n```")
428
+ output.append("\n**Validation steps:**")
429
+ output.append("1. Execute the example command")
430
+ output.append("2. Verify it runs WITHOUT asking for approval")
431
+ output.append("3. Verify it returns results successfully")
432
+ output.append(f"4. Mark result: [ ] ✅ Pass | [ ] ❌ Fail\n")
433
+ output.append("---\n")
434
+
435
+ # DENY section
436
+ output.append("\n## 2. DENY Rules - Should Block Automatically\n")
437
+ output.append("These commands should be BLOCKED WITHOUT asking.\n")
438
+ output.append("**Expected behavior:** Commands are blocked with an error message.\n")
439
+
440
+ for i, result in enumerate(deny_results, 1):
441
+ output.append(f"### Test {i}: {result.pattern}")
442
+ output.append(f"**Source:** `{result.file_source}`")
443
+ output.append(f"**Status:** {'✅ Valid' if result.is_valid else '❌ Invalid'}")
444
+ if not result.is_valid:
445
+ output.append(f"**Issue:** {result.reason}")
446
+ output.append("\n**Example commands:**")
447
+ for example in result.examples:
448
+ output.append(f"```bash\n{example}\n```")
449
+ output.append("\n**Validation steps:**")
450
+ output.append("1. Execute the example command")
451
+ output.append("2. Verify it is BLOCKED immediately")
452
+ output.append("3. Verify an error message is shown")
453
+ output.append("4. Verify NO approval prompt is shown")
454
+ output.append(f"5. Mark result: [ ] ✅ Pass | [ ] ❌ Fail\n")
455
+ output.append("---\n")
456
+
457
+ # ASK section
458
+ output.append("\n## 3. ASK Rules - Should Prompt for Approval\n")
459
+ output.append("These commands should ASK for user approval before execution.\n")
460
+ output.append("**Expected behavior:** User is prompted to approve/deny before execution.\n")
461
+
462
+ for i, result in enumerate(ask_results, 1):
463
+ output.append(f"### Test {i}: {result.pattern}")
464
+ output.append(f"**Source:** `{result.file_source}`")
465
+ output.append(f"**Status:** {'✅ Valid' if result.is_valid else '❌ Invalid'}")
466
+ if not result.is_valid:
467
+ output.append(f"**Issue:** {result.reason}")
468
+ output.append("\n**Example commands:**")
469
+ for example in result.examples:
470
+ output.append(f"```bash\n{example}\n```")
471
+ output.append("\n**Validation steps:**")
472
+ output.append("1. Execute the example command")
473
+ output.append("2. Verify an approval prompt is shown")
474
+ output.append("3. Test DENY: Select 'No' and verify command is blocked")
475
+ output.append("4. Test APPROVE: Select 'Yes' and verify command executes")
476
+ output.append(f"5. Mark result: [ ] ✅ Pass | [ ] ❌ Fail\n")
477
+ output.append("---\n")
478
+
479
+ # Summary checklist
480
+ output.append("\n## Validation Summary Checklist\n")
481
+ output.append(f"- [ ] All {len(allow_results)} ALLOW rules execute automatically")
482
+ output.append(f"- [ ] All {len(deny_results)} DENY rules block automatically")
483
+ output.append(f"- [ ] All {len(ask_results)} ASK rules prompt for approval")
484
+ output.append("- [ ] settings.json is strict (standard operations)")
485
+ output.append("- [ ] settings.local.json is more open (query operations)")
486
+
487
+ # Write to file
488
+ with open(output_path, 'w') as f:
489
+ f.write('\n'.join(output))
490
+
491
+ print(f"\n📝 Manual validation guide generated: {output_path}")
492
+
493
+
494
+ def main():
495
+ """Main test execution"""
496
+ # Paths
497
+ base_path = Path("/home/jaguilar/aaxis/rnd/repositories/ops/.claude-shared")
498
+ settings_path = base_path / "settings.json"
499
+ settings_local_path = base_path / "settings.local.json"
500
+ output_path = Path(__file__).parent / "MANUAL_VALIDATION.md"
501
+
502
+ # Validate files exist
503
+ if not settings_path.exists():
504
+ print(f"❌ Error: {settings_path} not found")
505
+ sys.exit(1)
506
+
507
+ # Create validator
508
+ validator = PermissionsValidator(
509
+ str(settings_path),
510
+ str(settings_local_path) if settings_local_path.exists() else None
511
+ )
512
+
513
+ # Run validation
514
+ summary = validator.run_validation()
515
+
516
+ # Print results
517
+ all_valid = validator.print_summary()
518
+
519
+ # Generate manual validation guide
520
+ validator.generate_manual_validation_markdown(str(output_path))
521
+
522
+ # Exit with appropriate code
523
+ sys.exit(0 if all_valid else 1)
524
+
525
+
526
+ if __name__ == "__main__":
527
+ main()
File without changes