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,459 @@
|
|
|
1
|
+
"""Effective config resolution for team profiles (Phase 2: Federation).
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- EffectiveConfig: Complete resolved configuration for a team
|
|
5
|
+
- ConfigFetchError: Error when fetching federated team config fails
|
|
6
|
+
- resolve_effective_config(): Main orchestrator (T2a-18, to be implemented)
|
|
7
|
+
|
|
8
|
+
EffectiveConfig serves as the unified result type for both inline and federated
|
|
9
|
+
team configurations, providing backwards compatibility via to_phase1_format().
|
|
10
|
+
|
|
11
|
+
Design Decision:
|
|
12
|
+
EffectiveConfig wraps the plugin computation results (from compute.py) with
|
|
13
|
+
additional metadata about the configuration source. This allows the CLI to
|
|
14
|
+
display federated vs inline status, version info, and trust state.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from scc_cli.marketplace.compute import (
|
|
23
|
+
BlockedPlugin,
|
|
24
|
+
EffectivePlugins,
|
|
25
|
+
compute_effective_plugins,
|
|
26
|
+
compute_effective_plugins_federated,
|
|
27
|
+
)
|
|
28
|
+
from scc_cli.marketplace.team_fetch import fetch_team_config_with_fallback
|
|
29
|
+
from scc_cli.marketplace.trust import TrustViolationError, validate_team_config_trust
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from scc_cli.marketplace.schema import (
|
|
33
|
+
ConfigSource,
|
|
34
|
+
MarketplaceSource,
|
|
35
|
+
OrganizationConfig,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
# Exceptions
|
|
41
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigFetchError(Exception):
|
|
45
|
+
"""Error when fetching federated team configuration fails.
|
|
46
|
+
|
|
47
|
+
Provides structured error information with remediation hints.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
team_id: str,
|
|
53
|
+
source_type: str,
|
|
54
|
+
source_url: str,
|
|
55
|
+
error: str,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.team_id = team_id
|
|
58
|
+
self.source_type = source_type
|
|
59
|
+
self.source_url = source_url
|
|
60
|
+
self.error = error
|
|
61
|
+
|
|
62
|
+
# Build message with remediation hints
|
|
63
|
+
remediation = _get_fetch_remediation(source_type, error)
|
|
64
|
+
message = (
|
|
65
|
+
f"Failed to fetch config for team '{team_id}' from {source_type} "
|
|
66
|
+
f"source ({source_url}): {error}"
|
|
67
|
+
)
|
|
68
|
+
if remediation:
|
|
69
|
+
message += f" {remediation}"
|
|
70
|
+
|
|
71
|
+
super().__init__(message)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_fetch_remediation(source_type: str, error: str) -> str:
|
|
75
|
+
"""Get remediation hint based on source type and error pattern."""
|
|
76
|
+
error_lower = error.lower()
|
|
77
|
+
|
|
78
|
+
# Network issues
|
|
79
|
+
if "network" in error_lower or "connection" in error_lower:
|
|
80
|
+
return "Try 'scc org update --team <name>' to refresh when connected."
|
|
81
|
+
|
|
82
|
+
# Cache expired
|
|
83
|
+
if "max_stale_age" in error_lower or "expired" in error_lower:
|
|
84
|
+
return "The cached config has expired. Connect to network to refresh."
|
|
85
|
+
|
|
86
|
+
# Git/GitHub specific
|
|
87
|
+
if source_type in ("github", "git"):
|
|
88
|
+
if "clone" in error_lower or "repository" in error_lower:
|
|
89
|
+
return "Check repository access permissions and URL."
|
|
90
|
+
if "branch" in error_lower:
|
|
91
|
+
return "Verify the branch name exists in the repository."
|
|
92
|
+
if "path" in error_lower or "not found" in error_lower:
|
|
93
|
+
return "Check that team-config.json exists at the specified path."
|
|
94
|
+
|
|
95
|
+
# URL specific
|
|
96
|
+
if source_type == "url":
|
|
97
|
+
if "401" in error or "unauthorized" in error_lower:
|
|
98
|
+
return "Add authentication headers to config_source."
|
|
99
|
+
if "403" in error or "forbidden" in error_lower:
|
|
100
|
+
return "Check access permissions for the config URL."
|
|
101
|
+
if "404" in error or "not found" in error_lower:
|
|
102
|
+
return "Verify the config URL is correct."
|
|
103
|
+
|
|
104
|
+
# Generic fallback
|
|
105
|
+
return "Run 'scc org update --team <name>' to retry fetching."
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
# EffectiveConfig Dataclass
|
|
110
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class EffectiveConfig:
|
|
115
|
+
"""Complete resolved configuration for a team.
|
|
116
|
+
|
|
117
|
+
This is the unified result type for both inline and federated team
|
|
118
|
+
configurations. It contains all the information needed to:
|
|
119
|
+
- Launch Claude Code with the correct plugins
|
|
120
|
+
- Display status information to the user
|
|
121
|
+
- Validate security compliance
|
|
122
|
+
|
|
123
|
+
For backwards compatibility, use to_phase1_format() to get the
|
|
124
|
+
(EffectivePlugins, marketplaces) tuple expected by Phase 1 code.
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
team_id: Team/profile identifier
|
|
128
|
+
is_federated: True if config came from external source
|
|
129
|
+
enabled_plugins: Set of enabled plugin references (name@marketplace)
|
|
130
|
+
config_source: External config source (if federated)
|
|
131
|
+
config_commit_sha: Git commit SHA (for git/github sources)
|
|
132
|
+
config_etag: HTTP ETag (for URL sources)
|
|
133
|
+
blocked_plugins: Plugins blocked by security policy
|
|
134
|
+
disabled_plugins: Plugins removed by disabled_plugins patterns
|
|
135
|
+
not_allowed_plugins: Plugins rejected by allowed_plugins filter
|
|
136
|
+
marketplaces: Effective marketplace sources
|
|
137
|
+
extra_marketplaces: Additional marketplace IDs to enable
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Required fields
|
|
141
|
+
team_id: str
|
|
142
|
+
is_federated: bool
|
|
143
|
+
enabled_plugins: set[str]
|
|
144
|
+
|
|
145
|
+
# Optional federation metadata
|
|
146
|
+
config_source: ConfigSource | None = None
|
|
147
|
+
config_commit_sha: str | None = None
|
|
148
|
+
config_etag: str | None = None
|
|
149
|
+
|
|
150
|
+
# Cache status (T2b-03: cached configs validated against current org security)
|
|
151
|
+
used_cached_config: bool = False
|
|
152
|
+
cache_is_stale: bool = False
|
|
153
|
+
staleness_warning: str | None = None
|
|
154
|
+
|
|
155
|
+
# Plugin filtering results
|
|
156
|
+
blocked_plugins: list[BlockedPlugin] = field(default_factory=list)
|
|
157
|
+
disabled_plugins: list[str] = field(default_factory=list)
|
|
158
|
+
not_allowed_plugins: list[str] = field(default_factory=list)
|
|
159
|
+
|
|
160
|
+
# Marketplace configuration
|
|
161
|
+
marketplaces: dict[str, MarketplaceSource] = field(default_factory=dict)
|
|
162
|
+
extra_marketplaces: list[str] = field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
165
|
+
# Computed Properties
|
|
166
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def has_security_violations(self) -> bool:
|
|
170
|
+
"""Check if any plugins were blocked by security policy.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if blocked_plugins is non-empty
|
|
174
|
+
"""
|
|
175
|
+
return len(self.blocked_plugins) > 0
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def plugin_count(self) -> int:
|
|
179
|
+
"""Get total number of enabled plugins.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Count of enabled plugins
|
|
183
|
+
"""
|
|
184
|
+
return len(self.enabled_plugins)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def source_description(self) -> str:
|
|
188
|
+
"""Get human-readable description of config source.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
'inline' for non-federated, or source URL for federated
|
|
192
|
+
"""
|
|
193
|
+
if not self.is_federated or self.config_source is None:
|
|
194
|
+
return "inline"
|
|
195
|
+
|
|
196
|
+
# Import here to avoid circular imports
|
|
197
|
+
from scc_cli.marketplace.schema import (
|
|
198
|
+
ConfigSourceGit,
|
|
199
|
+
ConfigSourceGitHub,
|
|
200
|
+
ConfigSourceURL,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if isinstance(self.config_source, ConfigSourceGitHub):
|
|
204
|
+
return f"github.com/{self.config_source.owner}/{self.config_source.repo}"
|
|
205
|
+
elif isinstance(self.config_source, ConfigSourceGit):
|
|
206
|
+
# Normalize git URL for display
|
|
207
|
+
url = self.config_source.url
|
|
208
|
+
if url.startswith("https://"):
|
|
209
|
+
url = url[8:]
|
|
210
|
+
elif url.startswith("git@"):
|
|
211
|
+
url = url[4:].replace(":", "/", 1)
|
|
212
|
+
if url.endswith(".git"):
|
|
213
|
+
url = url[:-4]
|
|
214
|
+
return url
|
|
215
|
+
elif isinstance(self.config_source, ConfigSourceURL):
|
|
216
|
+
# Strip protocol for display
|
|
217
|
+
url = self.config_source.url
|
|
218
|
+
if url.startswith("https://"):
|
|
219
|
+
url = url[8:]
|
|
220
|
+
return url
|
|
221
|
+
else:
|
|
222
|
+
return "unknown"
|
|
223
|
+
|
|
224
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
# Phase 1 Compatibility
|
|
226
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def to_phase1_format(self) -> tuple[EffectivePlugins, dict[str, MarketplaceSource]]:
|
|
229
|
+
"""Convert to Phase 1 format for backwards compatibility.
|
|
230
|
+
|
|
231
|
+
This adapter method allows EffectiveConfig to work with existing
|
|
232
|
+
Phase 1 code that expects:
|
|
233
|
+
- EffectivePlugins for plugin computation results
|
|
234
|
+
- dict[str, MarketplaceSource] for marketplace configuration
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Tuple of (EffectivePlugins, marketplaces dict)
|
|
238
|
+
"""
|
|
239
|
+
plugins = EffectivePlugins(
|
|
240
|
+
enabled=self.enabled_plugins,
|
|
241
|
+
blocked=self.blocked_plugins,
|
|
242
|
+
not_allowed=self.not_allowed_plugins,
|
|
243
|
+
disabled=self.disabled_plugins,
|
|
244
|
+
extra_marketplaces=self.extra_marketplaces,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return (plugins, self.marketplaces)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
# Resolution Orchestrator
|
|
252
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def resolve_effective_config(
|
|
256
|
+
config: OrganizationConfig,
|
|
257
|
+
team_id: str,
|
|
258
|
+
) -> EffectiveConfig:
|
|
259
|
+
"""Resolve effective configuration for a team (inline or federated).
|
|
260
|
+
|
|
261
|
+
This is the main orchestrator for Phase 2 configuration resolution.
|
|
262
|
+
It determines whether a team uses inline or federated configuration
|
|
263
|
+
and applies the appropriate resolution strategy.
|
|
264
|
+
|
|
265
|
+
For inline teams (no config_source):
|
|
266
|
+
- Uses compute_effective_plugins() from compute.py
|
|
267
|
+
- Returns EffectiveConfig with is_federated=False
|
|
268
|
+
|
|
269
|
+
For federated teams (has config_source):
|
|
270
|
+
- Fetches external team config via fetch_team_config()
|
|
271
|
+
- Uses compute_effective_plugins_federated() from compute.py
|
|
272
|
+
- Returns EffectiveConfig with is_federated=True and metadata
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
config: Organization configuration with profiles and security
|
|
276
|
+
team_id: The profile/team ID to resolve plugins for
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
EffectiveConfig with complete resolved configuration
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
TeamNotFoundError: If team_id is not in config.profiles
|
|
283
|
+
RuntimeError: If federated fetch fails
|
|
284
|
+
"""
|
|
285
|
+
# Import here to avoid circular imports
|
|
286
|
+
# Validate team exists (this also raises TeamNotFoundError if missing)
|
|
287
|
+
from scc_cli.marketplace.compute import TeamNotFoundError
|
|
288
|
+
from scc_cli.marketplace.schema import TeamConfig
|
|
289
|
+
|
|
290
|
+
if team_id not in config.profiles:
|
|
291
|
+
raise TeamNotFoundError(
|
|
292
|
+
team_id=team_id,
|
|
293
|
+
available_teams=list(config.profiles.keys()),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
profile = config.profiles[team_id]
|
|
297
|
+
org_marketplaces = config.marketplaces or {}
|
|
298
|
+
|
|
299
|
+
# Check if team has config_source (federated) or not (inline)
|
|
300
|
+
if profile.config_source is None:
|
|
301
|
+
# ─────────────────────────────────────────────────────────────────
|
|
302
|
+
# INLINE TEAM: No external config source
|
|
303
|
+
# ─────────────────────────────────────────────────────────────────
|
|
304
|
+
plugins = compute_effective_plugins(config, team_id)
|
|
305
|
+
|
|
306
|
+
return EffectiveConfig(
|
|
307
|
+
team_id=team_id,
|
|
308
|
+
is_federated=False,
|
|
309
|
+
enabled_plugins=plugins.enabled,
|
|
310
|
+
blocked_plugins=plugins.blocked,
|
|
311
|
+
disabled_plugins=plugins.disabled,
|
|
312
|
+
not_allowed_plugins=plugins.not_allowed,
|
|
313
|
+
marketplaces=org_marketplaces,
|
|
314
|
+
extra_marketplaces=plugins.extra_marketplaces,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
else:
|
|
318
|
+
# ─────────────────────────────────────────────────────────────────
|
|
319
|
+
# FEDERATED TEAM: Has external config source
|
|
320
|
+
# ─────────────────────────────────────────────────────────────────
|
|
321
|
+
source = profile.config_source
|
|
322
|
+
|
|
323
|
+
# Fetch the external team config with cache fallback (T2b-01+02)
|
|
324
|
+
fallback_result = fetch_team_config_with_fallback(source, team_id)
|
|
325
|
+
|
|
326
|
+
if not fallback_result.success or fallback_result.result.team_config is None:
|
|
327
|
+
fetch_result = fallback_result.result
|
|
328
|
+
raise ConfigFetchError(
|
|
329
|
+
team_id=team_id,
|
|
330
|
+
source_type=fetch_result.source_type,
|
|
331
|
+
source_url=fetch_result.source_url,
|
|
332
|
+
error=fetch_result.error or "Unknown fetch error",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Extract cache status for caller visibility
|
|
336
|
+
fetch_result = fallback_result.result
|
|
337
|
+
used_cached_config = fallback_result.used_cache
|
|
338
|
+
cache_is_stale = fallback_result.is_stale
|
|
339
|
+
staleness_warning = fallback_result.staleness_warning
|
|
340
|
+
|
|
341
|
+
# Parse the fetched config as TeamConfig
|
|
342
|
+
team_config = TeamConfig.model_validate(fetch_result.team_config)
|
|
343
|
+
|
|
344
|
+
# Validate team config against trust grants (T2a-27: marketplace collisions)
|
|
345
|
+
trust = profile.trust
|
|
346
|
+
if trust:
|
|
347
|
+
validate_team_config_trust(
|
|
348
|
+
team_config=team_config,
|
|
349
|
+
trust=trust,
|
|
350
|
+
team_name=team_id,
|
|
351
|
+
org_marketplaces=org_marketplaces,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Use federated plugin computation
|
|
355
|
+
plugins = compute_effective_plugins_federated(config, team_id, team_config)
|
|
356
|
+
|
|
357
|
+
# Build effective marketplaces based on trust grants (T2a-28)
|
|
358
|
+
# If inherit_org_marketplaces=false, org marketplaces are NOT inherited
|
|
359
|
+
effective_marketplaces: dict[str, MarketplaceSource] = {}
|
|
360
|
+
|
|
361
|
+
if trust and not trust.inherit_org_marketplaces:
|
|
362
|
+
# T2a-28: Validate that defaults.enabled_plugins don't require org marketplaces
|
|
363
|
+
# when inherit_org_marketplaces=false
|
|
364
|
+
_validate_defaults_dont_need_org_marketplaces(
|
|
365
|
+
config=config,
|
|
366
|
+
org_marketplaces=org_marketplaces,
|
|
367
|
+
team_id=team_id,
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
# inherit_org_marketplaces=true (default) - include org marketplaces
|
|
371
|
+
effective_marketplaces = dict(org_marketplaces)
|
|
372
|
+
|
|
373
|
+
# Add team marketplaces if trust allows
|
|
374
|
+
if team_config.marketplaces and trust and trust.allow_additional_marketplaces:
|
|
375
|
+
effective_marketplaces.update(team_config.marketplaces)
|
|
376
|
+
|
|
377
|
+
return EffectiveConfig(
|
|
378
|
+
team_id=team_id,
|
|
379
|
+
is_federated=True,
|
|
380
|
+
enabled_plugins=plugins.enabled,
|
|
381
|
+
config_source=source,
|
|
382
|
+
config_commit_sha=fetch_result.commit_sha,
|
|
383
|
+
config_etag=fetch_result.etag,
|
|
384
|
+
# Cache status - T2b-03: note that validate_team_config_trust() above
|
|
385
|
+
# uses CURRENT org security, so cached configs are always validated
|
|
386
|
+
# against the latest org policy
|
|
387
|
+
used_cached_config=used_cached_config,
|
|
388
|
+
cache_is_stale=cache_is_stale,
|
|
389
|
+
staleness_warning=staleness_warning,
|
|
390
|
+
blocked_plugins=plugins.blocked,
|
|
391
|
+
disabled_plugins=plugins.disabled,
|
|
392
|
+
not_allowed_plugins=plugins.not_allowed,
|
|
393
|
+
marketplaces=effective_marketplaces,
|
|
394
|
+
extra_marketplaces=plugins.extra_marketplaces,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
399
|
+
# Helper Functions
|
|
400
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _validate_defaults_dont_need_org_marketplaces(
|
|
404
|
+
config: OrganizationConfig,
|
|
405
|
+
org_marketplaces: dict[str, MarketplaceSource],
|
|
406
|
+
team_id: str,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Validate that defaults.enabled_plugins don't require org marketplaces.
|
|
409
|
+
|
|
410
|
+
When a federated team sets inherit_org_marketplaces=false, they won't have
|
|
411
|
+
access to org-defined marketplaces. However, defaults.enabled_plugins may
|
|
412
|
+
reference plugins from those marketplaces, creating a conflict.
|
|
413
|
+
|
|
414
|
+
This function validates that no such conflict exists.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
config: Organization configuration with defaults
|
|
418
|
+
org_marketplaces: Dict of org-defined marketplaces
|
|
419
|
+
team_id: Team name for error messages
|
|
420
|
+
|
|
421
|
+
Raises:
|
|
422
|
+
TrustViolationError: If defaults.enabled_plugins reference org marketplaces
|
|
423
|
+
"""
|
|
424
|
+
# Import here to avoid circular imports
|
|
425
|
+
from scc_cli.marketplace.constants import IMPLICIT_MARKETPLACES
|
|
426
|
+
|
|
427
|
+
if not config.defaults or not config.defaults.enabled_plugins:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
# Check each default plugin for org marketplace references
|
|
431
|
+
conflicting_plugins: list[str] = []
|
|
432
|
+
conflicting_marketplaces: set[str] = set()
|
|
433
|
+
|
|
434
|
+
for plugin_ref in config.defaults.enabled_plugins:
|
|
435
|
+
if "@" in plugin_ref:
|
|
436
|
+
# Extract marketplace from plugin@marketplace format
|
|
437
|
+
marketplace_name = plugin_ref.split("@")[1]
|
|
438
|
+
|
|
439
|
+
# Skip implicit marketplaces - they're always available
|
|
440
|
+
if marketplace_name in IMPLICIT_MARKETPLACES:
|
|
441
|
+
continue
|
|
442
|
+
|
|
443
|
+
# Check if this marketplace is org-defined
|
|
444
|
+
if marketplace_name in org_marketplaces:
|
|
445
|
+
conflicting_plugins.append(plugin_ref)
|
|
446
|
+
conflicting_marketplaces.add(marketplace_name)
|
|
447
|
+
|
|
448
|
+
if conflicting_plugins:
|
|
449
|
+
plugins_str = ", ".join(conflicting_plugins)
|
|
450
|
+
marketplaces_str = ", ".join(sorted(conflicting_marketplaces))
|
|
451
|
+
raise TrustViolationError(
|
|
452
|
+
team_name=team_id,
|
|
453
|
+
violation=(
|
|
454
|
+
f"Team has inherit_org_marketplaces=false but defaults.enabled_plugins "
|
|
455
|
+
f"reference org marketplaces. Conflicting plugins: [{plugins_str}]. "
|
|
456
|
+
f"These require org marketplaces: [{marketplaces_str}]. "
|
|
457
|
+
"Either set inherit_org_marketplaces=true or remove these plugins from defaults."
|
|
458
|
+
),
|
|
459
|
+
)
|