Skip to content

Commit 41ec3f9

Browse files
authored
detect conflicting env keys set by enterprise settings (#250)
* detect conflicting env keys set by enterprise settings * check ANTHROPIC_MODEL not set in managed settings
1 parent f654a50 commit 41ec3f9

1 file changed

Lines changed: 51 additions & 14 deletions

File tree

src/ucode/agents/claude.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,46 @@ def _managed_settings_path() -> Path | None:
129129
return None
130130

131131

132-
def _managed_api_key_helper_path() -> Path | None:
133-
"""Path to the enterprise managed-settings.json when it sets an apiKeyHelper,
134-
else None. The managed scope always wins over --settings and subscription
135-
OAuth, so a managed apiKeyHelper silently shadows relayed auth."""
132+
def _managed_relayed_conflicts() -> tuple[Path, list[str]] | None:
133+
"""Enterprise managed-settings keys that would break relayed auth, if any.
134+
The managed scope always wins (per key) over the --settings file and
135+
subscription OAuth, so a managed value here overrides what ucode writes:
136+
'apiKeyHelper' shadows the subscription login, 'env.ANTHROPIC_BASE_URL'
137+
clobbers our loopback proxy URL, and 'env.ANTHROPIC_CUSTOM_HEADERS' drops
138+
the Databricks-Model-Provider-Service routing headers — each sends traffic
139+
somewhere the relayed token swap can't reach or route correctly.
140+
Returns (path, conflicting-key-labels) or None when there's no conflict."""
136141
path = _managed_settings_path()
137142
if path is None or not path.is_file():
138143
return None
139144
settings = read_json_safe(path)
140-
if isinstance(settings, dict) and settings.get("apiKeyHelper"):
141-
return path
142-
return None
145+
conflicts: list[str] = []
146+
if settings.get("apiKeyHelper"):
147+
conflicts.append("apiKeyHelper")
148+
env = settings.get("env")
149+
if isinstance(env, dict):
150+
if env.get("ANTHROPIC_BASE_URL"):
151+
conflicts.append("env.ANTHROPIC_BASE_URL")
152+
if env.get("ANTHROPIC_CUSTOM_HEADERS"):
153+
conflicts.append("env.ANTHROPIC_CUSTOM_HEADERS")
154+
return (path, conflicts) if conflicts else None
155+
156+
157+
def _managed_pinned_model() -> tuple[Path, str] | None:
158+
"""Model that enterprise managed settings force Claude Code to launch with,
159+
if any. Only `ANTHROPIC_MODEL` sets the launch model — the
160+
`ANTHROPIC_DEFAULT_*` family aliases just remap what each tier resolves to
161+
when selected, so they don't change the default. Doesn't break relayed auth,
162+
but silently overrides Claude Code's model, so we surface it. Returns
163+
(path, model_id) or None when the file is absent or `ANTHROPIC_MODEL` is unset."""
164+
path = _managed_settings_path()
165+
if path is None or not path.is_file():
166+
return None
167+
settings = read_json_safe(path)
168+
env = settings.get("env")
169+
if not isinstance(env, dict) or not env.get("ANTHROPIC_MODEL"):
170+
return None
171+
return (path, str(env["ANTHROPIC_MODEL"]))
143172

144173

145174
def relayed_proxy_base_url(state: dict) -> str:
@@ -800,17 +829,25 @@ def _launch_relayed(state: dict, binary: str, tool_args: list[str]) -> None:
800829
exec, so we spawn-and-wait rather than replacing the process)."""
801830
from ucode.gateway_proxy import start_proxy
802831

803-
managed_path = _managed_api_key_helper_path()
804-
if managed_path is not None:
832+
conflict = _managed_relayed_conflicts()
833+
if conflict is not None:
834+
managed_path, keys = conflict
805835
print_err(
806-
f"Enterprise managed settings ({managed_path}) set an 'apiKeyHelper', "
807-
"which Claude Code always applies over relayed (Claude Max/Enterprise) "
808-
"auth — you'd be billed for API usage instead of using your subscription. "
809-
"Remove the 'apiKeyHelper' from that file (or ask your admin to) before "
810-
"running relayed auth. Not starting Claude Code."
836+
"Enterprise managed settings are present, which Claude Code always "
837+
"applies over relayed (Claude Max/Enterprise) auth. Remove "
838+
f"{', '.join(keys)} from {managed_path} file before running relayed auth."
811839
)
812840
raise SystemExit(1)
813841

842+
pinned_model = _managed_pinned_model()
843+
if pinned_model is not None:
844+
managed_path, model_id = pinned_model
845+
print_warning(
846+
f"Default model ANTHROPIC_MODEL: {model_id} is set in your "
847+
f"enterprise-managed settings ({managed_path}) and may override ucode "
848+
"settings. Remove this entry if you encounter issues."
849+
)
850+
814851
_ensure_subscription_login()
815852
workspace = state["workspace"]
816853
port = state.get("relayed_proxy_port")

0 commit comments

Comments
 (0)