Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 95 additions & 34 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)
from ucode.agents.codex import revert_legacy_shared_config
from ucode.agents.pi import PI_SETTINGS_BACKUP_PATH, PI_SETTINGS_PATH
from ucode.config_io import restore_file, set_dry_run
from ucode.config_io import is_dry_run, restore_file, set_dry_run
from ucode.databricks import (
apply_pat_environment,
build_shared_base_urls,
Expand Down Expand Up @@ -62,6 +62,7 @@
render_budget_panel,
)
from ucode.managed_config import (
MANAGED_CONFIG_ENV_VAR,
get_model_recommendation,
load_managed_state,
managed_agent_config_enabled,
Expand Down Expand Up @@ -1346,16 +1347,14 @@ def _reject_disabled_agent(managed: dict | None, tool: str) -> None:
)


def _fetch_managed_config(state: dict, *, skip_preflight: bool) -> dict | None:
def _fetch_managed_config(state: dict) -> dict | None:
"""The workspace's managed config for this launch, or None when there is none.

``skip_preflight`` mirrors the launch flag: it reads the last persisted copy instead of
re-fetching, so the config can be stale until a normal launch refreshes it.
Returns None when managed configs are switched off — either the feature is disabled or the launch
passed ``--skip-managed-config`` (which clears the enabling env var for the process).
"""
if not managed_agent_config_enabled():
return None
if skip_preflight:
return load_managed_state(state.get("workspace")) or None
with spinner("Checking for a managed coding agent config..."):
return refresh_managed_config(state)

Expand All @@ -1379,15 +1378,16 @@ def _note_recommended_agent(recommendation: dict | None, tool: str) -> None:
)


def _fetch_budget_recommendation(
state: dict, managed: dict | None, *, skip_preflight: bool
) -> dict | None:
def _fetch_budget_recommendation(state: dict, managed: dict | None) -> dict | None:
"""The agent and model the caller's budget tier allows, or None when there is no budget to read.

Enforcement is server-side, so a failed read only costs the recommendation: the config's own
``default_model`` still applies and the launch proceeds.
"""
if managed is None or skip_preflight:
# No managed config (feature off, --skip-managed-config, or none published) means no budget to
# read. --dry-run resolves the agent from the last saved config alone, so it must not reach the
# control plane — mirroring the managed-config read, which is likewise skipped under --dry-run.
Comment on lines +1387 to +1389

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you rm this comment

