@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.
- package/CHANGELOG.md +137 -1
- package/README.en.md +29 -23
- package/README.md +24 -17
- package/agents/{claude-architect.md → gaia.md} +6 -6
- package/commands/{architect.md → gaia.md} +6 -6
- package/config/AGENTS.md +5 -5
- package/config/agent-catalog.md +14 -14
- package/config/context-contracts.md +4 -4
- package/config/embeddings_info.json +14 -0
- package/config/intent_embeddings.json +2002 -0
- package/config/intent_embeddings.npy +0 -0
- package/index.js +3 -1
- package/package.json +3 -2
- package/speckit/README.en.md +20 -69
- package/templates/CLAUDE.template.md +5 -13
- package/tests/README.en.md +224 -0
- package/tests/README.md +338 -0
- package/tests/fixtures/project-context.aws.json +53 -0
- package/tests/fixtures/project-context.gcp.json +53 -0
- package/tests/integration/RUN_TESTS.md +185 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/test_hooks_integration.py +473 -0
- package/tests/integration/test_hooks_workflow.py +397 -0
- package/tests/permissions-validation/MANUAL_VALIDATION.md +434 -0
- package/tests/permissions-validation/test_permissions_validation.py +527 -0
- package/tests/system/__init__.py +0 -0
- package/tests/system/permissions_helpers.py +318 -0
- package/tests/system/test_agent_definitions.py +166 -0
- package/tests/system/test_configuration_files.py +121 -0
- package/tests/system/test_directory_structure.py +231 -0
- package/tests/system/test_permissions_system.py +1006 -0
- package/tests/tools/__init__.py +0 -0
- package/tests/tools/test_agent_router.py +266 -0
- package/tests/tools/test_clarify_engine.py +413 -0
- package/tests/tools/test_context_provider.py +157 -0
- package/tests/validators/__init__.py +0 -0
- package/tests/validators/test_approval_gate.py +415 -0
- package/tests/validators/test_commit_validator.py +446 -0
- package/tools/context_provider.py +28 -7
- package/tools/generate_embeddings.py +3 -3
- 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
|