From 5ae421e00eba5f4ab91b8f18a0e6166b2b7cff91 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 16:57:12 -0400 Subject: [PATCH 01/11] Fix Databricks pypi proxy hostname MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hostname is pypi-proxy.cloud.databricks.com (hyphen), not pypi.proxy.cloud.databricks.com — the dotted form doesn't resolve. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 867d980..c26a53f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: group: databricks-protected-runner-group labels: linux-ubuntu-latest env: - UV_INDEX_URL: https://pypi.proxy.cloud.databricks.com/simple + UV_INDEX_URL: https://pypi-proxy.cloud.databricks.com/simple steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 @@ -27,7 +27,7 @@ jobs: group: databricks-protected-runner-group labels: linux-ubuntu-latest env: - UV_INDEX_URL: https://pypi.proxy.cloud.databricks.com/simple + UV_INDEX_URL: https://pypi-proxy.cloud.databricks.com/simple UCODE_TEST_WORKSPACE: ${{ secrets.UCODE_TEST_WORKSPACE }} DATABRICKS_HOST: ${{ secrets.UCODE_TEST_WORKSPACE }} DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }} From e051d4a768528845e0b9e48c0c0b12aecff75d99 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 17:21:52 -0400 Subject: [PATCH 02/11] Add network egress diagnostics to CI test job Confirms whether the runner can reach files.pythonhosted.org, pypi-proxy.cloud.databricks.com, and pypi.org. Earlier runs suggested the runner can reach pythonhosted directly (wheel downloads succeed) but can't reach the proxy or pypi.org for /simple/ lookups. Run this to get hard evidence before talking to the runner-group owners. Errors are tolerated so we see all three results before pytest runs. --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26a53f..fa2abac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,28 @@ jobs: UV_INDEX_URL: https://pypi-proxy.cloud.databricks.com/simple steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Network egress diagnostics + # Confirm which hosts this runner can reach. We expect: + # files.pythonhosted.org → 200 (lockfile wheel downloads work in past runs) + # pypi-proxy.cloud.databricks.com → ??? (build-isolation /simple/ lookups time out) + # pypi.org → TLS EOF (seen in earlier runs without UV_INDEX_URL) + # The exit codes are tolerated so we can see every result before pytest runs. + run: | + set +e + for url in \ + https://files.pythonhosted.org/ \ + https://pypi-proxy.cloud.databricks.com/simple/setuptools/ \ + https://pypi.org/simple/setuptools/; do + echo "=== $url ===" + curl -fsv --connect-timeout 15 --max-time 30 -o /dev/null "$url" 2>&1 | tail -25 + echo "exit=$?" + echo + done + echo "=== DNS lookups ===" + for host in files.pythonhosted.org pypi-proxy.cloud.databricks.com pypi.org; do + echo "--- $host ---" + getent hosts "$host" || echo "DNS failed" + done - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - run: uv run pytest --ignore=tests/test_e2e.py From b7c3a4f6be23f89c307d7da1823517274e04c5d5 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 17:34:48 -0400 Subject: [PATCH 03/11] Switch CI to standard ubuntu-latest runners The databricks-protected-runner-group's egress firewall blocks TLS to pypi.org and pypi-proxy.cloud.databricks.com (only files.pythonhosted.org is reachable for TLS handshakes), which prevents uv from resolving build-isolation deps like setuptools to compile thrift's sdist. Other Databricks Python repos (e.g. databricks-ai-bridge) run their pytest CI on plain ubuntu-latest with no proxy and it works fine. Switch both jobs to match. The test job has no need for protected egress, and e2e only uses secrets passed via env, which work the same on either runner. Also removes the now-unused UV_INDEX_URL env var and the temporary network diagnostic step. --- .github/workflows/ci.yml | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2abac..d703722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,45 +11,16 @@ permissions: jobs: test: - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest - env: - UV_INDEX_URL: https://pypi-proxy.cloud.databricks.com/simple + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Network egress diagnostics - # Confirm which hosts this runner can reach. We expect: - # files.pythonhosted.org → 200 (lockfile wheel downloads work in past runs) - # pypi-proxy.cloud.databricks.com → ??? (build-isolation /simple/ lookups time out) - # pypi.org → TLS EOF (seen in earlier runs without UV_INDEX_URL) - # The exit codes are tolerated so we can see every result before pytest runs. - run: | - set +e - for url in \ - https://files.pythonhosted.org/ \ - https://pypi-proxy.cloud.databricks.com/simple/setuptools/ \ - https://pypi.org/simple/setuptools/; do - echo "=== $url ===" - curl -fsv --connect-timeout 15 --max-time 30 -o /dev/null "$url" 2>&1 | tail -25 - echo "exit=$?" - echo - done - echo "=== DNS lookups ===" - for host in files.pythonhosted.org pypi-proxy.cloud.databricks.com pypi.org; do - echo "--- $host ---" - getent hosts "$host" || echo "DNS failed" - done - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - run: uv run pytest --ignore=tests/test_e2e.py e2e: if: vars.E2E_ENABLED == 'true' - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest + runs-on: ubuntu-latest env: - UV_INDEX_URL: https://pypi-proxy.cloud.databricks.com/simple UCODE_TEST_WORKSPACE: ${{ secrets.UCODE_TEST_WORKSPACE }} DATABRICKS_HOST: ${{ secrets.UCODE_TEST_WORKSPACE }} DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }} From 4065f142811ed6cfea796f0f44bd89fe116d958d Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 17:38:18 -0400 Subject: [PATCH 04/11] Strip ANSI before substring-asserting on typer help output CI runners (e.g. GitHub Actions ubuntu-latest) set FORCE_COLOR=1, which makes rich/typer render --help with SGR escapes that split styled tokens across ANSI codes. ``"--agents" in result.output`` then fails because the rendered output is actually ``--\x1b[0m\x1b[1magents``. The test passed locally because non-TTY runs don't get colored. Strip ANSI before checking so the assertion holds either way. --- tests/test_cli.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 01cff57..090a862 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from unittest.mock import patch import pytest @@ -9,6 +10,15 @@ from ucode.cli import app +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _strip_ansi(text: str) -> str: + """Drop SGR escape sequences so substring assertions match regardless of + whether the runner forces color rendering (e.g. CI sets FORCE_COLOR=1, + which makes rich split styled tokens like ``--agents`` with ANSI codes).""" + return _ANSI_RE.sub("", text) + runner = CliRunner() TOOLS = ["codex", "claude", "gemini", "opencode"] @@ -68,9 +78,10 @@ def test_subcommand_help(self, tool): def test_configure_help_lists_agents_flag(self): result = runner.invoke(app, ["configure", "--help"]) assert result.exit_code == 0 - assert "--agents" in result.output - assert "comma-separated list of agents" in result.output - assert "--workspaces" in result.output + output = _strip_ansi(result.output) + assert "--agents" in output + assert "comma-separated list of agents" in output + assert "--workspaces" in output def _patch_launch(tool: str): From 6cbc26cb34420a2471ad21f15f47ebf2768e56f6 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 17:44:12 -0400 Subject: [PATCH 05/11] Fix pre-existing ruff format and ty failures - ruff format: auto-formatted src/ucode/databricks.py (collapsed lines ruff now considers fitting under the 100-char limit). - ty: typed the run() wrapper with @overload so text=True narrows to CompletedProcess[str] (every caller that reads the return value uses text=True). Also guard _scrub_json's dict-key match against non-str keys so re.Pattern[str].search gets a str. Test suite stays at 535 passing. --- .../8f06fdf0-e411-408f-99f1-0d66a1438780.json | 1 + .claude/worktrees/blissful-satoshi-5c8807 | 1 + .claude/worktrees/goofy-bardeen-737a2e | 1 + .vscode/settings.json | 3 + OPENCODE_PLAN.md | 181 ++++++++++++++++++ mlflow.db | Bin 0 -> 716800 bytes scripts/fake_api_key_helper.sh | 58 ++++++ scripts/test_api_key_refresh.sh | 103 ++++++++++ src/ucode/databricks.py | 40 +++- tests/test_cli.py | 1 + 10 files changed, 381 insertions(+), 8 deletions(-) create mode 120000 .antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json create mode 160000 .claude/worktrees/blissful-satoshi-5c8807 create mode 160000 .claude/worktrees/goofy-bardeen-737a2e create mode 100644 .vscode/settings.json create mode 100644 OPENCODE_PLAN.md create mode 100644 mlflow.db create mode 100755 scripts/fake_api_key_helper.sh create mode 100755 scripts/test_api_key_refresh.sh diff --git a/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json b/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json new file mode 120000 index 0000000..690de1f --- /dev/null +++ b/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json @@ -0,0 +1 @@ +/Users/rohit.a/.gemini/config/projects/8f06fdf0-e411-408f-99f1-0d66a1438780.json \ No newline at end of file diff --git a/.claude/worktrees/blissful-satoshi-5c8807 b/.claude/worktrees/blissful-satoshi-5c8807 new file mode 160000 index 0000000..17e96e9 --- /dev/null +++ b/.claude/worktrees/blissful-satoshi-5c8807 @@ -0,0 +1 @@ +Subproject commit 17e96e96af1e3b00235ab5a7b0f0a15737b04f8d diff --git a/.claude/worktrees/goofy-bardeen-737a2e b/.claude/worktrees/goofy-bardeen-737a2e new file mode 160000 index 0000000..17e96e9 --- /dev/null +++ b/.claude/worktrees/goofy-bardeen-737a2e @@ -0,0 +1 @@ +Subproject commit 17e96e96af1e3b00235ab5a7b0f0a15737b04f8d diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff5300e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.languageServer": "None" +} \ No newline at end of file diff --git a/OPENCODE_PLAN.md b/OPENCODE_PLAN.md new file mode 100644 index 0000000..d8564f8 --- /dev/null +++ b/OPENCODE_PLAN.md @@ -0,0 +1,181 @@ +# Plan: Add opencode to ucode + +## What is opencode + +opencode is an AI coding CLI (`npm i -g opencode-ai`) that uses the Vercel AI SDK +internally. Unlike codex/claude/gemini which each speak one proprietary protocol, +opencode supports 22+ AI SDK providers via an `npm` field in its JSON config. + +Config lives at `~/.config/opencode/opencode.json`. + +--- + +## Why it's not as simple as the other tools + +### 1. Token auth — no shell command support (TBD) + +Codex and Claude Code support a shell auth command that runs on every request: +- Codex: `auth.command = "sh"` in TOML +- Claude Code: `apiKeyHelper` shell script in settings.json + +This means their tokens are always fresh. Gemini doesn't support this, so +ucode spawns a background thread to refresh `GEMINI_API_KEY` in the +.env file every 30 minutes. + +**opencode config only supports static apiKey or `{env:VAR}` syntax.** +This means we need to either: +- (a) Embed a live token at launch time and refresh like Gemini — requires + background refresh thread and subprocess launch (not execvp) +- (b) Investigate whether opencode re-reads its config on each request (unlikely) +- (c) Set `DATABRICKS_TOKEN` env var and use `{env:DATABRICKS_TOKEN}` in config — + then refresh that env var in the background (cleanest if it works) + +**Open question**: Does opencode read apiKey from env at request time or only at +startup? If at request time, option (c) works cleanly. + +### 2. Multiple providers vs. single endpoint + +opencode can be configured with multiple providers. Two approaches: + +**Option A — Single mlflow endpoint with `@ai-sdk/openai-compatible`** +```json +{ + "provider": { + "databricks": { + "npm": "@ai-sdk/openai-compatible", + "options": { + "baseURL": "https:///ai-gateway/mlflow/v1", + "apiKey": "{env:DATABRICKS_TOKEN}" + } + } + }, + "model": "databricks/databricks-claude-sonnet-4-6" +} +``` +- Simple, one endpoint to check +- Some models have known incompatibilities (content-as-array for Gemini 3.x thinking) +- User picks from `databricks-*` model IDs + +**Option B — Dedicated provider endpoints per model family** +```json +{ + "provider": { + "databricks-anthropic": { + "npm": "@ai-sdk/anthropic", + "options": { + "baseURL": "https:///ai-gateway/anthropic", + "apiKey": "{env:DATABRICKS_TOKEN}" + } + }, + "databricks-google": { + "npm": "@ai-sdk/google", + "options": { + "baseURL": "https:///ai-gateway/gemini", + "apiKey": "{env:DATABRICKS_TOKEN}" + } + }, + "databricks-openai": { + "npm": "@ai-sdk/openai", + "options": { + "baseURL": "https:///ai-gateway/codex/v1", + "apiKey": "{env:DATABRICKS_TOKEN}" + } + } + }, + "model": "databricks-anthropic/databricks-claude-sonnet-4-6" +} +``` +- Native SDK per family → better compatibility (thinking models work correctly) +- More config complexity — 3 providers, 3 endpoints to check +- User picks a model + its provider is inferred from the model name prefix + +**Open question**: Does `@ai-sdk/anthropic` work against the Databricks Anthropic +gateway with `databricks-*` model IDs? Same for `@ai-sdk/google` against the +Gemini gateway? Needs testing. + +### 3. Model selection + +Unlike codex (no model selection), opencode needs a model specified in config. +The model list should come from the `providers/databricks/models/` TOMLs in +models.dev (or from querying the workspace endpoint directly). + +`classify_tool_from_text()` and `discover_workspace_models()` would need an +"opencode" case, but since opencode supports ANY model family, the full +`databricks-*` list is valid — not just one family like claude/gemini. + +Alternatively: skip workspace discovery entirely, let user type a model name, +show the list from models.dev as a hint. + +### 4. Config path + +`~/.config/opencode/opencode.json` — different from the other tools which use +home-dir dotfiles. Needs new path constants. + +### 5. Usage tracking + +The Spark SQL query in `build_usage_report_query()` filters by user_agent +containing "codex", "claude", or "gemini". Need to verify opencode sends +"opencode" in its user-agent (likely yes, confirmed from worker.ts in models.dev). +Add "opencode" case to the query. + +### 6. Gateway endpoint check + +`check_gateway_endpoint()` needs an opencode case. For Option A (mlflow), probe +`/ai-gateway/mlflow/v1/models`. For Option B, probe each provider endpoint. + +### 7. Validation test command + +`validate_tool()` needs an opencode case. opencode's CLI args are TBD — need to +check if it supports a single `-p "prompt"` style invocation or requires +interactive mode only. + +--- + +## What changes in cli.py + +| Section | Change | +|---|---| +| Path constants | Add `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_PATH`, `OPENCODE_BACKUP_PATH` | +| `TOOL_SPECS` | Add opencode entry: binary, package, display, config_path, backup_path | +| `TOOL_ALIASES` | Add "opencode" alias | +| `DEFAULT_SELECTED_MODELS` | Add default model (e.g. `databricks-claude-sonnet-4-6`) | +| `build_tool_base_url()` | Add opencode case (mlflow endpoint or per-family) | +| `render_opencode_config()` | New function — generates opencode.json content | +| `write_opencode_tool_config()` | New function — backup/write/mark managed | +| `configure_tool()` | Add opencode elif branch | +| `check_gateway_endpoint()` | Add opencode elif branch | +| `validate_tool()` | Add opencode elif branch | +| `build_usage_report_query()` | Add opencode to user_agent filters | +| `classify_tool_from_text()` | Add "opencode" detection | +| Launch path | Either reuse `launch_tool()` (execvp) or new `launch_opencode_tool()` with token refresh | + +--- + +## Open questions to resolve before implementing + +1. **Does opencode re-read `{env:VAR}` at request time or only at startup?** + → Determines auth approach (static refresh vs. env var) + +2. **Does `@ai-sdk/anthropic` work against `/ai-gateway/anthropic` with `databricks-*` model IDs?** + → Determines Option A vs. Option B for provider config + +3. **What are opencode's CLI flags for non-interactive single-prompt invocation?** + → Needed for `validate_tool()` test command + +4. **Does opencode emit "opencode" in its user-agent?** + → Needed for usage tracking SQL + +5. **Which approach for model selection?** + → Workspace discovery (requires opencode case in classify_tool_from_text) + or hardcoded list from models.dev + +--- + +## Recommended next steps + +1. Test Option B (native provider SDKs) against Databricks endpoints — run a quick + script using `@ai-sdk/anthropic` with baseURL set to the Databricks Anthropic + gateway to confirm model IDs and auth work +2. Check opencode source for env var re-read behavior +3. Check opencode CLI flags for non-interactive mode +4. Decide Option A vs B, then implement the 12 changes above diff --git a/mlflow.db b/mlflow.db new file mode 100644 index 0000000000000000000000000000000000000000..12c072a308690f8ffc0e711ae167c6e24d1807d8 GIT binary patch literal 716800 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 tX_xXzm0gbi$ "$CALL_COUNT_FILE" + +timestamp=$(date '+%Y-%m-%dT%H:%M:%S') + +if [[ "$count" -eq 1 ]]; then + # First call: get a real token + token=$(env -u DATABRICKS_TOKEN -u DATABRICKS_CLIENT_ID -u DATABRICKS_CLIENT_SECRET \ + -u DATABRICKS_USERNAME -u DATABRICKS_PASSWORD -u DATABRICKS_AUTH_TYPE \ + databricks auth token --host "$WORKSPACE" --output json 2>/dev/null \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token', ''))" 2>/dev/null) + echo "$timestamp call=$count returning=real_token len=${#token}" >> "$LOG_FILE" + echo "$token" +else + mode="${FAKE_HELPER_FAILURE_MODE:-empty}" + if [[ "$mode" == "nonzero" ]]; then + echo "$timestamp call=$count returning=nonzero_exit (simulated failure)" >> "$LOG_FILE" + echo "databricks auth: token refresh failed (simulated)" >&2 + exit 1 + elif [[ "$mode" == "reauth" ]]; then + # Simulate: first failure triggers re-auth, subsequent calls succeed + if [[ "$count" -eq 2 ]]; then + # This is the "re-auth" call — takes a moment, then returns a real token + echo "$timestamp call=$count returning=real_token_after_reauth (simulated re-auth)" >> "$LOG_FILE" + else + echo "$timestamp call=$count returning=real_token (recovered)" >> "$LOG_FILE" + fi + token=$(env -u DATABRICKS_TOKEN -u DATABRICKS_CLIENT_ID -u DATABRICKS_CLIENT_SECRET \ + -u DATABRICKS_USERNAME -u DATABRICKS_PASSWORD -u DATABRICKS_AUTH_TYPE \ + databricks auth token --host "$WORKSPACE" --output json 2>/dev/null \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token', ''))" 2>/dev/null) + echo "$token" + else + echo "$timestamp call=$count returning=empty (simulated failure)" >> "$LOG_FILE" + echo "" + fi +fi diff --git a/scripts/test_api_key_refresh.sh b/scripts/test_api_key_refresh.sh new file mode 100755 index 0000000..526c76b --- /dev/null +++ b/scripts/test_api_key_refresh.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Test whether Claude Code handles apiKeyHelper failures gracefully. +# +# Runs two scenarios back-to-back: +# Mode A (empty): helper returns "" with exit 0 → hangs silently (known bad behavior) +# Mode B (nonzero): helper exits 1 with stderr msg → should surface an error and exit +# +# Usage: bash scripts/test_api_key_refresh.sh + +set -euo pipefail + +SETTINGS="$HOME/.claude/settings.json" +BACKUP="$HOME/.claude/settings.json.test_backup" +CALL_COUNT_FILE="/tmp/fake_api_key_helper_call_count" +LOG_FILE="/tmp/fake_api_key_helper.log" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HELPER="$SCRIPT_DIR/fake_api_key_helper.sh" +TIMEOUT=30 # seconds before giving up on a hung claude invocation + +cleanup() { + if [[ -f "$BACKUP" ]]; then + echo "" + echo "--- Restoring original settings.json ---" + cp "$BACKUP" "$SETTINGS" + rm "$BACKUP" + fi +} +trap cleanup EXIT + +chmod +x "$HELPER" + +patch_settings() { + python3 - "$SETTINGS" "$HELPER" <<'EOF' +import json, sys +path, helper = sys.argv[1], sys.argv[2] +with open(path) as f: + s = json.load(f) +s["apiKeyHelper"] = helper +s.setdefault("env", {})["CLAUDE_CODE_API_KEY_HELPER_TTL_MS"] = "5000" +with open(path, "w") as f: + json.dump(s, f, indent=2) +EOF +} + +run_scenario() { + local mode="$1" + echo "" + echo "======================================================" + echo " SCENARIO: failure mode = $mode" + echo "======================================================" + + # Reset call counter and log + rm -f "$CALL_COUNT_FILE" "$LOG_FILE" + + # Backup and patch settings + cp "$SETTINGS" "$BACKUP" + patch_settings + + echo "" + echo "--- Run 1: first invocation (real token) ---" + FAKE_HELPER_FAILURE_MODE="$mode" \ + timeout "$TIMEOUT" claude --dangerously-skip-permissions -p "Reply with only: HELLO_ONE" 2>&1 \ + | tail -5 || echo "(exit code: $?)" + + echo "" + echo "--- Waiting 8s for TTL to expire ---" + sleep 8 + + echo "" + echo "--- Run 2: second invocation (helper will fail with mode=$mode) ---" + FAKE_HELPER_FAILURE_MODE="$mode" \ + timeout "$TIMEOUT" claude --dangerously-skip-permissions -p "Reply with only: HELLO_TWO" 2>&1 \ + | tail -10 \ + && echo "(exited 0)" || echo "(exited non-zero / timed out after ${TIMEOUT}s)" + + echo "" + echo "--- Helper log ---" + if [[ -f "$LOG_FILE" ]]; then + cat "$LOG_FILE" + else + echo "(helper was never called)" + fi + echo "Total helper calls: $(cat "$CALL_COUNT_FILE" 2>/dev/null || echo 0)" + + # Restore settings before next scenario + cp "$BACKUP" "$SETTINGS" + rm "$BACKUP" +} + +echo "=== Claude Code apiKeyHelper failure mode comparison ===" +echo "Helper: $HELPER" + +run_scenario "empty" +run_scenario "nonzero" +run_scenario "reauth" + +echo "" +echo "=== Complete. Compare the three scenarios above. ===" +echo "" +echo "Expected results:" +echo " empty — retries forever, times out (broken)" +echo " nonzero — retries forever, times out (broken)" +echo " reauth — recovers on second call, returns HELLO_TWO (good)" diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index adc1824..c2f82ca 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -14,7 +14,7 @@ import shutil import subprocess from pathlib import Path -from typing import cast +from typing import Literal, cast, overload from urllib import error as urllib_error from urllib import request as urllib_request from urllib.parse import urlparse @@ -90,9 +90,7 @@ def _debug(label: str, detail: str) -> None: logger.debug("%s: %s", label, detail) -_SECRET_KEY_PATTERN = re.compile( - r"(token|secret|password|bearer|api_key|apikey)", re.IGNORECASE -) +_SECRET_KEY_PATTERN = re.compile(r"(token|secret|password|bearer|api_key|apikey)", re.IGNORECASE) def _format_subprocess_result( @@ -127,7 +125,11 @@ def _scrub_databrickscfg(text: str) -> str: def _scrub_json(value: object) -> object: if isinstance(value, dict): return { - k: ("" if _SECRET_KEY_PATTERN.search(k) else _scrub_json(v)) + k: ( + "" + if isinstance(k, str) and _SECRET_KEY_PATTERN.search(k) + else _scrub_json(v) + ) for k, v in value.items() } if isinstance(value, list): @@ -225,6 +227,30 @@ def _http_get_json( return None, f"network error: {exc.reason}" +@overload +def run( + args: list[str], + *, + check: bool = True, + capture_output: bool = False, + text: Literal[True], + env: dict[str, str] | None = None, + timeout: int | None = None, +) -> subprocess.CompletedProcess[str]: ... + + +@overload +def run( + args: list[str], + *, + check: bool = True, + capture_output: bool = False, + text: Literal[False] = False, + env: dict[str, str] | None = None, + timeout: int | None = None, +) -> subprocess.CompletedProcess[bytes]: ... + + def run( args: list[str], *, @@ -429,9 +455,7 @@ def get_databricks_token(workspace: str, *, force_refresh: bool = False) -> str: "get_databricks_token.env", "set=" + ",".join( - sorted( - k for k in env if k.startswith("DATABRICKS_") or k in {"BUNDLE_PROFILE"} - ) + sorted(k for k in env if k.startswith("DATABRICKS_") or k in {"BUNDLE_PROFILE"}) ), ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 090a862..0664921 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,6 +19,7 @@ def _strip_ansi(text: str) -> str: which makes rich split styled tokens like ``--agents`` with ANSI codes).""" return _ANSI_RE.sub("", text) + runner = CliRunner() TOOLS = ["codex", "claude", "gemini", "opencode"] From 6780704dcf66a66928802a43fe42304f015fa897 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 17:44:40 -0400 Subject: [PATCH 06/11] Drop accidentally-committed local-only files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit ran `git add -A` and swept up local-only scratch files (.antigravitycli/, .claude/, .vscode/, OPENCODE_PLAN.md, mlflow.db, scripts/) that should never have been tracked. Removing them from the index — they remain on disk locally. --- .../8f06fdf0-e411-408f-99f1-0d66a1438780.json | 1 - .claude/worktrees/blissful-satoshi-5c8807 | 1 - .claude/worktrees/goofy-bardeen-737a2e | 1 - .vscode/settings.json | 3 - OPENCODE_PLAN.md | 181 ------------------ mlflow.db | Bin 716800 -> 0 bytes scripts/fake_api_key_helper.sh | 58 ------ scripts/test_api_key_refresh.sh | 103 ---------- 8 files changed, 348 deletions(-) delete mode 120000 .antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json delete mode 160000 .claude/worktrees/blissful-satoshi-5c8807 delete mode 160000 .claude/worktrees/goofy-bardeen-737a2e delete mode 100644 .vscode/settings.json delete mode 100644 OPENCODE_PLAN.md delete mode 100644 mlflow.db delete mode 100755 scripts/fake_api_key_helper.sh delete mode 100755 scripts/test_api_key_refresh.sh diff --git a/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json b/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json deleted file mode 120000 index 690de1f..0000000 --- a/.antigravitycli/8f06fdf0-e411-408f-99f1-0d66a1438780.json +++ /dev/null @@ -1 +0,0 @@ -/Users/rohit.a/.gemini/config/projects/8f06fdf0-e411-408f-99f1-0d66a1438780.json \ No newline at end of file diff --git a/.claude/worktrees/blissful-satoshi-5c8807 b/.claude/worktrees/blissful-satoshi-5c8807 deleted file mode 160000 index 17e96e9..0000000 --- a/.claude/worktrees/blissful-satoshi-5c8807 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17e96e96af1e3b00235ab5a7b0f0a15737b04f8d diff --git a/.claude/worktrees/goofy-bardeen-737a2e b/.claude/worktrees/goofy-bardeen-737a2e deleted file mode 160000 index 17e96e9..0000000 --- a/.claude/worktrees/goofy-bardeen-737a2e +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17e96e96af1e3b00235ab5a7b0f0a15737b04f8d diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ff5300e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.languageServer": "None" -} \ No newline at end of file diff --git a/OPENCODE_PLAN.md b/OPENCODE_PLAN.md deleted file mode 100644 index d8564f8..0000000 --- a/OPENCODE_PLAN.md +++ /dev/null @@ -1,181 +0,0 @@ -# Plan: Add opencode to ucode - -## What is opencode - -opencode is an AI coding CLI (`npm i -g opencode-ai`) that uses the Vercel AI SDK -internally. Unlike codex/claude/gemini which each speak one proprietary protocol, -opencode supports 22+ AI SDK providers via an `npm` field in its JSON config. - -Config lives at `~/.config/opencode/opencode.json`. - ---- - -## Why it's not as simple as the other tools - -### 1. Token auth — no shell command support (TBD) - -Codex and Claude Code support a shell auth command that runs on every request: -- Codex: `auth.command = "sh"` in TOML -- Claude Code: `apiKeyHelper` shell script in settings.json - -This means their tokens are always fresh. Gemini doesn't support this, so -ucode spawns a background thread to refresh `GEMINI_API_KEY` in the -.env file every 30 minutes. - -**opencode config only supports static apiKey or `{env:VAR}` syntax.** -This means we need to either: -- (a) Embed a live token at launch time and refresh like Gemini — requires - background refresh thread and subprocess launch (not execvp) -- (b) Investigate whether opencode re-reads its config on each request (unlikely) -- (c) Set `DATABRICKS_TOKEN` env var and use `{env:DATABRICKS_TOKEN}` in config — - then refresh that env var in the background (cleanest if it works) - -**Open question**: Does opencode read apiKey from env at request time or only at -startup? If at request time, option (c) works cleanly. - -### 2. Multiple providers vs. single endpoint - -opencode can be configured with multiple providers. Two approaches: - -**Option A — Single mlflow endpoint with `@ai-sdk/openai-compatible`** -```json -{ - "provider": { - "databricks": { - "npm": "@ai-sdk/openai-compatible", - "options": { - "baseURL": "https:///ai-gateway/mlflow/v1", - "apiKey": "{env:DATABRICKS_TOKEN}" - } - } - }, - "model": "databricks/databricks-claude-sonnet-4-6" -} -``` -- Simple, one endpoint to check -- Some models have known incompatibilities (content-as-array for Gemini 3.x thinking) -- User picks from `databricks-*` model IDs - -**Option B — Dedicated provider endpoints per model family** -```json -{ - "provider": { - "databricks-anthropic": { - "npm": "@ai-sdk/anthropic", - "options": { - "baseURL": "https:///ai-gateway/anthropic", - "apiKey": "{env:DATABRICKS_TOKEN}" - } - }, - "databricks-google": { - "npm": "@ai-sdk/google", - "options": { - "baseURL": "https:///ai-gateway/gemini", - "apiKey": "{env:DATABRICKS_TOKEN}" - } - }, - "databricks-openai": { - "npm": "@ai-sdk/openai", - "options": { - "baseURL": "https:///ai-gateway/codex/v1", - "apiKey": "{env:DATABRICKS_TOKEN}" - } - } - }, - "model": "databricks-anthropic/databricks-claude-sonnet-4-6" -} -``` -- Native SDK per family → better compatibility (thinking models work correctly) -- More config complexity — 3 providers, 3 endpoints to check -- User picks a model + its provider is inferred from the model name prefix - -**Open question**: Does `@ai-sdk/anthropic` work against the Databricks Anthropic -gateway with `databricks-*` model IDs? Same for `@ai-sdk/google` against the -Gemini gateway? Needs testing. - -### 3. Model selection - -Unlike codex (no model selection), opencode needs a model specified in config. -The model list should come from the `providers/databricks/models/` TOMLs in -models.dev (or from querying the workspace endpoint directly). - -`classify_tool_from_text()` and `discover_workspace_models()` would need an -"opencode" case, but since opencode supports ANY model family, the full -`databricks-*` list is valid — not just one family like claude/gemini. - -Alternatively: skip workspace discovery entirely, let user type a model name, -show the list from models.dev as a hint. - -### 4. Config path - -`~/.config/opencode/opencode.json` — different from the other tools which use -home-dir dotfiles. Needs new path constants. - -### 5. Usage tracking - -The Spark SQL query in `build_usage_report_query()` filters by user_agent -containing "codex", "claude", or "gemini". Need to verify opencode sends -"opencode" in its user-agent (likely yes, confirmed from worker.ts in models.dev). -Add "opencode" case to the query. - -### 6. Gateway endpoint check - -`check_gateway_endpoint()` needs an opencode case. For Option A (mlflow), probe -`/ai-gateway/mlflow/v1/models`. For Option B, probe each provider endpoint. - -### 7. Validation test command - -`validate_tool()` needs an opencode case. opencode's CLI args are TBD — need to -check if it supports a single `-p "prompt"` style invocation or requires -interactive mode only. - ---- - -## What changes in cli.py - -| Section | Change | -|---|---| -| Path constants | Add `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_PATH`, `OPENCODE_BACKUP_PATH` | -| `TOOL_SPECS` | Add opencode entry: binary, package, display, config_path, backup_path | -| `TOOL_ALIASES` | Add "opencode" alias | -| `DEFAULT_SELECTED_MODELS` | Add default model (e.g. `databricks-claude-sonnet-4-6`) | -| `build_tool_base_url()` | Add opencode case (mlflow endpoint or per-family) | -| `render_opencode_config()` | New function — generates opencode.json content | -| `write_opencode_tool_config()` | New function — backup/write/mark managed | -| `configure_tool()` | Add opencode elif branch | -| `check_gateway_endpoint()` | Add opencode elif branch | -| `validate_tool()` | Add opencode elif branch | -| `build_usage_report_query()` | Add opencode to user_agent filters | -| `classify_tool_from_text()` | Add "opencode" detection | -| Launch path | Either reuse `launch_tool()` (execvp) or new `launch_opencode_tool()` with token refresh | - ---- - -## Open questions to resolve before implementing - -1. **Does opencode re-read `{env:VAR}` at request time or only at startup?** - → Determines auth approach (static refresh vs. env var) - -2. **Does `@ai-sdk/anthropic` work against `/ai-gateway/anthropic` with `databricks-*` model IDs?** - → Determines Option A vs. Option B for provider config - -3. **What are opencode's CLI flags for non-interactive single-prompt invocation?** - → Needed for `validate_tool()` test command - -4. **Does opencode emit "opencode" in its user-agent?** - → Needed for usage tracking SQL - -5. **Which approach for model selection?** - → Workspace discovery (requires opencode case in classify_tool_from_text) - or hardcoded list from models.dev - ---- - -## Recommended next steps - -1. Test Option B (native provider SDKs) against Databricks endpoints — run a quick - script using `@ai-sdk/anthropic` with baseURL set to the Databricks Anthropic - gateway to confirm model IDs and auth work -2. Check opencode source for env var re-read behavior -3. Check opencode CLI flags for non-interactive mode -4. Decide Option A vs B, then implement the 12 changes above diff --git a/mlflow.db b/mlflow.db deleted file mode 100644 index 12c072a308690f8ffc0e711ae167c6e24d1807d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 716800 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 tX_xXzm0gbi$ "$CALL_COUNT_FILE" - -timestamp=$(date '+%Y-%m-%dT%H:%M:%S') - -if [[ "$count" -eq 1 ]]; then - # First call: get a real token - token=$(env -u DATABRICKS_TOKEN -u DATABRICKS_CLIENT_ID -u DATABRICKS_CLIENT_SECRET \ - -u DATABRICKS_USERNAME -u DATABRICKS_PASSWORD -u DATABRICKS_AUTH_TYPE \ - databricks auth token --host "$WORKSPACE" --output json 2>/dev/null \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token', ''))" 2>/dev/null) - echo "$timestamp call=$count returning=real_token len=${#token}" >> "$LOG_FILE" - echo "$token" -else - mode="${FAKE_HELPER_FAILURE_MODE:-empty}" - if [[ "$mode" == "nonzero" ]]; then - echo "$timestamp call=$count returning=nonzero_exit (simulated failure)" >> "$LOG_FILE" - echo "databricks auth: token refresh failed (simulated)" >&2 - exit 1 - elif [[ "$mode" == "reauth" ]]; then - # Simulate: first failure triggers re-auth, subsequent calls succeed - if [[ "$count" -eq 2 ]]; then - # This is the "re-auth" call — takes a moment, then returns a real token - echo "$timestamp call=$count returning=real_token_after_reauth (simulated re-auth)" >> "$LOG_FILE" - else - echo "$timestamp call=$count returning=real_token (recovered)" >> "$LOG_FILE" - fi - token=$(env -u DATABRICKS_TOKEN -u DATABRICKS_CLIENT_ID -u DATABRICKS_CLIENT_SECRET \ - -u DATABRICKS_USERNAME -u DATABRICKS_PASSWORD -u DATABRICKS_AUTH_TYPE \ - databricks auth token --host "$WORKSPACE" --output json 2>/dev/null \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token', ''))" 2>/dev/null) - echo "$token" - else - echo "$timestamp call=$count returning=empty (simulated failure)" >> "$LOG_FILE" - echo "" - fi -fi diff --git a/scripts/test_api_key_refresh.sh b/scripts/test_api_key_refresh.sh deleted file mode 100755 index 526c76b..0000000 --- a/scripts/test_api_key_refresh.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# Test whether Claude Code handles apiKeyHelper failures gracefully. -# -# Runs two scenarios back-to-back: -# Mode A (empty): helper returns "" with exit 0 → hangs silently (known bad behavior) -# Mode B (nonzero): helper exits 1 with stderr msg → should surface an error and exit -# -# Usage: bash scripts/test_api_key_refresh.sh - -set -euo pipefail - -SETTINGS="$HOME/.claude/settings.json" -BACKUP="$HOME/.claude/settings.json.test_backup" -CALL_COUNT_FILE="/tmp/fake_api_key_helper_call_count" -LOG_FILE="/tmp/fake_api_key_helper.log" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HELPER="$SCRIPT_DIR/fake_api_key_helper.sh" -TIMEOUT=30 # seconds before giving up on a hung claude invocation - -cleanup() { - if [[ -f "$BACKUP" ]]; then - echo "" - echo "--- Restoring original settings.json ---" - cp "$BACKUP" "$SETTINGS" - rm "$BACKUP" - fi -} -trap cleanup EXIT - -chmod +x "$HELPER" - -patch_settings() { - python3 - "$SETTINGS" "$HELPER" <<'EOF' -import json, sys -path, helper = sys.argv[1], sys.argv[2] -with open(path) as f: - s = json.load(f) -s["apiKeyHelper"] = helper -s.setdefault("env", {})["CLAUDE_CODE_API_KEY_HELPER_TTL_MS"] = "5000" -with open(path, "w") as f: - json.dump(s, f, indent=2) -EOF -} - -run_scenario() { - local mode="$1" - echo "" - echo "======================================================" - echo " SCENARIO: failure mode = $mode" - echo "======================================================" - - # Reset call counter and log - rm -f "$CALL_COUNT_FILE" "$LOG_FILE" - - # Backup and patch settings - cp "$SETTINGS" "$BACKUP" - patch_settings - - echo "" - echo "--- Run 1: first invocation (real token) ---" - FAKE_HELPER_FAILURE_MODE="$mode" \ - timeout "$TIMEOUT" claude --dangerously-skip-permissions -p "Reply with only: HELLO_ONE" 2>&1 \ - | tail -5 || echo "(exit code: $?)" - - echo "" - echo "--- Waiting 8s for TTL to expire ---" - sleep 8 - - echo "" - echo "--- Run 2: second invocation (helper will fail with mode=$mode) ---" - FAKE_HELPER_FAILURE_MODE="$mode" \ - timeout "$TIMEOUT" claude --dangerously-skip-permissions -p "Reply with only: HELLO_TWO" 2>&1 \ - | tail -10 \ - && echo "(exited 0)" || echo "(exited non-zero / timed out after ${TIMEOUT}s)" - - echo "" - echo "--- Helper log ---" - if [[ -f "$LOG_FILE" ]]; then - cat "$LOG_FILE" - else - echo "(helper was never called)" - fi - echo "Total helper calls: $(cat "$CALL_COUNT_FILE" 2>/dev/null || echo 0)" - - # Restore settings before next scenario - cp "$BACKUP" "$SETTINGS" - rm "$BACKUP" -} - -echo "=== Claude Code apiKeyHelper failure mode comparison ===" -echo "Helper: $HELPER" - -run_scenario "empty" -run_scenario "nonzero" -run_scenario "reauth" - -echo "" -echo "=== Complete. Compare the three scenarios above. ===" -echo "" -echo "Expected results:" -echo " empty — retries forever, times out (broken)" -echo " nonzero — retries forever, times out (broken)" -echo " reauth — recovers on second call, returns HELLO_TWO (good)" From e884ba79da5394d13eda98f34a8575d4699b1f73 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 18:18:34 -0400 Subject: [PATCH 07/11] Close stdin on the e2e pytest run The Databricks CLI's `auth login --no-browser` fallback path reads stdin for the user to paste an OAuth code. In CI the runner's stdin sometimes keeps the subprocess alive past Python's timeout, hanging the whole job for the full ~6h workflow limit. Redirecting stdin to /dev/null makes that fallback EOF immediately, so if M2M env vars don't yield a token we fail fast with a clear error instead of hanging. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d703722..f4ee3b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,4 +31,7 @@ jobs: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: databricks/setup-cli@bdb89f81c11a5bd647fd55b585b7c396ec68a25a # v1.0.0 - run: uv tool install . - - run: uv run pytest tests/test_e2e.py -v + # Redirect stdin so any interactive `databricks auth login --no-browser` + # fallback EOFs instead of hanging the runner. CI must rely solely on + # the M2M env vars; if those don't yield a token, fail fast. + - run: uv run pytest tests/test_e2e.py -v < /dev/null From bf9ef108d4e16a9ad6959f31fc962eba32f7961b Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 18:19:34 -0400 Subject: [PATCH 08/11] Add M2M auth diagnostic step before e2e pytest Last CI run failed with `Databricks CLI returned no access token` after get_databricks_token's pytest fixture couldn't authenticate via M2M env vars. The CLI's stderr is captured (and discarded) by ucode so we never see *why* M2M failed. This diagnostic step prints the CLI version, which auth env vars are set, whether ~/.databrickscfg exists, and the raw output of `databricks auth token` + `current-user me` so the next run will tell us exactly what's wrong. --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ee3b5..aac2d6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,27 @@ jobs: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: databricks/setup-cli@bdb89f81c11a5bd647fd55b585b7c396ec68a25a # v1.0.0 - run: uv tool install . + - name: Probe M2M auth (surfaces the CLI's real error if M2M is broken) + run: | + set +e + echo "=== databricks --version ===" + databricks --version + echo + echo "=== env presence (values redacted) ===" + for v in DATABRICKS_HOST DATABRICKS_CLIENT_ID DATABRICKS_CLIENT_SECRET DATABRICKS_CONFIG_FILE DATABRICKS_AUTH_TYPE; do + if [ -n "${!v:-}" ]; then echo "$v="; else echo "$v="; fi + done + echo + echo "=== ~/.databrickscfg presence ===" + ls -la ~/.databrickscfg 2>&1 || echo "no databrickscfg" + echo + echo "=== databricks auth token --host \$DATABRICKS_HOST ===" + databricks auth token --host "$DATABRICKS_HOST" --output json + echo "exit=$?" + echo + echo "=== databricks current-user me (uses M2M if env vars set) ===" + databricks current-user me 2>&1 | head -5 + echo "exit=$?" # Redirect stdin so any interactive `databricks auth login --no-browser` # fallback EOFs instead of hanging the runner. CI must rely solely on # the M2M env vars; if those don't yield a token, fail fast. From 0ae6d883a2b45cc0f7824d69b3b0212bfabbccb6 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 18:34:35 -0400 Subject: [PATCH 09/11] Use DATABRICKS_BEARER as CI escape hatch for e2e auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `databricks auth token` only returns cached user-OAuth tokens (from `databricks auth login`) — it has no M2M path. Hosted-runner CI has no ~/.databrickscfg and no cached login, so the CLI command always errors with "OAuth is not configured for this host", and ucode's token helper hangs on the `auth login --no-browser` fallback waiting for stdin. Teach has_valid_databricks_auth and get_databricks_token to short-circuit to a pre-fetched DATABRICKS_BEARER env var (the same env var build_auth_shell_command already honors). The e2e workflow pulls it from a repo secret; user fetches a fresh bearer (e.g. M2M client_credentials against /oidc/v1/token) and pastes it into Settings → Secrets when the token expires (~1h). Removes DATABRICKS_CLIENT_ID/_SECRET from the workflow — they're no longer load-bearing now that we don't call the CLI for auth. --- .github/workflows/ci.yml | 37 ++++++++++++------------------------- src/ucode/databricks.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aac2d6e..7fde8e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,36 +23,23 @@ jobs: env: UCODE_TEST_WORKSPACE: ${{ secrets.UCODE_TEST_WORKSPACE }} DATABRICKS_HOST: ${{ secrets.UCODE_TEST_WORKSPACE }} - DATABRICKS_CLIENT_ID: ${{ secrets.DATABRICKS_CLIENT_ID }} - DATABRICKS_CLIENT_SECRET: ${{ secrets.DATABRICKS_CLIENT_SECRET }} + # DATABRICKS_BEARER is the CI escape hatch: `databricks auth token` + # only retrieves cached user-OAuth tokens, so on a hosted runner + # (no databrickscfg, no cached login) it can never produce a bearer. + # Pre-fetch one (e.g. via M2M OAuth client_credentials against + # /oidc/v1/token) and store it as a repo secret. Both + # has_valid_databricks_auth + get_databricks_token + the agents' + # apiKeyHelper short-circuit to this value when set. Tokens are + # short-lived (~1h); rotate when CI starts failing with 401s. + DATABRICKS_BEARER: ${{ secrets.DATABRICKS_BEARER }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: databricks/setup-cli@bdb89f81c11a5bd647fd55b585b7c396ec68a25a # v1.0.0 - run: uv tool install . - - name: Probe M2M auth (surfaces the CLI's real error if M2M is broken) - run: | - set +e - echo "=== databricks --version ===" - databricks --version - echo - echo "=== env presence (values redacted) ===" - for v in DATABRICKS_HOST DATABRICKS_CLIENT_ID DATABRICKS_CLIENT_SECRET DATABRICKS_CONFIG_FILE DATABRICKS_AUTH_TYPE; do - if [ -n "${!v:-}" ]; then echo "$v="; else echo "$v="; fi - done - echo - echo "=== ~/.databrickscfg presence ===" - ls -la ~/.databrickscfg 2>&1 || echo "no databrickscfg" - echo - echo "=== databricks auth token --host \$DATABRICKS_HOST ===" - databricks auth token --host "$DATABRICKS_HOST" --output json - echo "exit=$?" - echo - echo "=== databricks current-user me (uses M2M if env vars set) ===" - databricks current-user me 2>&1 | head -5 - echo "exit=$?" # Redirect stdin so any interactive `databricks auth login --no-browser` - # fallback EOFs instead of hanging the runner. CI must rely solely on - # the M2M env vars; if those don't yield a token, fail fast. + # fallback EOFs instead of hanging the runner. With DATABRICKS_BEARER + # set, the auth code path doesn't shell out at all — this is a safety + # net for any code path we may have missed. - run: uv run pytest tests/test_e2e.py -v < /dev/null diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index c2f82ca..35c7f31 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -357,6 +357,11 @@ def install_databricks_cli() -> None: def has_valid_databricks_auth(workspace: str) -> bool: + # Honor the CI short-circuit (see ``get_databricks_token``): if a + # pre-fetched bearer is available, treat auth as valid and skip the + # `databricks auth token` shell-out (which only knows user-OAuth). + if os.environ.get("DATABRICKS_BEARER", "").strip(): + return True _log_auth_diagnostics() try: env = build_databricks_cli_env(workspace) @@ -445,6 +450,17 @@ def ensure_databricks_auth(workspace: str) -> None: def get_databricks_token(workspace: str, *, force_refresh: bool = False) -> str: + # ``DATABRICKS_BEARER`` is the CI escape hatch: when set, skip the + # `databricks auth token` subprocess entirely and return the pre-fetched + # bearer directly. Used by the e2e job, where the protected runner has + # no `databricks auth login` cache and `databricks auth token` only knows + # how to read user-OAuth caches (not M2M client_credentials). Mirrors the + # same short-circuit baked into ``build_auth_shell_command``. + bearer = os.environ.get("DATABRICKS_BEARER", "").strip() + if bearer: + _debug("get_databricks_token", "using DATABRICKS_BEARER env var") + return bearer + _log_auth_diagnostics() env = build_databricks_cli_env(workspace) cmd = ["databricks", "auth", "token", "--host", workspace, "--output", "json"] From 835adefcad576929af1133d33cc7674ea8761258 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 18:53:19 -0400 Subject: [PATCH 10/11] Fix e2e test monkeypatches + bump web-search timeout - tests/test_e2e.py: TestConfigureSubset patches `run_databricks_login` at the wrong module path. cli.py does `from ucode.databricks import run_databricks_login`, so patching `ucode.databricks.run_databricks_login` doesn't affect the local name cli uses, and the real function runs (which opens a browser). Patch `ucode.cli.run_databricks_login` instead. - mcp_web_search.py: bump urllib timeout from 60s to 180s. Codex Responses API with native web_search does a real web fetch + LLM completion, which legitimately can exceed 60s under load. --- src/ucode/mcp_web_search.py | 2 +- tests/test_e2e.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ucode/mcp_web_search.py b/src/ucode/mcp_web_search.py index 5173c21..7d08554 100644 --- a/src/ucode/mcp_web_search.py +++ b/src/ucode/mcp_web_search.py @@ -119,7 +119,7 @@ def _call_responses_api(query: str) -> dict[str, Any]: }, ) try: - with urllib_request.urlopen(request, timeout=60) as response: + with urllib_request.urlopen(request, timeout=180) as response: raw = response.read().decode("utf-8") except urllib_error.HTTPError as exc: detail = "" diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 6e49be5..53ca465 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -213,7 +213,7 @@ def test_only_picks_codex_writes_only_codex_config(self, tmp_path, monkeypatch, monkeypatch.setattr(state_mod, "STATE_PATH", tmp_path / "state.json") # Don't actually run `databricks auth login`; the developer running # this suite is already authenticated. - monkeypatch.setattr("ucode.databricks.run_databricks_login", lambda ws: None) + monkeypatch.setattr("ucode.cli.run_databricks_login", lambda ws: None) # Skip the workspace prompt and the multi-select picker. monkeypatch.setattr(cli_mod, "_prompt_for_configuration", lambda tool=None: e2e_workspace) monkeypatch.setattr(cli_mod, "prompt_for_tools", lambda available: ["codex"]) @@ -248,7 +248,7 @@ def test_rerun_with_different_pick_preserves_previous( self._redirect_config_paths(monkeypatch, tmp_path) monkeypatch.setattr(state_mod, "STATE_PATH", tmp_path / "state.json") - monkeypatch.setattr("ucode.databricks.run_databricks_login", lambda ws: None) + monkeypatch.setattr("ucode.cli.run_databricks_login", lambda ws: None) monkeypatch.setattr(cli_mod, "_prompt_for_configuration", lambda tool=None: e2e_workspace) monkeypatch.setattr( cli_mod, "install_tool_binary", lambda tool, strict=False, update_existing=False: True @@ -281,7 +281,7 @@ def test_empty_pick_returns_zero_and_writes_nothing(self, tmp_path, monkeypatch, codex_path = self._redirect_config_paths(monkeypatch, tmp_path) monkeypatch.setattr(state_mod, "STATE_PATH", tmp_path / "state.json") - monkeypatch.setattr("ucode.databricks.run_databricks_login", lambda ws: None) + monkeypatch.setattr("ucode.cli.run_databricks_login", lambda ws: None) monkeypatch.setattr(cli_mod, "_prompt_for_configuration", lambda tool=None: e2e_workspace) monkeypatch.setattr(cli_mod, "prompt_for_tools", lambda available: []) install_calls: list[str] = [] From 40c8c6fb7ef626bb66e2320e17b89723bd980d1b Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Fri, 22 May 2026 18:56:31 -0400 Subject: [PATCH 11/11] Install agent CLIs in CI so launch tests don't skip The six TestXxxLaunch tests all call `_require_binary("...")` and skip when the agent isn't on PATH. The runner has only Node.js, not the agents themselves, so every launch test skipped. Install all six via `npm install -g` so they exercise real binaries against the e2e workspace. --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fde8e3..b6069c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,17 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: databricks/setup-cli@bdb89f81c11a5bd647fd55b585b7c396ec68a25a # v1.0.0 + # The agent launch tests `_require_binary("codex")` etc. and skip when + # the CLI isn't on PATH. Install all six so each TestXxxLaunch test + # actually runs instead of skipping. + - name: Install agent CLIs + run: npm install -g + @anthropic-ai/claude-code + @openai/codex + @google/gemini-cli + opencode-ai + @github/copilot + @earendil-works/pi-coding-agent - run: uv tool install . # Redirect stdin so any interactive `databricks auth login --no-browser` # fallback EOFs instead of hanging the runner. With DATABRICKS_BEARER