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,485 @@
1
+ """
2
+ Claude Code Settings Adapter.
3
+
4
+ This module is the ONLY place that knows about Claude Code's settings format.
5
+ If Claude Code changes its format, update ONLY this file + test_claude_adapter.py.
6
+
7
+ Current known format (may change):
8
+ - extraKnownMarketplaces: dict of marketplace configs
9
+ - enabledPlugins: list of "plugin@marketplace" strings
10
+
11
+ MAINTENANCE RULE: If Claude Code changes format, update ONLY:
12
+ 1. claude_adapter.py - this file
13
+ 2. test_claude_adapter.py - adapter output shape tests
14
+
15
+ No other module should import or reference extraKnownMarketplaces or enabledPlugins.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from collections.abc import MutableMapping
22
+ from dataclasses import dataclass
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from scc_cli.auth import is_remote_command_allowed
26
+ from scc_cli.auth import resolve_auth as _resolve_auth_impl
27
+ from scc_cli.profiles import get_marketplace_url
28
+
29
+ if TYPE_CHECKING:
30
+ from scc_cli.profiles import EffectiveConfig, MCPServer
31
+
32
+
33
+ # ═══════════════════════════════════════════════════════════════════════════════
34
+ # Data Classes
35
+ # ═══════════════════════════════════════════════════════════════════════════════
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class AuthResult:
40
+ """Result of resolving marketplace auth.
41
+
42
+ Attributes:
43
+ env_name: Environment variable name for the token
44
+ token: The actual token value
45
+ also_set: Additional standard env var names to set (e.g., GITLAB_TOKEN)
46
+ """
47
+
48
+ env_name: str
49
+ token: str
50
+ also_set: tuple[str, ...] = ()
51
+
52
+
53
+ # ═══════════════════════════════════════════════════════════════════════════════
54
+ # Auth Resolution
55
+ # ═══════════════════════════════════════════════════════════════════════════════
56
+
57
+
58
+ def resolve_auth_with_name(
59
+ auth_spec: str | None,
60
+ allow_command: bool = False,
61
+ ) -> tuple[str | None, str | None]:
62
+ """Resolve auth spec to (token, env_name) tuple.
63
+
64
+ SECURITY: Uses auth.py module with shell=False to prevent shell injection.
65
+ Command execution is disabled by default (secure by default).
66
+
67
+ Supports:
68
+ - env:VAR_NAME - read from environment variable (always allowed)
69
+ - command:CMD - execute command (only if allow_command=True)
70
+
71
+ Args:
72
+ auth_spec: Auth specification string or None
73
+ allow_command: Whether to allow command: auth specs. Default False
74
+ for security (prevents arbitrary command execution from untrusted
75
+ sources like remote org config). Set True only for trusted sources
76
+ or when user explicitly opts in via SCC_ALLOW_REMOTE_COMMANDS=1.
77
+
78
+ Returns:
79
+ Tuple of (token, env_name). Token is None if not available.
80
+ env_name is always returned for env: specs (useful for error messages).
81
+ """
82
+ if not auth_spec:
83
+ return (None, None)
84
+
85
+ auth_spec = auth_spec.strip()
86
+ if not auth_spec:
87
+ return (None, None)
88
+
89
+ # Extract env_name for env: specs (even if token is missing - for error messages)
90
+ # This preserves the old behavior where env_name was always returned
91
+ env_name_fallback = None
92
+ if auth_spec.startswith("env:"):
93
+ env_name_fallback = auth_spec[4:]
94
+
95
+ try:
96
+ # Use secure auth.py implementation (shell=False, validated binary)
97
+ # Pass through allow_command to enforce trust model
98
+ result = _resolve_auth_impl(auth_spec, allow_command=allow_command)
99
+ if result:
100
+ # Use result.env_name if available, otherwise use our fallback
101
+ env_name = result.env_name if result.env_name else "SCC_AUTH_TOKEN"
102
+ return (result.token, env_name)
103
+ # Auth failed but we have env name from spec - return it for error messages
104
+ if env_name_fallback:
105
+ return (None, env_name_fallback)
106
+ return (None, None)
107
+ except (ValueError, RuntimeError):
108
+ # Auth resolution failed - return env_name for error messages if available
109
+ # ValueError: invalid auth spec format
110
+ # RuntimeError: command execution failed
111
+ if env_name_fallback:
112
+ return (None, env_name_fallback)
113
+ return (None, None)
114
+
115
+
116
+ def resolve_marketplace_auth(
117
+ marketplace: dict[str, Any],
118
+ allow_command: bool = False,
119
+ ) -> AuthResult | None:
120
+ """Resolve marketplace auth spec to AuthResult.
121
+
122
+ SECURITY: Command execution is disabled by default to prevent arbitrary
123
+ code execution from untrusted remote org configs.
124
+
125
+ Determine which standard env vars to also set based on marketplace type:
126
+ - gitlab: also set GITLAB_TOKEN
127
+ - github: also set GITHUB_TOKEN
128
+
129
+ Args:
130
+ marketplace: Marketplace config dict
131
+ allow_command: Whether to allow command: auth specs. Default False
132
+ for security. Use is_remote_command_allowed() to check if user
133
+ has opted in via SCC_ALLOW_REMOTE_COMMANDS=1.
134
+
135
+ Returns:
136
+ AuthResult with token and env var names, or None if no auth needed
137
+ """
138
+ auth_spec = marketplace.get("auth")
139
+ if not auth_spec:
140
+ return None
141
+
142
+ token, env_name = resolve_auth_with_name(auth_spec, allow_command=allow_command)
143
+ if not token or not env_name:
144
+ return None
145
+
146
+ # Determine standard env vars to also set based on marketplace type
147
+ marketplace_type = marketplace.get("type", "").lower()
148
+ also_set: tuple[str, ...] = ()
149
+
150
+ if marketplace_type == "gitlab":
151
+ also_set = ("GITLAB_TOKEN",)
152
+ elif marketplace_type == "github":
153
+ also_set = ("GITHUB_TOKEN",)
154
+ # https type: no standard vars to set
155
+
156
+ return AuthResult(env_name=env_name, token=token, also_set=also_set)
157
+
158
+
159
+ # ═══════════════════════════════════════════════════════════════════════════════
160
+ # Claude Code Settings Building
161
+ # ═══════════════════════════════════════════════════════════════════════════════
162
+
163
+
164
+ def _build_source_object(marketplace: dict[str, Any]) -> dict[str, Any]:
165
+ """Build Claude Code's source object from SCC marketplace config.
166
+
167
+ Handle the translation from SCC's org-config format to Claude's
168
+ extraKnownMarketplaces source format.
169
+
170
+ SCC type -> Claude source type mapping:
171
+ - github -> github (requires 'repo')
172
+ - gitlab -> git (builds URL from 'host' and 'repo')
173
+ - https -> url (requires 'url')
174
+
175
+ Args:
176
+ marketplace: SCC marketplace config dict with 'type' and type-specific fields
177
+
178
+ Returns:
179
+ Claude source object with 'source' type and appropriate fields
180
+
181
+ Raises:
182
+ ValueError: If required fields are missing for the marketplace type
183
+ """
184
+ marketplace_type = marketplace.get("type", "").lower()
185
+
186
+ if marketplace_type == "github":
187
+ # GitHub requires 'repo' field
188
+ repo = marketplace.get("repo")
189
+ if not repo:
190
+ raise ValueError(
191
+ f"GitHub marketplace '{marketplace.get('name', 'unknown')}' "
192
+ "missing required 'repo' field"
193
+ )
194
+ source = {"source": "github", "repo": repo}
195
+ # Optional ref field
196
+ if marketplace.get("ref"):
197
+ source["ref"] = marketplace["ref"]
198
+ return source
199
+
200
+ elif marketplace_type == "gitlab":
201
+ # GitLab maps to 'git' source type with constructed URL
202
+ repo = marketplace.get("repo")
203
+ host = marketplace.get("host", "gitlab.com")
204
+ if not repo:
205
+ raise ValueError(
206
+ f"GitLab marketplace '{marketplace.get('name', 'unknown')}' "
207
+ "missing required 'repo' field"
208
+ )
209
+ # Build HTTPS URL from host and repo
210
+ url = f"https://{host}/{repo}"
211
+ source = {"source": "git", "url": url}
212
+ # Optional ref field
213
+ if marketplace.get("ref"):
214
+ source["ref"] = marketplace["ref"]
215
+ return source
216
+
217
+ elif marketplace_type == "https":
218
+ # HTTPS maps to 'url' source type
219
+ https_url: str | None = marketplace.get("url")
220
+ if not https_url:
221
+ raise ValueError(
222
+ f"HTTPS marketplace '{marketplace.get('name', 'unknown')}' "
223
+ "missing required 'url' field"
224
+ )
225
+ return {"source": "url", "url": https_url}
226
+
227
+ else:
228
+ # Unknown type - try to build URL-based source as fallback
229
+ url = get_marketplace_url(marketplace)
230
+ if url:
231
+ return {"source": "url", "url": url}
232
+ raise ValueError(
233
+ f"Marketplace '{marketplace.get('name', 'unknown')}' has "
234
+ f"unknown type '{marketplace_type}' and no fallback URL"
235
+ )
236
+
237
+
238
+ def build_claude_settings(
239
+ profile: dict[str, Any], marketplace: dict[str, Any], org_id: str | None
240
+ ) -> dict[str, Any]:
241
+ """Build Claude Code settings payload.
242
+
243
+ This is the ONLY function that knows Claude Code's settings format.
244
+
245
+ Claude's extraKnownMarketplaces format (as of Dec 2024):
246
+ {
247
+ "marketplaceKey": {
248
+ "source": {"source": "github", "repo": "owner/repo", "ref": "main"}
249
+ }
250
+ }
251
+
252
+ Args:
253
+ profile: Resolved profile with 'plugin' key
254
+ marketplace: Resolved marketplace with URL info
255
+ org_id: Organization ID for namespacing (falls back to marketplace name)
256
+
257
+ Returns:
258
+ Settings dict to inject into Claude Code
259
+
260
+ Raises:
261
+ ValueError: If marketplace is missing required fields for its type
262
+ """
263
+ # Key is org_id if provided, otherwise marketplace name
264
+ marketplace_key = org_id or marketplace.get("name", "default")
265
+
266
+ # Build Claude's nested source object from SCC marketplace config
267
+ source_object = _build_source_object(marketplace)
268
+
269
+ # Build enabled plugins list
270
+ plugin_name = profile.get("plugin")
271
+ enabled_plugins = []
272
+ if plugin_name:
273
+ enabled_plugins.append(f"{plugin_name}@{marketplace_key}")
274
+
275
+ return {
276
+ "extraKnownMarketplaces": {
277
+ marketplace_key: {
278
+ "source": source_object,
279
+ }
280
+ },
281
+ "enabledPlugins": enabled_plugins,
282
+ }
283
+
284
+
285
+ def get_settings_file_content(settings: dict[str, Any]) -> str:
286
+ """Serialize settings for injection into container.
287
+
288
+ Args:
289
+ settings: Settings dict from build_claude_settings()
290
+
291
+ Returns:
292
+ Formatted JSON string
293
+ """
294
+ return json.dumps(settings, indent=2)
295
+
296
+
297
+ # ═══════════════════════════════════════════════════════════════════════════════
298
+ # V2 Settings Builder (EffectiveConfig)
299
+ # ═══════════════════════════════════════════════════════════════════════════════
300
+
301
+
302
+ def build_settings_from_effective_config(
303
+ effective_config: EffectiveConfig,
304
+ org_id: str | None = None,
305
+ marketplace: dict[str, Any] | None = None,
306
+ ) -> dict[str, Any]:
307
+ """Build Claude Code settings from EffectiveConfig.
308
+
309
+ This function translates the governance-aware EffectiveConfig
310
+ to Claude Code's settings format.
311
+
312
+ Args:
313
+ effective_config: The computed effective configuration with
314
+ plugins, MCP servers, and session settings
315
+ org_id: Organization ID for namespacing (optional)
316
+ marketplace: Marketplace config for source info (optional,
317
+ needed if extraKnownMarketplaces is required)
318
+
319
+ Returns:
320
+ Settings dict ready for injection into Claude Code
321
+ """
322
+
323
+ settings: dict[str, Any] = {}
324
+
325
+ # Build enabled plugins list
326
+ marketplace_key = org_id or "default"
327
+ enabled_plugins = []
328
+ for plugin in effective_config.plugins:
329
+ enabled_plugins.append(f"{plugin}@{marketplace_key}")
330
+
331
+ if enabled_plugins:
332
+ settings["enabledPlugins"] = enabled_plugins
333
+
334
+ # Build MCP servers config
335
+ if effective_config.mcp_servers:
336
+ mcp_servers: dict[str, Any] = {}
337
+ for server in effective_config.mcp_servers:
338
+ server_config = _build_mcp_server_config(server)
339
+ if server_config:
340
+ mcp_servers[server.name] = server_config
341
+ if mcp_servers:
342
+ settings["mcpServers"] = mcp_servers
343
+
344
+ # Include marketplace if provided
345
+ if marketplace:
346
+ try:
347
+ source_object = _build_source_object(marketplace)
348
+ settings["extraKnownMarketplaces"] = {marketplace_key: {"source": source_object}}
349
+ except ValueError:
350
+ # Skip if marketplace is incomplete
351
+ pass
352
+
353
+ return settings
354
+
355
+
356
+ def _build_mcp_server_config(server: MCPServer) -> dict[str, Any] | None:
357
+ """Build Claude Code MCP server config from MCPServer dataclass.
358
+
359
+ Claude Code MCP format (Dec 2024):
360
+ - HTTP: {"type": "http", "url": "...", "headers": {...}}
361
+ - SSE: {"type": "sse", "url": "...", "headers": {...}}
362
+ - Stdio: {"type": "stdio", "command": "...", "args": [...], "env": {...}}
363
+
364
+ Args:
365
+ server: MCPServer dataclass instance
366
+
367
+ Returns:
368
+ Dict in Claude Code's mcpServers format, or None if invalid
369
+ """
370
+ if server.type == "sse":
371
+ if not server.url:
372
+ return None
373
+ config: dict[str, Any] = {
374
+ "type": "sse",
375
+ "url": server.url,
376
+ }
377
+ if server.headers:
378
+ config["headers"] = server.headers
379
+ return config
380
+
381
+ elif server.type == "http":
382
+ if not server.url:
383
+ return None
384
+ config = {
385
+ "type": "http",
386
+ "url": server.url,
387
+ }
388
+ if server.headers:
389
+ config["headers"] = server.headers
390
+ return config
391
+
392
+ elif server.type == "stdio":
393
+ if not server.command:
394
+ return None
395
+ config = {
396
+ "type": "stdio",
397
+ "command": server.command,
398
+ }
399
+ if server.args:
400
+ config["args"] = server.args
401
+ if server.env:
402
+ config["env"] = server.env
403
+ return config
404
+
405
+ else:
406
+ return None
407
+
408
+
409
+ def translate_mcp_server(server: MCPServer) -> tuple[str, dict[str, Any]] | tuple[None, None]:
410
+ """Translate MCPServer to Claude Code format.
411
+
412
+ Return a tuple of (server_name, config_dict) for use in
413
+ Claude Code's mcpServers settings.
414
+
415
+ Args:
416
+ server: MCPServer dataclass instance
417
+
418
+ Returns:
419
+ Tuple of (name, config) or (None, None) if invalid
420
+ """
421
+ config = _build_mcp_server_config(server)
422
+ if config is None:
423
+ return None, None
424
+ return server.name, config
425
+
426
+
427
+ def build_mcp_servers(effective_config: EffectiveConfig) -> dict[str, Any]:
428
+ """Build MCP servers dict from EffectiveConfig.
429
+
430
+ Return the mcpServers dict in Claude Code's format:
431
+ {"server-name": {"type": "...", "url": "..."}, ...}
432
+
433
+ Args:
434
+ effective_config: The computed effective configuration
435
+
436
+ Returns:
437
+ Dict mapping server names to their configurations
438
+ """
439
+ mcp_servers: dict[str, Any] = {}
440
+ for server in effective_config.mcp_servers:
441
+ name, config = translate_mcp_server(server)
442
+ if name and config:
443
+ mcp_servers[name] = config
444
+ return mcp_servers
445
+
446
+
447
+ # ═══════════════════════════════════════════════════════════════════════════════
448
+ # Credential Injection
449
+ # ═══════════════════════════════════════════════════════════════════════════════
450
+
451
+
452
+ def inject_credentials(
453
+ marketplace: dict[str, Any],
454
+ docker_env: MutableMapping[str, str],
455
+ allow_command: bool | None = None,
456
+ ) -> None:
457
+ """Inject marketplace credentials into Docker environment.
458
+
459
+ SECURITY: By default, check SCC_ALLOW_REMOTE_COMMANDS env var to determine
460
+ if command: auth is allowed. This prevents arbitrary code execution from
461
+ untrusted remote org configs.
462
+
463
+ Use setdefault to preserve any user-provided overrides.
464
+
465
+ Args:
466
+ marketplace: Marketplace config dict
467
+ docker_env: Mutable dict to inject credentials into
468
+ allow_command: Whether to allow command: auth specs. If None (default),
469
+ use is_remote_command_allowed() to check env var. Pass True/False
470
+ to override.
471
+ """
472
+ # Determine if command auth is allowed
473
+ if allow_command is None:
474
+ allow_command = is_remote_command_allowed()
475
+
476
+ result = resolve_marketplace_auth(marketplace, allow_command=allow_command)
477
+ if not result:
478
+ return
479
+
480
+ # Set the original env var name
481
+ docker_env.setdefault(result.env_name, result.token)
482
+
483
+ # Also set standard names for convenience
484
+ for name in result.also_set:
485
+ docker_env.setdefault(name, result.token)