refactor(brain): Auto-Magie raus — ARIA entscheidet selbst, Stefan fragt im Zweifel
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`
|
- **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
|
- **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_<skill>` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy)
|
- **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_<skill>` …), 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** — 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.
|
||||||
- **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_<name>` 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/<skill>`) — damit lernt sie es cross-session, nicht nur in der aktuellen Konversation. Data-Source: `agent_stream.jsonl`, Cache 5 min
|
|
||||||
- **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden
|
- **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden
|
||||||
|
|
||||||
### Skill-Scaffold (Templates)
|
### Skill-Scaffold (Templates)
|
||||||
|
|||||||
+1
-145
@@ -849,65 +849,6 @@ class Agent:
|
|||||||
self._pending_events = []
|
self._pending_events = []
|
||||||
return 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 ──
|
# ── Hauptpfad: ein User-Turn → Tool-Loop → finaler Reply ──
|
||||||
|
|
||||||
MAX_TOOL_ITERATIONS = 8 # Schutz vor Endlos-Loops
|
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_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"
|
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_<name>`. 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_<name>` NICHT
|
|
||||||
# und ARIA muss `<tool_call>`-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,
|
system_prompt = build_system_prompt(hot, cold, skills=all_skills,
|
||||||
triggers=all_triggers,
|
triggers=all_triggers,
|
||||||
condition_vars=condition_vars,
|
condition_vars=condition_vars,
|
||||||
@@ -1051,8 +908,7 @@ class Agent:
|
|||||||
oauth_services=oauth_services,
|
oauth_services=oauth_services,
|
||||||
oauth_callback_host=oauth_host,
|
oauth_callback_host=oauth_host,
|
||||||
oauth_callback_port=oauth_port,
|
oauth_callback_port=oauth_port,
|
||||||
oauth_callback_tls=oauth_tls,
|
oauth_callback_tls=oauth_tls)
|
||||||
api_heuristic_section=api_heuristic_section)
|
|
||||||
messages = [ProxyMessage(role="system", content=system_prompt)]
|
messages = [ProxyMessage(role="system", content=system_prompt)]
|
||||||
for t in self.conversation.window():
|
for t in self.conversation.window():
|
||||||
messages.append(ProxyMessage(role=t.role, content=t.content))
|
messages.append(ProxyMessage(role=t.role, content=t.content))
|
||||||
|
|||||||
@@ -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_<skill>` 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)
|
|
||||||
@@ -805,52 +805,6 @@ class SkillScaffold(BaseModel):
|
|||||||
author: str = "stefan"
|
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_<skill>` 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")
|
@app.get("/skills/templates")
|
||||||
def skills_templates_list():
|
def skills_templates_list():
|
||||||
"""Liste der verfuegbaren Templates — fuer UI und Dokumentation."""
|
"""Liste der verfuegbaren Templates — fuer UI und Dokumentation."""
|
||||||
|
|||||||
@@ -340,18 +340,12 @@ def build_system_prompt(
|
|||||||
oauth_callback_host: str = "",
|
oauth_callback_host: str = "",
|
||||||
oauth_callback_port: str = "443",
|
oauth_callback_port: str = "443",
|
||||||
oauth_callback_tls: bool = True,
|
oauth_callback_tls: bool = True,
|
||||||
api_heuristic_section: str = "",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth."""
|
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth."""
|
||||||
parts = [build_hot_memory_section(pinned), "", build_time_section()]
|
parts = [build_hot_memory_section(pinned), "", build_time_section()]
|
||||||
if skills:
|
if skills:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(build_skills_section(skills))
|
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:
|
if condition_vars:
|
||||||
parts.append("")
|
parts.append("")
|
||||||
parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs))
|
parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs))
|
||||||
|
|||||||
+28
-32
@@ -440,45 +440,41 @@ SEED_RULES: List[dict] = [
|
|||||||
{
|
{
|
||||||
"migration_key": "seed/skill-rule/scaffold-reflex",
|
"migration_key": "seed/skill-rule/scaffold-reflex",
|
||||||
"type": "rule",
|
"type": "rule",
|
||||||
"title": "Skill-Regel: skill_scaffold-Reflex (mit Cross-Session-Counter)",
|
"title": "Skill-Regel: Skill-Frage statt Skill-Reflex",
|
||||||
"category": "skills",
|
"category": "skills",
|
||||||
"content": (
|
"content": (
|
||||||
"Brain trackt server-side wie oft Du in den letzten 24h dieselbe "
|
"Wenn Du dieselbe API mehrmals per Bash anrufst, frag Dich:\n"
|
||||||
"externe API per Bash-curl angerufen hast (Cross-Session-Counter, "
|
|
||||||
"siehe '## API-Heuristik'-Block im System-Prompt). \n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"AUTO-SCAFFOLD: Brain legt fuer wiederkehrende Hosts mit "
|
"1. **Parametrisierbar?** Stabile 1-5 Args (action, path, body) "
|
||||||
"bekanntem Template (Spotify, GitHub, OpenAI, OpenWeather, …) "
|
"→ Skill-Kandidat. Jeder Aufruf anders (neuer Endpoint, "
|
||||||
"automatisch einen Skill an — Du siehst ihn dann in `## Skills` "
|
"modifizierter Body, neue Hypothese) → KEIN Skill.\n"
|
||||||
"ohne dass Du ihn selbst gebaut hast (Markierung "
|
|
||||||
"`author=aria-auto`). NUTZE diesen Skill via `run_<name>` "
|
|
||||||
"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"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Wenn die API-Heuristik einen Eintrag OHNE Suggestion zeigt "
|
"2. **Wiederkehrend?** Stefan wird das mehrfach pro Tag/Woche "
|
||||||
"(unbekannter Host): rufe selbst `skill_scaffold` mit dem "
|
"brauchen → ja. Einmal-Spike heute → nein.\n"
|
||||||
"passenden Template (oauth-api / apikey-api / file-process), "
|
|
||||||
"BEVOR Du wieder Bash-curl machst.\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Warum: jede Chat-Anfrage ist eine eigene Claude-CLI-Session — "
|
"3. **Exploratory?** Pentest, Audit, Code-Review, Reverse-"
|
||||||
"Du siehst nicht dass Du gestern auch schon 10x Spotify gecurled "
|
"Engineering, Recherche → Hypothesen-Iteration. KEIN Skill, "
|
||||||
"hast. Der API-Heuristik-Block ist Dein Cross-Session-Gedaechtnis. "
|
"auch wenn 100x derselbe Host. Bleib bei ad-hoc Bash oder "
|
||||||
"Wenn er leer ist: alles OK, weitermachen. Wenn nicht: scaffolden.\n"
|
"`ssh aria@host` zur VM-Host.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Templates (ausfuehrliche Doku siehe skill_scaffold-Tool):\n"
|
"4. **Im Zweifel: frag Stefan.** Lieber 5 Sekunden Bestaetigung "
|
||||||
" - **oauth-api**: Spotify/GitHub/Reddit/Google/Discord. Token "
|
"als zehn unsinnige Skills im Friedhof. Beispiele:\n"
|
||||||
"kommt vom Brain mit Auto-Refresh.\n"
|
" - 'Stefan, das ist mein 3. X-Call diese Woche — soll ich "
|
||||||
" - **apikey-api**: OpenWeather/OpenAI/Twilio. Key landet im "
|
"daraus einen Skill machen?'\n"
|
||||||
"config_schema → CFG_<NAME> ENV. Stefan setzt ihn in Diagnostic.\n"
|
" - 'Das hier ist Pentest-Workflow, ich bleibe bei ad-hoc "
|
||||||
" - **file-process**: PDF/Bild/JSON-Wandler. process()-Stub, "
|
"Bash, ok?'\n"
|
||||||
"danach `skill_update` mit echtem Code.\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Belohnung konkret: ein Spotify-Skill macht 'welches lied laeuft' "
|
"Du musst NICHT automatisch scaffolden. Brain trackt NICHT mehr "
|
||||||
"in 1 Tool-Call (~3s) statt 3-5 Bash-Roundtrips (~13-20s). Stefan "
|
"wer wieviele Calls gegen welchen Host gemacht hat. Du "
|
||||||
"merkt das sofort. Ein einmaliger Scaffold-Aufwand spart hunderte "
|
"entscheidest mit Sinn und Verstand — oder fragst nach.\n"
|
||||||
"Bash-Roundtrips."
|
"\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)."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ services:
|
|||||||
cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js &&
|
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/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
|
||||||
cp /proxy-patches/routes.js $$DIST/server/routes.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"
|
claude-max-api"
|
||||||
volumes:
|
volumes:
|
||||||
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"PreToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Bash",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "node /proxy-patches/pre-tool-bash-block.js"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
Reference in New Issue
Block a user