Skip to content

Commit 5782bcf

Browse files
committed
ucode: add --skip-preflight to skip per-launch auth/gateway re-validation
Every `ucode <agent>` launch re-runs configure_shared_state, which does an auth login check, a token fetch, an AI Gateway v2 probe, and (outside provider mode) model discovery — ~5-10s of network round-trips — even when a prior `ucode configure` already validated all of it. Managed/headless launchers (e.g. omnigent) that run configure once and then launch repeatedly pay this on every turn. Add a launch-only `--skip-preflight` flag (distinct from the configure-only `--skip-validate`, which skips the post-configure model smoke test). It threads through _launch_tool as configure_shared_state(skip_preflight=True). configure_shared_state now assembles the shared workspace state (profile, base URLs, auth mode) up front and returns early when skip_preflight is set; otherwise it falls through into the preflight block (auth + AI Gateway validation, then model discovery). The PAT/bearer is already exported by apply_pat_environment and the gateway was verified by the earlier configure, so the skipped work is redundant, and the saved model lists are preserved. Wired into every launch command (codex/claude/gemini/opencode/copilot/pi), default off. Co-authored-by: Isaac
1 parent a0a0d1c commit 5782bcf

2 files changed

Lines changed: 200 additions & 41 deletions

File tree

src/ucode/cli.py

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def configure_shared_state(
216216
force_login: bool = False,
217217
use_pat: bool | None = None,
218218
skip_model_discovery: bool = False,
219+
skip_preflight: bool = False,
219220
) -> dict:
220221
"""Log into Databricks, enforce AI Gateway v2, fetch model lists, persist state.
221222
@@ -229,13 +230,59 @@ def configure_shared_state(
229230
``--profile`` to every CLI invocation so ambiguous `~/.databrickscfg`
230231
entries (e.g. DEFAULT and a named profile both pointing at the same host)
231232
don't error out. If ``None``, we resolve it from the host after login.
233+
If skip_preflight is True, skip the entire preflight block below — auth
234+
validation, the AI Gateway probe, and model discovery — trusting a prior
235+
``ucode configure``. The PAT/bearer is already exported (``apply_pat_environment``
236+
in ``_launch_tool``) and the gateway was verified by that earlier configure.
237+
Only the local profile resolution and the shared state assembly still run;
238+
the saved model lists are preserved.
232239
"""
233240
workspace = normalize_workspace_url(workspace)
234241
prior_state = load_state()
235242
previous_workspace = prior_state.get("workspace")
236243
if use_pat is None:
237244
use_pat = bool(prior_state.get("use_pat")) and previous_workspace == workspace
238245
fetch_all = tools is None
246+
247+
# Resolve the profile locally from ~/.databrickscfg. On a first-time OAuth
248+
# login the matching profile may not exist yet; the preflight below
249+
# re-resolves it once `databricks auth login` has created it.
250+
if profile is None:
251+
profile = find_profile_name_for_host(workspace)
252+
253+
# Assemble the shared workspace state that doesn't depend on model discovery:
254+
# workspace, profile, auth mode, base URLs. --skip-preflight persists exactly
255+
# this and returns, trusting a prior `ucode configure` — it already validated
256+
# auth + the AI Gateway and saved the model lists, which load_state carried
257+
# over above and we leave untouched.
258+
state = load_state()
259+
state["workspace"] = workspace
260+
if profile:
261+
state["profile"] = profile
262+
else:
263+
state.pop("profile", None)
264+
# UC discovery is now always-on; drop any flag persisted by older versions.
265+
state.pop("uc_enabled", None)
266+
# Persist the auth mode so launches rebuild the same (PAT-based) agent
267+
# auth command; an explicit re-configure without --use-pat clears it.
268+
if use_pat:
269+
state["use_pat"] = True
270+
else:
271+
state.pop("use_pat", None)
272+
state["base_urls"] = build_shared_base_urls(workspace)
273+
274+
if skip_preflight:
275+
save_state(state)
276+
# Scrub MCP entries ucode wrote for a previous workspace.
277+
if previous_workspace and previous_workspace != workspace:
278+
purge_cross_workspace_mcp_residue(state, workspace)
279+
# Diagnostic reasons are transient (attached after save_state so they
280+
# don't land on disk). No discovery ran, so there is nothing to report.
281+
state["_discovery_reasons"] = {"claude": None, "gemini": None, "codex": None, "oss": None}
282+
return state
283+
284+
# ── Preflight (bypassed above under --skip-preflight): validate Databricks
285+
# auth + the AI Gateway, then discover the available models. ──
239286
if use_pat:
240287
if not profile:
241288
raise RuntimeError(
@@ -260,9 +307,11 @@ def configure_shared_state(
260307
else:
261308
ensure_databricks_auth(workspace, profile)
262309
# After login the profile exists in ~/.databrickscfg, so a host->profile
263-
# lookup is reliable. Persist it so subsequent CLI calls disambiguate.
310+
# lookup is reliable even when it returned nothing above.
264311
if profile is None:
265312
profile = find_profile_name_for_host(workspace)
313+
if profile:
314+
state["profile"] = profile
266315
with spinner("Verifying Unity AI Gateway..."):
267316
token = get_databricks_token(workspace, profile)
268317
ensure_ai_gateway_v2(workspace, token)
@@ -283,6 +332,7 @@ def configure_shared_state(
283332
gemini_models = []
284333
codex_models = []
285334
oss_models = []
335+
opencode_models: dict[str, list[str]] = {}
286336
web_search_model: str | None = None
287337
if skip_model_discovery:
288338
# Provider mode: the agent routes through a Model Provider Service and
@@ -295,10 +345,11 @@ def configure_shared_state(
295345
if ws_models:
296346
web_search_model = ws_models[0]
297347
else:
298-
# UC-first, best-effort: one UC model-services call yields all families as
299-
# `system.ai.<model-name>` ids, bucketed by name. If a family comes back
300-
# empty (workspace without UC model-services, or the listing failed), fall
301-
# back to the per-family AI Gateway listing for that family only.
348+
# UC-first, best-effort: one UC model-services call yields all families
349+
# as `system.ai.<model-name>` ids, bucketed by name. If a family comes
350+
# back empty (workspace without UC model-services, or the listing
351+
# failed), fall back to the per-family AI Gateway listing for that
352+
# family only.
302353
with spinner("Fetching available models..."):
303354
ms_claude, ms_codex, ms_gemini, ms_oss, ms_reason = discover_model_services(
304355
workspace, token
@@ -317,30 +368,13 @@ def configure_shared_state(
317368
codex_models, codex_reason = discover_codex_models(workspace, token)
318369
if want_oss:
319370
oss_models, oss_reason = ms_oss, ms_reason
320-
opencode_models: dict[str, list[str]] = {}
321-
if claude_models:
322-
opencode_models["anthropic"] = list(claude_models.values())
323-
if gemini_models:
324-
opencode_models["gemini"] = gemini_models
325-
if oss_models:
326-
opencode_models["oss"] = oss_models
327-
328-
# Merge into existing workspace state so prior tool configs are preserved.
329-
state = load_state()
330-
state["workspace"] = workspace
331-
if profile:
332-
state["profile"] = profile
333-
else:
334-
state.pop("profile", None)
335-
# UC discovery is now always-on; drop any flag persisted by older versions.
336-
state.pop("uc_enabled", None)
337-
# Persist the auth mode so launches rebuild the same (PAT-based) agent
338-
# auth command; an explicit re-configure without --use-pat clears it.
339-
if use_pat:
340-
state["use_pat"] = True
341-
else:
342-
state.pop("use_pat", None)
343-
state["base_urls"] = build_shared_base_urls(workspace)
371+
if claude_models:
372+
opencode_models["anthropic"] = list(claude_models.values())
373+
if gemini_models:
374+
opencode_models["gemini"] = gemini_models
375+
if oss_models:
376+
opencode_models["oss"] = oss_models
377+
344378
if skip_model_discovery:
345379
# Don't clobber any previously-discovered Databricks model lists; provider
346380
# mode just doesn't refresh or use them. Persist the web-search model so
@@ -822,7 +856,12 @@ def _auto_configure_tool(tool: str) -> None:
822856
raise RuntimeError(f"{spec['display']} validation failed — config reverted.")
823857

824858

825-
def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None) -> None:
859+
def _launch_tool(
860+
tool_name: str,
861+
ctx: typer.Context,
862+
provider: str | None = None,
863+
skip_preflight: bool = False,
864+
) -> None:
826865
try:
827866
tool = normalize_tool(tool_name)
828867
existing = load_state()
@@ -860,6 +899,7 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
860899
profile=state.get("profile"),
861900
tools=[tool],
862901
skip_model_discovery=bool(provider),
902+
skip_preflight=skip_preflight,
863903
)
864904
if provider:
865905
# Routing through a Model Provider Service pins no Databricks model;
@@ -892,6 +932,20 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
892932
raise typer.Exit(130) from None
893933

894934

935+
# Launch-only escape hatch for managed/headless launchers (e.g. omnigent) that
936+
# have already run `ucode configure`: skip the ~5-10s per-launch auth + AI
937+
# Gateway re-validation. Distinct from the configure-only `--skip-validate`,
938+
# which skips the model smoke test.
939+
SkipPreflightOption = Annotated[
940+
bool,
941+
typer.Option(
942+
"--skip-preflight",
943+
help="Skip the per-launch Databricks auth + AI Gateway re-validation, trusting a "
944+
"prior `ucode configure`.",
945+
),
946+
]
947+
948+
895949
@app.command("codex", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
896950
def codex_cmd(
897951
ctx: typer.Context,
@@ -904,9 +958,10 @@ def codex_cmd(
904958
"before any `--` separator.",
905959
),
906960
] = None,
961+
skip_preflight: SkipPreflightOption = False,
907962
) -> None:
908963
"""Launch Codex via Databricks."""
909-
_launch_tool("codex", ctx, provider=provider)
964+
_launch_tool("codex", ctx, provider=provider, skip_preflight=skip_preflight)
910965

911966

912967
@app.command("claude", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
@@ -921,35 +976,36 @@ def claude_cmd(
921976
"before any `--` separator.",
922977
),
923978
] = None,
979+
skip_preflight: SkipPreflightOption = False,
924980
) -> None:
925981
"""Launch Claude Code via Databricks."""
926-
_launch_tool("claude", ctx, provider=provider)
982+
_launch_tool("claude", ctx, provider=provider, skip_preflight=skip_preflight)
927983

928984

929985
@app.command("gemini", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
930-
def gemini_cmd(ctx: typer.Context) -> None:
986+
def gemini_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
931987
"""Launch Gemini CLI via Databricks."""
932-
_launch_tool("gemini", ctx)
988+
_launch_tool("gemini", ctx, skip_preflight=skip_preflight)
933989

934990

935991
@app.command(
936992
"opencode", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
937993
)
938-
def opencode_cmd(ctx: typer.Context) -> None:
994+
def opencode_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
939995
"""Launch OpenCode via Databricks."""
940-
_launch_tool("opencode", ctx)
996+
_launch_tool("opencode", ctx, skip_preflight=skip_preflight)
941997

942998

943999
@app.command("copilot", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
944-
def copilot_cmd(ctx: typer.Context) -> None:
1000+
def copilot_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
9451001
"""Launch GitHub Copilot CLI via Databricks."""
946-
_launch_tool("copilot", ctx)
1002+
_launch_tool("copilot", ctx, skip_preflight=skip_preflight)
9471003

9481004

9491005
@app.command("pi", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
950-
def pi_cmd(ctx: typer.Context) -> None:
1006+
def pi_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
9511007
"""Launch Pi coding agent via Databricks."""
952-
_launch_tool("pi", ctx)
1008+
_launch_tool("pi", ctx, skip_preflight=skip_preflight)
9531009

9541010

9551011
@configure_app.callback(invoke_without_command=True)

tests/test_cli.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from __future__ import annotations
44

5+
import contextlib
56
import os
67
import re
7-
from unittest.mock import patch
8+
from unittest.mock import MagicMock, patch
89

910
import pytest
1011
from typer.testing import CliRunner
@@ -1313,3 +1314,105 @@ def _boom(*a, **k):
13131314
assert state["web_search_model"] == "databricks-gpt-5"
13141315
# Existing model list preserved, not overwritten to {}.
13151316
assert state["claude_models"] == {"opus": "databricks-claude-opus-4-8"}
1317+
1318+
1319+
class TestConfigureSharedStateSkipPreflight:
1320+
"""With skip_preflight (--skip-preflight), a prior configure is trusted:
1321+
no auth login, token fetch, gateway probe, or model discovery runs — but the
1322+
profile and base URLs are still resolved and state is persisted."""
1323+
1324+
WS = "https://cfg.databricks.com"
1325+
1326+
@staticmethod
1327+
def _stub(monkeypatch):
1328+
import ucode.cli as cli_mod
1329+
1330+
def _boom(name):
1331+
def _f(*a, **k):
1332+
raise AssertionError(f"{name} must not run under skip_preflight")
1333+
1334+
return _f
1335+
1336+
monkeypatch.setattr(cli_mod, "normalize_workspace_url", lambda w: w)
1337+
# Any network round-trip is a hard failure in this mode.
1338+
monkeypatch.setattr(cli_mod, "ensure_databricks_auth", _boom("ensure_databricks_auth"))
1339+
monkeypatch.setattr(cli_mod, "run_databricks_login", _boom("run_databricks_login"))
1340+
monkeypatch.setattr(cli_mod, "ensure_pat_bearer", _boom("ensure_pat_bearer"))
1341+
monkeypatch.setattr(cli_mod, "get_databricks_token", _boom("get_databricks_token"))
1342+
monkeypatch.setattr(cli_mod, "ensure_ai_gateway_v2", _boom("ensure_ai_gateway_v2"))
1343+
monkeypatch.setattr(cli_mod, "discover_model_services", _boom("discover_model_services"))
1344+
monkeypatch.setattr(cli_mod, "discover_codex_models", _boom("discover_codex_models"))
1345+
monkeypatch.setattr(cli_mod, "find_profile_name_for_host", lambda w: "resolved")
1346+
monkeypatch.setattr(cli_mod, "build_shared_base_urls", lambda w: {"codex": "u/codex"})
1347+
saved: list[dict] = []
1348+
monkeypatch.setattr(cli_mod, "save_state", lambda s: saved.append(dict(s)))
1349+
return cli_mod, saved
1350+
1351+
def test_skips_auth_gateway_and_discovery_but_persists(self, monkeypatch):
1352+
cli_mod, saved = self._stub(monkeypatch)
1353+
monkeypatch.setattr(
1354+
cli_mod,
1355+
"load_state",
1356+
lambda: {"workspace": self.WS, "codex_models": ["databricks-gpt-5"]},
1357+
)
1358+
1359+
state = cli_mod.configure_shared_state(
1360+
self.WS, profile="DEFAULT", tools=["codex"], skip_preflight=True
1361+
)
1362+
1363+
# base_urls rebuilt and state saved, but the prior model list is left intact.
1364+
assert state["base_urls"] == {"codex": "u/codex"}
1365+
assert state["codex_models"] == ["databricks-gpt-5"]
1366+
assert saved and saved[-1]["base_urls"] == {"codex": "u/codex"}
1367+
1368+
def test_resolves_profile_locally_when_missing(self, monkeypatch):
1369+
cli_mod, _ = self._stub(monkeypatch)
1370+
monkeypatch.setattr(cli_mod, "load_state", lambda: {"workspace": self.WS})
1371+
1372+
state = cli_mod.configure_shared_state(self.WS, profile=None, skip_preflight=True)
1373+
1374+
# find_profile_name_for_host is a local ~/.databrickscfg lookup (no network).
1375+
assert state["profile"] == "resolved"
1376+
1377+
1378+
class TestSkipPreflightFlag:
1379+
"""`--skip-preflight` on a launch command threads through _launch_tool to
1380+
configure_shared_state as skip_preflight."""
1381+
1382+
LAUNCH_TOOLS = ["codex", "claude", "gemini", "opencode", "copilot", "pi"]
1383+
1384+
@staticmethod
1385+
def _patches(cfg):
1386+
return [
1387+
patch("ucode.cli.ensure_bootstrap_dependencies"),
1388+
patch("ucode.cli._auto_configure_tool"),
1389+
patch("ucode.cli.load_state", return_value=MINIMAL_STATE),
1390+
patch("ucode.cli.ensure_provider_state", return_value=MINIMAL_STATE),
1391+
patch("ucode.cli.configure_shared_state", cfg),
1392+
patch(
1393+
"ucode.cli.resolve_launch_model",
1394+
return_value=(MINIMAL_STATE, "databricks-claude-sonnet-4"),
1395+
),
1396+
patch("ucode.cli.configure_tool", return_value=MINIMAL_STATE),
1397+
patch("ucode.cli.launch_agent"),
1398+
]
1399+
1400+
@pytest.mark.parametrize("tool", LAUNCH_TOOLS)
1401+
def test_flag_sets_skip_preflight_true(self, tool):
1402+
cfg = MagicMock(return_value=MINIMAL_STATE)
1403+
with contextlib.ExitStack() as stack:
1404+
for p in self._patches(cfg):
1405+
stack.enter_context(p)
1406+
result = runner.invoke(app, [tool, "--skip-preflight"])
1407+
assert result.exit_code == 0, result.output
1408+
assert cfg.call_args.kwargs["skip_preflight"] is True
1409+
1410+
@pytest.mark.parametrize("tool", ["codex", "gemini"])
1411+
def test_absent_flag_defaults_false(self, tool):
1412+
cfg = MagicMock(return_value=MINIMAL_STATE)
1413+
with contextlib.ExitStack() as stack:
1414+
for p in self._patches(cfg):
1415+
stack.enter_context(p)
1416+
result = runner.invoke(app, [tool])
1417+
assert result.exit_code == 0, result.output
1418+
assert cfg.call_args.kwargs["skip_preflight"] is False

0 commit comments

Comments
 (0)