mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
fix(skills): render an explicit empty allowed-tools as no tools, not as all (#5593)
* fix(skills): render an empty allowed-tools as no tools, not as all `_render_skill_metadata` truthiness-tested `Skill.allowed_tools`, so an explicitly empty allowlist (`allowed-tools: []`, parsed to `()` by `parse_allowed_tools`) rendered "Allowed tools: (all)" -- the same text an omitted field (`None`, unrestricted) produces. `allowed_tool_names_for_skills` distinguishes the two and strips every business tool for `()`, so the describe_skill output contradicted the policy applied to the same skill. * docs(skills): scope the rendered (all) to the skill's own declaration Review note: allowed_tool_names_for_skills makes a legacy None skill contribute no tools once any loaded skill declares allowed-tools, so "(all)" on that line reports the frontmatter of this skill rather than the tool set the middleware will allow in a mixed set. Say so where the tri-state is rendered; behaviour is unchanged.
This commit is contained in:
parent
825e742eb6
commit
dde0595ec8
@ -133,7 +133,13 @@ def _render_skill_metadata(skills: list, container_base_path: str) -> str:
|
||||
blocks: list[str] = []
|
||||
for s in skills:
|
||||
mutability = "[custom, editable]" if s.category == SkillCategory.CUSTOM else "[built-in]"
|
||||
tools_line = ", ".join(s.allowed_tools) if s.allowed_tools else "(all)"
|
||||
# `()` is an explicit empty allowlist — the policy middleware strips every
|
||||
# business tool for it — so only an omitted field (`None`) means unrestricted.
|
||||
# `(all)` describes this skill's own frontmatter, not the enforced union: once
|
||||
# any loaded skill declares allowed-tools,
|
||||
# ``allowed_tool_names_for_skills`` (tool_policy.py) gives a `None` skill no
|
||||
# tools, so a mixed set can render `(all)` while the middleware restricts it.
|
||||
tools_line = "(all)" if s.allowed_tools is None else (", ".join(s.allowed_tools) or "(none)")
|
||||
location = s.get_container_file_path(container_base_path)
|
||||
# name/description/allowed-tools come from untrusted ``.skill`` frontmatter;
|
||||
# escape so a value cannot forge a framework tag in the describe_skill output.
|
||||
|
||||
@ -11,6 +11,7 @@ from deerflow.skills.describe import (
|
||||
build_skill_search_setup,
|
||||
get_skill_index_prompt_section,
|
||||
)
|
||||
from deerflow.skills.tool_policy import allowed_tool_names_for_skills
|
||||
from deerflow.skills.types import Skill, SkillCategory
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
@ -281,3 +282,24 @@ def test_describe_tool_select_uncapped():
|
||||
content = result.update["messages"][0].content
|
||||
for s in many_skills:
|
||||
assert s.name in content, f"select: truncated — {s.name} missing from result"
|
||||
|
||||
|
||||
# ── Explicitly empty allowed-tools ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_render_explicit_empty_allowed_tools_does_not_claim_all():
|
||||
restricted = _make_skill("locked-down", allowed_tools=())
|
||||
rendered = _render_skill_metadata([restricted], "/mnt/skills")
|
||||
assert "Allowed tools: (all)" not in rendered
|
||||
assert "Allowed tools: (none)" in rendered
|
||||
|
||||
|
||||
def test_rendered_allowed_tools_agree_with_skill_tool_policy():
|
||||
"""`(all)` must mean unrestricted, which is the one state that renders it."""
|
||||
omitted = _make_skill("legacy")
|
||||
assert allowed_tool_names_for_skills([omitted]) is None
|
||||
assert "Allowed tools: (all)" in _render_skill_metadata([omitted], "/mnt/skills")
|
||||
|
||||
restricted = _make_skill("locked-down", allowed_tools=())
|
||||
assert allowed_tool_names_for_skills([restricted]) == set()
|
||||
assert "Allowed tools: (all)" not in _render_skill_metadata([restricted], "/mnt/skills")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user