scc-cli 1.4.0__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.

Files changed (112) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +683 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1400 -0
  16. scc_cli/cli_org.py +1433 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +858 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +603 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1082 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1405 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/compute.py +377 -0
  46. scc_cli/marketplace/constants.py +87 -0
  47. scc_cli/marketplace/managed.py +135 -0
  48. scc_cli/marketplace/materialize.py +723 -0
  49. scc_cli/marketplace/normalize.py +548 -0
  50. scc_cli/marketplace/render.py +238 -0
  51. scc_cli/marketplace/resolve.py +459 -0
  52. scc_cli/marketplace/schema.py +502 -0
  53. scc_cli/marketplace/sync.py +257 -0
  54. scc_cli/marketplace/team_cache.py +195 -0
  55. scc_cli/marketplace/team_fetch.py +688 -0
  56. scc_cli/marketplace/trust.py +244 -0
  57. scc_cli/models/__init__.py +41 -0
  58. scc_cli/models/exceptions.py +273 -0
  59. scc_cli/models/plugin_audit.py +434 -0
  60. scc_cli/org_templates.py +269 -0
  61. scc_cli/output_mode.py +167 -0
  62. scc_cli/panels.py +113 -0
  63. scc_cli/platform.py +350 -0
  64. scc_cli/profiles.py +1034 -0
  65. scc_cli/remote.py +443 -0
  66. scc_cli/schemas/__init__.py +1 -0
  67. scc_cli/schemas/org-v1.schema.json +456 -0
  68. scc_cli/schemas/team-config.v1.schema.json +163 -0
  69. scc_cli/sessions.py +425 -0
  70. scc_cli/setup.py +582 -0
  71. scc_cli/source_resolver.py +470 -0
  72. scc_cli/stats.py +378 -0
  73. scc_cli/stores/__init__.py +13 -0
  74. scc_cli/stores/exception_store.py +251 -0
  75. scc_cli/subprocess_utils.py +88 -0
  76. scc_cli/teams.py +339 -0
  77. scc_cli/templates/__init__.py +2 -0
  78. scc_cli/templates/org/__init__.py +0 -0
  79. scc_cli/templates/org/minimal.json +19 -0
  80. scc_cli/templates/org/reference.json +74 -0
  81. scc_cli/templates/org/strict.json +38 -0
  82. scc_cli/templates/org/teams.json +42 -0
  83. scc_cli/templates/statusline.sh +75 -0
  84. scc_cli/theme.py +348 -0
  85. scc_cli/ui/__init__.py +124 -0
  86. scc_cli/ui/branding.py +68 -0
  87. scc_cli/ui/chrome.py +395 -0
  88. scc_cli/ui/dashboard/__init__.py +62 -0
  89. scc_cli/ui/dashboard/_dashboard.py +669 -0
  90. scc_cli/ui/dashboard/loaders.py +369 -0
  91. scc_cli/ui/dashboard/models.py +184 -0
  92. scc_cli/ui/dashboard/orchestrator.py +337 -0
  93. scc_cli/ui/formatters.py +443 -0
  94. scc_cli/ui/gate.py +350 -0
  95. scc_cli/ui/help.py +157 -0
  96. scc_cli/ui/keys.py +521 -0
  97. scc_cli/ui/list_screen.py +431 -0
  98. scc_cli/ui/picker.py +700 -0
  99. scc_cli/ui/prompts.py +200 -0
  100. scc_cli/ui/wizard.py +490 -0
  101. scc_cli/update.py +680 -0
  102. scc_cli/utils/__init__.py +39 -0
  103. scc_cli/utils/fixit.py +264 -0
  104. scc_cli/utils/fuzzy.py +124 -0
  105. scc_cli/utils/locks.py +101 -0
  106. scc_cli/utils/ttl.py +376 -0
  107. scc_cli/validate.py +455 -0
  108. scc_cli-1.4.0.dist-info/METADATA +369 -0
  109. scc_cli-1.4.0.dist-info/RECORD +112 -0
  110. scc_cli-1.4.0.dist-info/WHEEL +4 -0
  111. scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
  112. scc_cli-1.4.0.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