if managed is None or is_dry_run():
return None
reason: str | None = None
recommendation = None
Expand Down Expand Up @@ -1532,7 +1532,7 @@ def _launch_tool(
# Bare `ucode` already fetched one to choose the agent; refetching would double the
# control-plane round trip and any fallback warning it printed.
if managed is None:
managed = _fetch_managed_config(state, skip_preflight=skip_preflight)
managed = _fetch_managed_config(state)
# Checked before discovery, which can take tens of seconds, so a blocked launch fails fast.
_reject_disabled_agent(managed, tool)
# Discovery exists to find models and isn't needed for managed config that already names them.
Expand All @@ -1554,9 +1554,7 @@ def _launch_tool(
# model are settled below — the two state files are never merged on disk.
# Bare `ucode` already read one to choose the agent; refetching would double the round trip.
if recommendation is None:
recommendation = _fetch_budget_recommendation(
state, managed, skip_preflight=skip_preflight
)
recommendation = _fetch_budget_recommendation(state, managed)
_note_recommended_agent(recommendation, tool)
if managed is not None:
state = resolve_state(managed, state, tool)
Expand Down Expand Up @@ -1732,9 +1730,9 @@ def _launch_tool(
_print_budget_panel(recommendation, tool, managed)
# Register the managed config's MCP servers so they reach the agent's `/mcp` list. Nothing
# else on this path does it — the config only lists them — so without this a
# workspace-published server never shows up. Skipped under --skip-preflight (deliberately
# unmanaged).
if managed is not None and not skip_preflight:
# workspace-published server never shows up. `managed` is already None when the config is
# skipped (--skip-managed-config / feature off); --dry-run writes nothing.
if managed is not None and not is_dry_run():
_register_managed_mcp_servers(managed, tool, state)
_apply_managed_skills(managed, tool, state)
print_success(f"Starting {TOOL_SPECS[tool]['display']}")
Expand All @@ -1750,17 +1748,42 @@ def _launch_tool(
# Launch-only escape hatch for managed/headless launchers (e.g. omnigent) that
# have already run `ucode configure`: skip the ~5-10s per-launch auth + AI
# Gateway re-validation. Distinct from the configure-only `--skip-validate`,
# which skips the model smoke test.
# which skips the model smoke test, and from `--skip-managed-config`, which
# controls whether the workspace's managed config is applied.
SkipPreflightOption = Annotated[
bool,
typer.Option(
"--skip-preflight",
help="Skip the per-launch Databricks auth + AI Gateway re-validation, trusting a "
"prior `ucode configure`. Launches with your own local settings, ignoring any "
"workspace managed config.",
"prior `ucode configure`.",
),
]

# Ignore the workspace's managed coding-agent config for this one command, on both
# `ucode configure` and the launchers. Accepted (and no-op) even when the managed-config
# feature is off, so a headless launcher can always pass it.
SkipManagedConfigOption = Annotated[
bool,
typer.Option(
"--skip-managed-config",
help="Ignore your workspace's managed coding-agent config for this run, as if managed "
"configs were switched off — use your own local settings instead.",
),
]


def _disable_managed_config_if_requested(skip_managed_config: bool) -> None:
"""Make this process behave as though ``ENABLE_MANAGED_AGENT_CONFIG`` were never set.

``managed_agent_config_enabled()`` reads the env var live and gates every managed-config path
(the launch fetch/apply, the budget read, MCP registration, the bare-``ucode`` agent picker, and
the ``configure`` reject-under-managed flow), so clearing it once here short-circuits them all
without threading a flag through each. Per-invocation only: it affects just the current command.
"""
if skip_managed_config:
os.environ.pop(MANAGED_CONFIG_ENV_VAR, None)


# Target this launch at a specific workspace, auto-configuring (and logging in)
# if it hasn't been set up yet — so a launch needs no prior `ucode configure`.
WorkspaceOption = Annotated[
Expand All @@ -1786,7 +1809,16 @@ def default(
is_eager=True,
),
] = False,
dry_run: Annotated[
bool,
typer.Option(
"--dry-run",
help="Print config files without writing them. Uses the last saved managed "
"config instead of fetching a fresh one.",
),
] = False,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
workspace: WorkspaceOption = None,
) -> None:
"""Configure and launch coding agents through Databricks AI Gateway.
Expand All @@ -1795,8 +1827,12 @@ def default(
"""
if ctx.invoked_subcommand is not None:
return
set_dry_run(dry_run)
_disable_managed_config_if_requested(skip_managed_config)
try:
_launch_managed_default(ctx, skip_preflight=skip_preflight, workspace=workspace)
_launch_managed_default(
ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace
)
except typer.Exit:
# `typer.Exit` subclasses RuntimeError, so it has to be re-raised ahead of the handler
# below. Otherwise a launch that already reported its own error is followed by
Expand All @@ -1810,6 +1846,7 @@ def default(
def _launch_managed_default(
ctx: typer.Context,
*,
dry_run: bool,
skip_preflight: bool,
workspace: str | None,
) -> None:
Expand All @@ -1825,20 +1862,18 @@ def _launch_managed_default(
if not current:
raise RuntimeError("No workspace configured. Run `ucode configure` first.")
apply_pat_environment(state)
if skip_preflight:
# Deliberately unmanaged, so no config is read at all — and there is none to name an agent.
raise RuntimeError(
"--skip-preflight launches with your own settings, so `ucode` has no managed config "
"to pick an agent from. Run `ucode <agent> --skip-preflight` instead."
)
with spinner("Checking for a managed coding agent config..."):
managed = refresh_managed_config(state)
# --dry-run avoids the fetch but still applies the last saved config.
if dry_run:
managed = load_managed_state(current)
else:
with spinner("Checking for a managed coding agent config..."):
managed = refresh_managed_config(state)
if not managed:
_print_no_managed_config_guidance(current, state.get("profile"))
return
# The budget tier can move the org to a cheaper agent, so it outranks the config's
# default_agent. Fetched here and handed to _launch_tool so it is read once per launch.
recommendation = _fetch_budget_recommendation(state, managed, skip_preflight=skip_preflight)
recommendation = _fetch_budget_recommendation(state, managed)
tool = recommended_agent(recommendation, managed) or next(
iter(managed.get("enabled_agents") or {}), None
)
Expand Down Expand Up @@ -1889,6 +1924,7 @@ def codex_cmd(
),
] = None,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
workspace: WorkspaceOption = None,
enable_smart_routing_flag: Annotated[
bool,
Expand All @@ -1906,6 +1942,7 @@ def codex_cmd(
] = False,
) -> None:
"""Launch Codex via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
if enable_smart_routing_flag and disable_smart_routing_flag:
print_err("Use only one of --enable-smart-routing or --disable-smart-routing.")
raise typer.Exit(1)
Expand Down Expand Up @@ -1946,6 +1983,7 @@ def claude_cmd(
),
] = None,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
workspace: WorkspaceOption = None,
enable_smart_routing_flag: Annotated[
bool,
Expand All @@ -1963,6 +2001,7 @@ def claude_cmd(
] = False,
) -> None:
"""Launch Claude Code via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
if enable_smart_routing_flag and disable_smart_routing_flag:
print_err("Use only one of --enable-smart-routing or --disable-smart-routing.")
raise typer.Exit(1)
Expand All @@ -1982,28 +2021,48 @@ def claude_cmd(


@app.command("gemini", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def gemini_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
def gemini_cmd(
ctx: typer.Context,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
) -> None:
"""Launch Gemini CLI via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
_launch_tool("gemini", ctx, skip_preflight=skip_preflight)


@app.command(
"opencode", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
)
def opencode_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
def opencode_cmd(
ctx: typer.Context,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
) -> None:
"""Launch OpenCode via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
_launch_tool("opencode", ctx, skip_preflight=skip_preflight)


@app.command("copilot", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def copilot_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
def copilot_cmd(
ctx: typer.Context,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
) -> None:
"""Launch GitHub Copilot CLI via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
_launch_tool("copilot", ctx, skip_preflight=skip_preflight)


@app.command("pi", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
def pi_cmd(ctx: typer.Context, skip_preflight: SkipPreflightOption = False) -> None:
def pi_cmd(
ctx: typer.Context,
skip_preflight: SkipPreflightOption = False,
skip_managed_config: SkipManagedConfigOption = False,
) -> None:
"""Launch Pi coding agent via Databricks."""
_disable_managed_config_if_requested(skip_managed_config)
_launch_tool("pi", ctx, skip_preflight=skip_preflight)


Expand Down Expand Up @@ -2144,6 +2203,7 @@ def configure(
"still applied.",
),
] = False,
skip_managed_config: SkipManagedConfigOption = False,
verbose: Annotated[
str,
typer.Option(
Expand All @@ -2156,6 +2216,7 @@ def configure(
"""Configure workspace URL and AI Gateway."""
if ctx.invoked_subcommand is not None:
return
_disable_managed_config_if_requested(skip_managed_config)
if verbose not in ("normal", "low"):
print_err("--verbose must be one of: normal, low.")
raise typer.Exit(2)
Expand Down
Loading
Loading