From d12bfd0302627c71b74c1d604c4814f19725b29e Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 02:47:32 +0200 Subject: [PATCH] =?UTF-8?q?refactor(brain):=20Auto-Magie=20raus=20?= =?UTF-8?q?=E2=80=94=20ARIA=20entscheidet=20selbst,=20Stefan=20fragt=20im?= =?UTF-8?q?=20Zweifel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mut zur Luecke: -595 Zeilen Auto-Magie-Code raus, weil sie heute Abend 4 Bugs verursacht und 0 echten Mehrwert geliefert hat. Plus Stefan hat zu Recht erkannt dass das System mit Pentest/Audit-Workflows kollidieren wuerde (Whitelist-Pflege noetig). Weg: - aria-brain/api_heuristic.py geloescht (282 Zeilen Cross-Session- Tracking, Hint-Generation, Bypass-Detection) - aria-brain/agent.py: Auto-Scaffold-Block, Bypass-Detection-Block, _upsert_bypass_lesson-Methode (-146 Zeilen) - aria-brain/main.py: /skills/can-bash-host Endpoint - aria-brain/prompts.py: api_heuristic_section-Parameter - docker-compose.yml: managed-settings-Copy aus proxy-Command - proxy-patches/pre-tool-bash-block.js (PreToolUse-Hook) - proxy-patches/managed-settings.json (claude-CLI Hook-Config) Bleibt (kostet nichts, hilft): - Alle 18 seed_rules (sind in DB, machen keine Last) - skill_scaffold Tool (ARIA kann es manuell nutzen) - Anti-Friedhof + snake_case + Safe-Name-Mapping (passive Validierung) - Versionierung + Rollback (P4, hat sich bei PATH-Bug bewaehrt) - 50k stdout Truncate-Fix scaffold-reflex seed_rule umgeschrieben: kein 'SOFORT scaffold'- Reflex mehr, stattdessen 4-Punkte-Heuristik (parametrisierbar? wiederkehrend? exploratory? im Zweifel: Stefan fragen). Pentest- Workflows bleiben damit ad-hoc Bash ohne false-positive Skill-Vorschlaege. Existierende auto-feedback-Memories in der DB bleiben — sind nuetzliche Lehren, werden nicht mehr automatisch erweitert. Stefan kann sie via Diagnostic-Gehirn-Tab loeschen wenn sie nerven. Dank git ist alles rueckholbar. Wenn doch wieder Auto-Magie gewuenscht: git revert auf 8d5991f. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 3 +- aria-brain/agent.py | 146 +------------- aria-brain/api_heuristic.py | 282 --------------------------- aria-brain/main.py | 46 ----- aria-brain/prompts.py | 6 - aria-brain/seed_rules.py | 60 +++--- docker-compose.yml | 2 - proxy-patches/managed-settings.json | 15 -- proxy-patches/pre-tool-bash-block.js | 95 --------- 9 files changed, 30 insertions(+), 625 deletions(-) delete mode 100644 aria-brain/api_heuristic.py delete mode 100644 proxy-patches/managed-settings.json delete mode 100644 proxy-patches/pre-tool-bash-block.js diff --git a/README.md b/README.md index bd3d542..8cfce77 100644 --- a/README.md +++ b/README.md @@ -402,8 +402,7 @@ jedem Chat-Turn im Hot-Memory-Block auf: - **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize` - **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird - **runtime-topology** (architektur) — ARIA laeuft als `claude`-CLI-Subprocess IM aria-proxy Container (alpine — kein python3/jq), NICHT im aria-brain. `/data/skills/` und `BRAIN_INTERNAL_URL` existieren dort nicht. Brain-Resources via Brain-Tools (`oauth_get_token`, `memory_search`, `run_` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy) -- **PreToolUse-Hard-Block** *(claude-CLI Hook)*: `proxy-patches/pre-tool-bash-block.js` ist als PreToolUse-Hook fuer das Bash-Tool im aria-proxy-Container registriert (via `/etc/claude-code/managed-settings.json`). Vor JEDEM Bash-Tool-Call wird Brain-Endpoint `/skills/can-bash-host` gefragt — wenn die URL gegen einen Host laeuft fuer den bereits ein matching Skill existiert, exit 2 + Stderr → claude-CLI lehnt den Tool-Call ab und gibt ARIA einen echten Tool-Error zurueck *„BLOCKED — nutze run_X stattdessen"*. Im Gegensatz zu seed_rules ist das echter Zwang, kein Hinweis den sie ignorieren kann. Fail-open: bei Brain-Timeout/Fehler greift der Block nicht (kein Lockout). -- **scaffold-reflex** — Brain trackt cross-session welche externen Hosts via Bash-curl wiederholt (≥3× in 24h) ohne passenden Skill aufgerufen wurden. Ergebnis landet als `## API-Heuristik`-Block im System-Prompt. **Auto-Scaffold**: bei bekannten Hosts (Spotify, GitHub, OpenAI etc.) legt Brain den Skill automatisch an — ARIA findet ihn beim nächsten Turn vor (author=`aria-auto`) und nutzt `run_` statt curlen. Toggle via ENV `BRAIN_AUTO_SCAFFOLD=false`. **Bypass-Lehre**: wenn ARIA trotz vorhandenem Skill weiter curlt (Skill-Bypass), erkennt Brain das im agent_stream und (1) injiziert einen drastischen `🚨 SKILL-BYPASS`-Hint im aktuellen System-Prompt und (2) speichert ein pinned `type=rule, source=auto-feedback` Memory mit Skill+Host (idempotent via migration_key `auto/skill-bypass/`) — damit lernt sie es cross-session, nicht nur in der aktuellen Konversation. Data-Source: `agent_stream.jsonl`, Cache 5 min +- **scaffold-reflex** — ARIA entscheidet selbst ob ein wiederkehrender Bash-Pattern Skill-würdig ist (parametrisierbar + wiederkehrend + nicht-exploratory). Im Zweifel fragt sie Stefan. **Kein Auto-Scaffold, kein Tracking, keine Pflege** — Skills werden bewusst angelegt, nicht magisch. Pentest/Audit/Recherche bleibt ad-hoc Bash, auch bei 100× derselbe Host. - **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden ### Skill-Scaffold (Templates) diff --git a/aria-brain/agent.py b/aria-brain/agent.py index ad654cc..494b156 100644 --- a/aria-brain/agent.py +++ b/aria-brain/agent.py @@ -849,65 +849,6 @@ class Agent: self._pending_events = [] return events - def _upsert_bypass_lesson(self, ev: dict) -> None: - """Speichert die Lehre aus einem Skill-Bypass als pinned Memory. - Idempotent ueber migration_key — bei Wiederholung wird der vorhandene - Punkt aktualisiert (Counter hoeher). So lernt ARIA cross-session, - nicht nur in der aktuellen Konversation.""" - from datetime import datetime, timezone - import uuid as _uuid - from memory.vector_store import COLLECTION - from qdrant_client.http import models as _qm - - skill_name = ev["skill_name"] - host = ev["host"] - count = ev["count"] - migration_key = f"auto/skill-bypass/{skill_name}" - title = f"Skill '{skill_name}' nutzen, nicht curl" - 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. " - f"Nutze `{run_tool}(...)` als Brain-Tool, NICHT Bash-curl gegen {host}. " - f"Brain hat {count}× erkannt dass dieser Skill umgangen wurde " - f"(letzter Vorfall: heute). Ein Skill-Aufruf = 1 Tool-Call (~3s) " - f"vs. Bash-Wrapper = 3-5 Tool-Calls (~13-20s)." - ) - - # Alte Version mit gleicher migration_key entfernen (Counter-Update) - try: - self.store.client.delete( - collection_name=COLLECTION, - points_selector=_qm.FilterSelector(filter=_qm.Filter(must=[ - _qm.FieldCondition(key="migration_key", - match=_qm.MatchValue(value=migration_key)) - ])), - ) - except Exception: - pass - - vec = self.embedder.embed(content) - now = datetime.now(timezone.utc).isoformat() - payload = { - "type": "rule", - "title": title, - "content": content, - "pinned": True, - "category": "skills", - "source": "auto-feedback", - "tags": [], - "created_at": now, - "updated_at": now, - "migration_key": migration_key, - "attachments": [], - } - self.store.client.upsert( - collection_name=COLLECTION, - points=[_qm.PointStruct(id=str(_uuid.uuid4()), vector=vec, payload=payload)], - ) - logger.info("bypass-lesson upserted: skill=%s host=%s count=%d", - skill_name, host, count) - # ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ── MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops @@ -959,90 +900,6 @@ class Agent: oauth_port = os.environ.get("RVS_PORT_PUBLIC", os.environ.get("RVS_PORT", "443")).strip() oauth_tls = os.environ.get("RVS_TLS", "true").strip().lower() != "false" - # API-Heuristik: wenn ARIA gegen externe APIs wiederholt via Bash - # gecurled hat (cross-session, aus persistiertem agent_stream.jsonl), - # injiziert das einen Hinweis-Block der ihr scaffolden empfiehlt. - api_heuristic_section = "" - try: - import api_heuristic as _ah - hints = _ah.compute_hints(existing_skills=all_skills) - - # AUTO-SCAFFOLD (Variante C): wenn ein Hinweis ein konkretes - # (name, template, params) hat UND der Skill noch nicht existiert, - # legt Brain ihn JETZT an — bevor ARIA wieder Bash-curl macht. - # ARIA findet den Skill in den naechsten Tool-Listen vor und - # nutzt ihn direkt via `run_`. Toggle via ENV. - auto_scaffold = os.environ.get("BRAIN_AUTO_SCAFFOLD", "true").strip().lower() != "false" - if auto_scaffold and hints: - existing_names = {s.get("name") for s in all_skills} - scaffolded_any = False - for hint in hints: - sug = hint.get("suggestion") - if not sug: - continue - sname, stpl, sparams = sug - if sname in existing_names: - continue - try: - new_manifest = skills_mod.scaffold_skill( - name=sname, template=stpl, params=sparams, author="aria-auto", - ) - logger.info("auto_scaffold: '%s' aus '%s' angelegt (trigger: %s mit %d Calls)", - sname, stpl, hint["host"], hint["count"]) - self._pending_events.append({ - "type": "skill_created", - "skill": { - "name": new_manifest["name"], - "description": new_manifest.get("description", ""), - "execution": new_manifest.get("execution", ""), - "active": new_manifest.get("active", True), - "setup_error": new_manifest.get("setup_error"), - "auto_scaffolded": True, - "from_template": stpl, - "trigger_host": hint["host"], - "trigger_count": hint["count"], - }, - }) - scaffolded_any = True - except Exception as exc: - logger.warning("auto_scaffold '%s' fehlgeschlagen: %s", sname, exc) - if scaffolded_any: - # Skills-Liste refresh damit der frische Skill im Prompt sichtbar ist - all_skills = skills_mod.list_skills(active_only=False) - active_skills = [s for s in all_skills if s.get("active", True)] - # WICHTIG: tools NEU bauen, sonst kennt der claude-CLI- - # Subprocess den frisch gescaffoldeten `run_` NICHT - # und ARIA muss ``-Tags halluzinieren. - tools = list(META_TOOLS) + [_skill_to_tool(s) for s in active_skills] - _ah.invalidate_cache() - # Heuristik neu rechnen — die scaffold-targets sind jetzt weg - hints = _ah.compute_hints(existing_skills=all_skills, force=True) - - api_heuristic_section = _ah.build_section(hints) - - # BYPASS-DETECTION (Variante 3 / Lerneffekt): - # Hat ARIA in den letzten ~10min Bash-curl gegen einen Host - # gemacht OBWOHL der Skill existiert? → drastischer Hint im - # Prompt JETZT + pinned Memory speichern, damit's beim - # naechsten Turn / naechster Session weiter sichtbar ist - # ("echtes Lernen via Brain-Memory"). - bypass_events = _ah.detect_recent_bypass(all_skills, since_sec=600) - if bypass_events: - bypass_section = _ah.build_bypass_section(bypass_events) - if bypass_section: - api_heuristic_section = ( - (bypass_section + "\n\n" + api_heuristic_section) - if api_heuristic_section else bypass_section - ) - # Pinned-Memory pro Skill speichern, idempotent ueber migration_key - for ev in bypass_events: - try: - self._upsert_bypass_lesson(ev) - except Exception as exc: - logger.warning("bypass-lesson upsert fehlgeschlagen: %s", exc) - except Exception as exc: - logger.warning("api_heuristic fehlgeschlagen: %s", exc) - system_prompt = build_system_prompt(hot, cold, skills=all_skills, triggers=all_triggers, condition_vars=condition_vars, @@ -1051,8 +908,7 @@ class Agent: oauth_services=oauth_services, oauth_callback_host=oauth_host, oauth_callback_port=oauth_port, - oauth_callback_tls=oauth_tls, - api_heuristic_section=api_heuristic_section) + oauth_callback_tls=oauth_tls) messages = [ProxyMessage(role="system", content=system_prompt)] for t in self.conversation.window(): messages.append(ProxyMessage(role=t.role, content=t.content)) diff --git a/aria-brain/api_heuristic.py b/aria-brain/api_heuristic.py deleted file mode 100644 index dd23a95..0000000 --- a/aria-brain/api_heuristic.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -API-Heuristik — Cross-Session-Tracker fuer wiederkehrende externe API-Calls. - -Problem: ARIA driftet bei trivialen API-Calls zu Bash-curl statt Skills -zu bauen. Die seed_rule "scaffold-reflex" greift nicht zuverlaessig weil -jede Chat-Anfrage eine eigene Claude-CLI-Session ist — in der aktuellen -Session sieht sie nicht dass dieselbe API gestern auch schon 10x via -curl angerufen wurde. - -Loesung: Brain trackt server-side. Beim Bauen des System-Prompts wird -`agent_stream.jsonl` der letzten 24h gescannt, Bash-curl-Calls werden -nach Hostname aggregiert. Hosts ueber Schwelle bei denen noch kein -matching Skill existiert landen als Hinweis-Block im System-Prompt — -ARIA sieht "du machst 17x curl gegen api.spotify.com, scaffold bitte". - -Caching: Ergebnis 5 min gehalten, sonst grep wir bei jedem Turn die -log-Datei. Bei <1 MB log file ist das eh schnell. -""" - -from __future__ import annotations - -import json -import logging -import re -import time -from pathlib import Path - -logger = logging.getLogger(__name__) - -AGENT_STREAM_LOG = Path("/shared/logs/agent_stream.jsonl") - -# Schwellen / Lookback — bewusst niedrig gehalten weil "2x ist Pattern" stimmt -LOOKBACK_HOURS = 24 -THRESHOLD = 3 -CACHE_TTL_SEC = 300 - -# Hosts die wir IGNORIEREN — interne Endpoints / Defaults -_IGNORED_HOSTS = { - "aria-brain", "brain", "localhost", "127.0.0.1", "0.0.0.0", - "api.example.com", # template-default in skill_templates - "aria-bridge", "aria-rvs", "aria-qdrant", "aria-proxy", "aria-diagnostic", - "172.17.0.1", # docker-host-bridge -} - -# Bekannte Hosts → Template-Vorschlag fuer scaffold -_SUGGESTIONS: dict[str, tuple[str, str, dict]] = { - "api.spotify.com": ("spotify", "oauth-api", {"service": "spotify"}), - "api.github.com": ("github", "oauth-api", {"service": "github", "base_url": "https://api.github.com"}), - "api.openai.com": ("openai", "apikey-api", - {"api_name": "OpenAI", "key_env": "OPENAI_API_KEY", - "base_url": "https://api.openai.com"}), - "api.openweathermap.org": ("openweather", "apikey-api", - {"api_name": "OpenWeather", "key_env": "OWM_API_KEY", - "base_url": "https://api.openweathermap.org"}), - "api.telegram.org": ("telegram", "apikey-api", - {"api_name": "Telegram-Bot", "key_env": "TELEGRAM_BOT_TOKEN", - "auth_header": "", "auth_prefix": "", - "base_url": "https://api.telegram.org"}), - "graph.microsoft.com": ("microsoft", "oauth-api", - {"service": "microsoft", "base_url": "https://graph.microsoft.com"}), - "discord.com": ("discord", "oauth-api", - {"service": "discord", "base_url": "https://discord.com/api"}), - "api.notion.com": ("notion", "oauth-api", - {"service": "notion", "base_url": "https://api.notion.com"}), - "reddit.com": ("reddit", "oauth-api", - {"service": "reddit", "base_url": "https://oauth.reddit.com"}), - "oauth.reddit.com": ("reddit", "oauth-api", - {"service": "reddit", "base_url": "https://oauth.reddit.com"}), -} - - -_cache: dict = {"computed_at": 0.0, "hints": []} - - -def invalidate_cache() -> None: - """Cache leeren — sinnvoll nach skill_create / scaffold damit der neue - Skill sofort beim naechsten Aufruf erkannt wird.""" - _cache.update(computed_at=0.0, hints=[]) - - -def detect_recent_bypass( - existing_skills: list[dict], - since_sec: int = 600, -) -> list[dict]: - """Findet Skill-Bypass-Vorfaelle: Bash-curl gegen einen Host fuer den - bereits ein matching Skill existiert. ARIA haette `run_` nutzen - sollen, hat aber gecurled. Das ist Drift — wir wollen es Brain merken. - - Returns: liste {host, skill_name, count, last_ts} fuer Hosts wo ein - Bypass in den letzten `since_sec` Sekunden vorkam. - """ - if not AGENT_STREAM_LOG.exists() or not existing_skills: - return [] - cutoff_ms = (time.time() - since_sec) * 1000 - # Map host → matching skill_name - host_to_skill = {} - for s in existing_skills: - sname = (s.get("name") or "").lower() - if not sname: - continue - # Heuristik wie in _host_already_has_skill: stem des Skill-Namens - # mit Hostnamen verglichen. Fuer scaffolded skills nehmen wir den - # Skill-Namen als stem (z.B. "spotify" -> matched api.spotify.com) - host_to_skill[sname] = sname - - bypass_events: dict[str, dict] = {} - try: - with AGENT_STREAM_LOG.open(encoding="utf-8") as f: - for line in f: - if not line.strip(): - continue - try: - e = json.loads(line) - except Exception: - continue - if e.get("kind") != "tool_use": - continue - if (e.get("name") or "") != "Bash": - continue - ts = e.get("ts") or 0 - if ts < cutoff_ms: - continue - for host in _extract_hosts_from_bash_input(e.get("input") or ""): - h = host.lower() - if h in _IGNORED_HOSTS: - continue - # Welcher Skill-Name matched diesen Host? - matched_skill = None - for skill_stem in host_to_skill: - if skill_stem in h: - matched_skill = host_to_skill[skill_stem] - break - if not matched_skill: - continue - entry = bypass_events.setdefault(h, { - "host": h, "skill_name": matched_skill, - "count": 0, "last_ts": 0, - }) - entry["count"] += 1 - if ts > entry["last_ts"]: - entry["last_ts"] = ts - except Exception as exc: - logger.warning("detect_recent_bypass: konnte log nicht lesen: %s", exc) - return [] - return list(bypass_events.values()) - - -def build_bypass_section(bypass_events: list[dict]) -> str: - """Drastischer Block fuer den System-Prompt wenn ARIA gerade gegen einen - Host gecurled hat OBWOHL der Skill existiert. Inhalt soll sie spuerbar - ermahnen — wirkt nur in der aktuellen Session.""" - if not bypass_events: - return "" - lines = [ - "## 🚨 SKILL-BYPASS ERKANNT", - "", - "Du hast gerade — IN DEN LETZTEN MINUTEN — Bash-curl gegen Hosts " - "gemacht obwohl ein passender Skill existiert. Das ist Verschwendung: " - "5 Bash-Roundtrips à 3s statt 1 Tool-Call à 3s. Stefan wartet doppelt. " - "AB JETZT in diesem Chat:", - "", - ] - for ev in bypass_events: - 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_{safe}(...)` statt curl. " - f"Der Skill ist da. Nutze ihn.") - lines.append("") - return "\n".join(lines) - - -def _extract_hosts_from_bash_input(input_str: str) -> list[str]: - """Hostnames aus URLs in einem Bash-Command. Sehr robust — sucht `https?://host`.""" - if not input_str: - return [] - return re.findall(r'https?://([a-zA-Z0-9.\-]+)', input_str) - - -def _host_already_has_skill(host: str, skills: list[dict]) -> bool: - """Heuristik: Skill-Name enthaelt den 'wesentlichen' Teil des Hosts. - - 'api.spotify.com' → Stem 'spotify'. Wenn ein Skill 'spotify*' existiert: ja. - """ - parts = [p for p in host.split(".") if p and p not in ("api", "www", "oauth")] - if not parts: - return False - stem = parts[0].lower() - for s in skills: - sname = (s.get("name") or "").lower() - if stem and stem in sname: - return True - return False - - -def compute_hints(existing_skills: list[dict] | None = None, force: bool = False) -> list[dict]: - """Aggregiert Bash-curl-Calls der letzten LOOKBACK_HOURS aus dem - agent_stream.jsonl. Returns Liste von Hinweisen, geordnet nach Count - absteigend; nur Hosts ohne matching Skill, nur >= THRESHOLD Calls. - - Hint-Format: {host, count, lookback_hours, suggestion: (name, template, params) | None} - """ - skills = existing_skills or [] - now = time.time() - if not force and (now - _cache["computed_at"]) < CACHE_TTL_SEC: - return _cache["hints"] - - if not AGENT_STREAM_LOG.exists(): - _cache.update(computed_at=now, hints=[]) - return [] - - cutoff_ms = (now - LOOKBACK_HOURS * 3600) * 1000 - counts: dict[str, int] = {} - try: - # Stream-Read damit grosse Files (50 MB cap) nicht in den Speicher kippen - with AGENT_STREAM_LOG.open(encoding="utf-8") as f: - for line in f: - if not line.strip(): - continue - try: - e = json.loads(line) - except Exception: - continue - if e.get("kind") != "tool_use": - continue - if (e.get("name") or "") != "Bash": - continue - if (e.get("ts") or 0) < cutoff_ms: - continue - for host in _extract_hosts_from_bash_input(e.get("input") or ""): - h = host.lower() - if h in _IGNORED_HOSTS: - continue - counts[h] = counts.get(h, 0) + 1 - except Exception as exc: - logger.warning("api_heuristic: konnte agent_stream nicht lesen: %s", exc) - return [] - - hints = [] - for host, count in counts.items(): - if count < THRESHOLD: - continue - if _host_already_has_skill(host, skills): - continue - hints.append({ - "host": host, - "count": count, - "lookback_hours": LOOKBACK_HOURS, - "suggestion": _SUGGESTIONS.get(host), - }) - hints.sort(key=lambda x: -x["count"]) - _cache.update(computed_at=now, hints=hints) - return hints - - -def build_section(hints: list[dict]) -> str: - """Formatiert einen kompakten System-Prompt-Block. Leer wenn nichts.""" - if not hints: - return "" - lines = [ - "## API-Heuristik (Cross-Session-Counter)", - "", - "Du hast in den letzten 24h diese externe(n) API(s) per Bash-curl " - "wiederholt angerufen, OHNE dass ein Skill dafuer existiert. Beim " - "naechsten Aufruf gegen einen dieser Hosts: BAUE ZUERST den Skill " - "via `skill_scaffold`, dann nutze ihn. Spart Stefan Wartezeit " - "und Dir Tool-Roundtrips.", - "", - ] - for h in hints[:5]: # max 5 Eintraege damit Prompt nicht explodiert - sug = h.get("suggestion") - if sug: - name, tpl, params = sug - params_json = json.dumps(params, ensure_ascii=False) - sug_str = f"`skill_scaffold('{name}', '{tpl}', {params_json})`" - else: - sug_str = "`skill_scaffold` mit passendem Template (oauth-api / apikey-api)" - lines.append(f"- **{h['host']}** ({h['count']}x in 24h) → {sug_str}") - lines.append("") - return "\n".join(lines) diff --git a/aria-brain/main.py b/aria-brain/main.py index ea78352..4240c2e 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -805,52 +805,6 @@ class SkillScaffold(BaseModel): author: str = "stefan" -class SkillCanBashHostIn(BaseModel): - command: str - - -@app.post("/skills/can-bash-host") -def skills_can_bash_host(body: SkillCanBashHostIn): - """Prueft ob ein Bash-Command gegen einen Host laufen will fuer den - bereits ein matching Skill existiert. Wird vom claude-CLI PreToolUse- - Hook im aria-proxy gefragt — wenn block=True, weist der Hook den - Bash-Call mit Fehlermeldung zurueck und ARIA muss `run_` nehmen. - - Antwort: {block: bool, host?: str, skill?: str, safe_tool?: str} - """ - import re as _re - cmd = (body.command or "").strip() - if not cmd: - return {"block": False} - skills = skills_mod.list_skills(active_only=False) - if not skills: - return {"block": False} - - # Stem-Map: jeder Skill-Name als potentieller Hostname-Match - # (yt_dlp_download → 'yt_dlp_download', 'spotify' → 'spotify' etc.) - stem_to_skill = {} - for s in skills: - sname = (s.get("name") or "").lower() - if sname: - stem_to_skill[sname] = sname - # Underscore-Variante auch als Stem akzeptieren - stem_to_skill[sname.replace("_", "-")] = sname - - # Alle https-URLs im Command einsammeln + matchen - for url_host in _re.findall(r'https?://([a-zA-Z0-9.\-]+)', cmd): - host_lower = url_host.lower() - for stem, skill_name in stem_to_skill.items(): - if stem and stem in host_lower: - safe_tool = "run_" + _re.sub(r"[^a-zA-Z0-9_]", "_", skill_name) - return { - "block": True, - "host": url_host, - "skill": skill_name, - "safe_tool": safe_tool, - } - return {"block": False} - - @app.get("/skills/templates") def skills_templates_list(): """Liste der verfuegbaren Templates — fuer UI und Dokumentation.""" diff --git a/aria-brain/prompts.py b/aria-brain/prompts.py index d8dd9b1..7303345 100644 --- a/aria-brain/prompts.py +++ b/aria-brain/prompts.py @@ -340,18 +340,12 @@ def build_system_prompt( oauth_callback_host: str = "", oauth_callback_port: str = "443", oauth_callback_tls: bool = True, - api_heuristic_section: str = "", ) -> str: """Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth.""" parts = [build_hot_memory_section(pinned), "", build_time_section()] if skills: parts.append("") parts.append(build_skills_section(skills)) - if api_heuristic_section: - # Direkt nach Skills weil thematisch verwandt ("welche Skills gibt's, " - # welche Skills FEHLEN") - parts.append("") - parts.append(api_heuristic_section) if condition_vars: parts.append("") parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs)) diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py index 8f855ab..416d06a 100644 --- a/aria-brain/seed_rules.py +++ b/aria-brain/seed_rules.py @@ -440,45 +440,41 @@ SEED_RULES: List[dict] = [ { "migration_key": "seed/skill-rule/scaffold-reflex", "type": "rule", - "title": "Skill-Regel: skill_scaffold-Reflex (mit Cross-Session-Counter)", + "title": "Skill-Regel: Skill-Frage statt Skill-Reflex", "category": "skills", "content": ( - "Brain trackt server-side wie oft Du in den letzten 24h dieselbe " - "externe API per Bash-curl angerufen hast (Cross-Session-Counter, " - "siehe '## API-Heuristik'-Block im System-Prompt). \n" + "Wenn Du dieselbe API mehrmals per Bash anrufst, frag Dich:\n" "\n" - "AUTO-SCAFFOLD: Brain legt fuer wiederkehrende Hosts mit " - "bekanntem Template (Spotify, GitHub, OpenAI, OpenWeather, …) " - "automatisch einen Skill an — Du siehst ihn dann in `## Skills` " - "ohne dass Du ihn selbst gebaut hast (Markierung " - "`author=aria-auto`). NUTZE diesen Skill via `run_` " - "direkt, NICHT mehr Bash-curl gegen den Host. Beispiel: wenn " - "`spotify` plotzlich in der Skill-Liste auftaucht → " - "`run_spotify({method:'GET', path:'/v1/me/player'})` statt " - "Token holen + curl.\n" + "1. **Parametrisierbar?** Stabile 1-5 Args (action, path, body) " + "→ Skill-Kandidat. Jeder Aufruf anders (neuer Endpoint, " + "modifizierter Body, neue Hypothese) → KEIN Skill.\n" "\n" - "Wenn die API-Heuristik einen Eintrag OHNE Suggestion zeigt " - "(unbekannter Host): rufe selbst `skill_scaffold` mit dem " - "passenden Template (oauth-api / apikey-api / file-process), " - "BEVOR Du wieder Bash-curl machst.\n" + "2. **Wiederkehrend?** Stefan wird das mehrfach pro Tag/Woche " + "brauchen → ja. Einmal-Spike heute → nein.\n" "\n" - "Warum: jede Chat-Anfrage ist eine eigene Claude-CLI-Session — " - "Du siehst nicht dass Du gestern auch schon 10x Spotify gecurled " - "hast. Der API-Heuristik-Block ist Dein Cross-Session-Gedaechtnis. " - "Wenn er leer ist: alles OK, weitermachen. Wenn nicht: scaffolden.\n" + "3. **Exploratory?** Pentest, Audit, Code-Review, Reverse-" + "Engineering, Recherche → Hypothesen-Iteration. KEIN Skill, " + "auch wenn 100x derselbe Host. Bleib bei ad-hoc Bash oder " + "`ssh aria@host` zur VM-Host.\n" "\n" - "Templates (ausfuehrliche Doku siehe skill_scaffold-Tool):\n" - " - **oauth-api**: Spotify/GitHub/Reddit/Google/Discord. Token " - "kommt vom Brain mit Auto-Refresh.\n" - " - **apikey-api**: OpenWeather/OpenAI/Twilio. Key landet im " - "config_schema → CFG_ ENV. Stefan setzt ihn in Diagnostic.\n" - " - **file-process**: PDF/Bild/JSON-Wandler. process()-Stub, " - "danach `skill_update` mit echtem Code.\n" + "4. **Im Zweifel: frag Stefan.** Lieber 5 Sekunden Bestaetigung " + "als zehn unsinnige Skills im Friedhof. Beispiele:\n" + " - 'Stefan, das ist mein 3. X-Call diese Woche — soll ich " + "daraus einen Skill machen?'\n" + " - 'Das hier ist Pentest-Workflow, ich bleibe bei ad-hoc " + "Bash, ok?'\n" "\n" - "Belohnung konkret: ein Spotify-Skill macht 'welches lied laeuft' " - "in 1 Tool-Call (~3s) statt 3-5 Bash-Roundtrips (~13-20s). Stefan " - "merkt das sofort. Ein einmaliger Scaffold-Aufwand spart hunderte " - "Bash-Roundtrips." + "Du musst NICHT automatisch scaffolden. Brain trackt NICHT mehr " + "wer wieviele Calls gegen welchen Host gemacht hat. Du " + "entscheidest mit Sinn und Verstand — oder fragst nach.\n" + "\n" + "Wenn Du einen Skill bauen willst, hast Du drei Tools:\n" + " - `skill_scaffold` mit Template — einfachster Weg fuer " + "Standard-Pattern (siehe oauth-api/apikey-api/file-process).\n" + " - `skill_create` mit eigenem entry_code — fuer alles was " + "in kein Template passt.\n" + " - `skill_update` — wenn ein vorhandener Skill nur erweitert " + "werden muss (was meistens der Fall ist)." ), }, { diff --git a/docker-compose.yml b/docker-compose.yml index e3ae226..7b514ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,6 @@ services: cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js && cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js && cp /proxy-patches/routes.js $$DIST/server/routes.js && - mkdir -p /etc/claude-code && - cp /proxy-patches/managed-settings.json /etc/claude-code/managed-settings.json && claude-max-api" volumes: - ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json) diff --git a/proxy-patches/managed-settings.json b/proxy-patches/managed-settings.json deleted file mode 100644 index 1f3b321..0000000 --- a/proxy-patches/managed-settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "node /proxy-patches/pre-tool-bash-block.js" - } - ] - } - ] - } -} diff --git a/proxy-patches/pre-tool-bash-block.js b/proxy-patches/pre-tool-bash-block.js deleted file mode 100644 index 371a497..0000000 --- a/proxy-patches/pre-tool-bash-block.js +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env node -/** - * ARIA claude-CLI PreToolUse-Hook: blockiert Bash-Calls gegen externe APIs - * fuer die bereits ein matching Skill im Brain existiert. - * - * Wird von claude-CLI PRO Tool-Use vor der Ausfuehrung mit dem Tool-Use- - * JSON via stdin aufgerufen. Wenn wir exit 2 mit Stderr returnen, lehnt - * claude-CLI den Tool-Call ab und gibt die Stderr als tool_use_error - * an das LLM zurueck — ARIA bekommt also eine echte Fehlermeldung und - * MUSS umdenken (nicht nur Prompt-Anweisung die sie ignorieren kann). - * - * Fail-open: bei jeder Art von Fehler (Brain nicht erreichbar, kaputtes - * JSON etc.) exit 0 — wir blockieren Stefan's eigentliche Arbeit nicht - * nur weil der Block-Mechanismus selber haengt. - */ - -const http = require("http"); - -const BRAIN_URL = process.env.BRAIN_INTERNAL_URL || "http://aria-brain:8080"; -const BRAIN_TIMEOUT_MS = 3000; - -function fail_open(reason) { - if (process.env.HOOK_DEBUG) console.error(`hook-skip: ${reason}`); - process.exit(0); -} - -function block(message) { - // exit 2 = block in claude-CLI PreToolUse hook contract - process.stderr.write(message); - process.exit(2); -} - -let stdinBuf = ""; -process.stdin.on("data", chunk => stdinBuf += chunk); -process.stdin.on("end", () => { - let payload; - try { - payload = JSON.parse(stdinBuf || "{}"); - } catch (_) { - return fail_open("stdin not json"); - } - // claude-CLI Hook-Format kann je nach Version variieren — - // wir akzeptieren tool_name oder hook_event_name in Kombination - const toolName = payload.tool_name || payload.tool || ""; - if (toolName !== "Bash") return fail_open("tool != Bash"); - const command = (payload.tool_input && payload.tool_input.command) || - payload.command || ""; - if (!command) return fail_open("no command"); - // Schnellfilter: nur wenn ueberhaupt eine URL drin ist - if (!/https?:\/\//i.test(command)) return fail_open("no url"); - - // Brain fragen ob ein matching Skill existiert - const body = JSON.stringify({ command }); - const req = http.request( - BRAIN_URL + "/skills/can-bash-host", - { - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(body), - }, - timeout: BRAIN_TIMEOUT_MS, - }, - (res) => { - let chunks = ""; - res.on("data", d => chunks += d); - res.on("end", () => { - let r; - try { r = JSON.parse(chunks); } catch (_) { return fail_open("brain bad json"); } - if (!r || !r.block) return fail_open("brain says ok"); - const skill = r.skill || "?"; - const host = r.host || "?"; - const safeTool = r.safe_tool || `run_${skill}`; - const msg = - `🚨 BASH GEGEN ${host} BLOCKIERT.\n\n` + - `Es existiert bereits ein Skill '${skill}' fuer diesen Host. ` + - `Stefan hat das System so eingerichtet dass Skills via ` + - `\`${safeTool}\` direkt aufgerufen werden — das ist 5-10x ` + - `schneller als der Bash-Curl-Wrapper.\n\n` + - `Konkret: nutze JETZT \`${safeTool}\` mit den passenden ` + - `Parametern (method/path/body) statt curl. Wenn der Skill ` + - `nicht das liefert was Du brauchst: skill_update mit Fix, ` + - `nicht zurueck zu Bash.`; - block(msg); - }); - } - ); - req.on("error", () => fail_open("brain network error")); - req.on("timeout", () => { req.destroy(); fail_open("brain timeout"); }); - req.write(body); - req.end(); -}); - -// Falls stdin nie ein 'end' triggert — Timeout damit wir nicht haengen -setTimeout(() => fail_open("stdin timeout"), 4000);