fix(brain): run_<skill> Tool-Namen safe escapen — Bindestriche kippten Tools-Liste

Beobachtung beim zweiten Live-Test (01:13:41): ARIA versuchte echten
Tool-Call `run_spotify` — bekam aber Error: 'No such tool available'.

Ursache: _skill_to_tool baute Tool-Namen via `run_{s['name']}`. Bei
Skills wie 'yt-dlp-download' wurde daraus 'run_yt-dlp-download' mit
Bindestrich. Anthropic-Tool-Name-Schema ist eigentlich [a-zA-Z0-9_-],
ABER der claude-max-api-proxy konvertiert intern auf OpenAI-Format
und faellt bei Bindestrichen um — wenn EIN Tool ungueltig ist, kippt
die GANZE Tool-Liste, ARIA sieht nichts von 'run_*' inklusive
'run_spotify' obwohl der ja Bindestrich-frei war.

Fix:
- _skill_to_tool: name = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", s["name"])
  → run_yt_dlp_download statt run_yt-dlp-download.
- Dispatcher: bei tool_name='run_X' wird zuerst X als skill_name probiert,
  bei Miss wird ueber die Liste der existierenden Skills gemappt — der
  Skill mit safe_name(name)==X wird dann genommen.
- Bypass-Lesson + Bypass-Section: gleiche safe-Logik fuer den
  empfohlenen run_<tool>-String im Memory/Prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:17:48 +02:00
parent 15f95ed196
commit 1ea7ab5ab1
2 changed files with 24 additions and 4 deletions
+22 -3
View File
@@ -787,10 +787,18 @@ def _skill_to_tool(s: dict) -> dict:
}
if a.get("required"):
required.append(name)
# Tool-Namen duerfen in der Anthropic/Claude tool_use-API nur
# [a-zA-Z0-9_-]{1,64} sein, aber der claude-max-api-proxy (OpenAI-
# Format-Adapter) ist restriktiver und faellt bei Bindestrichen auf
# die Nase — die GANZE Tool-Liste wird dann verworfen und ARIA
# bekommt "No such tool available". Skill-Namen wie 'yt-dlp-download'
# oder 'pdf-umfrage-generator' muessen daher zu run_yt_dlp_download
# bzw. run_pdf_umfrage_generator gemappt werden.
safe_name = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", s["name"])
return {
"type": "function",
"function": {
"name": f"run_{s['name']}",
"name": safe_name,
"description": s.get("description", "(ohne Beschreibung)"),
"parameters": {
"type": "object",
@@ -844,7 +852,7 @@ class Agent:
count = ev["count"]
migration_key = f"auto/skill-bypass/{skill_name}"
title = f"Skill '{skill_name}' nutzen, nicht curl"
run_tool = f"run_{skill_name.replace('-', '_')}"
run_tool = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", skill_name)
content = (
f"WICHTIG fuer Performance + Stefans Wartezeit: "
f"Skill '{skill_name}' existiert und deckt {host} ab. "
@@ -1262,7 +1270,18 @@ class Agent:
f"Sicherheits-Snapshot des vorherigen Stands: {res.get('safety_snapshot')}"
)
if name.startswith("run_"):
skill_name = name[len("run_"):]
# Tool-Namen sind 'safe' (nur _), Skill-Namen koennen aber
# Bindestriche enthalten (z.B. yt-dlp-download). Wir suchen
# zuerst exakt, dann ueber Underscore-zu-Bindestrich-Mapping.
tool_suffix = name[len("run_"):]
skill_name = tool_suffix
if skills_mod.read_manifest(skill_name) is None:
# ggf. Bindestriche zurueckmappen
for cand in skills_mod.list_skills(active_only=False):
cand_name = cand.get("name") or ""
if re.sub(r"[^a-zA-Z0-9_]", "_", cand_name) == tool_suffix:
skill_name = cand_name
break
res = skills_mod.run_skill(skill_name, args=arguments)
snippet = (res.get("stdout") or "")[:2000] or "(kein stdout)"
err = (res.get("stderr") or "")[:500]
+2 -1
View File
@@ -164,8 +164,9 @@ def build_bypass_section(bypass_events: list[dict]) -> str:
sname = ev["skill_name"]
host = ev["host"]
count = ev["count"]
safe = re.sub(r"[^a-zA-Z0-9_]", "_", sname)
lines.append(f"- gegen **{host}** ({count}x kuerzlich) → nutze "
f"`run_{sname.replace('-', '_')}(...)` statt curl. "
f"`run_{safe}(...)` statt curl. "
f"Der Skill ist da. Nutze ihn.")
lines.append("")
return "\n".join(lines)