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,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)
|