scc-cli 1.5.3__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 +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -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 +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -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/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -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 +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -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/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -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 +383 -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 +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -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 +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- scc_cli-1.5.3.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/git.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git operations - backward-compatible facade.
|
|
3
|
+
|
|
4
|
+
This module is a pure re-export facade after Phase 4 refactoring:
|
|
5
|
+
- Data functions: services/git/ (core.py, branch.py, worktree.py, hooks.py)
|
|
6
|
+
- Rendering functions: ui/git_render.py
|
|
7
|
+
- Interactive UI functions: ui/git_interactive.py
|
|
8
|
+
|
|
9
|
+
All symbols are re-exported for backward compatibility - existing imports
|
|
10
|
+
like `from scc_cli.git import WorktreeInfo` continue to work.
|
|
11
|
+
|
|
12
|
+
NO Rich imports in this module - that's a key acceptance criterion.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# Re-exports from services/git/ for backward compatibility
|
|
17
|
+
# These imports ARE used - they're intentional re-exports for the public API
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
# Branch operations
|
|
21
|
+
from .services.git.branch import ( # noqa: F401
|
|
22
|
+
PROTECTED_BRANCHES,
|
|
23
|
+
get_current_branch,
|
|
24
|
+
get_default_branch,
|
|
25
|
+
get_display_branch,
|
|
26
|
+
get_uncommitted_files,
|
|
27
|
+
is_protected_branch,
|
|
28
|
+
list_branches_without_worktrees,
|
|
29
|
+
sanitize_branch_name,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Core operations
|
|
33
|
+
from .services.git.core import ( # noqa: F401
|
|
34
|
+
check_git_available,
|
|
35
|
+
check_git_installed,
|
|
36
|
+
create_empty_initial_commit,
|
|
37
|
+
detect_workspace_root,
|
|
38
|
+
get_git_version,
|
|
39
|
+
has_commits,
|
|
40
|
+
has_remote,
|
|
41
|
+
init_repo,
|
|
42
|
+
is_git_repo,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Hooks
|
|
46
|
+
from .services.git.hooks import ( # noqa: F401
|
|
47
|
+
SCC_HOOK_MARKER,
|
|
48
|
+
_write_scc_hook,
|
|
49
|
+
install_pre_push_hook,
|
|
50
|
+
is_scc_hook,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Worktree operations
|
|
54
|
+
from .services.git.worktree import ( # noqa: F401
|
|
55
|
+
WorktreeInfo,
|
|
56
|
+
find_main_worktree,
|
|
57
|
+
find_worktree_by_query,
|
|
58
|
+
get_workspace_mount_path,
|
|
59
|
+
get_worktree_main_repo,
|
|
60
|
+
get_worktree_status,
|
|
61
|
+
is_worktree,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Keep _get_worktrees_data as alias for backward compatibility
|
|
65
|
+
from .services.git.worktree import get_worktrees_data as _get_worktrees_data # noqa: F401
|
|
66
|
+
|
|
67
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
# Re-exports from ui/ for backward compatibility
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# Interactive UI functions (extracted in Phase 4B)
|
|
71
|
+
from .ui.git_interactive import ( # noqa: F401
|
|
72
|
+
check_branch_safety,
|
|
73
|
+
cleanup_worktree,
|
|
74
|
+
clone_repo,
|
|
75
|
+
create_worktree,
|
|
76
|
+
install_dependencies,
|
|
77
|
+
install_hooks,
|
|
78
|
+
list_worktrees,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Pure rendering functions
|
|
82
|
+
from .ui.git_render import format_git_status as _format_git_status # noqa: F401
|
|
83
|
+
from .ui.git_render import render_worktrees # noqa: F401
|
|
84
|
+
from .ui.git_render import render_worktrees_table as _render_worktrees_table # noqa: F401
|
scc_cli/json_command.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON command decorator for first-class JSON output support.
|
|
3
|
+
|
|
4
|
+
Provide a decorator that wraps command output in JSON envelopes.
|
|
5
|
+
|
|
6
|
+
IMPORTANT: Commands using this decorator MUST explicitly declare these parameters:
|
|
7
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON")
|
|
8
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON")
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from scc_cli.json_command import json_command
|
|
12
|
+
from scc_cli.kinds import Kind
|
|
13
|
+
|
|
14
|
+
@team_app.command("list")
|
|
15
|
+
@json_command(Kind.TEAM_LIST)
|
|
16
|
+
def team_list(
|
|
17
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
18
|
+
pretty: bool = typer.Option(False, "--pretty"),
|
|
19
|
+
) -> dict:
|
|
20
|
+
# Return data dict, decorator handles envelope
|
|
21
|
+
return {"teams": [...]}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from functools import wraps
|
|
26
|
+
from typing import Any, TypeVar
|
|
27
|
+
|
|
28
|
+
import typer
|
|
29
|
+
|
|
30
|
+
from .core.errors import ConfigError, PolicyViolationError, PrerequisiteError, SCCError
|
|
31
|
+
from .core.exit_codes import (
|
|
32
|
+
EXIT_CANCELLED,
|
|
33
|
+
EXIT_CONFIG,
|
|
34
|
+
EXIT_ERROR,
|
|
35
|
+
EXIT_GOVERNANCE,
|
|
36
|
+
EXIT_PREREQ,
|
|
37
|
+
EXIT_SUCCESS,
|
|
38
|
+
EXIT_VALIDATION,
|
|
39
|
+
)
|
|
40
|
+
from .json_output import build_envelope
|
|
41
|
+
from .kinds import Kind
|
|
42
|
+
from .output_mode import _pretty_mode, json_command_mode, json_output_mode, print_json
|
|
43
|
+
|
|
44
|
+
# Type variable for decorated functions
|
|
45
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
49
|
+
# Exception to Exit Code Mapping
|
|
50
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_exit_code_for_exception(exc: Exception) -> int:
|
|
54
|
+
"""Map exception types to exit codes."""
|
|
55
|
+
if isinstance(exc, PolicyViolationError):
|
|
56
|
+
return EXIT_GOVERNANCE
|
|
57
|
+
if isinstance(exc, PrerequisiteError):
|
|
58
|
+
return EXIT_PREREQ
|
|
59
|
+
if isinstance(exc, ConfigError):
|
|
60
|
+
return EXIT_CONFIG
|
|
61
|
+
if isinstance(exc, SCCError):
|
|
62
|
+
# Check exit_code attribute if available
|
|
63
|
+
return getattr(exc, "exit_code", EXIT_ERROR)
|
|
64
|
+
# Check for validation-like errors by name
|
|
65
|
+
if "Validation" in type(exc).__name__:
|
|
66
|
+
return EXIT_VALIDATION
|
|
67
|
+
return EXIT_ERROR
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
71
|
+
# JSON Command Decorator
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def json_command(kind: Kind) -> Callable[[F], F]:
|
|
76
|
+
"""Decorator for commands with --json support.
|
|
77
|
+
|
|
78
|
+
This decorator:
|
|
79
|
+
1. Checks for json_output and pretty parameters in kwargs
|
|
80
|
+
2. Enters json_output_mode() when json_output=True
|
|
81
|
+
3. Builds the envelope with the correct kind
|
|
82
|
+
4. Catches exceptions and maps to exit codes
|
|
83
|
+
5. Prints JSON output and exits with appropriate code
|
|
84
|
+
|
|
85
|
+
IMPORTANT: The decorated function MUST explicitly declare these parameters:
|
|
86
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON")
|
|
87
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON")
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
kind: The JSON envelope kind for this command
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A decorator function
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
@team_app.command("list")
|
|
97
|
+
@json_command(Kind.TEAM_LIST)
|
|
98
|
+
def team_list(
|
|
99
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
100
|
+
pretty: bool = typer.Option(False, "--pretty"),
|
|
101
|
+
) -> dict:
|
|
102
|
+
return {"teams": [...]}
|
|
103
|
+
|
|
104
|
+
Note:
|
|
105
|
+
The decorated function should return a dict that becomes
|
|
106
|
+
the "data" field in the JSON envelope. When not in JSON mode,
|
|
107
|
+
the function runs normally (for human-readable output).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def decorator(func: F) -> F:
|
|
111
|
+
@wraps(func)
|
|
112
|
+
def wrapper(**kwargs: Any) -> Any:
|
|
113
|
+
# Extract json_output and pretty from kwargs
|
|
114
|
+
json_output = kwargs.pop("json_output", False)
|
|
115
|
+
pretty = kwargs.pop("pretty", False)
|
|
116
|
+
|
|
117
|
+
# --pretty implies --json
|
|
118
|
+
if pretty:
|
|
119
|
+
json_output = True
|
|
120
|
+
_pretty_mode.set(True)
|
|
121
|
+
|
|
122
|
+
if json_output:
|
|
123
|
+
with json_output_mode(), json_command_mode():
|
|
124
|
+
try:
|
|
125
|
+
# Call the wrapped function
|
|
126
|
+
result = func(**kwargs)
|
|
127
|
+
|
|
128
|
+
# Build and print success envelope
|
|
129
|
+
envelope = build_envelope(kind, data=result or {})
|
|
130
|
+
print_json(envelope)
|
|
131
|
+
raise typer.Exit(EXIT_SUCCESS)
|
|
132
|
+
|
|
133
|
+
except typer.Exit:
|
|
134
|
+
# Re-raise typer exits (including our own)
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
except KeyboardInterrupt:
|
|
138
|
+
envelope = build_envelope(
|
|
139
|
+
kind,
|
|
140
|
+
data={},
|
|
141
|
+
ok=False,
|
|
142
|
+
errors=["Cancelled"],
|
|
143
|
+
)
|
|
144
|
+
print_json(envelope)
|
|
145
|
+
raise typer.Exit(EXIT_CANCELLED)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
# Map exception to exit code
|
|
149
|
+
exit_code = _get_exit_code_for_exception(e)
|
|
150
|
+
|
|
151
|
+
# Build and print error envelope
|
|
152
|
+
envelope = build_envelope(
|
|
153
|
+
kind,
|
|
154
|
+
data={},
|
|
155
|
+
ok=False,
|
|
156
|
+
errors=[str(e)],
|
|
157
|
+
)
|
|
158
|
+
print_json(envelope)
|
|
159
|
+
raise typer.Exit(exit_code)
|
|
160
|
+
else:
|
|
161
|
+
# Normal human-readable mode
|
|
162
|
+
return func(**kwargs)
|
|
163
|
+
|
|
164
|
+
return wrapper # type: ignore[return-value]
|
|
165
|
+
|
|
166
|
+
return decorator
|