Skip to content
Merged
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
18 changes: 13 additions & 5 deletions src/ucode/managed_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@
"codex": "managed_config.toml",
}

# Shown whenever the workspace's coding-agent-config APIs return FEATURE_DISABLED.
CODING_AGENT_CONFIGS_DISABLED_MESSAGE = (
"Workspace-managed coding agent configuration is not available on this workspace. Use "
"`ucode configure` to set up agents for individual users instead."
)

BUDGET_POLICY_BLURB = (
"As the workspace spends more of a budget, a tiered spend policy automatically switches "
"everyone's default agent and model to a cheaper one — for example Claude Code / Opus normally, "
Expand Down Expand Up @@ -1277,6 +1283,10 @@ def _handle_existing_config(workspace: str, token: str) -> tuple[bool, dict | No
with spinner("Checking for an existing managed config..."):
existing, reason = get_managed_config(workspace, token)
if reason is not None:
if "feature_disabled" in reason.lower():
# Authoring a draft that the workspace cannot publish only leads the admin through a
# dead-end wizard. Stop before model discovery and point them to per-user setup instead.
raise RuntimeError(CODING_AGENT_CONFIGS_DISABLED_MESSAGE)
print_note(f"Could not check for an existing config: {reason}")
return True, None
if existing is None:
Expand Down Expand Up @@ -1928,11 +1938,7 @@ def show_command() -> int:
def _explain_publish_failure(reason: str) -> str:
lowered = reason.lower()
if "feature_disabled" in lowered:
return (
"Managed coding-agent configs aren't enabled on this workspace yet. Ask your Databricks "
"contact to enable the `codingAgentConfigCrudEnabled` flag for it, then re-run "
"`ucode apply`."
)
return CODING_AGENT_CONFIGS_DISABLED_MESSAGE
if "permission_denied" in lowered or "http 403" in lowered:
return (
"Publishing a managed config requires workspace admin. Your account can read the "
Expand Down Expand Up @@ -2031,6 +2037,8 @@ def apply_command(*, yes: bool = False) -> int:
with spinner("Checking for an existing managed config..."):
existing, reason = get_managed_config(workspace, token)
if reason is not None:
if "feature_disabled" in reason.lower():
raise RuntimeError(CODING_AGENT_CONFIGS_DISABLED_MESSAGE)
raise RuntimeError(
f"Could not check whether {workspace} already has a managed config: {reason}. "
"Refusing to publish without knowing, since that could overwrite a config silently."
Expand Down
46 changes: 44 additions & 2 deletions tests/test_managed_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,29 @@ def test_read_failure_continues_with_a_note(self):
assert not select.called
assert note.called

def test_feature_disabled_blocks_setup_with_an_actionable_error(self):
# When the coding-agent-config APIs aren't enabled, the read fails with a FEATURE_DISABLED
# 404. Stop before authoring a draft that can never be published.
reason = (
'HTTP 404 Not Found: {"error_code":"FEATURE_DISABLED",'
'"message":"Coding agent config APIs are not enabled for this workspace."}'
)
with (
patch.object(wizard, "get_managed_config", return_value=(None, reason)),
patch.object(wizard, "prompt_for_selection") as select,
patch.object(wizard, "print_note") as note,
pytest.raises(RuntimeError) as exc_info,
):
wizard._handle_existing_config(WORKSPACE, "token")
assert not select.called
message = str(exc_info.value)
assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE
assert "`ucode configure`" in message
# The raw 404 / JSON body must not leak into the message.
assert "404" not in message
assert "FEATURE_DISABLED" not in message
assert not note.called

def test_choosing_create_continues_authoring(self):
with (
patch.object(
Expand Down Expand Up @@ -2518,6 +2541,24 @@ def fake_create(*a, **k):
)
assert created["called"] is False

def test_feature_disabled_read_uses_the_shared_blocking_message(self):
managed_config_mod.save_managed_state(WORKSPACE, self.MANIFEST)
created = {"called": False}

def fake_create(*a, **k):
created["called"] = True
return {}, None

reason = 'HTTP 404 Not Found: {"error_code":"FEATURE_DISABLED"}'
with pytest.raises(RuntimeError) as exc_info:
self._run(
get_managed_config=lambda *a, **k: (None, reason),
create_coding_agent_config=fake_create,
)
assert str(exc_info.value) == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE
assert "FEATURE_DISABLED" not in str(exc_info.value)
assert created["called"] is False

def test_existing_config_without_a_resource_name_is_an_error(self):
managed_config_mod.save_managed_state(WORKSPACE, self.MANIFEST)
with pytest.raises(RuntimeError, match="resource name"):
Expand All @@ -2527,11 +2568,12 @@ def test_existing_config_without_a_resource_name_is_an_error(self):
class TestPublishFailureMessages:
"""The server's error codes, turned into something an admin can act on."""

def test_feature_disabled_names_the_flag(self):
def test_feature_disabled_uses_the_shared_message(self):
message = wizard._explain_publish_failure(
'HTTP 400 Bad Request: {"error_code":"FEATURE_DISABLED","message":"..."}'
)
assert "codingAgentConfigCrudEnabled" in message
assert message == wizard.CODING_AGENT_CONFIGS_DISABLED_MESSAGE
assert "`ucode configure`" in message

def test_permission_denied_says_admin_is_required(self):
message = wizard._explain_publish_failure(
Expand Down
Loading