From b562164fc47c0e3fe452d814318a08218445244c Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Tue, 26 May 2026 21:13:56 +0000 Subject: [PATCH 1/5] Strip ambient profile from host-scoped Databricks auth --- src/ucode/agents/codex.py | 101 ++++--------------------------- src/ucode/databricks.py | 24 ++++---- tests/test_agent_codex.py | 112 ++++++++++++++--------------------- tests/test_databricks.py | 46 ++++++++++++++ tests/test_e2e.py | 74 ++++++++++++++--------- tests/test_e2e_user_agent.py | 24 ++++++-- 6 files changed, 176 insertions(+), 205 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 2077bee..ce7aa3c 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -1,9 +1,8 @@ -"""Codex agent: writes ~/.codex/ucode.config.toml for Databricks-backed Codex.""" +"""Codex agent: writes ~/.codex/config.toml with a Databricks-backed model provider.""" from __future__ import annotations import os -import re from pathlib import Path from ucode.agent_updates import available_npm_package_update @@ -24,13 +23,10 @@ from ucode.telemetry import agent_version, ucode_version CODEX_CONFIG_DIR = Path.home() / ".codex" -CODEX_PROFILE_NAME = "ucode" -CODEX_CONFIG_PATH = CODEX_CONFIG_DIR / f"{CODEX_PROFILE_NAME}.config.toml" -CODEX_BACKUP_PATH = APP_DIR / "codex-ucode-config.backup.toml" +CODEX_CONFIG_PATH = CODEX_CONFIG_DIR / "config.toml" +CODEX_BACKUP_PATH = APP_DIR / "codex-config.backup.toml" +LEGACY_CODEX_PROFILE_NAME = "ucode" CODEX_MODEL_PROVIDER_NAME = "ucode-databricks" -MINIMUM_CODEX_VERSION = (0, 134, 0) -MINIMUM_CODEX_VERSION_TEXT = "0.134.0" - SPEC: ToolSpec = { "binary": "codex", @@ -41,8 +37,8 @@ } MANAGED_KEYS: list[list[str]] = [ - ["model_provider"], ["model"], + ["model_provider"], ["model_providers", CODEX_MODEL_PROVIDER_NAME], ["model_providers", CODEX_MODEL_PROVIDER_NAME, "http_headers"], ] @@ -52,49 +48,6 @@ def is_update_available() -> tuple[str, str] | None: return available_npm_package_update(SPEC["package"]) -def _parse_version(value: str) -> tuple[int, int, int] | None: - match = re.search(r"(\d+)\.(\d+)\.(\d+)", value) - if not match: - return None - major, minor, patch = match.groups() - return int(major), int(minor), int(patch) - - -def _installed_version_status() -> tuple[str, bool] | None: - version = agent_version(SPEC["binary"]) - parsed = _parse_version(version) - if parsed is None: - return None - return version, parsed < MINIMUM_CODEX_VERSION - - -def minimum_version_error() -> str | None: - status = _installed_version_status() - if status is None: - return None - version, is_too_old = status - if not is_too_old: - return None - return ( - f"Codex CLI {version} is too old for ucode's Codex profile config. " - f"Codex CLI must be updated to {MINIMUM_CODEX_VERSION_TEXT} or newer; " - f"run `npm install -g {SPEC['package']}` or `ucode configure`." - ) - - -def required_update_message() -> str | None: - status = _installed_version_status() - if status is None: - return None - version, is_too_old = status - if not is_too_old: - return None - return ( - f"Codex CLI {version} is older than required {MINIMUM_CODEX_VERSION_TEXT}; " - "updating Codex is required for ucode's Codex profile config." - ) - - def render_overlay( workspace: str, model: str | None = None, databricks_profile: str | None = None ) -> dict: @@ -122,47 +75,17 @@ def render_overlay( return overlay -def _legacy_config_path() -> Path: - return CODEX_CONFIG_PATH.parent / "config.toml" - - -def _legacy_backup_path() -> Path: - return CODEX_BACKUP_PATH.with_name("codex-legacy-config.backup.toml") - - -def _remove_legacy_ucode_profile() -> None: - """Remove ucode's old [profiles.ucode] entry from shared Codex config.""" - path = _legacy_config_path() - if path == CODEX_CONFIG_PATH or not path.exists(): - return - - doc = read_toml_safe(path) - changed = False - - profiles = doc.get("profiles") - if isinstance(profiles, dict) and CODEX_PROFILE_NAME in profiles: - backup_existing_file(path, _legacy_backup_path()) - profiles.pop(CODEX_PROFILE_NAME, None) - if not profiles: - doc.pop("profiles", None) - changed = True - - if doc.get("profile") == CODEX_PROFILE_NAME: - backup_existing_file(path, _legacy_backup_path()) - doc.pop("profile", None) - changed = True - - if changed: - write_toml_file(path, doc) - - def write_tool_config(state: dict, model: str | None = None) -> dict: - _remove_legacy_ucode_profile() backup_existing_file(CODEX_CONFIG_PATH, CODEX_BACKUP_PATH) overlay = render_overlay( state["workspace"], model or default_model(state), state.get("profile") ) doc = read_toml_safe(CODEX_CONFIG_PATH) + legacy_profiles = doc.get("profiles") + if isinstance(legacy_profiles, dict): + legacy_profiles.pop(LEGACY_CODEX_PROFILE_NAME, None) + if not legacy_profiles: + doc.pop("profiles", None) deep_merge_dict(doc, overlay) write_toml_file(CODEX_CONFIG_PATH, doc) state = mark_tool_managed(state, "codex", MANAGED_KEYS) @@ -180,14 +103,12 @@ def launch(state: dict, tool_args: list[str]) -> None: workspace = state.get("workspace") if workspace: os.environ["OAUTH_TOKEN"] = get_databricks_token(workspace, state.get("profile")) - os.execvp(binary, [binary, "--profile", CODEX_PROFILE_NAME, *tool_args]) + os.execvp(binary, [binary, *tool_args]) def validate_cmd(binary: str) -> list[str]: return [ binary, - "--profile", - CODEX_PROFILE_NAME, "exec", "--skip-git-repo-check", "say hi in 5 words or less", diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 1d0115c..8367ff6 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -278,9 +278,11 @@ def run( ) -def build_databricks_cli_env(workspace: str) -> dict[str, str]: +def build_databricks_cli_env(workspace: str, profile: str | None = None) -> dict[str, str]: env = os.environ.copy() env["DATABRICKS_HOST"] = workspace + if profile is None: + env.pop("DATABRICKS_CONFIG_PROFILE", None) return env @@ -380,12 +382,8 @@ def has_valid_databricks_auth(workspace: str, profile: str | None = None) -> boo if os.environ.get("DATABRICKS_BEARER", "").strip(): return True _log_auth_diagnostics() - # Mirror run_databricks_login: when ~/.databrickscfg has multiple - # profiles for the same host, `databricks auth token --host …` refuses - # to disambiguate without --profile, so resolve it from the host here. - profile = profile or find_profile_name_for_host(workspace) try: - env = build_databricks_cli_env(workspace) + env = build_databricks_cli_env(workspace, profile) result = run( [ "databricks", @@ -492,7 +490,7 @@ def run_databricks_login(workspace: str, profile: str | None = None) -> None: workspace, *_profile_args(profile_name), ] - run(cmd, env=build_databricks_cli_env(workspace), timeout=300) + run(cmd, env=build_databricks_cli_env(workspace, profile_name), timeout=300) except subprocess.CalledProcessError as exc: raise RuntimeError("`databricks auth login` failed.") from exc except subprocess.TimeoutExpired as exc: @@ -528,10 +526,7 @@ def get_databricks_token( return bearer _log_auth_diagnostics() - # See has_valid_databricks_auth: resolve the profile from the host when - # the caller didn't supply one, so duplicate-host cfgs don't break us. - profile = profile or find_profile_name_for_host(workspace) - env = build_databricks_cli_env(workspace) + env = build_databricks_cli_env(workspace, profile) cmd = [ "databricks", "auth", @@ -595,12 +590,13 @@ def _fetch() -> str: token = _fetch() if not token: + profile_name = profile or find_profile_name_for_host(workspace) stale_profile_hint = "" - if profile: + if profile_name: stale_profile_hint = ( " The saved Databricks CLI profile may be stale or invalid. Try:\n" - f" databricks auth logout --profile {profile}\n" - f" databricks auth login --host {workspace} --profile {profile}" + f" databricks auth logout --profile {profile_name}\n" + f" databricks auth login --host {workspace} --profile {profile_name}" ) raise RuntimeError( f"Databricks CLI returned no access token for {workspace}. " diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index 45c709f..0c0f087 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -3,9 +3,9 @@ from __future__ import annotations import os +import tomllib from ucode.agents import codex -from ucode.config_io import read_toml_safe WS = "https://example.databricks.com" @@ -22,10 +22,11 @@ def test_display(self): class TestRenderOverlay: - def test_uses_profile_file_shape_without_legacy_profiles(self): + def test_sets_provider_without_legacy_profile(self): overlay = codex.render_overlay(WS) assert "profile" not in overlay assert "profiles" not in overlay + assert overlay["model_provider"] == "ucode-databricks" def test_sets_model_provider(self): overlay = codex.render_overlay(WS) @@ -74,68 +75,9 @@ def test_managed_keys_include_http_headers(self): # Revert must clean up the new key. assert ["model_providers", "ucode-databricks", "http_headers"] in codex.MANAGED_KEYS - -class TestCodexWriteConfig: - def test_writes_ucode_profile_config_file(self, tmp_path, monkeypatch): - config_path = tmp_path / ".codex" / "ucode.config.toml" - backup_path = tmp_path / "codex-ucode-config.backup.toml" - monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) - monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) - monkeypatch.setattr(codex, "save_state", lambda state: None) - - codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) - - doc = read_toml_safe(config_path) - assert doc["model_provider"] == "ucode-databricks" - assert doc["model"] == "gpt-5" - assert "profiles" not in doc - - def test_removes_legacy_ucode_profile_from_shared_config(self, tmp_path, monkeypatch): - config_dir = tmp_path / ".codex" - config_dir.mkdir() - profile_path = config_dir / "ucode.config.toml" - legacy_path = config_dir / "config.toml" - legacy_path.write_text( - 'profile = "ucode"\n\n' - "[profiles.ucode]\n" - 'model_provider = "old"\n\n' - "[profiles.other]\n" - 'model_provider = "keep"\n', - encoding="utf-8", - ) - backup_path = tmp_path / "codex-ucode-config.backup.toml" - legacy_backup_path = tmp_path / "codex-legacy-config.backup.toml" - monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", profile_path) - monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) - monkeypatch.setattr(codex, "save_state", lambda state: None) - - codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) - - doc = read_toml_safe(legacy_path) - assert "profile" not in doc - assert "ucode" not in doc["profiles"] - assert doc["profiles"]["other"]["model_provider"] == "keep" - assert legacy_backup_path.exists() - - -class TestCodexMinimumVersion: - def test_no_error_when_codex_is_new_enough(self, monkeypatch): - monkeypatch.setattr(codex, "agent_version", lambda binary: "0.134.0") - - assert codex.minimum_version_error() is None - assert codex.required_update_message() is None - - def test_errors_when_codex_is_too_old(self, monkeypatch): - monkeypatch.setattr(codex, "agent_version", lambda binary: "0.133.0") - - assert "Codex CLI must be updated to 0.134.0 or newer" in codex.minimum_version_error() - assert "updating Codex is required" in codex.required_update_message() - - def test_unknown_version_does_not_block(self, monkeypatch): - monkeypatch.setattr(codex, "agent_version", lambda binary: "unknown") - - assert codex.minimum_version_error() is None - assert codex.required_update_message() is None + def test_managed_keys_include_top_level_model_selection(self): + assert ["model"] in codex.MANAGED_KEYS + assert ["model_provider"] in codex.MANAGED_KEYS class TestCodexDefaultModel: @@ -155,9 +97,9 @@ def test_uses_exec_subcommand(self): cmd = codex.validate_cmd("codex") assert "exec" in cmd - def test_uses_ucode_profile(self): + def test_does_not_use_legacy_profile_flag(self): cmd = codex.validate_cmd("codex") - assert cmd[:3] == ["codex", "--profile", "ucode"] + assert "--profile" not in cmd def test_has_prompt(self): cmd = codex.validate_cmd("codex") @@ -171,7 +113,7 @@ def test_skips_git_repo_check(self): class TestCodexLaunch: - def test_sets_oauth_token_and_ucode_profile_before_exec(self, monkeypatch): + def test_sets_oauth_token_before_exec(self, monkeypatch): exec_calls: list[tuple[str, list[str]]] = [] def fake_execvp(binary: str, args: list[str]) -> None: @@ -190,4 +132,38 @@ def fake_execvp(binary: str, args: list[str]) -> None: assert str(exc) == "stop" assert os.environ["OAUTH_TOKEN"] == "fresh-token" - assert exec_calls == [("codex", ["codex", "--profile", "ucode", "--search"])] + assert exec_calls == [("codex", ["codex", "--search"])] + + +class TestCodexWriteToolConfig: + def test_removes_legacy_ucode_profile(self, tmp_path, monkeypatch): + config_path = tmp_path / "config.toml" + backup_path = tmp_path / "backup.toml" + config_path.write_text( + """ +[profiles.ucode] +model = "old-model" +model_provider = "ucode-databricks" + +[profiles.personal] +model = "o3" + +[model_providers.ucode-databricks] +name = "old" +base_url = "https://old.example.com" +wire_api = "responses" +""".strip() + ) + monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) + monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) + monkeypatch.setattr(codex, "save_state", lambda state: None) + + codex.write_tool_config( + {"workspace": WS, "codex_models": ["databricks-gpt-5"]}, + ) + + written = tomllib.loads(config_path.read_text()) + assert "ucode" not in written["profiles"] + assert written["profiles"]["personal"]["model"] == "o3" + assert written["model"] == "databricks-gpt-5" + assert written["model_provider"] == "ucode-databricks" diff --git a/tests/test_databricks.py b/tests/test_databricks.py index 190c0d0..8588051 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -51,6 +51,22 @@ def test_sets_databricks_host(self): env = build_databricks_cli_env(WS) assert env["DATABRICKS_HOST"] == WS + def test_strips_ambient_profile_without_explicit_profile(self, monkeypatch): + monkeypatch.setenv("DATABRICKS_CONFIG_PROFILE", "other-workspace") + + env = build_databricks_cli_env(WS) + + assert env["DATABRICKS_HOST"] == WS + assert "DATABRICKS_CONFIG_PROFILE" not in env + + def test_preserves_ambient_profile_with_explicit_profile(self, monkeypatch): + monkeypatch.setenv("DATABRICKS_CONFIG_PROFILE", "other-workspace") + + env = build_databricks_cli_env(WS, profile="stablebox") + + assert env["DATABRICKS_HOST"] == WS + assert env["DATABRICKS_CONFIG_PROFILE"] == "other-workspace" + class TestBuildToolBaseUrl: def test_codex(self): @@ -275,6 +291,36 @@ def test_returns_token_on_success(self, tmp_path, monkeypatch): token = get_databricks_token(WS) assert token == "good-token" + def test_strips_ambient_profile_when_profile_not_provided(self, tmp_path, monkeypatch): + profile_log = tmp_path / "profile" + env = self._fake_databricks( + tmp_path, + f'printf "%s" "${{DATABRICKS_CONFIG_PROFILE:-}}" > {profile_log}\n' + 'echo \'{"access_token": "good-token", "token_type": "Bearer"}\'', + ) + env["DATABRICKS_CONFIG_PROFILE"] = "other-workspace" + monkeypatch.setattr("os.environ", env) + + token = get_databricks_token(WS) + + assert token == "good-token" + assert profile_log.read_text() == "" + + def test_has_valid_auth_strips_ambient_profile_without_explicit_profile( + self, tmp_path, monkeypatch + ): + profile_log = tmp_path / "profile" + env = self._fake_databricks( + tmp_path, + f'printf "%s" "${{DATABRICKS_CONFIG_PROFILE:-}}" > {profile_log}\n' + 'echo \'{"access_token": "good-token", "token_type": "Bearer"}\'', + ) + env["DATABRICKS_CONFIG_PROFILE"] = "other-workspace" + monkeypatch.setattr("os.environ", env) + + assert db_mod.has_valid_databricks_auth(WS) + assert profile_log.read_text() == "" + def test_reauths_and_retries_when_token_empty(self, tmp_path, monkeypatch): call_count = tmp_path / "calls" call_count.write_text("0") diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 22a8d3b..dc273ac 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -13,6 +13,7 @@ import os import shutil import subprocess +from pathlib import Path import pytest @@ -57,6 +58,18 @@ def _run_agent( ) +def _codex_home_for_e2e(tmp_path: Path) -> Path: + """Return an isolated Codex home outside pytest's temporary directory. + + Newer Codex releases refuse to create helper binaries when + ``CODEX_HOME`` is under ``/tmp``. Keep the e2e config isolated from + the user's real ``~/.codex`` while avoiding Codex's temp-home guard. + """ + codex_home = Path.home() / ".cache" / "ucode-e2e" / tmp_path.name / "codex_home" + codex_home.mkdir(parents=True, exist_ok=True) + return codex_home + + # --------------------------------------------------------------------------- # Databricks auth / token # --------------------------------------------------------------------------- @@ -182,7 +195,7 @@ def _redirect_config_paths(self, monkeypatch, tmp_path): codex_dir = tmp_path / "codex_home" / ".codex" codex_dir.mkdir(parents=True, exist_ok=True) - monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", codex_dir / "ucode.config.toml") + monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", codex_dir / "config.toml") monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", tmp_path / "codex.backup.toml") monkeypatch.setattr(claude, "CLAUDE_SETTINGS_PATH", tmp_path / "claude-settings.json") @@ -200,7 +213,7 @@ def _redirect_config_paths(self, monkeypatch, tmp_path): monkeypatch.setattr(pi, "PI_CONFIG_PATH", tmp_path / "pi-models.json") monkeypatch.setattr(pi, "PI_BACKUP_PATH", tmp_path / "pi-models.backup.json") - return codex_dir / "ucode.config.toml" + return codex_dir / "config.toml" def test_only_picks_codex_writes_only_codex_config(self, tmp_path, monkeypatch, e2e_workspace): """User selects only codex → only codex's config file is written and @@ -340,37 +353,40 @@ def test_launch_codex_per_model(self, tmp_path, monkeypatch, e2e_state, e2e_work models = self._codex_models(e2e_state) monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) - config_dir = tmp_path / "codex_home" / ".codex" - config_dir.mkdir(parents=True) - config_path = config_dir / "ucode.config.toml" + codex_home = _codex_home_for_e2e(tmp_path) + config_dir = codex_home + config_path = config_dir / "config.toml" backup_path = tmp_path / "codex-config.backup.toml" monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) - failures = [] - timeout_seconds = int(os.environ.get("UCODE_E2E_AGENT_TIMEOUT", "60")) - for model in models: - state = {**e2e_state, "workspace": e2e_workspace} - with pytest.MonkeyPatch().context() as mp: - mp.setattr("ucode.state.save_state", lambda s: None) - codex.write_tool_config(state, model) - - cmd = codex.validate_cmd("codex") - try: - result = _run_agent( - cmd, - env={**os.environ, "CODEX_HOME": str(config_dir)}, - timeout=timeout_seconds, - ) - except subprocess.TimeoutExpired: - failures.append(f"model={model} timed out after {timeout_seconds}s") - continue - - if result.returncode != 0 or not (result.stdout or result.stderr).strip(): - failures.append( - f"model={model} rc={result.returncode} " - f"stdout={result.stdout[:200]!r} stderr={result.stderr[:200]!r}" - ) + try: + failures = [] + timeout_seconds = int(os.environ.get("UCODE_E2E_AGENT_TIMEOUT", "60")) + for model in models: + state = {**e2e_state, "workspace": e2e_workspace} + with pytest.MonkeyPatch().context() as mp: + mp.setattr("ucode.state.save_state", lambda s: None) + codex.write_tool_config(state, model) + + cmd = codex.validate_cmd("codex") + try: + result = _run_agent( + cmd, + env={**os.environ, "CODEX_HOME": str(config_dir)}, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired: + failures.append(f"model={model} timed out after {timeout_seconds}s") + continue + + if result.returncode != 0 or not (result.stdout or result.stderr).strip(): + failures.append( + f"model={model} rc={result.returncode} " + f"stdout={result.stdout[:200]!r} stderr={result.stderr[:200]!r}" + ) + finally: + shutil.rmtree(codex_home, ignore_errors=True) assert not failures, "Codex launch failures:\n" + "\n".join(failures) diff --git a/tests/test_e2e_user_agent.py b/tests/test_e2e_user_agent.py index cea8830..bff198f 100644 --- a/tests/test_e2e_user_agent.py +++ b/tests/test_e2e_user_agent.py @@ -20,6 +20,7 @@ import subprocess import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path import pytest @@ -31,6 +32,18 @@ def _require_binary(binary: str): pytest.skip(f"`{binary}` is not installed") +def _codex_home_for_e2e(tmp_path: Path) -> Path: + """Return an isolated Codex home outside pytest's temporary directory. + + Newer Codex releases refuse to create helper binaries when + ``CODEX_HOME`` is under ``/tmp``. Keep the e2e config isolated from + the user's real ``~/.codex`` while avoiding Codex's temp-home guard. + """ + codex_home = Path.home() / ".cache" / "ucode-e2e" / tmp_path.name / "codex_home" + codex_home.mkdir(parents=True, exist_ok=True) + return codex_home + + class _CapturedRequest: """Bag of fields recorded by the capture server for one inbound request.""" @@ -210,9 +223,9 @@ def test_user_agent_arrives_at_gateway(self, tmp_path, monkeypatch, capture_serv from ucode.agents import codex _require_binary("codex") - config_dir = tmp_path / "codex_home" / ".codex" - config_dir.mkdir(parents=True) - config_path = config_dir / "ucode.config.toml" + codex_home = _codex_home_for_e2e(tmp_path) + config_dir = codex_home + config_path = config_dir / "config.toml" monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) @@ -229,7 +242,10 @@ def test_user_agent_arrives_at_gateway(self, tmp_path, monkeypatch, capture_serv "CODEX_HOME": str(config_dir), "OPENAI_API_KEY": "test-key-not-real", } - result = _run_until_first_request(codex.validate_cmd("codex"), env) + try: + result = _run_until_first_request(codex.validate_cmd("codex"), env) + finally: + shutil.rmtree(codex_home, ignore_errors=True) req = capture_server.first_request_with_path_prefix("/ai-gateway/codex") assert req is not None, _no_request_msg(capture_server, result) From e39f8b036f1055eae4b56357465bccd39d081655 Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Tue, 26 May 2026 22:32:37 +0000 Subject: [PATCH 2/5] revert codex changes --- src/ucode/agents/codex.py | 52 +++++++++++++++++------------------- tests/test_agent_codex.py | 56 ++++++--------------------------------- 2 files changed, 33 insertions(+), 75 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index ce7aa3c..3a1b2aa 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -25,7 +25,7 @@ CODEX_CONFIG_DIR = Path.home() / ".codex" CODEX_CONFIG_PATH = CODEX_CONFIG_DIR / "config.toml" CODEX_BACKUP_PATH = APP_DIR / "codex-config.backup.toml" -LEGACY_CODEX_PROFILE_NAME = "ucode" +CODEX_PROFILE_NAME = "ucode" CODEX_MODEL_PROVIDER_NAME = "ucode-databricks" SPEC: ToolSpec = { @@ -37,8 +37,7 @@ } MANAGED_KEYS: list[list[str]] = [ - ["model"], - ["model_provider"], + ["profiles", CODEX_PROFILE_NAME], ["model_providers", CODEX_MODEL_PROVIDER_NAME], ["model_providers", CODEX_MODEL_PROVIDER_NAME, "http_headers"], ] @@ -53,26 +52,28 @@ def render_overlay( ) -> dict: auth_command = build_auth_shell_command(workspace, databricks_profile) base_url = build_tool_base_url("codex", workspace) - overlay: dict = {"model_provider": CODEX_MODEL_PROVIDER_NAME} + codex_profile_cfg: dict[str, str] = {"model_provider": CODEX_MODEL_PROVIDER_NAME} if model: - overlay["model"] = model - overlay["model_providers"] = { - CODEX_MODEL_PROVIDER_NAME: { - "name": "Databricks AI Gateway", - "base_url": base_url, - "wire_api": "responses", - "http_headers": { - "User-Agent": f"ucode/{ucode_version()} codex/{agent_version('codex')}", - }, - "auth": { - "command": "sh", - "args": ["-c", auth_command], - "timeout_ms": 5000, - "refresh_interval_ms": 900000, - }, - } + codex_profile_cfg["model"] = model + return { + "profiles": {CODEX_PROFILE_NAME: codex_profile_cfg}, + "model_providers": { + CODEX_MODEL_PROVIDER_NAME: { + "name": "Databricks AI Gateway", + "base_url": base_url, + "wire_api": "responses", + "http_headers": { + "User-Agent": f"ucode/{ucode_version()} codex/{agent_version('codex')}", + }, + "auth": { + "command": "sh", + "args": ["-c", auth_command], + "timeout_ms": 5000, + "refresh_interval_ms": 900000, + }, + } + }, } - return overlay def write_tool_config(state: dict, model: str | None = None) -> dict: @@ -81,11 +82,6 @@ def write_tool_config(state: dict, model: str | None = None) -> dict: state["workspace"], model or default_model(state), state.get("profile") ) doc = read_toml_safe(CODEX_CONFIG_PATH) - legacy_profiles = doc.get("profiles") - if isinstance(legacy_profiles, dict): - legacy_profiles.pop(LEGACY_CODEX_PROFILE_NAME, None) - if not legacy_profiles: - doc.pop("profiles", None) deep_merge_dict(doc, overlay) write_toml_file(CODEX_CONFIG_PATH, doc) state = mark_tool_managed(state, "codex", MANAGED_KEYS) @@ -103,12 +99,14 @@ def launch(state: dict, tool_args: list[str]) -> None: workspace = state.get("workspace") if workspace: os.environ["OAUTH_TOKEN"] = get_databricks_token(workspace, state.get("profile")) - os.execvp(binary, [binary, *tool_args]) + os.execvp(binary, [binary, "--profile", CODEX_PROFILE_NAME, *tool_args]) def validate_cmd(binary: str) -> list[str]: return [ binary, + "--profile", + CODEX_PROFILE_NAME, "exec", "--skip-git-repo-check", "say hi in 5 words or less", diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index 0c0f087..f9cbc3f 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -3,7 +3,6 @@ from __future__ import annotations import os -import tomllib from ucode.agents import codex @@ -22,19 +21,18 @@ def test_display(self): class TestRenderOverlay: - def test_sets_provider_without_legacy_profile(self): + def test_creates_ucode_profile_without_setting_global_default(self): overlay = codex.render_overlay(WS) assert "profile" not in overlay - assert "profiles" not in overlay - assert overlay["model_provider"] == "ucode-databricks" + assert "ucode" in overlay["profiles"] def test_sets_model_provider(self): overlay = codex.render_overlay(WS) - assert overlay["model_provider"] == "ucode-databricks" + assert overlay["profiles"]["ucode"]["model_provider"] == "ucode-databricks" def test_sets_model_when_provided(self): overlay = codex.render_overlay(WS, "databricks-gpt-5") - assert overlay["model"] == "databricks-gpt-5" + assert overlay["profiles"]["ucode"]["model"] == "databricks-gpt-5" def test_provider_base_url(self): overlay = codex.render_overlay(WS) @@ -75,10 +73,6 @@ def test_managed_keys_include_http_headers(self): # Revert must clean up the new key. assert ["model_providers", "ucode-databricks", "http_headers"] in codex.MANAGED_KEYS - def test_managed_keys_include_top_level_model_selection(self): - assert ["model"] in codex.MANAGED_KEYS - assert ["model_provider"] in codex.MANAGED_KEYS - class TestCodexDefaultModel: def test_returns_first_codex_model(self): @@ -97,9 +91,9 @@ def test_uses_exec_subcommand(self): cmd = codex.validate_cmd("codex") assert "exec" in cmd - def test_does_not_use_legacy_profile_flag(self): + def test_uses_ucode_profile(self): cmd = codex.validate_cmd("codex") - assert "--profile" not in cmd + assert cmd[:3] == ["codex", "--profile", "ucode"] def test_has_prompt(self): cmd = codex.validate_cmd("codex") @@ -113,7 +107,7 @@ def test_skips_git_repo_check(self): class TestCodexLaunch: - def test_sets_oauth_token_before_exec(self, monkeypatch): + def test_sets_oauth_token_and_ucode_profile_before_exec(self, monkeypatch): exec_calls: list[tuple[str, list[str]]] = [] def fake_execvp(binary: str, args: list[str]) -> None: @@ -132,38 +126,4 @@ def fake_execvp(binary: str, args: list[str]) -> None: assert str(exc) == "stop" assert os.environ["OAUTH_TOKEN"] == "fresh-token" - assert exec_calls == [("codex", ["codex", "--search"])] - - -class TestCodexWriteToolConfig: - def test_removes_legacy_ucode_profile(self, tmp_path, monkeypatch): - config_path = tmp_path / "config.toml" - backup_path = tmp_path / "backup.toml" - config_path.write_text( - """ -[profiles.ucode] -model = "old-model" -model_provider = "ucode-databricks" - -[profiles.personal] -model = "o3" - -[model_providers.ucode-databricks] -name = "old" -base_url = "https://old.example.com" -wire_api = "responses" -""".strip() - ) - monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) - monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) - monkeypatch.setattr(codex, "save_state", lambda state: None) - - codex.write_tool_config( - {"workspace": WS, "codex_models": ["databricks-gpt-5"]}, - ) - - written = tomllib.loads(config_path.read_text()) - assert "ucode" not in written["profiles"] - assert written["profiles"]["personal"]["model"] == "o3" - assert written["model"] == "databricks-gpt-5" - assert written["model_provider"] == "ucode-databricks" + assert exec_calls == [("codex", ["codex", "--profile", "ucode", "--search"])] From df992def6f11fdedc9d2b9d7d580edd2d3cf8203 Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Wed, 27 May 2026 18:56:14 +0000 Subject: [PATCH 3/5] ci: retrigger checks after rebase From dfd27bbabf3a1b014443b012fa78d199b3ca9424 Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Wed, 27 May 2026 20:36:21 +0000 Subject: [PATCH 4/5] revert: drop Codex-related changes; keep Databricks-only --- src/ucode/agents/codex.py | 129 ++++++++++++++++++++++++++++------- tests/test_agent_codex.py | 72 +++++++++++++++++-- tests/test_e2e.py | 74 ++++++++------------ tests/test_e2e_user_agent.py | 24 ++----- 4 files changed, 206 insertions(+), 93 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 3a1b2aa..2077bee 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -1,8 +1,9 @@ -"""Codex agent: writes ~/.codex/config.toml with a Databricks-backed model provider.""" +"""Codex agent: writes ~/.codex/ucode.config.toml for Databricks-backed Codex.""" from __future__ import annotations import os +import re from pathlib import Path from ucode.agent_updates import available_npm_package_update @@ -23,10 +24,13 @@ from ucode.telemetry import agent_version, ucode_version CODEX_CONFIG_DIR = Path.home() / ".codex" -CODEX_CONFIG_PATH = CODEX_CONFIG_DIR / "config.toml" -CODEX_BACKUP_PATH = APP_DIR / "codex-config.backup.toml" CODEX_PROFILE_NAME = "ucode" +CODEX_CONFIG_PATH = CODEX_CONFIG_DIR / f"{CODEX_PROFILE_NAME}.config.toml" +CODEX_BACKUP_PATH = APP_DIR / "codex-ucode-config.backup.toml" CODEX_MODEL_PROVIDER_NAME = "ucode-databricks" +MINIMUM_CODEX_VERSION = (0, 134, 0) +MINIMUM_CODEX_VERSION_TEXT = "0.134.0" + SPEC: ToolSpec = { "binary": "codex", @@ -37,7 +41,8 @@ } MANAGED_KEYS: list[list[str]] = [ - ["profiles", CODEX_PROFILE_NAME], + ["model_provider"], + ["model"], ["model_providers", CODEX_MODEL_PROVIDER_NAME], ["model_providers", CODEX_MODEL_PROVIDER_NAME, "http_headers"], ] @@ -47,36 +52,112 @@ def is_update_available() -> tuple[str, str] | None: return available_npm_package_update(SPEC["package"]) +def _parse_version(value: str) -> tuple[int, int, int] | None: + match = re.search(r"(\d+)\.(\d+)\.(\d+)", value) + if not match: + return None + major, minor, patch = match.groups() + return int(major), int(minor), int(patch) + + +def _installed_version_status() -> tuple[str, bool] | None: + version = agent_version(SPEC["binary"]) + parsed = _parse_version(version) + if parsed is None: + return None + return version, parsed < MINIMUM_CODEX_VERSION + + +def minimum_version_error() -> str | None: + status = _installed_version_status() + if status is None: + return None + version, is_too_old = status + if not is_too_old: + return None + return ( + f"Codex CLI {version} is too old for ucode's Codex profile config. " + f"Codex CLI must be updated to {MINIMUM_CODEX_VERSION_TEXT} or newer; " + f"run `npm install -g {SPEC['package']}` or `ucode configure`." + ) + + +def required_update_message() -> str | None: + status = _installed_version_status() + if status is None: + return None + version, is_too_old = status + if not is_too_old: + return None + return ( + f"Codex CLI {version} is older than required {MINIMUM_CODEX_VERSION_TEXT}; " + "updating Codex is required for ucode's Codex profile config." + ) + + def render_overlay( workspace: str, model: str | None = None, databricks_profile: str | None = None ) -> dict: auth_command = build_auth_shell_command(workspace, databricks_profile) base_url = build_tool_base_url("codex", workspace) - codex_profile_cfg: dict[str, str] = {"model_provider": CODEX_MODEL_PROVIDER_NAME} + overlay: dict = {"model_provider": CODEX_MODEL_PROVIDER_NAME} if model: - codex_profile_cfg["model"] = model - return { - "profiles": {CODEX_PROFILE_NAME: codex_profile_cfg}, - "model_providers": { - CODEX_MODEL_PROVIDER_NAME: { - "name": "Databricks AI Gateway", - "base_url": base_url, - "wire_api": "responses", - "http_headers": { - "User-Agent": f"ucode/{ucode_version()} codex/{agent_version('codex')}", - }, - "auth": { - "command": "sh", - "args": ["-c", auth_command], - "timeout_ms": 5000, - "refresh_interval_ms": 900000, - }, - } - }, + overlay["model"] = model + overlay["model_providers"] = { + CODEX_MODEL_PROVIDER_NAME: { + "name": "Databricks AI Gateway", + "base_url": base_url, + "wire_api": "responses", + "http_headers": { + "User-Agent": f"ucode/{ucode_version()} codex/{agent_version('codex')}", + }, + "auth": { + "command": "sh", + "args": ["-c", auth_command], + "timeout_ms": 5000, + "refresh_interval_ms": 900000, + }, + } } + return overlay + + +def _legacy_config_path() -> Path: + return CODEX_CONFIG_PATH.parent / "config.toml" + + +def _legacy_backup_path() -> Path: + return CODEX_BACKUP_PATH.with_name("codex-legacy-config.backup.toml") + + +def _remove_legacy_ucode_profile() -> None: + """Remove ucode's old [profiles.ucode] entry from shared Codex config.""" + path = _legacy_config_path() + if path == CODEX_CONFIG_PATH or not path.exists(): + return + + doc = read_toml_safe(path) + changed = False + + profiles = doc.get("profiles") + if isinstance(profiles, dict) and CODEX_PROFILE_NAME in profiles: + backup_existing_file(path, _legacy_backup_path()) + profiles.pop(CODEX_PROFILE_NAME, None) + if not profiles: + doc.pop("profiles", None) + changed = True + + if doc.get("profile") == CODEX_PROFILE_NAME: + backup_existing_file(path, _legacy_backup_path()) + doc.pop("profile", None) + changed = True + + if changed: + write_toml_file(path, doc) def write_tool_config(state: dict, model: str | None = None) -> dict: + _remove_legacy_ucode_profile() backup_existing_file(CODEX_CONFIG_PATH, CODEX_BACKUP_PATH) overlay = render_overlay( state["workspace"], model or default_model(state), state.get("profile") diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index f9cbc3f..45c709f 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -5,6 +5,7 @@ import os from ucode.agents import codex +from ucode.config_io import read_toml_safe WS = "https://example.databricks.com" @@ -21,18 +22,18 @@ def test_display(self): class TestRenderOverlay: - def test_creates_ucode_profile_without_setting_global_default(self): + def test_uses_profile_file_shape_without_legacy_profiles(self): overlay = codex.render_overlay(WS) assert "profile" not in overlay - assert "ucode" in overlay["profiles"] + assert "profiles" not in overlay def test_sets_model_provider(self): overlay = codex.render_overlay(WS) - assert overlay["profiles"]["ucode"]["model_provider"] == "ucode-databricks" + assert overlay["model_provider"] == "ucode-databricks" def test_sets_model_when_provided(self): overlay = codex.render_overlay(WS, "databricks-gpt-5") - assert overlay["profiles"]["ucode"]["model"] == "databricks-gpt-5" + assert overlay["model"] == "databricks-gpt-5" def test_provider_base_url(self): overlay = codex.render_overlay(WS) @@ -74,6 +75,69 @@ def test_managed_keys_include_http_headers(self): assert ["model_providers", "ucode-databricks", "http_headers"] in codex.MANAGED_KEYS +class TestCodexWriteConfig: + def test_writes_ucode_profile_config_file(self, tmp_path, monkeypatch): + config_path = tmp_path / ".codex" / "ucode.config.toml" + backup_path = tmp_path / "codex-ucode-config.backup.toml" + monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) + monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) + monkeypatch.setattr(codex, "save_state", lambda state: None) + + codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) + + doc = read_toml_safe(config_path) + assert doc["model_provider"] == "ucode-databricks" + assert doc["model"] == "gpt-5" + assert "profiles" not in doc + + def test_removes_legacy_ucode_profile_from_shared_config(self, tmp_path, monkeypatch): + config_dir = tmp_path / ".codex" + config_dir.mkdir() + profile_path = config_dir / "ucode.config.toml" + legacy_path = config_dir / "config.toml" + legacy_path.write_text( + 'profile = "ucode"\n\n' + "[profiles.ucode]\n" + 'model_provider = "old"\n\n' + "[profiles.other]\n" + 'model_provider = "keep"\n', + encoding="utf-8", + ) + backup_path = tmp_path / "codex-ucode-config.backup.toml" + legacy_backup_path = tmp_path / "codex-legacy-config.backup.toml" + monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", profile_path) + monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) + monkeypatch.setattr(codex, "save_state", lambda state: None) + + codex.write_tool_config({"workspace": WS, "codex_models": ["gpt-5"]}) + + doc = read_toml_safe(legacy_path) + assert "profile" not in doc + assert "ucode" not in doc["profiles"] + assert doc["profiles"]["other"]["model_provider"] == "keep" + assert legacy_backup_path.exists() + + +class TestCodexMinimumVersion: + def test_no_error_when_codex_is_new_enough(self, monkeypatch): + monkeypatch.setattr(codex, "agent_version", lambda binary: "0.134.0") + + assert codex.minimum_version_error() is None + assert codex.required_update_message() is None + + def test_errors_when_codex_is_too_old(self, monkeypatch): + monkeypatch.setattr(codex, "agent_version", lambda binary: "0.133.0") + + assert "Codex CLI must be updated to 0.134.0 or newer" in codex.minimum_version_error() + assert "updating Codex is required" in codex.required_update_message() + + def test_unknown_version_does_not_block(self, monkeypatch): + monkeypatch.setattr(codex, "agent_version", lambda binary: "unknown") + + assert codex.minimum_version_error() is None + assert codex.required_update_message() is None + + class TestCodexDefaultModel: def test_returns_first_codex_model(self): assert codex.default_model({"codex_models": ["gpt-5", "gpt-4o"]}) == "gpt-5" diff --git a/tests/test_e2e.py b/tests/test_e2e.py index dc273ac..22a8d3b 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -13,7 +13,6 @@ import os import shutil import subprocess -from pathlib import Path import pytest @@ -58,18 +57,6 @@ def _run_agent( ) -def _codex_home_for_e2e(tmp_path: Path) -> Path: - """Return an isolated Codex home outside pytest's temporary directory. - - Newer Codex releases refuse to create helper binaries when - ``CODEX_HOME`` is under ``/tmp``. Keep the e2e config isolated from - the user's real ``~/.codex`` while avoiding Codex's temp-home guard. - """ - codex_home = Path.home() / ".cache" / "ucode-e2e" / tmp_path.name / "codex_home" - codex_home.mkdir(parents=True, exist_ok=True) - return codex_home - - # --------------------------------------------------------------------------- # Databricks auth / token # --------------------------------------------------------------------------- @@ -195,7 +182,7 @@ def _redirect_config_paths(self, monkeypatch, tmp_path): codex_dir = tmp_path / "codex_home" / ".codex" codex_dir.mkdir(parents=True, exist_ok=True) - monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", codex_dir / "config.toml") + monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", codex_dir / "ucode.config.toml") monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", tmp_path / "codex.backup.toml") monkeypatch.setattr(claude, "CLAUDE_SETTINGS_PATH", tmp_path / "claude-settings.json") @@ -213,7 +200,7 @@ def _redirect_config_paths(self, monkeypatch, tmp_path): monkeypatch.setattr(pi, "PI_CONFIG_PATH", tmp_path / "pi-models.json") monkeypatch.setattr(pi, "PI_BACKUP_PATH", tmp_path / "pi-models.backup.json") - return codex_dir / "config.toml" + return codex_dir / "ucode.config.toml" def test_only_picks_codex_writes_only_codex_config(self, tmp_path, monkeypatch, e2e_workspace): """User selects only codex → only codex's config file is written and @@ -353,40 +340,37 @@ def test_launch_codex_per_model(self, tmp_path, monkeypatch, e2e_state, e2e_work models = self._codex_models(e2e_state) monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) - codex_home = _codex_home_for_e2e(tmp_path) - config_dir = codex_home - config_path = config_dir / "config.toml" + config_dir = tmp_path / "codex_home" / ".codex" + config_dir.mkdir(parents=True) + config_path = config_dir / "ucode.config.toml" backup_path = tmp_path / "codex-config.backup.toml" monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) monkeypatch.setattr(codex, "CODEX_BACKUP_PATH", backup_path) - try: - failures = [] - timeout_seconds = int(os.environ.get("UCODE_E2E_AGENT_TIMEOUT", "60")) - for model in models: - state = {**e2e_state, "workspace": e2e_workspace} - with pytest.MonkeyPatch().context() as mp: - mp.setattr("ucode.state.save_state", lambda s: None) - codex.write_tool_config(state, model) - - cmd = codex.validate_cmd("codex") - try: - result = _run_agent( - cmd, - env={**os.environ, "CODEX_HOME": str(config_dir)}, - timeout=timeout_seconds, - ) - except subprocess.TimeoutExpired: - failures.append(f"model={model} timed out after {timeout_seconds}s") - continue - - if result.returncode != 0 or not (result.stdout or result.stderr).strip(): - failures.append( - f"model={model} rc={result.returncode} " - f"stdout={result.stdout[:200]!r} stderr={result.stderr[:200]!r}" - ) - finally: - shutil.rmtree(codex_home, ignore_errors=True) + failures = [] + timeout_seconds = int(os.environ.get("UCODE_E2E_AGENT_TIMEOUT", "60")) + for model in models: + state = {**e2e_state, "workspace": e2e_workspace} + with pytest.MonkeyPatch().context() as mp: + mp.setattr("ucode.state.save_state", lambda s: None) + codex.write_tool_config(state, model) + + cmd = codex.validate_cmd("codex") + try: + result = _run_agent( + cmd, + env={**os.environ, "CODEX_HOME": str(config_dir)}, + timeout=timeout_seconds, + ) + except subprocess.TimeoutExpired: + failures.append(f"model={model} timed out after {timeout_seconds}s") + continue + + if result.returncode != 0 or not (result.stdout or result.stderr).strip(): + failures.append( + f"model={model} rc={result.returncode} " + f"stdout={result.stdout[:200]!r} stderr={result.stderr[:200]!r}" + ) assert not failures, "Codex launch failures:\n" + "\n".join(failures) diff --git a/tests/test_e2e_user_agent.py b/tests/test_e2e_user_agent.py index bff198f..cea8830 100644 --- a/tests/test_e2e_user_agent.py +++ b/tests/test_e2e_user_agent.py @@ -20,7 +20,6 @@ import subprocess import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path import pytest @@ -32,18 +31,6 @@ def _require_binary(binary: str): pytest.skip(f"`{binary}` is not installed") -def _codex_home_for_e2e(tmp_path: Path) -> Path: - """Return an isolated Codex home outside pytest's temporary directory. - - Newer Codex releases refuse to create helper binaries when - ``CODEX_HOME`` is under ``/tmp``. Keep the e2e config isolated from - the user's real ``~/.codex`` while avoiding Codex's temp-home guard. - """ - codex_home = Path.home() / ".cache" / "ucode-e2e" / tmp_path.name / "codex_home" - codex_home.mkdir(parents=True, exist_ok=True) - return codex_home - - class _CapturedRequest: """Bag of fields recorded by the capture server for one inbound request.""" @@ -223,9 +210,9 @@ def test_user_agent_arrives_at_gateway(self, tmp_path, monkeypatch, capture_serv from ucode.agents import codex _require_binary("codex") - codex_home = _codex_home_for_e2e(tmp_path) - config_dir = codex_home - config_path = config_dir / "config.toml" + config_dir = tmp_path / "codex_home" / ".codex" + config_dir.mkdir(parents=True) + config_path = config_dir / "ucode.config.toml" monkeypatch.setattr(config_io_mod, "APP_DIR", tmp_path) monkeypatch.setattr(codex, "CODEX_CONFIG_PATH", config_path) @@ -242,10 +229,7 @@ def test_user_agent_arrives_at_gateway(self, tmp_path, monkeypatch, capture_serv "CODEX_HOME": str(config_dir), "OPENAI_API_KEY": "test-key-not-real", } - try: - result = _run_until_first_request(codex.validate_cmd("codex"), env) - finally: - shutil.rmtree(codex_home, ignore_errors=True) + result = _run_until_first_request(codex.validate_cmd("codex"), env) req = capture_server.first_request_with_path_prefix("/ai-gateway/codex") assert req is not None, _no_request_msg(capture_server, result) From 96f2eeb55b87a73f5557fbdc31f2b65daf48ddae Mon Sep 17 00:00:00 2001 From: AarushiShah-db Date: Wed, 27 May 2026 20:44:16 +0000 Subject: [PATCH 5/5] auth: resolve profile by host when unspecified to handle duplicate-host cfgs; preserve helpful comments --- src/ucode/databricks.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 8367ff6..8047f20 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -382,6 +382,10 @@ def has_valid_databricks_auth(workspace: str, profile: str | None = None) -> boo if os.environ.get("DATABRICKS_BEARER", "").strip(): return True _log_auth_diagnostics() + # Mirror run_databricks_login: when ~/.databrickscfg has multiple + # profiles for the same host, `databricks auth token --host …` refuses + # to disambiguate without --profile, so resolve it from the host here. + profile = profile or find_profile_name_for_host(workspace) try: env = build_databricks_cli_env(workspace, profile) result = run( @@ -526,6 +530,9 @@ def get_databricks_token( return bearer _log_auth_diagnostics() + # See has_valid_databricks_auth: resolve the profile from the host when + # the caller didn't supply one, so duplicate-host cfgs don't break us. + profile = profile or find_profile_name_for_host(workspace) env = build_databricks_cli_env(workspace, profile) cmd = [ "databricks",