scc-cli 1.4.1__py3-none-any.whl
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.
Potentially problematic release.
This version of scc-cli might be problematic. Click here for more details.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +259 -0
- scc_cli/cli_admin.py +706 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/exit_codes.py +55 -0
- scc_cli/git.py +1521 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +588 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +382 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +538 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +675 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/validate.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema validation for organization configs.
|
|
3
|
+
|
|
4
|
+
Provide offline-capable validation using bundled JSON schemas.
|
|
5
|
+
Treat $schema field as documentation, not something to fetch at runtime.
|
|
6
|
+
|
|
7
|
+
Key functions:
|
|
8
|
+
- validate_org_config(): Validate org config against bundled schema (org-v1.schema.json)
|
|
9
|
+
- validate_config_invariants(): Validate governance invariants (enabled ⊆ allowed, enabled ∩ blocked = ∅)
|
|
10
|
+
- check_version_compatibility(): Unified version compatibility gate
|
|
11
|
+
- check_schema_version(): Check schema version compatibility
|
|
12
|
+
- check_min_cli_version(): Check CLI meets minimum version requirement
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from importlib.resources import files
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
21
|
+
|
|
22
|
+
from jsonschema import Draft7Validator
|
|
23
|
+
|
|
24
|
+
from .constants import CLI_VERSION, CURRENT_SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
# Invariant Validation Types
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class InvariantViolation:
|
|
37
|
+
"""Result of a config invariant check.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
rule: The invariant rule that was violated (e.g., "enabled_must_be_allowed").
|
|
41
|
+
message: Human-readable description of the violation.
|
|
42
|
+
severity: "error" for hard failures, "warning" for advisory.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
rule: str
|
|
46
|
+
message: str
|
|
47
|
+
severity: Literal["error", "warning"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
# Compatibility Result Types
|
|
52
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class VersionCompatibility:
|
|
57
|
+
"""Result of version compatibility check.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
compatible: Whether the config is usable with this CLI.
|
|
61
|
+
blocking_error: Error message if not compatible (requires upgrade).
|
|
62
|
+
warnings: Non-blocking warnings (e.g., newer minor version).
|
|
63
|
+
schema_version: Detected schema version from config.
|
|
64
|
+
min_cli_version: Minimum CLI version from config, if specified.
|
|
65
|
+
current_cli_version: Current CLI version for reference.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
compatible: bool
|
|
69
|
+
blocking_error: str | None = None
|
|
70
|
+
warnings: list[str] = field(default_factory=list)
|
|
71
|
+
schema_version: str | None = None
|
|
72
|
+
min_cli_version: str | None = None
|
|
73
|
+
current_cli_version: str = CLI_VERSION
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
77
|
+
# Schema Loading
|
|
78
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_bundled_schema(version: str = "v1") -> dict[Any, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Load schema from package resources.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
version: Schema version (default: "v1")
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Schema dict
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
FileNotFoundError: If schema version doesn't exist
|
|
93
|
+
"""
|
|
94
|
+
schema_file = files("scc_cli.schemas").joinpath(f"org-{version}.schema.json")
|
|
95
|
+
try:
|
|
96
|
+
content = schema_file.read_text()
|
|
97
|
+
return cast(dict[Any, Any], json.loads(content))
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
raise FileNotFoundError(f"Schema version '{version}' not found")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
103
|
+
# Config Validation
|
|
104
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def validate_org_config(config: dict[str, Any], schema_version: str = "v1") -> list[str]:
|
|
108
|
+
"""
|
|
109
|
+
Validate org config against bundled schema.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
config: Organization config dict to validate
|
|
113
|
+
schema_version: Schema version to validate against (default: "v1")
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of error strings. Empty list means config is valid.
|
|
117
|
+
"""
|
|
118
|
+
schema = load_bundled_schema(schema_version)
|
|
119
|
+
validator = Draft7Validator(schema)
|
|
120
|
+
|
|
121
|
+
errors = []
|
|
122
|
+
for error in validator.iter_errors(config):
|
|
123
|
+
# Include config path for easy debugging
|
|
124
|
+
path = "/".join(str(p) for p in error.path) or "(root)"
|
|
125
|
+
errors.append(f"{path}: {error.message}")
|
|
126
|
+
|
|
127
|
+
return errors
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
131
|
+
# Version Compatibility Checks
|
|
132
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def parse_semver(version_string: str) -> tuple[int, int, int]:
|
|
136
|
+
"""
|
|
137
|
+
Parse semantic version string into tuple of (major, minor, patch).
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
version_string: Version string in format "X.Y.Z"
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (major, minor, patch) integers
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If version string is not valid semver format
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
parts = version_string.split(".")
|
|
150
|
+
if len(parts) != 3:
|
|
151
|
+
raise ValueError(f"Invalid semver format: {version_string}")
|
|
152
|
+
return (int(parts[0]), int(parts[1]), int(parts[2]))
|
|
153
|
+
except (ValueError, AttributeError) as e:
|
|
154
|
+
raise ValueError(f"Invalid semver format: {version_string}") from e
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def check_schema_version(config_version: str, cli_version: str) -> tuple[bool, str | None]:
|
|
158
|
+
"""
|
|
159
|
+
Check schema version compatibility.
|
|
160
|
+
|
|
161
|
+
Compatibility rules:
|
|
162
|
+
- Same major version: compatible
|
|
163
|
+
- Config major > CLI major: incompatible (need CLI upgrade)
|
|
164
|
+
- CLI major > Config major: compatible (CLI is newer)
|
|
165
|
+
- Higher minor in config: compatible with warning (ignore unknown fields)
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
config_version: Schema version from org config (e.g., "1.5.0")
|
|
169
|
+
cli_version: Current CLI schema version (e.g., "1.2.0")
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Tuple of (compatible: bool, message: str | None)
|
|
173
|
+
"""
|
|
174
|
+
config_major, config_minor, _ = parse_semver(config_version)
|
|
175
|
+
cli_major, cli_minor, _ = parse_semver(cli_version)
|
|
176
|
+
|
|
177
|
+
# Different major versions: check if upgrade needed
|
|
178
|
+
if config_major > cli_major:
|
|
179
|
+
return (
|
|
180
|
+
False,
|
|
181
|
+
f"Config requires schema v{config_major}.x but CLI only supports v{cli_major}.x. "
|
|
182
|
+
f"Please upgrade SCC CLI.",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Config minor version higher than CLI: warn but continue
|
|
186
|
+
if config_major == cli_major and config_minor > cli_minor:
|
|
187
|
+
return (
|
|
188
|
+
True,
|
|
189
|
+
f"Config uses schema {config_version}, CLI supports {cli_version}. "
|
|
190
|
+
f"Some features may be ignored.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Compatible
|
|
194
|
+
return (True, None)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def detect_schema_version(config: dict[str, Any]) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Detect schema version from config.
|
|
200
|
+
|
|
201
|
+
Currently only v1 is supported. This function validates the format
|
|
202
|
+
and always returns "v1" for any valid semver.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
config: Organization config dict
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Schema version string (always "v1")
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ValueError: If schema_version format is invalid
|
|
212
|
+
"""
|
|
213
|
+
schema_version = config.get("schema_version", "1.0.0")
|
|
214
|
+
|
|
215
|
+
# Validate format (must be X.Y.Z semver)
|
|
216
|
+
try:
|
|
217
|
+
parts = schema_version.split(".")
|
|
218
|
+
if len(parts) != 3:
|
|
219
|
+
raise ValueError(f"Invalid schema_version format: {schema_version}")
|
|
220
|
+
# Validate all parts are integers
|
|
221
|
+
int(parts[0])
|
|
222
|
+
int(parts[1])
|
|
223
|
+
int(parts[2])
|
|
224
|
+
except (ValueError, AttributeError) as e:
|
|
225
|
+
raise ValueError(f"Invalid schema_version format: {schema_version}") from e
|
|
226
|
+
|
|
227
|
+
# Only v1 schema is supported
|
|
228
|
+
return "v1"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_min_cli_version(min_version: str, cli_version: str) -> tuple[bool, str | None]:
|
|
232
|
+
"""
|
|
233
|
+
Check if CLI meets minimum version requirement.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
min_version: Minimum required CLI version (from config)
|
|
237
|
+
cli_version: Current CLI version
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple of (ok: bool, message: str | None)
|
|
241
|
+
"""
|
|
242
|
+
min_major, min_minor, min_patch = parse_semver(min_version)
|
|
243
|
+
cli_major, cli_minor, cli_patch = parse_semver(cli_version)
|
|
244
|
+
|
|
245
|
+
# Compare version tuples
|
|
246
|
+
min_tuple = (min_major, min_minor, min_patch)
|
|
247
|
+
cli_tuple = (cli_major, cli_minor, cli_patch)
|
|
248
|
+
|
|
249
|
+
if cli_tuple < min_tuple:
|
|
250
|
+
return (
|
|
251
|
+
False,
|
|
252
|
+
f"Config requires SCC CLI >= {min_version}, but you have {cli_version}. "
|
|
253
|
+
f"Please upgrade SCC CLI.",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return (True, None)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
260
|
+
# Unified Compatibility Gate
|
|
261
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def check_version_compatibility(config: dict[str, Any]) -> VersionCompatibility:
|
|
265
|
+
"""Check version compatibility for an org config.
|
|
266
|
+
|
|
267
|
+
This is the primary entry point for version validation. It combines:
|
|
268
|
+
1. Schema version check (major version must be supported)
|
|
269
|
+
2. Min CLI version check (CLI must meet minimum requirement)
|
|
270
|
+
|
|
271
|
+
The function returns immediately on blocking errors (requires upgrade)
|
|
272
|
+
but collects all warnings for informational purposes.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
config: Organization config dict to validate.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
VersionCompatibility result with compatibility status and messages.
|
|
279
|
+
|
|
280
|
+
Examples:
|
|
281
|
+
>>> result = check_version_compatibility({"schema_version": "1.0.0"})
|
|
282
|
+
>>> result.compatible
|
|
283
|
+
True
|
|
284
|
+
|
|
285
|
+
>>> result = check_version_compatibility({"min_cli_version": "99.0.0"})
|
|
286
|
+
>>> result.compatible
|
|
287
|
+
False
|
|
288
|
+
>>> "upgrade" in result.blocking_error.lower()
|
|
289
|
+
True
|
|
290
|
+
"""
|
|
291
|
+
warnings: list[str] = []
|
|
292
|
+
schema_version = config.get("schema_version")
|
|
293
|
+
min_cli_version = config.get("min_cli_version")
|
|
294
|
+
|
|
295
|
+
# Check schema version compatibility
|
|
296
|
+
if schema_version:
|
|
297
|
+
try:
|
|
298
|
+
schema_ok, schema_msg = check_schema_version(schema_version, CURRENT_SCHEMA_VERSION)
|
|
299
|
+
if not schema_ok:
|
|
300
|
+
return VersionCompatibility(
|
|
301
|
+
compatible=False,
|
|
302
|
+
blocking_error=schema_msg,
|
|
303
|
+
schema_version=schema_version,
|
|
304
|
+
min_cli_version=min_cli_version,
|
|
305
|
+
)
|
|
306
|
+
if schema_msg: # Warning but still compatible
|
|
307
|
+
warnings.append(schema_msg)
|
|
308
|
+
except ValueError as e:
|
|
309
|
+
return VersionCompatibility(
|
|
310
|
+
compatible=False,
|
|
311
|
+
blocking_error=f"Invalid schema_version format: {e}",
|
|
312
|
+
schema_version=schema_version,
|
|
313
|
+
min_cli_version=min_cli_version,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Validate schema version is in supported list
|
|
317
|
+
if schema_version:
|
|
318
|
+
detected = detect_schema_version(config)
|
|
319
|
+
if detected not in SUPPORTED_SCHEMA_VERSIONS:
|
|
320
|
+
return VersionCompatibility(
|
|
321
|
+
compatible=False,
|
|
322
|
+
blocking_error=(
|
|
323
|
+
f"Schema version '{detected}' is not supported. "
|
|
324
|
+
f"Supported versions: {', '.join(SUPPORTED_SCHEMA_VERSIONS)}"
|
|
325
|
+
),
|
|
326
|
+
schema_version=schema_version,
|
|
327
|
+
min_cli_version=min_cli_version,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Check minimum CLI version
|
|
331
|
+
if min_cli_version:
|
|
332
|
+
try:
|
|
333
|
+
cli_ok, cli_msg = check_min_cli_version(min_cli_version, CLI_VERSION)
|
|
334
|
+
if not cli_ok:
|
|
335
|
+
return VersionCompatibility(
|
|
336
|
+
compatible=False,
|
|
337
|
+
blocking_error=cli_msg,
|
|
338
|
+
schema_version=schema_version,
|
|
339
|
+
min_cli_version=min_cli_version,
|
|
340
|
+
)
|
|
341
|
+
except ValueError as e:
|
|
342
|
+
return VersionCompatibility(
|
|
343
|
+
compatible=False,
|
|
344
|
+
blocking_error=f"Invalid min_cli_version format: {e}",
|
|
345
|
+
schema_version=schema_version,
|
|
346
|
+
min_cli_version=min_cli_version,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# All checks passed
|
|
350
|
+
return VersionCompatibility(
|
|
351
|
+
compatible=True,
|
|
352
|
+
warnings=warnings,
|
|
353
|
+
schema_version=schema_version,
|
|
354
|
+
min_cli_version=min_cli_version,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
359
|
+
# Config Invariant Validation
|
|
360
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def validate_config_invariants(config: dict[str, Any]) -> list[InvariantViolation]:
|
|
364
|
+
"""Validate governance invariants on raw dict config.
|
|
365
|
+
|
|
366
|
+
This function checks semantic constraints that JSON Schema cannot express:
|
|
367
|
+
- enabled plugins must be subset of allowed (enabled ⊆ allowed)
|
|
368
|
+
- enabled plugins must not be blocked (enabled ∩ blocked = ∅)
|
|
369
|
+
|
|
370
|
+
Called AFTER Pydantic structural validation passes in the Validation Gate.
|
|
371
|
+
Works on raw dicts because that's what the CLI uses throughout.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
config: Organization config dict (raw, not Pydantic model).
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of InvariantViolation objects. Empty list means all invariants satisfied.
|
|
378
|
+
|
|
379
|
+
Semantics for allowed_plugins:
|
|
380
|
+
- Missing/None: unrestricted (all plugins allowed)
|
|
381
|
+
- []: deny all (no plugins allowed)
|
|
382
|
+
- ["*"]: explicit unrestricted (all plugins allowed via wildcard)
|
|
383
|
+
- ["pattern@marketplace"]: specific whitelist with fnmatch patterns
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
>>> config = {"defaults": {"enabled_plugins": ["a@mp"]}}
|
|
387
|
+
>>> validate_config_invariants(config) # Missing allowed = unrestricted
|
|
388
|
+
[]
|
|
389
|
+
|
|
390
|
+
>>> config = {"defaults": {"enabled_plugins": ["a@mp"], "allowed_plugins": []}}
|
|
391
|
+
>>> violations = validate_config_invariants(config) # Empty = deny all
|
|
392
|
+
>>> len(violations) == 1 and violations[0].rule == "enabled_must_be_allowed"
|
|
393
|
+
True
|
|
394
|
+
"""
|
|
395
|
+
# Import here to avoid circular dependency
|
|
396
|
+
from scc_cli.marketplace.normalize import matches_pattern
|
|
397
|
+
|
|
398
|
+
violations: list[InvariantViolation] = []
|
|
399
|
+
|
|
400
|
+
# Extract config sections with safe defaults
|
|
401
|
+
defaults = config.get("defaults", {})
|
|
402
|
+
enabled = defaults.get("enabled_plugins", [])
|
|
403
|
+
allowed = defaults.get("allowed_plugins") # None = unrestricted
|
|
404
|
+
|
|
405
|
+
security = config.get("security", {})
|
|
406
|
+
blocked = security.get("blocked_plugins", [])
|
|
407
|
+
|
|
408
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
409
|
+
# Invariant 1: enabled ⊆ allowed (enabled plugins must be in allowed list)
|
|
410
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
if allowed is not None:
|
|
412
|
+
if allowed == []:
|
|
413
|
+
# Empty array = nothing allowed (explicit deny all)
|
|
414
|
+
for plugin in enabled:
|
|
415
|
+
violations.append(
|
|
416
|
+
InvariantViolation(
|
|
417
|
+
rule="enabled_must_be_allowed",
|
|
418
|
+
message=(
|
|
419
|
+
f"Plugin '{plugin}' is enabled but allowed_plugins is empty "
|
|
420
|
+
"(nothing allowed)"
|
|
421
|
+
),
|
|
422
|
+
severity="error",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
elif allowed != ["*"]:
|
|
426
|
+
# Specific whitelist - check each enabled plugin against patterns
|
|
427
|
+
for plugin in enabled:
|
|
428
|
+
if not any(matches_pattern(plugin, pattern) for pattern in allowed):
|
|
429
|
+
violations.append(
|
|
430
|
+
InvariantViolation(
|
|
431
|
+
rule="enabled_must_be_allowed",
|
|
432
|
+
message=f"Plugin '{plugin}' is enabled but not in allowed list",
|
|
433
|
+
severity="error",
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
# If allowed == ["*"], all plugins are allowed (explicit unrestricted)
|
|
437
|
+
|
|
438
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
439
|
+
# Invariant 2: enabled ∩ blocked = ∅ (enabled must not be blocked)
|
|
440
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
441
|
+
for plugin in enabled:
|
|
442
|
+
for pattern in blocked:
|
|
443
|
+
if matches_pattern(plugin, pattern):
|
|
444
|
+
violations.append(
|
|
445
|
+
InvariantViolation(
|
|
446
|
+
rule="enabled_not_blocked",
|
|
447
|
+
message=(
|
|
448
|
+
f"Plugin '{plugin}' is enabled but matches blocked pattern '{pattern}'"
|
|
449
|
+
),
|
|
450
|
+
severity="error",
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
break # One match is enough to flag this plugin
|
|
454
|
+
|
|
455
|
+
return violations
|