fix(banner): don't advertise toolsets/skills the agent wasn't given (#50497)

The welcome banner's 'Available Tools' merged in every toolset from the
global check_tool_availability() registry walk, regardless of whether it
was enabled for the current platform. On a Blank Slate CLI (file +
terminal only) that surfaced discord / feishu / kanban tools the agent
was never actually given — they are not in the agent's tool schema, but
the banner displayed them, making it look like they were exposed.

- Filter the unavailable-toolset merge to toolsets actually in
  enabled_toolsets (a toolset that's enabled but has unmet deps still
  legitimately shows as disabled/lazy).
- Gate the 'Available Skills' section on the skills toolset being
  enabled — when it's off, the agent can't load any skill, so show
  'Skills toolset disabled' instead of the on-disk catalog.

When enabled_toolsets is empty (older callers), behavior is unchanged.

Validation: blank-slate banner now shows only file + terminal and
'Skills toolset disabled'; a skills-enabled banner still lists the
catalog. Added regression tests; full banner suite green (15/15).
This commit is contained in:
Teknium 2026-06-21 19:08:54 -07:00 committed by GitHub
parent 8cfcbd327d
commit 5bf23ff251
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 3 deletions

View file

@ -575,6 +575,18 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
enabled_toolsets = enabled_toolsets or []
_, unavailable_toolsets = check_tool_availability(quiet=True)
# The availability check walks the GLOBAL toolset registry, so it includes
# toolsets that aren't part of this agent's platform set at all (e.g.
# `discord`, `feishu_doc` on a CLI session). Those must never surface in the
# banner's "Available Tools" — they aren't exposed to the agent. Restrict to
# toolsets actually enabled for this agent; a toolset that's enabled but
# currently has unmet deps legitimately shows as disabled/lazy below.
_enabled_ts = {str(t) for t in enabled_toolsets}
if _enabled_ts:
unavailable_toolsets = [
item for item in unavailable_toolsets
if str(item.get("id", item.get("name", ""))) in _enabled_ts
]
disabled_tools = set()
# Tools whose toolset has a check_fn are lazy-initialized (e.g. honcho,
# homeassistant) — they show as unavailable at banner time because the
@ -722,10 +734,21 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
right_lines.append("")
right_lines.append(f"[bold {accent}]Available Skills[/]")
skills_by_category = get_available_skills()
total_skills = sum(len(s) for s in skills_by_category.values())
# The skills catalog is only reachable when the `skills` toolset is enabled
# (it exposes skill_view / skill_manage). When it's disabled — e.g. a Blank
# Slate install — the agent literally cannot load any skill, so advertising
# the on-disk catalog here is misleading. Reflect the real state instead.
_skills_enabled = (not _enabled_ts) or ("skills" in _enabled_ts)
if _skills_enabled:
skills_by_category = get_available_skills()
total_skills = sum(len(s) for s in skills_by_category.values())
else:
skills_by_category = {}
total_skills = 0
if skills_by_category:
if not _skills_enabled:
right_lines.append(f"[dim {dim}]Skills toolset disabled[/]")
elif skills_by_category:
for category in sorted(skills_by_category.keys()):
skill_names = sorted(skills_by_category[category])
if len(skill_names) > 8:

View file

@ -200,3 +200,81 @@ def test_build_welcome_banner_configured_mcp_is_not_failed():
assert "docker-profile" in output
assert "configured" in output
assert "failed" not in output
def test_banner_hides_toolsets_not_enabled_for_platform():
"""A globally-registered toolset that isn't enabled for this agent (e.g.
discord / feishu on a CLI session) must NOT appear in 'Available Tools'.
Regression: check_tool_availability() walks the global registry, so the
banner used to merge in every unavailable toolset regardless of whether it
was part of this platform's set. On a Blank Slate CLI (file + terminal only)
that surfaced discord/feishu tools the agent was never given.
"""
with (
patch.object(
model_tools,
"check_tool_availability",
return_value=(
["file", "terminal"],
[
{"name": "discord", "tools": ["discord_fetch_messages"]},
{"name": "feishu_doc", "tools": ["feishu_doc_read"]},
],
),
),
patch.object(banner, "get_available_skills", return_value={}),
patch.object(banner, "get_update_result", return_value=None),
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
):
console = Console(record=True, force_terminal=False, color_system=None, width=160)
banner.build_welcome_banner(
console=console,
model="anthropic/test-model",
cwd="/tmp/project",
tools=[{"function": {"name": "read_file"}}],
enabled_toolsets=["file", "terminal"],
get_toolset_for_tool=lambda n: "file",
)
output = console.export_text()
assert "discord" not in output
assert "feishu" not in output
def test_banner_skills_section_reflects_disabled_skills_toolset():
"""When the `skills` toolset is disabled (Blank Slate), the banner must not
advertise the on-disk skill catalog the agent can't load any of them."""
fake_skills = {"creative": ["ascii-art", "p5js"], "devops": ["bug-triage-work"]}
# skills toolset DISABLED -> catalog hidden, "disabled" message shown
with (
patch.object(model_tools, "check_tool_availability", return_value=(["file", "terminal"], [])),
patch.object(banner, "get_available_skills", return_value=fake_skills),
patch.object(banner, "get_update_result", return_value=None),
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
):
console = Console(record=True, force_terminal=False, color_system=None, width=160)
banner.build_welcome_banner(
console=console, model="m", cwd="/tmp", tools=[{"function": {"name": "read_file"}}],
enabled_toolsets=["file", "terminal"], get_toolset_for_tool=lambda n: "file",
)
out_disabled = console.export_text()
assert "Skills toolset disabled" in out_disabled
assert "ascii-art" not in out_disabled
# skills toolset ENABLED -> catalog listed as before
with (
patch.object(model_tools, "check_tool_availability", return_value=(["file", "terminal", "skills"], [])),
patch.object(banner, "get_available_skills", return_value=fake_skills),
patch.object(banner, "get_update_result", return_value=None),
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
):
console = Console(record=True, force_terminal=False, color_system=None, width=160)
banner.build_welcome_banner(
console=console, model="m", cwd="/tmp", tools=[{"function": {"name": "read_file"}}],
enabled_toolsets=["file", "terminal", "skills"], get_toolset_for_tool=lambda n: "file",
)
out_enabled = console.export_text()
assert "Skills toolset disabled" not in out_enabled
assert "ascii-art" in out_enabled