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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Evaluation layer for SCC exception system.
|
|
2
|
+
|
|
3
|
+
Provide pure functions for evaluating configs and applying exceptions.
|
|
4
|
+
All IO is isolated to the stores layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from scc_cli.evaluation.apply_exceptions import (
|
|
8
|
+
apply_local_overrides,
|
|
9
|
+
apply_policy_exceptions,
|
|
10
|
+
)
|
|
11
|
+
from scc_cli.evaluation.evaluate import evaluate
|
|
12
|
+
from scc_cli.evaluation.models import (
|
|
13
|
+
BlockedItem,
|
|
14
|
+
Decision,
|
|
15
|
+
DeniedAddition,
|
|
16
|
+
EvaluationResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BlockedItem",
|
|
21
|
+
"Decision",
|
|
22
|
+
"DeniedAddition",
|
|
23
|
+
"EvaluationResult",
|
|
24
|
+
"apply_local_overrides",
|
|
25
|
+
"apply_policy_exceptions",
|
|
26
|
+
"evaluate",
|
|
27
|
+
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Apply exceptions to evaluation results.
|
|
2
|
+
|
|
3
|
+
Contain the core exception application logic. All functions are pure (no IO)
|
|
4
|
+
and operate on immutable data structures.
|
|
5
|
+
|
|
6
|
+
Key rules:
|
|
7
|
+
- apply_policy_exceptions() can override ANY block (security or delegation)
|
|
8
|
+
- apply_local_overrides() can ONLY override DELEGATION blocks
|
|
9
|
+
- Expired exceptions have no effect
|
|
10
|
+
- Wildcard patterns (e.g., "jira-*") are supported
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import fnmatch
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Literal
|
|
18
|
+
|
|
19
|
+
from scc_cli.evaluation.models import (
|
|
20
|
+
Decision,
|
|
21
|
+
EvaluationResult,
|
|
22
|
+
)
|
|
23
|
+
from scc_cli.models.exceptions import AllowTargets, BlockReason
|
|
24
|
+
from scc_cli.models.exceptions import Exception as SccException
|
|
25
|
+
from scc_cli.utils.ttl import format_relative
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_expired(exception: SccException) -> bool:
|
|
29
|
+
"""Check if an exception has expired."""
|
|
30
|
+
try:
|
|
31
|
+
expires = datetime.fromisoformat(exception.expires_at.replace("Z", "+00:00"))
|
|
32
|
+
return expires <= datetime.now(timezone.utc)
|
|
33
|
+
except (ValueError, AttributeError):
|
|
34
|
+
return True # Invalid expiration = expired
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _matches_target(pattern: str, target: str) -> bool:
|
|
38
|
+
"""Check if a pattern matches a target, supporting wildcards."""
|
|
39
|
+
# Exact match first
|
|
40
|
+
if pattern == target:
|
|
41
|
+
return True
|
|
42
|
+
# Wildcard match (fnmatch supports * and ?)
|
|
43
|
+
return fnmatch.fnmatch(target, pattern)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_allowed_targets(
|
|
47
|
+
allow: AllowTargets, target_type: Literal["plugin", "mcp_server", "base_image"]
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
"""Get the list of allowed targets for a specific type."""
|
|
50
|
+
if target_type == "plugin":
|
|
51
|
+
return allow.plugins or []
|
|
52
|
+
elif target_type == "mcp_server":
|
|
53
|
+
return allow.mcp_servers or []
|
|
54
|
+
elif target_type == "base_image":
|
|
55
|
+
return allow.base_images or []
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _find_matching_exception(
|
|
60
|
+
target: str,
|
|
61
|
+
target_type: Literal["plugin", "mcp_server", "base_image"],
|
|
62
|
+
exceptions: list[SccException],
|
|
63
|
+
) -> SccException | None:
|
|
64
|
+
"""Find the first non-expired exception that matches the target.
|
|
65
|
+
|
|
66
|
+
Prefers exact matches over wildcard matches.
|
|
67
|
+
"""
|
|
68
|
+
exact_match = None
|
|
69
|
+
wildcard_match = None
|
|
70
|
+
|
|
71
|
+
for exc in exceptions:
|
|
72
|
+
if _is_expired(exc):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
allowed = _get_allowed_targets(exc.allow, target_type)
|
|
76
|
+
for pattern in allowed:
|
|
77
|
+
if pattern == target:
|
|
78
|
+
exact_match = exc
|
|
79
|
+
break # Exact match found, use it
|
|
80
|
+
elif _matches_target(pattern, target) and wildcard_match is None:
|
|
81
|
+
wildcard_match = exc
|
|
82
|
+
|
|
83
|
+
if exact_match:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
return exact_match or wildcard_match
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _calculate_expires_in(exception: SccException) -> str | None:
|
|
90
|
+
"""Calculate the relative time until expiration."""
|
|
91
|
+
try:
|
|
92
|
+
expires = datetime.fromisoformat(exception.expires_at.replace("Z", "+00:00"))
|
|
93
|
+
return format_relative(expires)
|
|
94
|
+
except (ValueError, AttributeError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def apply_policy_exceptions(
|
|
99
|
+
result: EvaluationResult,
|
|
100
|
+
exceptions: list[SccException],
|
|
101
|
+
) -> EvaluationResult:
|
|
102
|
+
"""Apply policy exceptions to an evaluation result.
|
|
103
|
+
|
|
104
|
+
Policy exceptions can override ANY block - both security blocks and
|
|
105
|
+
delegation denials. This is the first layer of exception application.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
result: The current evaluation result
|
|
109
|
+
exceptions: List of policy exceptions to apply
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
New EvaluationResult with matching items removed and decisions added
|
|
113
|
+
"""
|
|
114
|
+
new_result = result.copy()
|
|
115
|
+
|
|
116
|
+
# Process blocked items (security blocks)
|
|
117
|
+
remaining_blocked = []
|
|
118
|
+
for blocked in result.blocked_items:
|
|
119
|
+
matching_exc = _find_matching_exception(blocked.target, blocked.target_type, exceptions)
|
|
120
|
+
if matching_exc:
|
|
121
|
+
# Create decision record
|
|
122
|
+
decision = Decision(
|
|
123
|
+
item=blocked.target,
|
|
124
|
+
item_type=blocked.target_type,
|
|
125
|
+
result="allowed",
|
|
126
|
+
reason="Policy exception applied",
|
|
127
|
+
source="policy",
|
|
128
|
+
exception_id=matching_exc.id,
|
|
129
|
+
expires_in=_calculate_expires_in(matching_exc),
|
|
130
|
+
)
|
|
131
|
+
new_result.decisions.append(decision)
|
|
132
|
+
else:
|
|
133
|
+
remaining_blocked.append(blocked)
|
|
134
|
+
|
|
135
|
+
new_result.blocked_items = remaining_blocked
|
|
136
|
+
|
|
137
|
+
# Process denied additions (delegation blocks)
|
|
138
|
+
remaining_denied = []
|
|
139
|
+
for denied in result.denied_additions:
|
|
140
|
+
matching_exc = _find_matching_exception(denied.target, denied.target_type, exceptions)
|
|
141
|
+
if matching_exc:
|
|
142
|
+
decision = Decision(
|
|
143
|
+
item=denied.target,
|
|
144
|
+
item_type=denied.target_type,
|
|
145
|
+
result="allowed",
|
|
146
|
+
reason="Policy exception applied",
|
|
147
|
+
source="policy",
|
|
148
|
+
exception_id=matching_exc.id,
|
|
149
|
+
expires_in=_calculate_expires_in(matching_exc),
|
|
150
|
+
)
|
|
151
|
+
new_result.decisions.append(decision)
|
|
152
|
+
else:
|
|
153
|
+
remaining_denied.append(denied)
|
|
154
|
+
|
|
155
|
+
new_result.denied_additions = remaining_denied
|
|
156
|
+
|
|
157
|
+
return new_result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def apply_local_overrides(
|
|
161
|
+
result: EvaluationResult,
|
|
162
|
+
overrides: list[SccException],
|
|
163
|
+
source: Literal["repo", "user"],
|
|
164
|
+
) -> EvaluationResult:
|
|
165
|
+
"""Apply local overrides to an evaluation result.
|
|
166
|
+
|
|
167
|
+
Local overrides can ONLY override DELEGATION blocks (denied additions).
|
|
168
|
+
Security blocks are immutable to local overrides - this is a critical
|
|
169
|
+
security boundary.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
result: The current evaluation result
|
|
173
|
+
overrides: List of local overrides to apply
|
|
174
|
+
source: Where the overrides came from ("repo" or "user")
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
New EvaluationResult with matching denied additions removed and decisions added
|
|
178
|
+
"""
|
|
179
|
+
new_result = result.copy()
|
|
180
|
+
|
|
181
|
+
# ONLY process denied additions (delegation blocks)
|
|
182
|
+
# Security blocks (blocked_items) are NEVER affected by local overrides
|
|
183
|
+
remaining_denied = []
|
|
184
|
+
for denied in result.denied_additions:
|
|
185
|
+
# Only delegation blocks can be overridden locally
|
|
186
|
+
if denied.reason != BlockReason.DELEGATION:
|
|
187
|
+
remaining_denied.append(denied)
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
matching_exc = _find_matching_exception(denied.target, denied.target_type, overrides)
|
|
191
|
+
if matching_exc:
|
|
192
|
+
decision = Decision(
|
|
193
|
+
item=denied.target,
|
|
194
|
+
item_type=denied.target_type,
|
|
195
|
+
result="allowed",
|
|
196
|
+
reason="Local override applied",
|
|
197
|
+
source=source,
|
|
198
|
+
exception_id=matching_exc.id,
|
|
199
|
+
expires_in=_calculate_expires_in(matching_exc),
|
|
200
|
+
)
|
|
201
|
+
new_result.decisions.append(decision)
|
|
202
|
+
else:
|
|
203
|
+
remaining_denied.append(denied)
|
|
204
|
+
|
|
205
|
+
new_result.denied_additions = remaining_denied
|
|
206
|
+
|
|
207
|
+
return new_result
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Bridge function to convert EffectiveConfig to EvaluationResult.
|
|
2
|
+
|
|
3
|
+
Provide the evaluate() function that converts the governance layer models
|
|
4
|
+
(profiles.py) to the exception system models (evaluation/models.py) with
|
|
5
|
+
proper BlockReason annotations.
|
|
6
|
+
|
|
7
|
+
This is a pure function with no IO - all input comes from the EffectiveConfig
|
|
8
|
+
parameter and output is a new EvaluationResult.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING, Literal
|
|
14
|
+
|
|
15
|
+
from scc_cli.evaluation.models import (
|
|
16
|
+
BlockedItem,
|
|
17
|
+
DeniedAddition,
|
|
18
|
+
EvaluationResult,
|
|
19
|
+
)
|
|
20
|
+
from scc_cli.models.exceptions import BlockReason
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from scc_cli.profiles import EffectiveConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def evaluate(config: EffectiveConfig) -> EvaluationResult:
|
|
27
|
+
"""Convert EffectiveConfig to EvaluationResult with BlockReason annotations.
|
|
28
|
+
|
|
29
|
+
This function bridges the governance layer (profiles.py models) to the
|
|
30
|
+
exception system (evaluation/models.py) by converting:
|
|
31
|
+
|
|
32
|
+
- profiles.BlockedItem -> evaluation.BlockedItem with BlockReason.SECURITY
|
|
33
|
+
- profiles.DelegationDenied -> evaluation.DeniedAddition with BlockReason.DELEGATION
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: The EffectiveConfig from the profile merge process
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
EvaluationResult with properly annotated blocked items and denied additions
|
|
40
|
+
"""
|
|
41
|
+
blocked_items: list[BlockedItem] = []
|
|
42
|
+
denied_additions: list[DeniedAddition] = []
|
|
43
|
+
|
|
44
|
+
# Convert blocked items (security blocks)
|
|
45
|
+
for blocked in config.blocked_items:
|
|
46
|
+
target_type = _normalize_target_type(blocked.target_type)
|
|
47
|
+
message = f"Blocked by security pattern '{blocked.blocked_by}'"
|
|
48
|
+
|
|
49
|
+
blocked_items.append(
|
|
50
|
+
BlockedItem(
|
|
51
|
+
target=blocked.item,
|
|
52
|
+
target_type=target_type,
|
|
53
|
+
reason=BlockReason.SECURITY,
|
|
54
|
+
message=message,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Convert denied additions (delegation denials)
|
|
59
|
+
for denied in config.denied_additions:
|
|
60
|
+
target_type = _normalize_target_type(denied.target_type)
|
|
61
|
+
# Use the original reason which contains useful context
|
|
62
|
+
message = denied.reason
|
|
63
|
+
|
|
64
|
+
denied_additions.append(
|
|
65
|
+
DeniedAddition(
|
|
66
|
+
target=denied.item,
|
|
67
|
+
target_type=target_type,
|
|
68
|
+
reason=BlockReason.DELEGATION,
|
|
69
|
+
message=message,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return EvaluationResult(
|
|
74
|
+
blocked_items=blocked_items,
|
|
75
|
+
denied_additions=denied_additions,
|
|
76
|
+
decisions=[], # Decisions are populated by apply_*_exceptions functions
|
|
77
|
+
warnings=[],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize_target_type(
|
|
82
|
+
target_type: str,
|
|
83
|
+
) -> Literal["plugin", "mcp_server", "base_image"]:
|
|
84
|
+
"""Normalize target_type to valid literal values.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
target_type: The target type from profiles.py models
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Normalized target type literal
|
|
91
|
+
"""
|
|
92
|
+
if target_type == "mcp_server":
|
|
93
|
+
return "mcp_server"
|
|
94
|
+
elif target_type == "base_image":
|
|
95
|
+
return "base_image"
|
|
96
|
+
else:
|
|
97
|
+
return "plugin" # Default to plugin for unknown types
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Define data models for the evaluation layer.
|
|
2
|
+
|
|
3
|
+
Represent the results of evaluating configs and applying exceptions.
|
|
4
|
+
All models are immutable and support the pure functional evaluation approach.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from scc_cli.models.exceptions import BlockReason
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class BlockedItem:
|
|
17
|
+
"""Represents an item blocked by security policy.
|
|
18
|
+
|
|
19
|
+
Security blocks can only be overridden by policy exceptions.
|
|
20
|
+
Local overrides have no effect on these.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
target: str # The blocked item (plugin name, MCP server, image ref)
|
|
24
|
+
target_type: Literal["plugin", "mcp_server", "base_image"]
|
|
25
|
+
reason: BlockReason # Always SECURITY for blocked items
|
|
26
|
+
message: str # Human-readable explanation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class DeniedAddition:
|
|
31
|
+
"""Represents an addition denied by delegation policy.
|
|
32
|
+
|
|
33
|
+
Delegation denials can be overridden by either policy exceptions
|
|
34
|
+
or local overrides (from repo or user stores).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
target: str # The denied item
|
|
38
|
+
target_type: Literal["plugin", "mcp_server", "base_image"]
|
|
39
|
+
reason: BlockReason # Always DELEGATION for denied additions
|
|
40
|
+
message: str # Human-readable explanation
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Decision:
|
|
45
|
+
"""Records a decision made when applying an exception.
|
|
46
|
+
|
|
47
|
+
Captures the full context of why an item was allowed or blocked,
|
|
48
|
+
including the exception that allowed it and when it expires.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
item: str # The item being decided on
|
|
52
|
+
item_type: Literal["plugin", "mcp_server", "base_image"]
|
|
53
|
+
result: Literal["allowed", "blocked", "denied"]
|
|
54
|
+
reason: str # Human-readable explanation
|
|
55
|
+
source: Literal["policy", "org", "team", "project", "repo", "user"] | None
|
|
56
|
+
exception_id: str | None # ID of the exception that allowed it
|
|
57
|
+
expires_in: str | None # Relative time like "7h45m"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class EvaluationResult:
|
|
62
|
+
"""The complete result of evaluating a config with exceptions applied.
|
|
63
|
+
|
|
64
|
+
Maintains lists of blocked items, denied additions, and decisions.
|
|
65
|
+
This is the primary data structure passed through the evaluation pipeline.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
blocked_items: list[BlockedItem] = field(default_factory=list)
|
|
69
|
+
denied_additions: list[DeniedAddition] = field(default_factory=list)
|
|
70
|
+
decisions: list[Decision] = field(default_factory=list)
|
|
71
|
+
warnings: list[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
def copy(self) -> EvaluationResult:
|
|
74
|
+
"""Create a shallow copy for immutable-style updates."""
|
|
75
|
+
return EvaluationResult(
|
|
76
|
+
blocked_items=list(self.blocked_items),
|
|
77
|
+
denied_additions=list(self.denied_additions),
|
|
78
|
+
decisions=list(self.decisions),
|
|
79
|
+
warnings=list(self.warnings),
|
|
80
|
+
)
|
scc_cli/exit_codes.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exit codes for SCC CLI.
|
|
3
|
+
|
|
4
|
+
Standardized exit codes following Unix conventions with semantic meaning.
|
|
5
|
+
All commands MUST use these constants for consistency.
|
|
6
|
+
|
|
7
|
+
Note: Click/Typer argument parsing errors (EXIT_USAGE) occur before
|
|
8
|
+
commands run, so they emit to stderr without JSON envelope.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Success
|
|
12
|
+
EXIT_SUCCESS = 0 # Command completed successfully
|
|
13
|
+
|
|
14
|
+
# Errors (1-6)
|
|
15
|
+
EXIT_ERROR = 1 # General/unexpected error
|
|
16
|
+
EXIT_USAGE = 2 # Invalid usage/arguments (Click default)
|
|
17
|
+
EXIT_CONFIG = 3 # Config or network error
|
|
18
|
+
EXIT_VALIDATION = 4 # Validation failed (schema, semantic checks)
|
|
19
|
+
EXIT_PREREQ = 5 # Prerequisites not met (Docker, Git)
|
|
20
|
+
EXIT_GOVERNANCE = 6 # Blocked by governance policy
|
|
21
|
+
|
|
22
|
+
# Cancellation (SIGINT convention)
|
|
23
|
+
EXIT_CANCELLED = 130 # User cancelled operation (SIGINT)
|
|
24
|
+
|
|
25
|
+
# Map exception types to exit codes (for json_command decorator)
|
|
26
|
+
# Note: Import from errors module only when needed to avoid circular imports
|
|
27
|
+
EXIT_CODE_MAP = {
|
|
28
|
+
"ConfigError": EXIT_CONFIG,
|
|
29
|
+
"ProfileNotFoundError": EXIT_CONFIG,
|
|
30
|
+
"ValidationError": EXIT_VALIDATION,
|
|
31
|
+
"PolicyViolationError": EXIT_GOVERNANCE,
|
|
32
|
+
"PrerequisiteError": EXIT_PREREQ,
|
|
33
|
+
"DockerNotFoundError": EXIT_PREREQ,
|
|
34
|
+
"GitNotFoundError": EXIT_PREREQ,
|
|
35
|
+
"UsageError": EXIT_USAGE,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_exit_code_for_exception(exc: Exception) -> int:
|
|
40
|
+
"""Return the appropriate exit code for an exception type.
|
|
41
|
+
|
|
42
|
+
Walk up the exception's MRO to find a matching type in EXIT_CODE_MAP.
|
|
43
|
+
Fall back to EXIT_ERROR if no specific mapping exists.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
exc: The exception instance to map.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The standardized exit code for the exception type.
|
|
50
|
+
"""
|
|
51
|
+
for cls in type(exc).__mro__:
|
|
52
|
+
if cls.__name__ in EXIT_CODE_MAP:
|
|
53
|
+
return EXIT_CODE_MAP[cls.__name__]
|
|
54
|
+
|
|
55
|
+
return EXIT_ERROR
|