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.

Files changed (153) 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 +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. 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
+ )