Skip to content

Commit d29d9f3

Browse files
authored
ucode: add --skip-preflight to skip per-launch auth/gateway re-validation (#195)
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 e42de49 commit d29d9f3

2 files changed

Lines changed: 202 additions & 41 deletions

File tree

src/ucode/cli.py

Lines changed: 98 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,61 @@ 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+
# Assemble the shared workspace state that doesn't depend on model discovery:
248+
# workspace, profile, auth mode, base URLs. `profile` may still be None here;
249+
# each path below resolves it once, where a host->profile lookup is reliable
250+
# (the skip branch trusts the prior configure; the preflight resolves after
251+
# login). --skip-preflight persists exactly this and returns, trusting a prior
252+
# `ucode configure` — it already validated auth + the AI Gateway and saved the
253+
# model lists (carried over by load_state, left untouched).
254+
state = load_state()
255+
state["workspace"] = workspace
256+
if profile:
257+
state["profile"] = profile
258+
else:
259+
state.pop("profile", None)
260+
# UC discovery is now always-on; drop any flag persisted by older versions.
261+
state.pop("uc_enabled", None)
262+
# Persist the auth mode so launches rebuild the same (PAT-based) agent
263+
# auth command; an explicit re-configure without --use-pat clears it.
264+
if use_pat:
265+
state["use_pat"] = True
266+
else:
267+
state.pop("use_pat", None)
268+
state["base_urls"] = build_shared_base_urls(workspace)
269+
270+
if skip_preflight:
271+
# A prior `ucode configure` created the profile; resolve it locally (no
272+
# login needed) and persist it so launches disambiguate.
273+
if profile is None:
274+
profile = find_profile_name_for_host(workspace)
275+
if profile:
276+
state["profile"] = profile
277+
save_state(state)
278+
# Scrub MCP entries ucode wrote for a previous workspace.
279+
if previous_workspace and previous_workspace != workspace:
280+
purge_cross_workspace_mcp_residue(state, workspace)
281+
# Diagnostic reasons are transient (attached after save_state so they
282+
# don't land on disk). No discovery ran, so there is nothing to report.
283+
state["_discovery_reasons"] = {"claude": None, "gemini": None, "codex": None, "oss": None}
284+
return state
285+
286+
# ── Preflight (bypassed above under --skip-preflight): validate Databricks
287+
# auth + the AI Gateway, then discover the available models. ──
239288
if use_pat:
240289
if not profile:
241290
raise RuntimeError(
@@ -260,9 +309,11 @@ def configure_shared_state(
260309
else:
261310
ensure_databricks_auth(workspace, profile)
262311
# After login the profile exists in ~/.databrickscfg, so a host->profile
263-
# lookup is reliable. Persist it so subsequent CLI calls disambiguate.
312+
# lookup is reliable even when it returned nothing above.
264313
if profile is None:
265314
profile = find_profile_name_for_host(workspace)
315+
if profile:
316+
state["profile"] = profile
266317
with spinner("Verifying Unity AI Gateway..."):
267318
token = get_databricks_token(workspace, profile)
268319
ensure_ai_gateway_v2(workspace, token)
@@ -283,6 +334,7 @@ def configure_shared_state(
283334
gemini_models = []
284335
codex_models = []
285336
oss_models = []
337+
opencode_models: dict[str, list[str]] = {}
286338
web_search_model: str | None = None
287339
if skip_model_discovery:
288340
# Provider mode: the agent routes through a Model Provider Service and
@@ -295,10 +347,11 @@ def configure_shared_state(
295347
if ws_models:
296348
web_search_model = ws_models[0]
297349
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.
350+
# UC-first, best-effort: one UC model-services call yields all families
351+
# as `system.ai.<model-name>` ids, bucketed by name. If a family comes
352+
# back empty (workspace without UC model-services, or the listing
353+
# failed), fall back to the per-family AI Gateway listing for that
354+
# family only.
302355
with spinner("Fetching available models..."):
303356
ms_claude, ms_codex, ms_gemini, ms_oss, ms_reason = discover_model_services(
304357
workspace, token
@@ -317,30 +370,13 @@ def configure_shared_state(
317370
codex_models, codex_reason = discover_codex_models(workspace, token)
318371
if want_oss:
319372
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)
373+
if claude_models:
374+
opencode_models["anthropic"] = list(claude_models.values())
375+
if gemini_models:
376+
opencode_models["gemini"] = gemini_models
377+
if oss_models:
378+
opencode_models["oss"] = oss_models
379+
344380
if skip_model_discovery:
345381
# Don't clobber any previously-discovered Databricks model lists; provider
346382
# mode just doesn't refresh or use them. Persist the web-search model so
@@ -822,7 +858,12 @@ def _auto_configure_tool(tool: str) -> None:
822858
raise RuntimeError(f"{spec['display']} validation failed — config reverted.")
823859

824860

825-
def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None) -> None:
861+
def _launch_tool(
862+
tool_name: str,
863+
ctx: typer.Context,
864+
provider: str | None = None,
865+
skip_preflight: bool = False,
866+
) -> None:
826867
try:
827868
tool = normalize_tool(tool_name)
828869
existing = load_state()
@@ -860,6 +901,7 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
860901
profile=state.get("profile"),
861902
tools=[tool],
862903
skip_model_discovery=bool(provider),
904+
skip_preflight=skip_preflight,
863905
)
864906
if provider:
865907
# Routing through a Model Provider Service pins no Databricks model;
@@ -892,6 +934,20 @@ def _launch_tool(tool_name: str, ctx: typer.Context, provider: str | None = None
892934
raise typer.Exit(130) from None
893935

894936

937+
# Launch-only escape hatch for managed/headless launchers (e.g. omnigent) that
938+
# have already run `ucode configure`: skip the ~5-10s per-launch auth + AI
939+
# Gateway re-validation. Distinct from the configure-only `--skip-validate`,
940+
# which skips the model smoke test.
941+
SkipPreflightOption = Annotated[
942+
bool,
943+
typer.Option(
944+
"--skip-preflight",
945+
help="Skip the per-launch Databricks auth + AI Gateway re-validation, trusting a "
946+
"prior `ucode configure`.",
947+
),
948+
]
949+
950+
895951
@app.command("codex", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
896952
def codex_cmd(
897953
ctx: typer.Context,
@@ -904,9 +960,10 @@ def codex_cmd(
904960
"before any `--` separator.",
905961
),
906962
] = None,
963+
skip_preflight: SkipPreflightOption = False,
907964
) -> None:
908965
"""Launch Codex via Databricks."""
909-
_launch_tool("codex", ctx, provider=provider)
966+
_launch_tool("codex", ctx, provider=provider, skip_preflight=skip_preflight)
910967

911968

912969
@app.command("claude", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
@@ -921,35 +978,36 @@ def claude_cmd(
921978
"before any `--` separator.",
922979
),
923980
] = None,
981+
skip_preflight: SkipPreflightOption = False,
924982
) -> None:
925983
"""Launch Claude Code via Databricks."""
926-
_launch_tool("claude", ctx, provider=provider)
984+
_launch_tool("claude", ctx, provider=provider, skip_preflight=skip_preflight)
927985

928986

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

934992

935993
@app.command(
936994
"opencode", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
937995
)
938-
def opencode_cmd(ctx: typer.Context) -> None:
996+
def opencode_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
939997
"""Launch OpenCode via Databricks."""
940-
_launch_tool("opencode", ctx)
998+
_launch_tool("opencode", ctx, skip_preflight=skip_preflight)
941999

9421000

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

9481006

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

9541012

9551013
@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)