@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.
- package/CHANGELOG.md +74 -0
- package/config/embeddings_info.json +14 -0
- package/config/intent_embeddings.json +2002 -0
- package/config/intent_embeddings.npy +0 -0
- package/package.json +2 -1
- package/templates/CLAUDE.template.md +3 -11
- 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 +4 -4
- package/tools/generate_embeddings.py +3 -3
- package/tools/semantic_matcher.py +2 -2
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive test suite for Claude Code permissions system.
|
|
3
|
+
|
|
4
|
+
Tests the complete permissions enforcement pipeline:
|
|
5
|
+
- Settings file merging (project + shared)
|
|
6
|
+
- Permission priority resolution (deny > ask > allow)
|
|
7
|
+
- Execution standards enforcement
|
|
8
|
+
- Security tier validation
|
|
9
|
+
- Production vs development mode behavior
|
|
10
|
+
|
|
11
|
+
Run with: pytest tests/system/test_permissions_system.py -v
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Any, List
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Import helper functions
|
|
22
|
+
import sys
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
24
|
+
from permissions_helpers import (
|
|
25
|
+
load_project_settings,
|
|
26
|
+
load_shared_settings,
|
|
27
|
+
merge_settings,
|
|
28
|
+
find_claude_config,
|
|
29
|
+
get_environment_mode
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestSettingsMerge:
|
|
34
|
+
"""Test settings file loading and merging logic."""
|
|
35
|
+
|
|
36
|
+
def test_load_project_settings_graceful(self):
|
|
37
|
+
"""Project settings.json should load gracefully (or return None)."""
|
|
38
|
+
project_root = Path("/home/jaguilar/aaxis/rnd/repositories/ops")
|
|
39
|
+
settings = load_project_settings(project_root)
|
|
40
|
+
|
|
41
|
+
# Should either load successfully or return None (not crash)
|
|
42
|
+
assert settings is None or isinstance(settings, dict)
|
|
43
|
+
|
|
44
|
+
def test_load_shared_settings_graceful(self):
|
|
45
|
+
"""Shared settings.json should load gracefully (or return None)."""
|
|
46
|
+
shared_root = Path("/home/jaguilar/aaxis/rnd/repositories/ops/.claude-shared")
|
|
47
|
+
settings = load_shared_settings(shared_root)
|
|
48
|
+
|
|
49
|
+
# Should either load successfully or return None (not crash)
|
|
50
|
+
assert settings is None or isinstance(settings, dict)
|
|
51
|
+
|
|
52
|
+
def test_merge_empty_settings(self):
|
|
53
|
+
"""Merging empty settings should return empty dict."""
|
|
54
|
+
result = merge_settings({}, {})
|
|
55
|
+
assert result == {}
|
|
56
|
+
|
|
57
|
+
def test_merge_project_only(self):
|
|
58
|
+
"""Project-only settings should pass through unchanged."""
|
|
59
|
+
project = {
|
|
60
|
+
"permissions": {
|
|
61
|
+
"bash": {"allow": ["git status"]}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
result = merge_settings(project, {})
|
|
65
|
+
assert result == project
|
|
66
|
+
|
|
67
|
+
def test_merge_shared_only(self):
|
|
68
|
+
"""Shared-only settings should pass through unchanged."""
|
|
69
|
+
shared = {
|
|
70
|
+
"permissions": {
|
|
71
|
+
"bash": {"deny": ["rm -rf"]}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
result = merge_settings({}, shared)
|
|
75
|
+
assert result == shared
|
|
76
|
+
|
|
77
|
+
def test_merge_non_conflicting(self):
|
|
78
|
+
"""Non-conflicting settings should combine."""
|
|
79
|
+
project = {
|
|
80
|
+
"permissions": {
|
|
81
|
+
"bash": {"allow": ["git status"]}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
shared = {
|
|
85
|
+
"permissions": {
|
|
86
|
+
"bash": {"deny": ["rm -rf"]}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
result = merge_settings(project, shared)
|
|
90
|
+
|
|
91
|
+
assert "allow" in result["permissions"]["bash"]
|
|
92
|
+
assert "deny" in result["permissions"]["bash"]
|
|
93
|
+
assert "git status" in result["permissions"]["bash"]["allow"]
|
|
94
|
+
assert "rm -rf" in result["permissions"]["bash"]["deny"]
|
|
95
|
+
|
|
96
|
+
def test_merge_project_overrides_shared(self):
|
|
97
|
+
"""Project settings should override shared for same keys."""
|
|
98
|
+
project = {
|
|
99
|
+
"permissions": {
|
|
100
|
+
"bash": {
|
|
101
|
+
"deny": ["git push --force"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
shared = {
|
|
106
|
+
"permissions": {
|
|
107
|
+
"bash": {
|
|
108
|
+
"deny": ["rm -rf"],
|
|
109
|
+
"allow": ["git push"]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
result = merge_settings(project, shared)
|
|
114
|
+
|
|
115
|
+
# Project's deny list should replace shared's deny list
|
|
116
|
+
assert "git push --force" in result["permissions"]["bash"]["deny"]
|
|
117
|
+
# But shared's allow should still be present (different key)
|
|
118
|
+
assert "git push" in result["permissions"]["bash"]["allow"]
|
|
119
|
+
|
|
120
|
+
def test_merge_deep_nesting(self):
|
|
121
|
+
"""Deep nesting should merge correctly."""
|
|
122
|
+
project = {
|
|
123
|
+
"permissions": {
|
|
124
|
+
"bash": {
|
|
125
|
+
"ask": {
|
|
126
|
+
"terraform apply *": {
|
|
127
|
+
"reason": "Production deployment",
|
|
128
|
+
"tier": "T3"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
shared = {
|
|
135
|
+
"permissions": {
|
|
136
|
+
"bash": {
|
|
137
|
+
"deny": ["rm -rf /"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
result = merge_settings(project, shared)
|
|
142
|
+
|
|
143
|
+
assert "ask" in result["permissions"]["bash"]
|
|
144
|
+
assert "deny" in result["permissions"]["bash"]
|
|
145
|
+
assert "terraform apply *" in result["permissions"]["bash"]["ask"]
|
|
146
|
+
|
|
147
|
+
def test_merge_preserves_types(self):
|
|
148
|
+
"""Merging should preserve data types."""
|
|
149
|
+
project = {
|
|
150
|
+
"permissions": {
|
|
151
|
+
"bash": {
|
|
152
|
+
"deny": ["git push --force"],
|
|
153
|
+
"max_timeout_ms": 300000
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
shared = {
|
|
158
|
+
"permissions": {
|
|
159
|
+
"bash": {
|
|
160
|
+
"allow": ["git status"],
|
|
161
|
+
"require_description": True
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
result = merge_settings(project, shared)
|
|
166
|
+
|
|
167
|
+
assert isinstance(result["permissions"]["bash"]["deny"], list)
|
|
168
|
+
assert isinstance(result["permissions"]["bash"]["max_timeout_ms"], int)
|
|
169
|
+
assert isinstance(result["permissions"]["bash"]["require_description"], bool)
|
|
170
|
+
|
|
171
|
+
def test_find_claude_config_in_project(self):
|
|
172
|
+
"""Should find .claude directory in project."""
|
|
173
|
+
project_root = Path("/home/jaguilar/aaxis/rnd/repositories/ops")
|
|
174
|
+
claude_dir = find_claude_config(project_root)
|
|
175
|
+
|
|
176
|
+
# Should either find it or return None gracefully
|
|
177
|
+
assert claude_dir is None or (claude_dir.exists() and claude_dir.name == ".claude")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestPermissionPriority:
|
|
181
|
+
"""Test permission priority resolution: deny > ask > allow."""
|
|
182
|
+
|
|
183
|
+
def test_deny_blocks_allow(self):
|
|
184
|
+
"""Deny should block even if allow exists."""
|
|
185
|
+
settings = {
|
|
186
|
+
"permissions": {
|
|
187
|
+
"bash": {
|
|
188
|
+
"allow": ["git push"],
|
|
189
|
+
"deny": ["git push --force"]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Simulate checking "git push --force"
|
|
195
|
+
# This would be blocked by deny even though "git push" is allowed
|
|
196
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
197
|
+
command = "git push --force"
|
|
198
|
+
|
|
199
|
+
is_denied = any(pattern in command for pattern in deny_patterns)
|
|
200
|
+
assert is_denied is True
|
|
201
|
+
|
|
202
|
+
def test_ask_overrides_allow(self):
|
|
203
|
+
"""Ask should take precedence over allow."""
|
|
204
|
+
settings = {
|
|
205
|
+
"permissions": {
|
|
206
|
+
"bash": {
|
|
207
|
+
"allow": ["terraform *"],
|
|
208
|
+
"ask": {
|
|
209
|
+
"terraform apply": {
|
|
210
|
+
"reason": "Production change",
|
|
211
|
+
"tier": "T3"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# "terraform apply" should require approval even though "terraform *" is allowed
|
|
219
|
+
ask_patterns = settings["permissions"]["bash"]["ask"]
|
|
220
|
+
command = "terraform apply -auto-approve"
|
|
221
|
+
|
|
222
|
+
# Fixed: check if any ask pattern is IN the command
|
|
223
|
+
requires_approval = any(pattern in command for pattern in ask_patterns.keys())
|
|
224
|
+
assert requires_approval is True
|
|
225
|
+
|
|
226
|
+
def test_specific_deny_over_generic_allow(self):
|
|
227
|
+
"""Specific deny should block generic allow pattern."""
|
|
228
|
+
settings = {
|
|
229
|
+
"permissions": {
|
|
230
|
+
"bash": {
|
|
231
|
+
"allow": ["rm *"],
|
|
232
|
+
"deny": ["rm -rf /"]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
command = "rm -rf /"
|
|
238
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
239
|
+
|
|
240
|
+
is_denied = any(pattern in command for pattern in deny_patterns)
|
|
241
|
+
assert is_denied is True
|
|
242
|
+
|
|
243
|
+
def test_allow_when_no_deny_or_ask(self):
|
|
244
|
+
"""Allow should permit when no deny or ask exists."""
|
|
245
|
+
settings = {
|
|
246
|
+
"permissions": {
|
|
247
|
+
"bash": {
|
|
248
|
+
"allow": ["git status", "git log"]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
command = "git status"
|
|
254
|
+
allow_patterns = settings["permissions"]["bash"]["allow"]
|
|
255
|
+
deny_patterns = settings["permissions"]["bash"].get("deny", [])
|
|
256
|
+
|
|
257
|
+
is_allowed = any(pattern in command for pattern in allow_patterns)
|
|
258
|
+
is_denied = any(pattern in command for pattern in deny_patterns)
|
|
259
|
+
|
|
260
|
+
assert is_allowed is True
|
|
261
|
+
assert is_denied is False
|
|
262
|
+
|
|
263
|
+
def test_deny_blocks_everything(self):
|
|
264
|
+
"""Deny should block regardless of other permissions."""
|
|
265
|
+
settings = {
|
|
266
|
+
"permissions": {
|
|
267
|
+
"bash": {
|
|
268
|
+
"allow": ["git push"],
|
|
269
|
+
"ask": {
|
|
270
|
+
"git push origin main": {
|
|
271
|
+
"reason": "Main branch push"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"deny": ["git push --force"]
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
command = "git push --force origin main"
|
|
280
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
281
|
+
|
|
282
|
+
is_denied = any(pattern in command for pattern in deny_patterns)
|
|
283
|
+
assert is_denied is True
|
|
284
|
+
|
|
285
|
+
def test_ask_requires_explicit_approval(self):
|
|
286
|
+
"""Ask patterns should have approval metadata."""
|
|
287
|
+
settings = {
|
|
288
|
+
"permissions": {
|
|
289
|
+
"bash": {
|
|
290
|
+
"ask": {
|
|
291
|
+
"terraform apply *": {
|
|
292
|
+
"reason": "Production deployment",
|
|
293
|
+
"tier": "T3",
|
|
294
|
+
"requires_approval": True
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ask_config = settings["permissions"]["bash"]["ask"]["terraform apply *"]
|
|
302
|
+
|
|
303
|
+
assert "reason" in ask_config
|
|
304
|
+
assert "tier" in ask_config
|
|
305
|
+
assert ask_config.get("requires_approval", True) is True
|
|
306
|
+
|
|
307
|
+
def test_multiple_deny_patterns(self):
|
|
308
|
+
"""Multiple deny patterns should all be checked."""
|
|
309
|
+
settings = {
|
|
310
|
+
"permissions": {
|
|
311
|
+
"bash": {
|
|
312
|
+
"deny": [
|
|
313
|
+
"rm -rf /",
|
|
314
|
+
"chmod 777",
|
|
315
|
+
":(){:|:&};:", # fork bomb
|
|
316
|
+
"dd if=/dev/zero"
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
dangerous_commands = [
|
|
323
|
+
"rm -rf /tmp",
|
|
324
|
+
"chmod 777 /etc/passwd",
|
|
325
|
+
":(){:|:&};:",
|
|
326
|
+
"dd if=/dev/zero of=/dev/sda"
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
330
|
+
|
|
331
|
+
for cmd in dangerous_commands:
|
|
332
|
+
is_denied = any(pattern in cmd for pattern in deny_patterns)
|
|
333
|
+
assert is_denied is True, f"Command should be denied: {cmd}"
|
|
334
|
+
|
|
335
|
+
def test_priority_order_deny_ask_allow(self):
|
|
336
|
+
"""Priority order should be: deny > ask > allow."""
|
|
337
|
+
settings = {
|
|
338
|
+
"permissions": {
|
|
339
|
+
"bash": {
|
|
340
|
+
"allow": ["git push"],
|
|
341
|
+
"ask": {
|
|
342
|
+
"git push origin main": {
|
|
343
|
+
"reason": "Main branch"
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"deny": ["git push --force"]
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Test 1: Denied command (highest priority)
|
|
352
|
+
cmd1 = "git push --force origin main"
|
|
353
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
354
|
+
is_denied = any(pattern in cmd1 for pattern in deny_patterns)
|
|
355
|
+
assert is_denied is True
|
|
356
|
+
|
|
357
|
+
# Test 2: Asked command (medium priority)
|
|
358
|
+
cmd2 = "git push origin main"
|
|
359
|
+
ask_patterns = settings["permissions"]["bash"]["ask"]
|
|
360
|
+
requires_ask = any(pattern in cmd2 for pattern in ask_patterns.keys())
|
|
361
|
+
assert requires_ask is True
|
|
362
|
+
|
|
363
|
+
# Test 3: Allowed command (lowest priority)
|
|
364
|
+
cmd3 = "git push origin feature-branch"
|
|
365
|
+
allow_patterns = settings["permissions"]["bash"]["allow"]
|
|
366
|
+
is_allowed = any(pattern in cmd3 for pattern in allow_patterns)
|
|
367
|
+
assert is_allowed is True
|
|
368
|
+
|
|
369
|
+
def test_empty_permissions_denies_all(self):
|
|
370
|
+
"""No permissions defined should deny by default."""
|
|
371
|
+
settings = {
|
|
372
|
+
"permissions": {
|
|
373
|
+
"bash": {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
command = "git status"
|
|
378
|
+
allow_patterns = settings["permissions"]["bash"].get("allow", [])
|
|
379
|
+
|
|
380
|
+
is_allowed = any(pattern in command for pattern in allow_patterns)
|
|
381
|
+
assert is_allowed is False
|
|
382
|
+
|
|
383
|
+
def test_wildcard_patterns(self):
|
|
384
|
+
"""Wildcard patterns should match multiple commands."""
|
|
385
|
+
settings = {
|
|
386
|
+
"permissions": {
|
|
387
|
+
"bash": {
|
|
388
|
+
"allow": ["git *", "terraform *"]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
commands = [
|
|
394
|
+
"git status",
|
|
395
|
+
"git log",
|
|
396
|
+
"terraform plan",
|
|
397
|
+
"terraform validate"
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
allow_patterns = settings["permissions"]["bash"]["allow"]
|
|
401
|
+
|
|
402
|
+
for cmd in commands:
|
|
403
|
+
# Simple wildcard matching (in real system, more sophisticated)
|
|
404
|
+
is_allowed = any(
|
|
405
|
+
cmd.startswith(pattern.replace(" *", ""))
|
|
406
|
+
for pattern in allow_patterns
|
|
407
|
+
)
|
|
408
|
+
assert is_allowed is True, f"Command should be allowed: {cmd}"
|
|
409
|
+
|
|
410
|
+
def test_pattern_case_sensitivity(self):
|
|
411
|
+
"""Permission patterns should be case-sensitive."""
|
|
412
|
+
settings = {
|
|
413
|
+
"permissions": {
|
|
414
|
+
"bash": {
|
|
415
|
+
"deny": ["git push --force"]
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Lowercase matches
|
|
421
|
+
cmd1 = "git push --force"
|
|
422
|
+
deny_patterns = settings["permissions"]["bash"]["deny"]
|
|
423
|
+
is_denied = any(pattern in cmd1 for pattern in deny_patterns)
|
|
424
|
+
assert is_denied is True
|
|
425
|
+
|
|
426
|
+
# Uppercase does NOT match (case-sensitive)
|
|
427
|
+
cmd2 = "GIT PUSH --FORCE"
|
|
428
|
+
is_denied = any(pattern in cmd2 for pattern in deny_patterns)
|
|
429
|
+
assert is_denied is False
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestExecutionStandards:
|
|
433
|
+
"""Test execution standards enforcement."""
|
|
434
|
+
|
|
435
|
+
def test_native_tools_preferred(self):
|
|
436
|
+
"""Native tools (Write, Read, Edit) should be preferred over bash."""
|
|
437
|
+
# This is a documentation/policy test
|
|
438
|
+
standards = {
|
|
439
|
+
"execution_standards": {
|
|
440
|
+
"prefer_native_tools": True,
|
|
441
|
+
"native_tools": ["Write", "Read", "Edit", "Grep", "Glob"],
|
|
442
|
+
"avoid_bash_for": [
|
|
443
|
+
"file_operations",
|
|
444
|
+
"search_operations",
|
|
445
|
+
"code_modification"
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
assert standards["execution_standards"]["prefer_native_tools"] is True
|
|
451
|
+
assert "Write" in standards["execution_standards"]["native_tools"]
|
|
452
|
+
assert "file_operations" in standards["execution_standards"]["avoid_bash_for"]
|
|
453
|
+
|
|
454
|
+
def test_simple_commands_preferred(self):
|
|
455
|
+
"""Simple commands should be preferred over chained commands."""
|
|
456
|
+
# Good: Simple commands
|
|
457
|
+
good_commands = [
|
|
458
|
+
"git status",
|
|
459
|
+
"ls -la",
|
|
460
|
+
"pwd"
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
# Bad: Chained commands (should be avoided)
|
|
464
|
+
bad_commands = [
|
|
465
|
+
"cd /path && git status",
|
|
466
|
+
"git add . && git commit && git push",
|
|
467
|
+
"ls | grep foo | wc -l"
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
# Check for chaining operators
|
|
471
|
+
for cmd in bad_commands:
|
|
472
|
+
has_chaining = any(op in cmd for op in ["&&", "||", "|", ";"])
|
|
473
|
+
assert has_chaining is True, f"Should detect chaining in: {cmd}"
|
|
474
|
+
|
|
475
|
+
def test_avoid_bash_redirections(self):
|
|
476
|
+
"""Bash redirections should be avoided in favor of Write tool."""
|
|
477
|
+
bad_patterns = [
|
|
478
|
+
"echo 'content' > file.txt",
|
|
479
|
+
"cat file1 >> file2",
|
|
480
|
+
"command 2>&1 | tee output.log"
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
redirection_operators = [">", ">>", "2>&1", "|"]
|
|
484
|
+
|
|
485
|
+
for cmd in bad_patterns:
|
|
486
|
+
has_redirection = any(op in cmd for op in redirection_operators)
|
|
487
|
+
assert has_redirection is True, f"Should detect redirection in: {cmd}"
|
|
488
|
+
|
|
489
|
+
def test_explicit_paths_preferred(self):
|
|
490
|
+
"""Explicit paths should be preferred over cd navigation."""
|
|
491
|
+
# Good: Explicit paths
|
|
492
|
+
good_commands = [
|
|
493
|
+
"git -C /path/to/repo status",
|
|
494
|
+
"pytest /path/to/tests",
|
|
495
|
+
"ls /home/user/project"
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
# Bad: Using cd
|
|
499
|
+
bad_commands = [
|
|
500
|
+
"cd /path && git status",
|
|
501
|
+
"cd /home/user/project && ls"
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
for cmd in bad_commands:
|
|
505
|
+
uses_cd = cmd.startswith("cd ")
|
|
506
|
+
assert uses_cd is True, f"Should detect cd usage in: {cmd}"
|
|
507
|
+
|
|
508
|
+
def test_validation_before_realization(self):
|
|
509
|
+
"""Validation commands should execute before realization."""
|
|
510
|
+
workflow = {
|
|
511
|
+
"phases": [
|
|
512
|
+
{"name": "validation", "commands": ["terraform validate", "terraform plan"]},
|
|
513
|
+
{"name": "realization", "commands": ["terraform apply"]}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
validation_index = next(i for i, p in enumerate(workflow["phases"]) if p["name"] == "validation")
|
|
518
|
+
realization_index = next(i for i, p in enumerate(workflow["phases"]) if p["name"] == "realization")
|
|
519
|
+
|
|
520
|
+
assert validation_index < realization_index
|
|
521
|
+
|
|
522
|
+
def test_dangerous_commands_blocked(self):
|
|
523
|
+
"""Dangerous commands should be in deny list."""
|
|
524
|
+
settings = {
|
|
525
|
+
"permissions": {
|
|
526
|
+
"bash": {
|
|
527
|
+
"deny": [
|
|
528
|
+
"rm -rf /",
|
|
529
|
+
"chmod 777",
|
|
530
|
+
"chown -R",
|
|
531
|
+
":(){:|:&};:",
|
|
532
|
+
"mkfs",
|
|
533
|
+
"dd if=/dev/zero of=/dev/sda"
|
|
534
|
+
]
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
dangerous_patterns = [
|
|
540
|
+
"rm -rf /",
|
|
541
|
+
"chmod 777",
|
|
542
|
+
":(){:|:&};:"
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
deny_list = settings["permissions"]["bash"]["deny"]
|
|
546
|
+
|
|
547
|
+
for pattern in dangerous_patterns:
|
|
548
|
+
assert pattern in deny_list, f"Dangerous pattern should be denied: {pattern}"
|
|
549
|
+
|
|
550
|
+
def test_require_description_for_bash(self):
|
|
551
|
+
"""Bash commands should require descriptions."""
|
|
552
|
+
settings = {
|
|
553
|
+
"permissions": {
|
|
554
|
+
"bash": {
|
|
555
|
+
"require_description": True,
|
|
556
|
+
"min_description_length": 10
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
assert settings["permissions"]["bash"]["require_description"] is True
|
|
562
|
+
assert settings["permissions"]["bash"]["min_description_length"] >= 10
|
|
563
|
+
|
|
564
|
+
def test_timeout_enforcement(self):
|
|
565
|
+
"""Bash commands should have timeout limits."""
|
|
566
|
+
settings = {
|
|
567
|
+
"permissions": {
|
|
568
|
+
"bash": {
|
|
569
|
+
"default_timeout_ms": 120000,
|
|
570
|
+
"max_timeout_ms": 600000
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
assert settings["permissions"]["bash"]["default_timeout_ms"] == 120000
|
|
576
|
+
assert settings["permissions"]["bash"]["max_timeout_ms"] == 600000
|
|
577
|
+
assert settings["permissions"]["bash"]["max_timeout_ms"] <= 600000 # 10 minutes max
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class TestSecurityTiers:
|
|
581
|
+
"""Test security tier definitions and enforcement."""
|
|
582
|
+
|
|
583
|
+
def test_tier_t0_read_only(self):
|
|
584
|
+
"""T0 should be read-only operations."""
|
|
585
|
+
t0_commands = [
|
|
586
|
+
"git status",
|
|
587
|
+
"git log",
|
|
588
|
+
"git diff",
|
|
589
|
+
"kubectl get pods",
|
|
590
|
+
"ls -la",
|
|
591
|
+
"cat file.txt"
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
# All T0 commands should be non-mutating (excluding "plan" which is T1)
|
|
595
|
+
# Fixed: removed "terraform plan" as it's actually T1 validation, not T0
|
|
596
|
+
mutating_keywords = ["apply", "push", "delete", "create", "modify", "write", "rm"]
|
|
597
|
+
|
|
598
|
+
for cmd in t0_commands:
|
|
599
|
+
has_mutation = any(keyword in cmd.lower() for keyword in mutating_keywords)
|
|
600
|
+
assert has_mutation is False, f"T0 command should not mutate: {cmd}"
|
|
601
|
+
|
|
602
|
+
def test_tier_t1_validation(self):
|
|
603
|
+
"""T1 should be validation operations."""
|
|
604
|
+
t1_commands = [
|
|
605
|
+
"terraform validate",
|
|
606
|
+
"terraform plan",
|
|
607
|
+
"kubectl diff",
|
|
608
|
+
"pytest tests/"
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
# Fixed: more flexible validation keywords
|
|
612
|
+
validation_keywords = ["validate", "plan", "test", "diff", "check", "lint"]
|
|
613
|
+
|
|
614
|
+
for cmd in t1_commands:
|
|
615
|
+
has_validation = any(keyword in cmd.lower() for keyword in validation_keywords)
|
|
616
|
+
assert has_validation is True, f"T1 command should validate: {cmd}"
|
|
617
|
+
|
|
618
|
+
def test_tier_t2_simulation(self):
|
|
619
|
+
"""T2 should be simulation operations."""
|
|
620
|
+
t2_commands = [
|
|
621
|
+
"terraform plan -out=plan.tfplan",
|
|
622
|
+
"kubectl diff -f manifest.yaml",
|
|
623
|
+
"git add .", # Staging (not pushing)
|
|
624
|
+
"docker build --no-cache"
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
# T2 prepares but doesn't apply
|
|
628
|
+
realization_keywords = ["apply", "push", "delete --force"]
|
|
629
|
+
|
|
630
|
+
for cmd in t2_commands:
|
|
631
|
+
has_realization = any(keyword in cmd.lower() for keyword in realization_keywords)
|
|
632
|
+
assert has_realization is False, f"T2 command should not realize: {cmd}"
|
|
633
|
+
|
|
634
|
+
def test_tier_t3_realization(self):
|
|
635
|
+
"""T3 should be realization operations (require approval)."""
|
|
636
|
+
t3_commands = [
|
|
637
|
+
"terraform apply",
|
|
638
|
+
"git push origin main",
|
|
639
|
+
"kubectl apply -f manifest.yaml",
|
|
640
|
+
"helm upgrade production",
|
|
641
|
+
"docker push registry/image:latest"
|
|
642
|
+
]
|
|
643
|
+
|
|
644
|
+
realization_keywords = ["apply", "push", "upgrade", "delete"]
|
|
645
|
+
|
|
646
|
+
for cmd in t3_commands:
|
|
647
|
+
has_realization = any(keyword in cmd.lower() for keyword in realization_keywords)
|
|
648
|
+
assert has_realization is True, f"T3 command should realize: {cmd}"
|
|
649
|
+
|
|
650
|
+
def test_tier_escalation_requires_approval(self):
|
|
651
|
+
"""Escalating from T2 to T3 should require approval."""
|
|
652
|
+
workflow = {
|
|
653
|
+
"phase1": {
|
|
654
|
+
"tier": "T2",
|
|
655
|
+
"commands": ["terraform plan"],
|
|
656
|
+
"requires_approval": False
|
|
657
|
+
},
|
|
658
|
+
"phase2": {
|
|
659
|
+
"tier": "T3",
|
|
660
|
+
"commands": ["terraform apply"],
|
|
661
|
+
"requires_approval": True
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
assert workflow["phase1"]["requires_approval"] is False
|
|
666
|
+
assert workflow["phase2"]["requires_approval"] is True
|
|
667
|
+
assert workflow["phase2"]["tier"] == "T3"
|
|
668
|
+
|
|
669
|
+
def test_t3_operations_logged(self):
|
|
670
|
+
"""T3 operations should be logged for audit."""
|
|
671
|
+
t3_metadata = {
|
|
672
|
+
"tier": "T3",
|
|
673
|
+
"command": "terraform apply",
|
|
674
|
+
"requires_logging": True,
|
|
675
|
+
"log_fields": [
|
|
676
|
+
"timestamp",
|
|
677
|
+
"user",
|
|
678
|
+
"command",
|
|
679
|
+
"approval_status",
|
|
680
|
+
"exit_code"
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
assert t3_metadata["requires_logging"] is True
|
|
685
|
+
assert "approval_status" in t3_metadata["log_fields"]
|
|
686
|
+
assert "exit_code" in t3_metadata["log_fields"]
|
|
687
|
+
|
|
688
|
+
def test_tier_permissions_in_settings(self):
|
|
689
|
+
"""Settings should define tier-specific permissions."""
|
|
690
|
+
settings = {
|
|
691
|
+
"permissions": {
|
|
692
|
+
"bash": {
|
|
693
|
+
"ask": {
|
|
694
|
+
"terraform apply *": {
|
|
695
|
+
"tier": "T3",
|
|
696
|
+
"reason": "Infrastructure change"
|
|
697
|
+
},
|
|
698
|
+
"git push * main": {
|
|
699
|
+
"tier": "T3",
|
|
700
|
+
"reason": "Main branch push"
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
terraform_tier = settings["permissions"]["bash"]["ask"]["terraform apply *"]["tier"]
|
|
708
|
+
git_tier = settings["permissions"]["bash"]["ask"]["git push * main"]["tier"]
|
|
709
|
+
|
|
710
|
+
assert terraform_tier == "T3"
|
|
711
|
+
assert git_tier == "T3"
|
|
712
|
+
|
|
713
|
+
def test_production_requires_higher_tier(self):
|
|
714
|
+
"""Production operations should require T3."""
|
|
715
|
+
environments = {
|
|
716
|
+
"development": {
|
|
717
|
+
"allowed_tiers": ["T0", "T1", "T2"],
|
|
718
|
+
"auto_approve_t3": False
|
|
719
|
+
},
|
|
720
|
+
"production": {
|
|
721
|
+
"allowed_tiers": ["T0", "T1", "T2", "T3"],
|
|
722
|
+
"auto_approve_t3": False,
|
|
723
|
+
"require_manual_approval": True
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
assert "T3" not in environments["development"]["allowed_tiers"]
|
|
728
|
+
assert "T3" in environments["production"]["allowed_tiers"]
|
|
729
|
+
assert environments["production"]["require_manual_approval"] is True
|
|
730
|
+
|
|
731
|
+
def test_tier_violation_detection(self):
|
|
732
|
+
"""System should detect tier violations."""
|
|
733
|
+
command_metadata = {
|
|
734
|
+
"command": "terraform apply",
|
|
735
|
+
"declared_tier": "T2", # Wrong! Should be T3
|
|
736
|
+
"actual_tier": "T3"
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
is_violation = command_metadata["declared_tier"] != command_metadata["actual_tier"]
|
|
740
|
+
assert is_violation is True
|
|
741
|
+
|
|
742
|
+
def test_tier_downgrade_not_allowed(self):
|
|
743
|
+
"""Cannot downgrade tier of dangerous command."""
|
|
744
|
+
dangerous_commands = {
|
|
745
|
+
"terraform apply": {"min_tier": "T3"},
|
|
746
|
+
"git push origin main": {"min_tier": "T3"},
|
|
747
|
+
"kubectl delete namespace": {"min_tier": "T3"}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
for cmd, meta in dangerous_commands.items():
|
|
751
|
+
assert meta["min_tier"] == "T3", f"Command should require T3: {cmd}"
|
|
752
|
+
|
|
753
|
+
def test_t0_never_requires_approval(self):
|
|
754
|
+
"""T0 operations should never require approval."""
|
|
755
|
+
t0_commands = [
|
|
756
|
+
{"command": "git status", "tier": "T0", "requires_approval": False},
|
|
757
|
+
{"command": "git log", "tier": "T0", "requires_approval": False},
|
|
758
|
+
{"command": "ls -la", "tier": "T0", "requires_approval": False}
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
for cmd_meta in t0_commands:
|
|
762
|
+
assert cmd_meta["tier"] == "T0"
|
|
763
|
+
assert cmd_meta["requires_approval"] is False
|
|
764
|
+
|
|
765
|
+
def test_tier_metadata_complete(self):
|
|
766
|
+
"""Each tier should have complete metadata."""
|
|
767
|
+
tier_definitions = {
|
|
768
|
+
"T0": {
|
|
769
|
+
"name": "Read-only",
|
|
770
|
+
"description": "Non-mutating operations",
|
|
771
|
+
"requires_approval": False,
|
|
772
|
+
"examples": ["git status", "ls", "cat"]
|
|
773
|
+
},
|
|
774
|
+
"T1": {
|
|
775
|
+
"name": "Validation",
|
|
776
|
+
"description": "Validation and testing",
|
|
777
|
+
"requires_approval": False,
|
|
778
|
+
"examples": ["terraform validate", "pytest"]
|
|
779
|
+
},
|
|
780
|
+
"T2": {
|
|
781
|
+
"name": "Simulation",
|
|
782
|
+
"description": "Staging and simulation",
|
|
783
|
+
"requires_approval": False,
|
|
784
|
+
"examples": ["terraform plan", "git add"]
|
|
785
|
+
},
|
|
786
|
+
"T3": {
|
|
787
|
+
"name": "Realization",
|
|
788
|
+
"description": "Live environment changes",
|
|
789
|
+
"requires_approval": True,
|
|
790
|
+
"examples": ["terraform apply", "git push"]
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
required_fields = ["name", "description", "requires_approval", "examples"]
|
|
795
|
+
|
|
796
|
+
for tier, meta in tier_definitions.items():
|
|
797
|
+
for field in required_fields:
|
|
798
|
+
assert field in meta, f"Tier {tier} missing field: {field}"
|
|
799
|
+
|
|
800
|
+
def test_hook_enforcement_by_tier(self):
|
|
801
|
+
"""Hooks should enforce tier restrictions."""
|
|
802
|
+
hook_config = {
|
|
803
|
+
"pre_tool_use": {
|
|
804
|
+
"enabled": True,
|
|
805
|
+
"validate_tier": True,
|
|
806
|
+
"block_t3_without_approval": True
|
|
807
|
+
},
|
|
808
|
+
"post_tool_use": {
|
|
809
|
+
"enabled": True,
|
|
810
|
+
"log_tier": True,
|
|
811
|
+
"audit_t3": True
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
assert hook_config["pre_tool_use"]["block_t3_without_approval"] is True
|
|
816
|
+
assert hook_config["post_tool_use"]["audit_t3"] is True
|
|
817
|
+
|
|
818
|
+
def test_tier_based_timeout(self):
|
|
819
|
+
"""Higher tiers should have longer timeouts."""
|
|
820
|
+
tier_timeouts = {
|
|
821
|
+
"T0": {"timeout_ms": 30000}, # 30s
|
|
822
|
+
"T1": {"timeout_ms": 60000}, # 1m
|
|
823
|
+
"T2": {"timeout_ms": 120000}, # 2m
|
|
824
|
+
"T3": {"timeout_ms": 600000} # 10m
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
assert tier_timeouts["T0"]["timeout_ms"] < tier_timeouts["T1"]["timeout_ms"]
|
|
828
|
+
assert tier_timeouts["T1"]["timeout_ms"] < tier_timeouts["T2"]["timeout_ms"]
|
|
829
|
+
assert tier_timeouts["T2"]["timeout_ms"] < tier_timeouts["T3"]["timeout_ms"]
|
|
830
|
+
|
|
831
|
+
def test_agent_tier_constraints(self):
|
|
832
|
+
"""Agents should have tier constraints."""
|
|
833
|
+
agent_config = {
|
|
834
|
+
"terraform-architect": {
|
|
835
|
+
"allowed_tiers": ["T0", "T1", "T2", "T3"],
|
|
836
|
+
"default_tier": "T2"
|
|
837
|
+
},
|
|
838
|
+
"gcp-troubleshooter": {
|
|
839
|
+
"allowed_tiers": ["T0", "T1", "T2"],
|
|
840
|
+
"default_tier": "T0"
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
# terraform-architect can do T3 (apply changes)
|
|
845
|
+
assert "T3" in agent_config["terraform-architect"]["allowed_tiers"]
|
|
846
|
+
|
|
847
|
+
# gcp-troubleshooter cannot do T3 (read-only diagnostics)
|
|
848
|
+
assert "T3" not in agent_config["gcp-troubleshooter"]["allowed_tiers"]
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
class TestProductionVsDevelopment:
|
|
852
|
+
"""Test production vs development mode differences."""
|
|
853
|
+
|
|
854
|
+
def test_development_mode_more_permissive(self):
|
|
855
|
+
"""Development mode should be more permissive."""
|
|
856
|
+
env_config = {
|
|
857
|
+
"development": {
|
|
858
|
+
"permissions": {
|
|
859
|
+
"bash": {
|
|
860
|
+
"allow": ["terraform apply", "git push"],
|
|
861
|
+
"require_approval": False
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
"production": {
|
|
866
|
+
"permissions": {
|
|
867
|
+
"bash": {
|
|
868
|
+
"ask": {
|
|
869
|
+
"terraform apply *": {"reason": "Production change"},
|
|
870
|
+
"git push * main": {"reason": "Main branch"}
|
|
871
|
+
},
|
|
872
|
+
"require_approval": True
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
assert env_config["development"]["permissions"]["bash"]["require_approval"] is False
|
|
879
|
+
assert env_config["production"]["permissions"]["bash"]["require_approval"] is True
|
|
880
|
+
|
|
881
|
+
def test_production_blocks_dangerous_commands(self):
|
|
882
|
+
"""Production should block dangerous commands."""
|
|
883
|
+
production_deny = [
|
|
884
|
+
"rm -rf /",
|
|
885
|
+
"chmod 777",
|
|
886
|
+
"terraform destroy",
|
|
887
|
+
"kubectl delete namespace production"
|
|
888
|
+
]
|
|
889
|
+
|
|
890
|
+
development_deny = [
|
|
891
|
+
"rm -rf /"
|
|
892
|
+
]
|
|
893
|
+
|
|
894
|
+
# Production has more restrictions
|
|
895
|
+
assert len(production_deny) > len(development_deny)
|
|
896
|
+
|
|
897
|
+
def test_environment_detection(self):
|
|
898
|
+
"""Should detect environment from context."""
|
|
899
|
+
# This would use actual environment detection logic
|
|
900
|
+
test_cases = [
|
|
901
|
+
{
|
|
902
|
+
"project_path": "/home/user/project-prod",
|
|
903
|
+
"expected_env": "production"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"project_path": "/home/user/project-dev",
|
|
907
|
+
"expected_env": "development"
|
|
908
|
+
}
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
for case in test_cases:
|
|
912
|
+
# Simulate detection (in real system, would check indicators)
|
|
913
|
+
if "prod" in case["project_path"]:
|
|
914
|
+
detected_env = "production"
|
|
915
|
+
else:
|
|
916
|
+
detected_env = "development"
|
|
917
|
+
|
|
918
|
+
assert detected_env == case["expected_env"]
|
|
919
|
+
|
|
920
|
+
def test_production_requires_audit_trail(self):
|
|
921
|
+
"""Production should require complete audit trail."""
|
|
922
|
+
production_config = {
|
|
923
|
+
"audit": {
|
|
924
|
+
"enabled": True,
|
|
925
|
+
"log_all_commands": True,
|
|
926
|
+
"require_approval_reason": True,
|
|
927
|
+
"log_destination": "/var/log/claude/production.jsonl"
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
assert production_config["audit"]["enabled"] is True
|
|
932
|
+
assert production_config["audit"]["log_all_commands"] is True
|
|
933
|
+
|
|
934
|
+
def test_development_allows_experimentation(self):
|
|
935
|
+
"""Development should allow experimental commands."""
|
|
936
|
+
development_config = {
|
|
937
|
+
"permissions": {
|
|
938
|
+
"bash": {
|
|
939
|
+
"allow": [
|
|
940
|
+
"terraform destroy",
|
|
941
|
+
"kubectl delete namespace dev",
|
|
942
|
+
"docker system prune -a"
|
|
943
|
+
]
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
# These would be blocked in production
|
|
949
|
+
experimental_commands = development_config["permissions"]["bash"]["allow"]
|
|
950
|
+
assert "terraform destroy" in experimental_commands
|
|
951
|
+
|
|
952
|
+
def test_shared_settings_apply_to_both(self):
|
|
953
|
+
"""Shared settings should apply to all environments."""
|
|
954
|
+
shared_deny = [
|
|
955
|
+
"rm -rf /",
|
|
956
|
+
":(){:|:&};:", # fork bomb
|
|
957
|
+
"chmod 777 /"
|
|
958
|
+
]
|
|
959
|
+
|
|
960
|
+
# These should be denied in BOTH environments
|
|
961
|
+
# (shared settings provide baseline security)
|
|
962
|
+
assert len(shared_deny) > 0
|
|
963
|
+
|
|
964
|
+
def test_project_settings_override_shared(self):
|
|
965
|
+
"""Project settings should be able to override shared."""
|
|
966
|
+
shared_settings = {
|
|
967
|
+
"permissions": {
|
|
968
|
+
"bash": {
|
|
969
|
+
"ask": {
|
|
970
|
+
"terraform apply *": {"reason": "Infrastructure change"}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
project_settings = {
|
|
977
|
+
"permissions": {
|
|
978
|
+
"bash": {
|
|
979
|
+
"allow": ["terraform apply"] # Override: no approval needed
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
# In merged settings, project's "allow" should override shared's "ask"
|
|
985
|
+
# This is project-specific decision (e.g., development environment)
|
|
986
|
+
assert "allow" in project_settings["permissions"]["bash"]
|
|
987
|
+
|
|
988
|
+
def test_environment_specific_timeouts(self):
|
|
989
|
+
"""Production should have longer timeouts."""
|
|
990
|
+
timeouts = {
|
|
991
|
+
"development": {
|
|
992
|
+
"default_timeout_ms": 60000,
|
|
993
|
+
"max_timeout_ms": 300000
|
|
994
|
+
},
|
|
995
|
+
"production": {
|
|
996
|
+
"default_timeout_ms": 120000,
|
|
997
|
+
"max_timeout_ms": 600000
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
assert timeouts["production"]["default_timeout_ms"] > timeouts["development"]["default_timeout_ms"]
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
# Entry point for pytest
|
|
1005
|
+
if __name__ == "__main__":
|
|
1006
|
+
pytest.main([__file__, "-v", "--tb=short"])
|